cargo / socket2 / audit
cargo : socket2 @ 0.6.3
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

Claims

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

Summary

socket2 0.6.3 is a thin wrapper over OS socket APIs exposing the full setsockopt/getsockopt surface and low-level socket control that std::net omits. The 229 unsafe occurrences are structurally uniform: generic getsockopt/setsockopt wrappers with explicit type-safety contracts, SockAddr construction with family-checked casts, and SockRef lifetime management. All unsafe blocks carry SAFETY comments and the invariants hold on review. No findings were recorded.

Report

Subject

socket2 0.6.3 is a thin Rust abstraction over OS socket APIs (BSD sockets on Unix/WASI, WinSock2 on Windows). It exposes socket creation, connection, binding, sending, receiving, and the full range of setsockopt/getsockopt options that std::net does not surface. The primary types are Socket (an owned file-descriptor wrapper), SockAddr (a type-safe sockaddr_storage wrapper), SockRef (a borrowed-FD reference), and platform-specific newtypes for socket options and control messages.

Methodology

The published crate contents were compared against the upstream Git repository at the commit recorded in .cargo_vcs_info.json using diff -rq. All six source files were read in full (8126 lines total). The integration test directory (tests/socket.rs, 60 tests) and CI configuration (.github/) were surveyed in the VCS checkout. Source surveys were run for unsafe, FFI, network imports, filesystem, exec, environment, RNG, concurrency, and crypto patterns using grep. No code was compiled or run during this audit.

Results

The comparison between published crate contents and the VCS repository shows that source files are byte-for-byte identical. The only differences are Cargo.toml normalisation by cargo, the absence of tests/, CHANGELOG.md, .github/, and similar non-publication artefacts from the published crate.

The crate ships no binary artefacts (has-binaries) and no build.rs (has-build-exec, has-install-exec). The single optional feature all enables platform-specific socket options not available everywhere; no feature pulls in network access, filesystem operations, or additional unsafe capabilities beyond what is present in the default build.

The crate has 229 unsafe occurrences, concentrated in three areas. First, the generic getsockopt/setsockopt wrappers in src/sys/unix.rs (lines 1329-1363) and their Windows equivalents: both are declared unsafe fn with an explicit caller contract that the type parameter matches the option level and name. Each call site throughout socket.rs passes the correct type. Second, SockAddr construction in src/sockaddr.rs (lines 73-213): SockAddr::new and SockAddr::try_init are unsafe fn with documented contracts; all derived from_in_addr/from_in6_addr casts check ss_family before reinterpreting the storage. Third, SockRef in src/sockref.rs (lines 80-111) wraps a ManuallyDrop<Socket> with a correctly scoped lifetime parameter. Every unsafe block carries a // SAFETY: comment. No undocumented or unjustified unsafe block was found (unsafe-safe, unsafe-documented, unsafe-minimal).

The crate does not make network connections, access the filesystem, spawn processes, read environment variables, or use cryptography at the library level. It wraps the OS socket layer: the socket file descriptor is created and used by the caller. This justifies uses-network, uses-filesystem, uses-exec, uses-environment, uses-crypto.

The codebase uses uses-unsafe extensively, all of which is accounted for above. No concurrency primitives are used at the library level (uses-concurrency), no interpreter or JIT is used or embedded (uses-interpreter, uses-jit). No impl-* claims apply: the crate does not implement algorithms (impl-algorithm), data structures (impl-datastructure), parsers (impl-parser), protocols (impl-protocol), cryptography (impl-crypto), interpreters (impl-interpreter), JIT compilers (impl-jit), or concurrency primitives (impl-concurrency).

Unit tests (12 in src/) cover address round-trips and equality. The VCS integration suite (60 tests) covers socket creation, binding, option round-trips, and platform-specific calls (has-unit-tests, has-integration-tests, unsafe-tested). No fuzz or property tests exist (has-fuzz-tests, has-property-tests).

No malicious code, obfuscation, or suspicious behaviour was observed (is-benign).

Conclusion

No findings were recorded. The unsafe surface is large by count but structurally uniform: generic wrappers over getsockopt/setsockopt, well-typed address construction, and FD lifetime management. Each unsafe block carries a SAFETY comment and the invariants appear sound on review. The crate has two platform-conditional dependencies (libc on Unix/WASI, windows-sys on Windows) with no unconditional runtime dependencies.

Findings

No findings.

Annotations(4)

src/sockaddr.rs

