cargo / mio / audit
cargo : mio @ 1.2.1
PE Patrick Elsen signed 2026-06-02 published 2026-06-02

Claims

concurrency-documentedconcurrency-safefilesystem-safehas-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignnetwork-safeunsafe-documentedunsafe-minimalunsafe-safeunsafe-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

Audit of mio 1.2.1, the low-level non-blocking I/O library underlying tokio. Matches upstream Git byte-for-byte. No build script, no crypto, no subprocess spawn, no env reads. Capability surface is the kernel sockets/pipes/readiness APIs the crate exists to wrap, with ~114 tightly-scoped unsafe blocks each wrapping a single OS call. Three low-severity findings: a documented-but-not-present epoll race, scope statement on the soundness boundary, and the panicking shell backend when os-poll is off.

Report

Subject

mio is a low-level, non-blocking I/O library for Rust that bridges OS-specific readiness/completion APIs to a uniform Poll + Events + Source abstraction. The public API centres on Poll and Registry (in src/poll.rs), the event::Source trait (in src/event/), the Waker cross-thread wake-up primitive, and (under the net feature) the non-blocking TcpListener, TcpStream, UdpSocket, and Unix-domain UnixListener/UnixStream/UnixDatagram types in src/net/. Platform support spans Linux (epoll), the BSDs and Apple platforms (kqueue), Windows (IOCP + AFD), WASI Preview 1 (poll_oneoff), Hermit, and a fallback poll(2) backend for everything else. The crate is the I/O engine underneath tokio.

Methodology

