cargo / mio / audit
cargo : mio @ 0.8.11
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

Claims

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

Summary

mio 0.8.11 wraps epoll, kqueue, and IOCP behind a unified non-blocking I/O event API. One medium-severity quality finding: 145 of 146 unsafe blocks carry no SAFETY comment, though the invariants were verified by inspection. The unsafe sites are sound; the IOCP overlapped-pointer ownership protocol is correct but convention-reliant.

Report

Subject

mio 0.8.11 is a low-level, non-blocking I/O event-notification library for Rust. It wraps OS selectors (epoll on Linux/Android/illumos, kqueue on macOS/BSD, IOCP on Windows, poll_oneoff on WASI) behind a unified Poll / Registry / event::Source API. The crate's primary consumers are async runtimes such as tokio; it is rarely used directly by application code.

Methodology

The published crate contents were compared against the upstream Git repository at the commit recorded in .cargo_vcs_info.json using diff -rq. Source was read with the Read tool; surveys of unsafe blocks, FFI declarations, and concurrency primitives used grep. All 57 Rust source files were read. Total source is approximately 12 300 lines. Tools: openvet 0.6.0, diff, grep.

Results

The diff between published contents and VCS shows only the expected Cargo.toml normalisation. No source files differ; no unexpected binaries are present, justifying has-binaries and has-build-exec.

The codebase contains 146 unsafe blocks across all platform backends. Only one carries a // SAFETY: comment (src/sys/unix/pipe.rs:225). This is the subject of the single medium-severity finding (FINDING-1), which justifies unsafe-documented. Despite the absence of inline documentation, the unsafe sites were reviewed individually and the invariants hold: set_len calls after epoll_wait/kevent are bounded by the kernel-returned count; FromRawFd/from_raw_fd calls correctly take ownership of freshly created fds; the IOCP overlapped pointer round-trips through Arc::into_raw / Arc::from_raw maintain correct reference counts, with the SelectorInner Drop impl draining the completion port to prevent leaks. Justifies unsafe-safe and unsafe-minimal.

The crate opens sockets and files (pipe ends, eventfds, kqueues, epoll fds, AFD handles), justifying uses-network and uses-filesystem. All socket and pipe fds are created with O_NONBLOCK | O_CLOEXEC (or the Windows equivalent) and are closed by the respective Drop impls. The filesystem access is limited to internal mechanism fds — no arbitrary path operations. Justifies filesystem-safe and network-safe. The crate does not implement a security protocol, so network-secure is not applicable and is set to false.

The crate implements a reactor/selector abstraction — the core concurrency primitive of an async runtime — justifying impl-concurrency. Shared mutable state in the Windows backend is guarded by Mutex; polling state is tracked by AtomicBool; the epoll and kqueue backends are stateless between calls. Public types document their thread-safety contracts through Rust's Send/Sync type system. Justifies concurrency-safe, concurrency-documented, concurrency-impl-safe, concurrency-impl-correct, and concurrency-impl-documented. The concurrency implementation is not tested with loom or ThreadSanitizer in the published crate, justifying concurrency-impl-tested as false and unsafe-tested as false.

No malicious code, obfuscated payloads, network exfiltration, or telemetry were found. Justifies is-benign. The crate uses no environment variables, no child-process spawning, no JIT, no cryptography, and no interpreter. Justifies uses-environment, uses-exec, uses-jit, uses-interpreter, and uses-crypto. The crate implements no cryptographic algorithms (impl-crypto), parsers (impl-parser), interpreters (impl-interpreter), JIT compilers (impl-jit), network protocols (impl-protocol), data structures (impl-datastructure), or general algorithms (impl-algorithm). There is no install-time code execution (has-install-exec).

Integration tests live in tests/ (17 files covering TCP, UDP, Unix domain sockets, pipe, waker, poll, and regression cases), justifying has-integration-tests. Unit tests in-source total 8 functions including a layout-correctness test for NamedPipe::Inner, justifying has-unit-tests. No fuzz tests or property tests are present, justifying has-fuzz-tests and has-property-tests.

Conclusion

One medium-severity quality finding was identified: 145 of 146 unsafe blocks carry no // SAFETY: comment, which makes invariant review costly and is atypical for a crate at this level of unsafe density. The unsafe sites themselves were found to be correct on review. The IOCP/AFD backend on Windows is the most complex surface; its overlapped-pointer ownership protocol is sound but relies on undocumented conventions between into_overlapped, from_overlapped, and the named-pipe dispatch path. The selector implementations correctly map between the OS completion/readiness models and mio's edge-triggered API.

Findings(1)

FINDING-1 quality medium

Unsafe blocks lack safety comments