src/sockaddr.rs, line 73-213

    pub unsafe fn view_as<T>(&mut self) -> &mut T {
        assert!(size_of::<T>() <= size_of::<Self>());
        // SAFETY: This type is repr(transparent) over `sockaddr_storage` and `T` is one of the
        // `sockaddr_*` types defined by this platform.
        &mut *(self as *mut Self as *mut T)
    }
}

impl std::fmt::Debug for SockAddrStorage {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("sockaddr_storage")
            .field("ss_family", &self.storage.ss_family)
            .finish_non_exhaustive()
    }
}

/// The address of a socket.
///
/// `SockAddr`s may be constructed directly to and from the standard library
/// [`SocketAddr`], [`SocketAddrV4`], and [`SocketAddrV6`] types.
#[derive(Clone)]
pub struct SockAddr {
    storage: sockaddr_storage,
    len: socklen_t,
}

#[allow(clippy::len_without_is_empty)]
impl SockAddr {
    /// Create a `SockAddr` from the underlying storage and its length.
    ///
    /// # Safety
    ///
    /// Caller must ensure that the address family and length match the type of
    /// storage address. For example if `storage.ss_family` is set to `AF_INET`
    /// the `storage` must be initialised as `sockaddr_in`, setting the content
    /// and length appropriately.
    ///
    /// # Examples
    ///
    /// ```
    /// # fn main() -> std::io::Result<()> {
    /// # #[cfg(unix)] {
    /// use std::io;
    /// use std::os::fd::AsRawFd;
    ///
    /// use socket2::{SockAddr, SockAddrStorage, Socket, Domain, Type};
    ///
    /// let socket = Socket::new(Domain::IPV4, Type::STREAM, None)?;
    ///
    /// // Initialise a `SocketAddr` by calling `getsockname(2)`.
    /// let mut addr_storage = SockAddrStorage::zeroed();
    /// let mut len = addr_storage.size_of();
    ///
    /// // The `getsockname(2)` system call will initialize `storage` for
    /// // us, setting `len` to the correct length.
    /// let res = unsafe {
    ///     libc::getsockname(
    ///         socket.as_raw_fd(),
    ///         addr_storage.view_as(),
    ///         &mut len,
    ///     )
    /// };
    /// if res == -1 {
    ///     return Err(io::Error::last_os_error());
    /// }
    ///
    /// let address = unsafe { SockAddr::new(addr_storage, len) };
    /// # drop(address);
    /// # }
    /// # Ok(())
    /// # }
    /// ```
    pub const unsafe fn new(storage: SockAddrStorage, len: socklen_t) -> SockAddr {
        SockAddr {
            storage: storage.storage,
            len: len as socklen_t,
        }
    }

    /// Initialise a `SockAddr` by calling the function `init`.
    ///
    /// The type of the address storage and length passed to the function `init`
    /// is OS/architecture specific.
    ///
    /// The address is zeroed before `init` is called and is thus valid to
    /// dereference and read from. The length initialised to the maximum length
    /// of the storage.
    ///
    /// # Safety
    ///
    /// Caller must ensure that the address family and length match the type of
    /// storage address. For example if `storage.ss_family` is set to `AF_INET`
    /// the `storage` must be initialised as `sockaddr_in`, setting the content
    /// and length appropriately.
    ///
    /// # Examples
    ///
    /// ```
    /// # fn main() -> std::io::Result<()> {
    /// # #[cfg(unix)] {
    /// use std::io;
    /// use std::os::fd::AsRawFd;
    ///
    /// use socket2::{SockAddr, Socket, Domain, Type};
    ///
    /// let socket = Socket::new(Domain::IPV4, Type::STREAM, None)?;
    ///
    /// // Initialise a `SocketAddr` by calling `getsockname(2)`.
    /// let (_, address) = unsafe {
    ///     SockAddr::try_init(|addr_storage, len| {
    ///         // The `getsockname(2)` system call will initialize `storage` for
    ///         // us, setting `len` to the correct length.
    ///         if libc::getsockname(socket.as_raw_fd(), addr_storage.cast(), len) == -1 {
    ///             Err(io::Error::last_os_error())
    ///         } else {
    ///             Ok(())
    ///         }
    ///     })
    /// }?;
    /// # drop(address);
    /// # }
    /// # Ok(())
    /// # }
    /// ```
    pub unsafe fn try_init<F, T>(init: F) -> io::Result<(T, SockAddr)>
    where
        F: FnOnce(*mut SockAddrStorage, *mut socklen_t) -> io::Result<T>,
    {
        const STORAGE_SIZE: socklen_t = size_of::<sockaddr_storage>() as socklen_t;
        // NOTE: `SockAddr::unix` depends on the storage being zeroed before
        // calling `init`.
        // NOTE: calling `recvfrom` with an empty buffer also depends on the
        // storage being zeroed before calling `init` as the OS might not
        // initialise it.
        let mut storage = SockAddrStorage::zeroed();
        let mut len = STORAGE_SIZE;
        init(&mut storage, &mut len).map(|res| {
            debug_assert!(len <= STORAGE_SIZE, "overflown address storage");
            (res, SockAddr::new(storage, len))
        })
    }

SockAddr::new and SockAddr::try_init are unsafe fn requiring the caller to ensure ss_family and length match the actual contents. try_init zeroes the storage first (important for recvfrom with zero-length buffers and for Unix sockets), then delegates to a caller-supplied closure. Both functions have clear, accurate safety contracts in their doc comments. as_socket checks ss_family before casting to sockaddr_in / sockaddr_in6; uses ptr::addr_of! to avoid creating references to potentially unaligned/uninit memory. All address-family checks precede the casts. SockAddrStorage::view_as asserts size_of::<T>() <= size_of::<Self>() before the pointer cast.

Supports unsafe-safe, unsafe-documented.

src/sockaddr.rs, line 469-653

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ipv4() {
        use std::net::Ipv4Addr;
        let std = SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 9876);
        let addr = SockAddr::from(std);
        assert!(addr.is_ipv4());
        assert!(!addr.is_ipv6());
        #[cfg(not(target_os = "wasi"))]
        assert!(!addr.is_unix());
        assert_eq!(addr.family(), AF_INET as sa_family_t);
        assert_eq!(addr.domain(), Domain::IPV4);
        assert_eq!(addr.len(), size_of::<sockaddr_in>() as socklen_t);
        assert_eq!(addr.as_socket(), Some(SocketAddr::V4(std)));
        assert_eq!(addr.as_socket_ipv4(), Some(std));
        assert!(addr.as_socket_ipv6().is_none());