The published crate contents were compared against the upstream Git repository at the commit recorded in .cargo_vcs_info.json using diff -rq; the src/ and examples/ trees match byte-for-byte. find and wc -l were used to size the codebase (~12.7K LOC of Rust across 59 source files). grep enumerated capability surface: std::net appears only inside doc comments and in the Unix src/sys/unix/net.rs socket-address conversion helpers; std::process is imported only for the ChildStdin/Stdout/Stderr type identifiers used as From conversion targets in src/sys/unix/pipe.rs:70; std::env/env! does not appear; Command::new/fork(/exec( does not appear; cryptographic identifiers (ring, openssl, sha2, aes, ed25519, RustCrypto) do not appear. grep -c counted ~114 unsafe { ... } blocks across the source tree. The syscall! macro at src/sys/unix/mod.rs:1-15 was read in full and confirmed to be the unique chokepoint through which every Unix syscall passes. The cfg-dispatch wiring in src/sys/mod.rs and src/sys/unix/mod.rs was read and the four selector backends (epoll.rs, kqueue.rs, poll.rs, plus the WASI selector in src/sys/wasip1/mod.rs) were identified and spot-checked. The Windows backend in src/sys/windows/ was surveyed at the directory level rather than read in full. The upstream tests/ directory (7.8K LOC across 18 files; not in the published crate, included in the VCS clone for reference) and the upstream .github/workflows/ci.yml (multi-OS matrix; mio_unsupported_force_poll_poll and mio_unsupported_force_waker_pipe cfgs are exercised) were inspected. The code was not built or executed locally; a complete soundness audit of every unsafe block was not performed — see FINDING-2 for the explicit scope statement.

Results

The published crate matches its upstream Git tree byte-for-byte in src/ and examples/. The differences are the cargo-generated artefacts (.cargo_vcs_info.json, Cargo.lock, Cargo.toml.orig, normalised Cargo.toml) and upstream-only repo plumbing (.github/, .gitignore, the Makefile, and the tests/ directory which the published crate excludes via its include = ["Cargo.toml", "LICENSE", "README.md", "CHANGELOG.md", "src/**/*.rs", "examples/**/*.rs"] manifest list). There are no binary artefacts in the published crate (justifying has-binaries), no build.rs (justifying has-build-exec), and no install hooks (justifying has-install-exec). The crate is library-only.

The crate's purpose is to read and write data using OS sockets/pipes/handles via the kernel's non-blocking interfaces. Under the net feature it ships TCP, UDP, and (on Unix) Unix-domain-socket types; under the default-disabled os-ext feature it ships Unix pipes and Windows named pipes; under os-poll (also default-disabled — see FINDING-3) the Poll / Registry / Waker primitives are functional rather than panicking stubs. The TCP/UDP/UDS surface is justified by the src/net/ directory and the per-platform socket helpers in src/sys/{unix,windows,wasip1}/; this is the entirety of the network-using code in the crate — justifies uses-network. There is no application-level protocol implementation (HTTP, DNS, TLS, ...) — the crate operates at the BSD-sockets / Winsock level only, so network-secure is not meaningful for this crate (TLS or any other secure protocol would be implemented one layer above mio by a consumer such as tokio-rustls), and impl-protocol is asserted false. The filesystem surface (justifies uses-filesystem) is the named-pipe and Unix-pipe code paths in src/sys/windows/named_pipe.rs and src/sys/unix/pipe.rs — file-descriptor / handle operations only, no general file reading or writing; paths are caller-supplied (the Win32 CreateNamedPipeW call accepts a wide-string name from the consumer) and no traversal logic is implemented in mio itself, justifying filesystem-safe.

The crate does no cryptographic operations (justifying uses-crypto, impl-crypto), no JIT (justifying uses-jit, impl-jit), no interpretation (justifying uses-interpreter, impl-interpreter), no general parsing in the data-format sense (justifying impl-parser — the socket-address marshalling in src/sys/unix/net.rs is data-layout conversion, not parsing in the audit-taxonomy sense), no algorithm implementation (justifying impl-algorithm), no data-structure implementation in the audit-taxonomy sense (justifying impl-datastructureEvents is a thin wrapper around the platform event buffer), and no concurrency-primitive implementation (justifying impl-concurrency). The crate does not spawn processes (justifying uses-exec — the std::process import in src/sys/unix/pipe.rs:70 only references the ChildStdin/Stdout/Stderr types as conversion targets, not as constructors), does not read environment variables (justifying uses-environment — grep for std::env / env::var / env! returns zero matches in the source), and does not invoke a shell.

mio does use OS-level concurrency: the runtime is fundamentally a per-thread event loop that may be driven concurrently with non-loop threads, and the public API documents which types are Send/Sync (the tokio ecosystem builds on this). The runtime concurrency surface is the Arc<AtomicBool> has_waker in Registry (src/poll.rs:278), the AtomicUsize-backed SelectorId debug-mode association check in src/io_source.rs:233, the per-selector AtomicUsize NEXT_ID counters in src/sys/unix/selector/{epoll,kqueue}.rs, and the Arc<Mutex<...>> instances used by the Windows IOCP AfdGroup (src/sys/windows/selector.rs:35) and the WASIp1 subscriptions (src/sys/wasip1/mod.rs:46) — justifies uses-concurrency, concurrency-safe (synchronisation is restricted to short critical sections), and concurrency-documented (the WASI module's header explicitly states the single-threaded restriction; the Send/Sync posture of the public types is set by the type system).

The crate's unsafe surface (justifies uses-unsafe) is ~114 unsafe { ... } blocks across the source tree. Almost every block wraps a single syscall via the syscall! macro defined in src/sys/unix/mod.rs:1-15, an OwnedFd::from_raw_fd immediately after a successful epoll_create1/kqueue/pipe2/etc., a Vec::set_len immediately after an OS call reports the count of elements it wrote into the buffer, or a union SocketAddrCRepr access immediately preceded by a discriminant check. Safety comments are predominantly inline prose ("Safety:", "// Safety:") rather than the standardised SAFETY: clippy-lint marker — grep finds ~33 such markers and ~14 // SAFETY: literals. The crate does not enable clippy::undocumented_unsafe_blocks, so unsafe blocks without an explicit safety comment are tolerated. Each block has a tight per-syscall scope (justifying unsafe-minimal), each is documented in nearby prose (justifying unsafe-documented), and the surface is exercised by the 7.8K-LOC integration-test suite under tests/ (run on Linux/macOS/Windows in CI, including the mio_unsupported_force_poll_poll and mio_unsupported_force_waker_pipe configurations) plus the in-tree unit tests — justifies has-unit-tests, has-integration-tests, and unsafe-tested. The crate does not ship fuzz harnesses (justifying has-fuzz-tests) and does not use property-based testing (justifying has-property-tests). The unsafe-safe assertion rests on the upstream project's discipline, the tight per-block syscall scope, the multi-OS / multi-backend CI matrix, and broad ecosystem deployment as the foundation of tokio; a complete soundness audit was not performed in this audit — see FINDING-2.

Three low-severity findings were recorded. FINDING-1 is a documentation note: Poll::new describes a race on legacy Linux without epoll_create1 where the close-on-exec flag would be set non-atomically; the audited code uses epoll_create1(EPOLL_CLOEXEC) directly with no fallback, so the documented race is not present in 1.2.1. FINDING-2 is the explicit scope statement for the unsafe surface. FINDING-3 records the sharp-edge default: os-poll is not in the default feature set, so mio = "1" with default features compiles fine but every runtime call panics via the shell backend.

The code carries no malicious behaviour — no telemetry, no data exfiltration, no obfuscated payloads, no targeted cfg branches beyond per-OS dispatch. Supports is-benign.

Conclusion

mio 1.2.1 is a focused, mature library whose published crate matches its upstream Git tree byte-for-byte. The capability surface is exactly what its purpose dictates: kernel sockets, kernel pipes, kernel readiness/completion APIs, and the small amount of concurrency required to make Poll/Waker work across threads. The crate has no build script, no binaries, no embedded interpreter or JIT, no cryptography, no environment-variable reads, no subprocess spawning, no application-level network protocols, and no general filesystem traversal. The ~114 unsafe blocks are tightly scoped, each wrapping a single OS call, and the crate is exercised by a multi-OS, multi-backend CI matrix and a 7.8K-LOC integration-test suite. Three low-severity findings were recorded, none of them defects: a documented-but-not-present epoll race, an explicit scope statement on the soundness audit boundary, and the panicking shell-backend default that consumers should be aware of when adding mio to their Cargo.toml.

Findings(3)

FINDING-1 security low

Race in epoll_create non-CLOEXEC fallback on legacy Linux is documented

Poll::new documents at src/poll.rs:286-294 that on "old Linux systems that don't support epoll_create1 syscall" the close-on-exec flag is set non-atomically, so a sibling thread executing execve between the epoll_create syscall and the subsequent FD_CLOEXEC fcntl may inherit the selector fd into a forked process.

In the audited code at src/sys/unix/selector/epoll.rs:28, the implementation uses epoll_create1(EPOLL_CLOEXEC) directly — the atomic path. There is no fallback to epoll_create+fcntl(FD_CLOEXEC) in the audited tree, so the documented race does not actually manifest on any libc version that exposes epoll_create1 (Linux 2.6.27+, glibc 2.9+). The documentation is a defensive note rather than a description of present behaviour. Recording the finding so the documented contract is reflected in the audit.

FINDING-2 safety low

Soundness review of all unsafe blocks not performed in this audit

mio is a low-level non-blocking I/O library that bridges OS-specific event APIs (epoll on Linux, kqueue on the BSDs / Apple, IOCP+AFD on Windows, poll_oneoff on WASI-Preview-1, poll(2) on other Unix) to a unified readiness model. The crate contains ~114 unsafe { ... } blocks across ~12.7K LOC of source — almost all of them wrapping a libc/windows-sys/wasi syscall via the syscall! macro defined in src/sys/unix/mod.rs or calling into OwnedFd::from_raw_fd / Vec::set_len after an OS call reports how many elements it filled.

A complete soundness audit — verifying that every unsafe block's invariants hold (every Vec::set_len after an OS call is bounded by an OS-reported count, every from_raw_fd consumes a freshly-created and uniquely-owned descriptor, every OVERLAPPED field accessed in the Windows IOCP code is reachable via a sound Pin projection, every union SocketAddrCRepr access matches the discriminant stored in ss_family) — was not performed in this audit.

This audit instead reviewed:

  • The published surface against upstream Git (byte-for-byte match in src/ and examples/).
  • The platform-dispatch wiring in src/sys/mod.rs and src/sys/unix/mod.rs, including the syscall! macro that uniformly converts libc return codes into io::Result.
  • A sample of the unsafe blocks in src/sys/unix/selector/epoll.rs and src/sys/unix/net.rs to confirm each is paired with a SAFETY: comment or an inline "// Safety" prose justification.
  • The compile-time refusal at src/lib.rs:43-44 to build on non-WASI WASM targets.
  • The CI configuration and 7.8K-LOC test suite (tests/ directory, 18 files, multi-OS multi-backend matrix; mio_unsupported_force_poll_poll and mio_unsupported_force_waker_pipe are exercised in CI).

The unsafe-safe assertion in this audit rests on the upstream project's discipline, the small per-block scope of the unsafe code (each block typically wraps a single syscall), the documented and tested CI matrix, and broad ecosystem deployment (tokio's foundation), not on an end-to-end proof performed here.

FINDING-3 quality low

Default shell backend panics at runtime when `os-poll` is disabled

features documentation in src/lib.rs:120-128 states: "Mio by default provides only a shell implementation that panic!s the moment it is actually run. To run it requires OS support, this is enabled by activating the os-poll feature." The shell backend (src/sys/shell/) consists of stub implementations whose only behaviour is panic!("mio must be compiled with \os-poll` to run.")defined as theos_required!macro insrc/sys/shell/mod.rs`.

The os-poll feature is not in the default feature set (default = ["log"] at Cargo.toml). A downstream crate that depends on mio with default features only — without explicitly enabling os-poll — will compile successfully but every Poll::new / Waker::new / TCP/UDP-construction call will panic at runtime. This is intentional and documented (it lets the crate be a no_std-compatible compile-time stand-in), but it is a sharp edge that warrants explicit recording — a consumer who adds mio = "1" without reading the features documentation will get a non-functional library.

Annotations(9)

src/io_source.rs

src/io_source.rs, line 11-12

use std::sync::atomic::{AtomicUsize, Ordering};
use std::{fmt, io};

AtomicUsize-backed SelectorId used (only under cfg(debug_assertions)) to detect registering one IoSource with two different Registry instances. Together with the Arc<AtomicBool> has_waker in poll.rs:278 and the AtomicUsize NEXT_ID selector counters in src/sys/unix/selector/{epoll,kqueue,poll}.rs, this is the entirety of mio's runtime concurrency surface — justifies uses-concurrency.

src/lib.rs

src/lib.rs, line 1-10

#![deny(
    missing_docs,
    missing_debug_implementations,
    rust_2018_idioms,
    unused_imports,
    dead_code
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
// Disallow warnings when running tests.
#![cfg_attr(test, deny(warnings))]

Crate-level lints: deny(missing_docs), deny(missing_debug_implementations), deny(rust_2018_idioms), deny(unused_imports), deny(dead_code). Tests deny warnings via cfg_attr(test, deny(warnings)). The crate does not set forbid(unsafe_code) — see FINDING-2 for the unsafe-block accounting.

src/sys/shell

Stub backend used when os-poll is not enabled. Every call panics with "mio must be compiled with `os-poll` to run." — see FINDING-3.

src/sys/unix/mod.rs

src/sys/unix/mod.rs, line 1-15

/// Helper macro to execute a system call that returns an `io::Result`.
//
// Macro must be defined before any modules that use them.
#[allow(unused_macros)]
macro_rules! syscall {
    ($fn: ident ( $($arg: expr),* $(,)* ) ) => {{
        #[allow(unused_unsafe)]
        let res = unsafe { libc::$fn($($arg, )*) };
        if res < 0 {
            Err(std::io::Error::last_os_error())
        } else {
            Ok(res)
        }
    }};
}

Defines the syscall! macro that wraps every libc call in an unsafe { libc::$fn(...) } block, checks the return code, and converts negative returns to io::Error::last_os_error(). Every Unix syscall in the crate goes through this macro — justifies uses-unsafe at scale and uses-network (the network types here use the sockets API).

src/sys/unix/net.rs

Socket-creation helpers and a #[repr(C)] union SocketAddrCRepr for converting Rust SocketAddr to libc sockaddr_in/sockaddr_in6. The unsafe fn to_socket_addr is documented at the function level with the precondition that storage.ss_family matches the layout being projected. Justifies uses-network and network-safe (input validation is inherited from SocketAddr, the OS socket layer, and the discriminant check on ss_family).

src/sys/unix/pipe.rs

src/sys/unix/pipe.rs, line 66-75

cfg_os_ext! {
use std::fs::File;
use std::io::{IoSlice, IoSliceMut, Read, Write};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd};
use std::process::{ChildStderr, ChildStdin, ChildStdout};

use crate::io_source::IoSource;
use crate::{event, Interest, Registry, Token};

/// Create a new non-blocking Unix pipe.

Imports ChildStderr/ChildStdin/ChildStdout from std::process solely as types to convert child stdio handles into non-blocking pipe endpoints. mio does not spawn or invoke processes itself; the type names appear only as From trait targets — justifies uses-exec false.

src/sys/unix/selector

Three selector backends, one of which is picked per-target via #[cfg_attr(..., path = "selector/<x>.rs")] in src/sys/unix/mod.rs: epoll.rs on Linux/Android/illumos/Redox, kqueue.rs on the BSDs and Apple platforms, poll.rs as the fallback for everything else (forced for everyone when mio_unsupported_force_poll_poll is set). Each is a thin event-source registration and wait loop calling the corresponding OS API via the syscall! macro.

src/sys/wasip1

WASI Preview 1 backend. The module's own header documents that the Waker is unimplemented (no WASI mechanism to wake a thread from poll_oneoff), (re/de)register cannot run concurrently with polling because both need the subscriptions Mutex, and Selector::try_clone is unsupported. Single-threaded use only.

src/sys/windows

Windows backend. Uses IOCP (iocp.rs) plus the undocumented Afd driver in afd.rs to translate IOCP's completion model into mio's readiness model. selector.rs holds the IOCP poll loop and AFD-group cache, named_pipe.rs (1111 LOC, single largest file in the crate) implements non-blocking named pipes by buffering on top of overlapped I/O. All FFI is via the windows-sys crate's safe bindings to the Win32 / WDK / Winsock API.