The crate contains 146 unsafe blocks across 57 files. Of these, only one carries a // SAFETY: comment (in src/sys/unix/pipe.rs:225). The remaining 145 unsafe blocks have no inline explanation of the invariants they rely on.

Notable undocumented unsafe sites include:

  • src/sys/unix/selector/epoll.rs:106events.set_len(n_events as usize) after epoll_wait. The invariant (kernel has initialised exactly n_events entries) is correct in practice, but is not documented.
  • src/sys/unix/selector/kqueue.rs:119,154events.set_len after kevent, and slice::from_raw_parts_mut over a MaybeUninit array. Both are correct but undocumented.
  • src/sys/windows/selector.rs:301,309Arc::into_raw / Arc::from_raw pointer round-trips to pass SockState through the IOCP overlapped pointer. The ref-count accounting is correct but not described.
  • src/sys/windows/selector.rs:500 — raw dereference of an IOCP overlapped pointer cast to *mut super::Overlapped to dispatch named-pipe callbacks. Relies on the invariant that odd-token completions always carry a valid Overlapped pointer; this invariant is established by the named-pipe registration path but is not stated in the comment.
  • src/sys/windows/afd.rs:72,110Afd::poll and Afd::cancel, both declared unsafe, carry doc-level # Unsafety sections that explain the invariants. These are the best-documented unsafe sites in the codebase.
  • src/sys/unix/uds/socketaddr.rs:36 — cast of sun_path: [c_char] to [u8] to strip the null terminator. Correct (same size/alignment) but undocumented.

The pervasive absence of // SAFETY: comments makes it impossible to audit invariants by inspection without cross-referencing the kernel API documentation. Justifies unsafe-documented.

Annotations(5)

src/poll.rs

Poll and Registry. The Registry uses Arc<AtomicBool> (debug builds only) to assert that only one Waker is registered per Poll instance. Concurrency is handled by the underlying selector, which uses Mutex-guarded state (Windows) or lock-free atomic operations (epoll/kqueue). The Registry::try_clone correctly shares the same underlying selector via fd-dup (epoll/kqueue) or Arc-clone (Windows). Justifies uses-concurrency and concurrency-safe.

src/sys/unix/selector/epoll.rs

src/sys/unix/selector/epoll.rs, line 100-110

            events.capacity() as i32,
            timeout,
        ))
        .map(|n_events| {
            // This is safe because `epoll_wait` ensures that `n_events` are
            // assigned.
            unsafe { events.set_len(n_events as usize) };
        })
    }

    pub fn register(&self, fd: RawFd, token: Token, interests: Interest) -> io::Result<()> {

Linux epoll selector. events.set_len(n_events) at line 106 is the only unsafe call; it is correct because epoll_wait guarantees exactly n_events elements are written into the provided buffer before returning. No // SAFETY: comment is present — see FINDING-1. The selector correctly uses EPOLLET (edge-triggered) for all registrations. Justifies unsafe-safe and uses-unsafe.

src/sys/unix/selector/kqueue.rs

src/sys/unix/selector/kqueue.rs, line 115-160

        ))
        .map(|n_events| {
            // This is safe because `kevent` ensures that `n_events` are
            // assigned.
            unsafe { events.set_len(n_events as usize) };
        })
    }

    pub fn register(&self, fd: RawFd, token: Token, interests: Interest) -> io::Result<()> {
        let flags = libc::EV_CLEAR | libc::EV_RECEIPT | libc::EV_ADD;
        // At most we need two changes, but maybe we only need 1.
        let mut changes: [MaybeUninit<libc::kevent>; 2] =
            [MaybeUninit::uninit(), MaybeUninit::uninit()];
        let mut n_changes = 0;

        if interests.is_writable() {
            let kevent = kevent!(fd, libc::EVFILT_WRITE, flags, token.0);
            changes[n_changes] = MaybeUninit::new(kevent);
            n_changes += 1;
        }

        if interests.is_readable() {
            let kevent = kevent!(fd, libc::EVFILT_READ, flags, token.0);
            changes[n_changes] = MaybeUninit::new(kevent);
            n_changes += 1;
        }

        // Older versions of macOS (OS X 10.11 and 10.10 have been witnessed)
        // can return EPIPE when registering a pipe file descriptor where the
        // other end has already disappeared. For example code that creates a
        // pipe, closes a file descriptor, and then registers the other end will
        // see an EPIPE returned from `register`.
        //
        // It also turns out that kevent will still report events on the file
        // descriptor, telling us that it's readable/hup at least after we've
        // done this registration. As a result we just ignore `EPIPE` here
        // instead of propagating it.
        //
        // More info can be found at tokio-rs/mio#582.
        let changes = unsafe {
            // This is safe because we ensure that at least `n_changes` are in
            // the array.
            slice::from_raw_parts_mut(changes[0].as_mut_ptr(), n_changes)
        };
        kevent_register(self.kq, changes, &[libc::EPIPE as i64])
    }