        let addr = SockAddr::from(SocketAddr::from(std));
        assert_eq!(addr.family(), AF_INET as sa_family_t);
        assert_eq!(addr.len(), size_of::<sockaddr_in>() as socklen_t);
        assert_eq!(addr.as_socket(), Some(SocketAddr::V4(std)));
        assert_eq!(addr.as_socket_ipv4(), Some(std));
        assert!(addr.as_socket_ipv6().is_none());
        #[cfg(all(unix, not(target_os = "wasi")))]
        {
            assert!(addr.as_pathname().is_none());
            assert!(addr.as_abstract_namespace().is_none());
        }
    }

    #[test]
    fn ipv6() {
        use std::net::Ipv6Addr;
        let std = SocketAddrV6::new(Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8), 9876, 11, 12);
        let addr = SockAddr::from(std);
        assert!(addr.is_ipv6());
        assert!(!addr.is_ipv4());
        #[cfg(not(target_os = "wasi"))]
        assert!(!addr.is_unix());
        assert_eq!(addr.family(), AF_INET6 as sa_family_t);
        assert_eq!(addr.domain(), Domain::IPV6);
        assert_eq!(addr.len(), size_of::<sockaddr_in6>() as socklen_t);
        assert_eq!(addr.as_socket(), Some(SocketAddr::V6(std)));
        assert!(addr.as_socket_ipv4().is_none());
        assert_eq!(addr.as_socket_ipv6(), Some(std));

        let addr = SockAddr::from(SocketAddr::from(std));
        assert_eq!(addr.family(), AF_INET6 as sa_family_t);
        assert_eq!(addr.len(), size_of::<sockaddr_in6>() as socklen_t);
        assert_eq!(addr.as_socket(), Some(SocketAddr::V6(std)));
        assert!(addr.as_socket_ipv4().is_none());
        assert_eq!(addr.as_socket_ipv6(), Some(std));
        #[cfg(all(unix, not(target_os = "wasi")))]
        {
            assert!(addr.as_pathname().is_none());
            assert!(addr.as_abstract_namespace().is_none());
        }
    }

    #[test]
    fn ipv4_eq() {
        use std::net::Ipv4Addr;

        let std1 = SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 9876);
        let std2 = SocketAddrV4::new(Ipv4Addr::new(5, 6, 7, 8), 8765);

        test_eq(
            SockAddr::from(std1),
            SockAddr::from(std1),
            SockAddr::from(std2),
        );
    }

    #[test]
    fn ipv4_hash() {
        use std::net::Ipv4Addr;

        let std1 = SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 9876);
        let std2 = SocketAddrV4::new(Ipv4Addr::new(5, 6, 7, 8), 8765);

        test_hash(
            SockAddr::from(std1),
            SockAddr::from(std1),
            SockAddr::from(std2),
        );
    }

    #[test]
    fn ipv6_eq() {
        use std::net::Ipv6Addr;

        let std1 = SocketAddrV6::new(Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8), 9876, 11, 12);
        let std2 = SocketAddrV6::new(Ipv6Addr::new(3, 4, 5, 6, 7, 8, 9, 0), 7654, 13, 14);

        test_eq(
            SockAddr::from(std1),
            SockAddr::from(std1),
            SockAddr::from(std2),
        );
    }

    #[test]
    fn ipv6_hash() {
        use std::net::Ipv6Addr;

        let std1 = SocketAddrV6::new(Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8), 9876, 11, 12);
        let std2 = SocketAddrV6::new(Ipv6Addr::new(3, 4, 5, 6, 7, 8, 9, 0), 7654, 13, 14);

        test_hash(
            SockAddr::from(std1),
            SockAddr::from(std1),
            SockAddr::from(std2),
        );
    }

    #[test]
    fn ipv4_ipv6_eq() {
        use std::net::Ipv4Addr;
        use std::net::Ipv6Addr;

        let std1 = SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 9876);
        let std2 = SocketAddrV6::new(Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8), 9876, 11, 12);

        test_eq(
            SockAddr::from(std1),
            SockAddr::from(std1),
            SockAddr::from(std2),
        );

        test_eq(
            SockAddr::from(std2),
            SockAddr::from(std2),
            SockAddr::from(std1),
        );
    }

    #[test]
    fn ipv4_ipv6_hash() {
        use std::net::Ipv4Addr;
        use std::net::Ipv6Addr;

        let std1 = SocketAddrV4::new(Ipv4Addr::new(1, 2, 3, 4), 9876);
        let std2 = SocketAddrV6::new(Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8), 9876, 11, 12);

        test_hash(
            SockAddr::from(std1),
            SockAddr::from(std1),
            SockAddr::from(std2),
        );

        test_hash(
            SockAddr::from(std2),
            SockAddr::from(std2),
            SockAddr::from(std1),
        );
    }

    #[allow(clippy::eq_op)] // allow a0 == a0 check
    fn test_eq(a0: SockAddr, a1: SockAddr, b: SockAddr) {
        assert!(a0 == a0);
        assert!(a0 == a1);
        assert!(a1 == a0);
        assert!(a0 != b);
        assert!(b != a0);
    }

    fn test_hash(a0: SockAddr, a1: SockAddr, b: SockAddr) {
        assert!(calculate_hash(&a0) == calculate_hash(&a0));
        assert!(calculate_hash(&a0) == calculate_hash(&a1));
        // technically unequal values can have the same hash, in this case x != z and both have different hashes
        assert!(calculate_hash(&a0) != calculate_hash(&b));
    }

    fn calculate_hash(x: &SockAddr) -> u64 {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::Hasher;

        let mut hasher = DefaultHasher::new();
        x.hash(&mut hasher);
        hasher.finish()
    }
}

The package ships 12 #[test] unit tests in src/sockaddr.rs and src/sys/unix.rs covering IPv4/IPv6 address round-trips, equality, and hashing. The VCS repository contains an additional 60-test integration test suite in tests/socket.rs covering socket creation, binding, options, and platform-specific calls. The integration tests are not included in the published crate. The CI configuration (.github/) runs tests on Linux, macOS, Windows, and multiple targets.

Supports has-unit-tests, has-integration-tests, unsafe-tested.

src/socket.rs

src/socket.rs, line 2368-2400

impl Read for Socket {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        // Safety: the `recv` implementation promises not to write uninitialised
        // bytes to the `buf`fer, so this casting is safe.
        let buf = unsafe { &mut *(buf as *mut [u8] as *mut [MaybeUninit<u8>]) };
        self.recv(buf)
    }

    #[cfg(not(any(target_os = "redox", target_os = "wasi")))]
    fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
        // Safety: both `IoSliceMut` and `MaybeUninitSlice` promise to have the
        // same layout, that of `iovec`/`WSABUF`. Furthermore, `recv_vectored`
        // promises to not write uninitialised bytes to the `bufs` and pass it
        // directly to the `recvmsg` system call, so this is safe.
        let bufs = unsafe { &mut *(bufs as *mut [IoSliceMut<'_>] as *mut [MaybeUninitSlice<'_>]) };
        self.recv_vectored(bufs).map(|(n, _)| n)
    }
}