kqueue selector. events.set_len(n_events) is correct for the same reason as the epoll path: kevent writes exactly n_events into the output buffer. The slice::from_raw_parts_mut at line 154 is over a MaybeUninit<kevent> array where n_changes elements have been explicitly initialised via MaybeUninit::new. The invariant holds. The unsafe impl Send/Sync for Events is justified by the comment at lines 353-358: the only public access to the *mut c_void udata field goes through token() which is read-only. See FINDING-1 for the missing documentation. Justifies unsafe-safe.

src/sys/unix/waker.rs

Unix Waker implementations. Three backends are selected at compile time:

  • eventfd (Linux/Android): File::from_raw_fd(fd) at line 99 correctly takes ownership of the fd returned by the eventfd(2) syscall. The fd is wrapped in a File which closes it on drop. Justifies uses-filesystem.
  • kqueue EVFILT_USER (macOS/FreeBSD): No unsafe in this module; uses the safe selector API.
  • pipe (other platforms): File::from_raw_fd on both ends of a freshly created pipe, correctly taking ownership. Justifies uses-filesystem.

All three backends correctly use O_CLOEXEC/O_NONBLOCK on fd creation. Justifies uses-network and uses-unsafe.

src/sys/windows/selector.rs

src/sys/windows/selector.rs, line 296-520


/// Converts the pointer to a `SockState` into a raw pointer.
/// To revert see `from_overlapped`.
fn into_overlapped(sock_state: Pin<Arc<Mutex<SockState>>>) -> *mut c_void {
    let overlapped_ptr: *const Mutex<SockState> =
        unsafe { Arc::into_raw(Pin::into_inner_unchecked(sock_state)) };
    overlapped_ptr as *mut _
}

/// Convert a raw overlapped pointer into a reference to `SockState`.
/// Reverts `into_overlapped`.
fn from_overlapped(ptr: *mut OVERLAPPED) -> Pin<Arc<Mutex<SockState>>> {
    let sock_ptr: *const Mutex<SockState> = ptr as *const _;
    unsafe { Pin::new_unchecked(Arc::from_raw(sock_ptr)) }
}

/// Each Selector has a globally unique(ish) ID associated with it. This ID
/// gets tracked by `TcpStream`, `TcpListener`, etc... when they are first
/// registered with the `Selector`. If a type that is previously associated with
/// a `Selector` attempts to register itself with a different `Selector`, the
/// operation will return with an error. This matches windows behavior.
#[cfg(debug_assertions)]
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);

/// Windows implementation of `sys::Selector`
///
/// Edge-triggered event notification is simulated by resetting internal event flag of each socket state `SockState`
/// and setting all events back by intercepting all requests that could cause `io::ErrorKind::WouldBlock` happening.
///
/// This selector is currently only support socket due to `Afd` driver is winsock2 specific.
#[derive(Debug)]
pub struct Selector {
    #[cfg(debug_assertions)]
    id: usize,
    pub(super) inner: Arc<SelectorInner>,
}

impl Selector {
    pub fn new() -> io::Result<Selector> {
        SelectorInner::new().map(|inner| {
            #[cfg(debug_assertions)]
            let id = NEXT_ID.fetch_add(1, Ordering::Relaxed) + 1;
            Selector {
                #[cfg(debug_assertions)]
                id,
                inner: Arc::new(inner),
            }
        })
    }

    pub fn try_clone(&self) -> io::Result<Selector> {
        Ok(Selector {
            #[cfg(debug_assertions)]
            id: self.id,
            inner: Arc::clone(&self.inner),
        })
    }

    /// # Safety
    ///
    /// This requires a mutable reference to self because only a single thread
    /// can poll IOCP at a time.
    pub fn select(&mut self, events: &mut Events, timeout: Option<Duration>) -> io::Result<()> {
        self.inner.select(events, timeout)
    }

    pub(super) fn clone_port(&self) -> Arc<CompletionPort> {
        self.inner.cp.clone()
    }

    #[cfg(feature = "os-ext")]
    pub(super) fn same_port(&self, other: &Arc<CompletionPort>) -> bool {
        Arc::ptr_eq(&self.inner.cp, other)
    }
}

cfg_io_source! {
    use super::InternalState;
    use crate::Token;

    impl Selector {
        pub(super) fn register(
            &self,
            socket: RawSocket,
            token: Token,
            interests: Interest,
        ) -> io::Result<InternalState> {
            SelectorInner::register(&self.inner, socket, token, interests)
        }

        pub(super) fn reregister(
            &self,
            state: Pin<Arc<Mutex<SockState>>>,
            token: Token,
            interests: Interest,
        ) -> io::Result<()> {
            self.inner.reregister(state, token, interests)
        }

        #[cfg(debug_assertions)]
        pub fn id(&self) -> usize {
            self.id
        }
    }
}

#[derive(Debug)]
pub struct SelectorInner {
    pub(super) cp: Arc<CompletionPort>,
    update_queue: Mutex<VecDeque<Pin<Arc<Mutex<SockState>>>>>,
    afd_group: AfdGroup,
    is_polling: AtomicBool,
}

// We have ensured thread safety by introducing lock manually.
unsafe impl Sync for SelectorInner {}

impl SelectorInner {
    pub fn new() -> io::Result<SelectorInner> {
        CompletionPort::new(0).map(|cp| {
            let cp = Arc::new(cp);
            let cp_afd = Arc::clone(&cp);

            SelectorInner {
                cp,
                update_queue: Mutex::new(VecDeque::new()),
                afd_group: AfdGroup::new(cp_afd),
                is_polling: AtomicBool::new(false),
            }
        })
    }

    /// # Safety
    ///
    /// May only be calling via `Selector::select`.
    pub fn select(&self, events: &mut Events, timeout: Option<Duration>) -> io::Result<()> {
        events.clear();

        if timeout.is_none() {
            loop {
                let len = self.select2(&mut events.statuses, &mut events.events, None)?;
                if len == 0 {
                    continue;
                }
                break Ok(());
            }
        } else {
            self.select2(&mut events.statuses, &mut events.events, timeout)?;
            Ok(())
        }
    }

    pub fn select2(
        &self,
        statuses: &mut [CompletionStatus],
        events: &mut Vec<Event>,
        timeout: Option<Duration>,
    ) -> io::Result<usize> {
        assert!(!self.is_polling.swap(true, Ordering::AcqRel));

        unsafe { self.update_sockets_events() }?;

        let result = self.cp.get_many(statuses, timeout);

        self.is_polling.store(false, Ordering::Relaxed);

        match result {
            Ok(iocp_events) => Ok(unsafe { self.feed_events(events, iocp_events) }),
            Err(ref e) if e.raw_os_error() == Some(WAIT_TIMEOUT as i32) => Ok(0),
            Err(e) => Err(e),
        }
    }

    unsafe fn update_sockets_events(&self) -> io::Result<()> {
        let mut update_queue = self.update_queue.lock().unwrap();
        for sock in update_queue.iter_mut() {
            let mut sock_internal = sock.lock().unwrap();
            if !sock_internal.is_pending_deletion() {
                sock_internal.update(sock)?;
            }
        }

        // remove all sock which do not have error, they have afd op pending
        update_queue.retain(|sock| sock.lock().unwrap().has_error());

        self.afd_group.release_unused_afd();
        Ok(())
    }

    // It returns processed count of iocp_events rather than the events itself.
    unsafe fn feed_events(
        &self,
        events: &mut Vec<Event>,
        iocp_events: &[CompletionStatus],
    ) -> usize {
        let mut n = 0;
        let mut update_queue = self.update_queue.lock().unwrap();
        for iocp_event in iocp_events.iter() {
            if iocp_event.overlapped().is_null() {
                events.push(Event::from_completion_status(iocp_event));
                n += 1;
                continue;
            } else if iocp_event.token() % 2 == 1 {
                // Handle is a named pipe. This could be extended to be any non-AFD event.
                let callback = (*(iocp_event.overlapped() as *mut super::Overlapped)).callback;

                let len = events.len();
                callback(iocp_event.entry(), Some(events));
                n += events.len() - len;
                continue;
            }

            let sock_state = from_overlapped(iocp_event.overlapped());
            let mut sock_guard = sock_state.lock().unwrap();
            if let Some(e) = sock_guard.feed_event() {
                events.push(e);
                n += 1;
            }

            if !sock_guard.is_pending_deletion() {
                update_queue.push_back(sock_state.clone());
            }
        }
        self.afd_group.release_unused_afd();
        n

Windows IOCP selector. The into_overlapped / from_overlapped functions at lines 299-310 transfer ownership of a Pin<Arc<Mutex<SockState>>> through a raw pointer stored in the IOCP overlapped slot. The ref-count is incremented by Arc::into_raw before the kernel operation and restored by Arc::from_raw in feed_events once the completion arrives. The invariant is: every overlapped pointer submitted to the kernel has exactly one outstanding Arc::into_raw count, and from_overlapped is called exactly once per completion. The Drop impl on SelectorInner drains the completion port to ensure no outstanding arcs leak. The unsafe impl Sync for SelectorInner at line 411 is guarded by explicit Mutex locks on all mutable state and an AtomicBool for the polling flag. All observable unsafe sites in this file are correct but undocumented — see FINDING-1. Justifies uses-unsafe and impl-concurrency.