impl<'a> Read for &'a Socket {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        // Safety: see other `Read::read` impl.
        let buf = unsafe { &mut *(buf as *mut [u8] as *mut [MaybeUninit<u8>]) };
        self.recv(buf)
    }

    #[cfg(not(any(target_os = "redox", target_os = "wasi")))]
    fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
        // Safety: see other `Read::read` impl.
        let bufs = unsafe { &mut *(bufs as *mut [IoSliceMut<'_>] as *mut [MaybeUninitSlice<'_>]) };
        self.recv_vectored(bufs).map(|(n, _)| n)
    }
}

The Read::read impl casts &mut [u8] to &mut [MaybeUninit<u8>] using an unsafe pointer cast. The safety comment explains the invariant: recv (and the underlying libc::recv) will write valid bytes, so treating the already-valid buffer as MaybeUninit is sound. The read_vectored impl similarly casts IoSliceMut slices to MaybeUninitSlice slices, which have the same iovec representation. The doc comment acknowledges this.

Supports unsafe-safe, unsafe-documented.

src/sockref.rs

src/sockref.rs, line 80-111

#[cfg(any(unix, all(target_os = "wasi", not(target_env = "p1"))))]
impl<'s, S> From<&'s S> for SockRef<'s>
where
    S: AsFd,
{
    /// The caller must ensure `S` is actually a socket.
    fn from(socket: &'s S) -> Self {
        let fd = socket.as_fd().as_raw_fd();
        assert!(fd >= 0);
        SockRef {
            socket: ManuallyDrop::new(unsafe { Socket::from_raw_fd(fd) }),
            _lifetime: PhantomData,
        }
    }
}

/// On Unix, a corresponding `From<&impl AsFd>` implementation exists.
#[cfg(windows)]
impl<'s, S> From<&'s S> for SockRef<'s>
where
    S: AsSocket,
{
    /// See the `From<&impl AsFd>` implementation.
    fn from(socket: &'s S) -> Self {
        let socket = socket.as_socket().as_raw_socket();
        assert!(socket != windows_sys::Win32::Networking::WinSock::INVALID_SOCKET as _);
        SockRef {
            socket: ManuallyDrop::new(unsafe { Socket::from_raw_socket(socket) }),
            _lifetime: PhantomData,
        }
    }
}

SockRef wraps a ManuallyDrop<Socket> so the FD is not closed when the reference is dropped. On Unix, the From impl asserts fd >= 0 before calling Socket::from_raw_fd. On Windows, it asserts the SOCKET is not INVALID_SOCKET. The _lifetime: PhantomData<&'s Socket> ties the reference lifetime to the source socket, preventing use-after-free. The lifetime is sound.

Supports unsafe-safe.

src/sys/unix.rs

src/sys/unix.rs, line 1329-1363

/// Caller must ensure `T` is the correct type for `opt` and `val`.
pub(crate) unsafe fn getsockopt<T>(fd: RawSocket, opt: c_int, val: c_int) -> io::Result<T> {
    let mut payload: MaybeUninit<T> = MaybeUninit::uninit();
    let mut len = size_of::<T>() as libc::socklen_t;
    syscall!(getsockopt(
        fd,
        opt,
        val,
        payload.as_mut_ptr().cast(),
        &mut len,
    ))
    .map(|_| {
        debug_assert_eq!(len as usize, size_of::<T>());
        // Safety: `getsockopt` initialised `payload` for us.
        payload.assume_init()
    })
}

/// Caller must ensure `T` is the correct type for `opt` and `val`.
pub(crate) unsafe fn setsockopt<T>(
    fd: RawSocket,
    opt: c_int,
    val: c_int,
    payload: T,
) -> io::Result<()> {
    let payload = ptr::addr_of!(payload).cast();
    syscall!(setsockopt(
        fd,
        opt,
        val,
        payload,
        mem::size_of::<T>() as libc::socklen_t,
    ))
    .map(|_| ())
}

The getsockopt and setsockopt generic wrappers are the primary unsafe surface. Both are declared unsafe fn with a caller contract: the type parameter T must match the level/optname pair. The implementations are correct: getsockopt initialises a MaybeUninit<T>, passes its pointer and size_of::<T>() as the out-length, then calls assume_init() after the syscall returns. A debug_assert_eq! checks that the kernel's returned length equals size_of::<T>(). setsockopt passes addr_of!(payload) to avoid alignment violations. Every call site in socket.rs and sys/unix.rs passes a correctly typed value (e.g. c_int for boolean options, ip_mreq for multicast join, linger for linger options). No type confusion was found.

Justifies uses-unsafe, unsafe-safe, unsafe-documented, unsafe-minimal.