cargo / libredox / audit
cargo : libredox @ 0.1.17
PE Patrick Elsen signed 2026-06-02 published 2026-06-02

Claims

filesystem-safehas-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

libredox 0.1.17 is a thin Rust wrapper around the Redox kernel's stable C ABI: ~37 extern "C" syscalls (FDs, processes, signals, namespaces, mmap, time) plus a protocol module of call-number enums and C-layout structs. The crate matches its upstream commit, ships no binaries, build script, or proc-macro, and contains no network, exec, env, crypto, or concurrency code. Three low-severity findings: two unsafe helpers lack safety comments, and no test suite ships.

Report

Subject

libredox is a thin Rust wrapper around the Redox kernel's stable C ABI. It exposes ~37 redox_*_v1/v0 system calls as Rust functions (file descriptors, processes, signals, namespaces, memory mapping, time), along with a Redox-specific Error type, re-exports of Redox-relevant libc flag and errno constants, and a protocol module that defines the call-number enums (ProcCall, ThreadCall, SocketCall, FsCall) and shared C-layout structs (ProcMeta, RtSigInfo) used by Redox's per-process scheme servers.

The single source file (src/lib.rs, 1089 lines) is split into feature-gated modules (base, call, std, redox_syscall, protocol, mkns). The default feature set enables base, call, std, redox_syscall, and protocol.

Methodology

The published crate was downloaded by openvet audit new and unpacked under contents/; the upstream Git repository (https://gitlab.redox-os.org/redox-os/libredox.git) was cloned and checked out at the commit recorded in .cargo_vcs_info.json (7040cf7) under vcs/.

Tools used:

  • openvet audit (workspace creation, annotations, claims, findings, dependency narratives, report).
  • diff -r (GNU diffutils 3.x) to compare contents/ against vcs/.
  • grep to count unsafe occurrences and locate #[test]/#[cfg(test)] markers.
  • wc -l for line counts.
  • git log to survey upstream history at and before the audited tag.

The whole of src/lib.rs was read. Both Cargo.toml and Cargo.toml.orig were read. The LICENSE, .gitignore, .cargo_vcs_info.json, and Cargo.lock files were inspected. The upstream tree was enumerated to confirm absence of tests, build scripts, and CI configuration.

Results

The comparison between the published crate and the upstream Git commit shows that src/lib.rs and LICENSE match byte-for-byte. Cargo.toml differs only in cargo's standard normalisation (header banner, key reordering, alphabetisation, inline-table to table-section expansion); Cargo.toml.orig preserves the original manifest. Cargo.lock and .cargo_vcs_info.json are cargo-generated.

The crate ships no binary artefacts (justifying has-binaries), no build.rs (justifying has-build-exec), no installer hook (justifying has-install-exec), and no proc-macro entry (the [lib] section sets only name and path). It contains no calls into std::process, std::net, std::env, or any cryptographic crate, justifying uses-exec, uses-network, uses-environment, uses-crypto, uses-jit, and uses-interpreter. No threads are spawned and no async runtime, channel, lock, or atomic-beyond-language-primitive is used, justifying uses-concurrency. The crate does not itself implement any parser, interpreter, JIT, protocol, data structure, algorithm, cryptographic primitive, or concurrency primitive — the protocol module defines call-number enums and C-layout structs but does not implement the protocols themselves — justifying impl-parser, impl-interpreter, impl-jit, impl-protocol, impl-datastructure, impl-algorithm, impl-crypto, and impl-concurrency.

The crate is FFI-heavy: ~48 unsafe occurrences across one extern "C" declaration block (src/lib.rs:232-324) and the per-syscall wrappers in the call module (src/lib.rs:463-796), justifying uses-unsafe. The wrappers are uniformly thin: each forwards arguments to one extern function and runs the return value through Error::demux. mmap/munmap are appropriately marked pub unsafe fn; all other wrappers are safe. The Fd::drop impl closes the descriptor via the same FFI path. The unsafe code is sound modulo the assumption that the Redox kernel honours its documented ABI, supporting unsafe-safe and unsafe-minimal. The file-descriptor wrappers pass paths through to the kernel verbatim and add no traversal vector of their own, supporting filesystem-safe alongside uses-filesystem.

Three findings were recorded, all low-severity. FINDING-1 documents that data::timespec_from_bytes/timespec_from_mut_bytes (src/lib.rs:200-225) expose safe wrappers over raw-pointer casts without safety comments — the cast is sound (libc::timespec accepts every bit pattern at its size and alignment) but the soundness argument is not written down. FINDING-2 documents that call::strerror (src/lib.rs:748-763) uses core::str::from_utf8_unchecked on the kernel-filled buffer; sound only because the kernel is trusted to return UTF-8, again without a safety comment. Both are the basis for unsafe-documented = false. FINDING-3 records the absence of a test suite — no unit, integration, fuzz, or property tests in either the crate or the upstream repository, supporting unsafe-tested = false, has-unit-tests = false, has-integration-tests = false, has-fuzz-tests = false, and has-property-tests = false.

A code-level review of the wrappers found no behaviour at odds with what a syscall wrapper should do: no environment scraping, no hidden network calls, no obfuscated payloads, no target-conditional code beyond #[cfg(target_os = "redox")] re-exports of platform constants. This is the basis for is-benign.

Conclusion

libredox is what its description claims: a thin, focused wrapper around the Redox stable kernel ABI. The unsafe surface is necessary FFI and is uniformly small per call site. The three findings are all low-severity and concern documentation and test coverage rather than behavioural correctness or safety. No malicious code or supply-chain anomaly was observed; the crate matches its upstream source.

Findings(3)

FINDING-1 quality low

timespec byte-cast helpers expose unsafe-equivalent APIs without safety documentation

data::timespec_from_bytes (src/lib.rs:210) and data::timespec_from_mut_bytes (src/lib.rs:201) reinterpret a byte slice as a TimeSpec (libc::timespec) via a raw-pointer cast inside an unsafe block. Both functions are exposed as safe APIs, so callers do not see the unsafe boundary.

The conversion is in fact sound: libc::timespec is a repr(C) struct of two integer fields and every bit pattern is valid for it. The asserts ensure the slice meets size and alignment requirements before the cast.

What is missing is a comment explaining why the cast is sound. The rest of the crate's unsafe blocks are similarly bare. This is the basis for unsafe-documented = false. Both functions are marked with a // TODO: Remove comment, suggesting they are slated for removal.

FINDING-2 safety low

strerror uses from_utf8_unchecked on kernel-provided bytes without safety documentation

call::strerror (src/lib.rs:749) builds a &str from the buffer that redox_strerror_v1 fills, via core::str::from_utf8_unchecked. Soundness depends on the kernel symbol returning valid UTF-8 in the filled prefix of the buffer.

If the kernel ever returned non-UTF-8 bytes, the resulting &str would be UB. The function is exposed as a safe API and the unsafety is not visible to the caller. The unchecked path is not justified by a safety comment.

This is consistent with the rest of the crate, which treats the Redox kernel as a trusted source of well-formed data. A safety comment and/or a switch to checked UTF-8 conversion (with a fallback) would make the soundness argument explicit.

FINDING-3 quality low

No test suite ships with the crate

The published crate and the upstream VCS repository contain no tests: no #[test] items in src/lib.rs, no tests/ directory, no benches/ directory, no CI configuration. The crate is a thin FFI wrapper, so most of its surface is not directly testable on non-Redox hosts, but the small amount of pure Rust logic — Error::mux/Error::demux (src/lib.rs:33-48), the wif* / w*sig status decoders (src/lib.rs:949-986), and the ProcKillTarget::from_raw/raw round-trip (src/lib.rs:994-1014) — could be unit-tested without a Redox kernel.

Absence of tests is the basis for unsafe-tested = false, has-unit-tests = false, has-integration-tests = false, has-fuzz-tests = false, and has-property-tests = false.

Annotations(1)

src/lib.rs

src/lib.rs, line 232-324

extern "C" {
    // NOTE: Although there are version suffixes, there'd have to be strong reasons for adding new
    // version.
    fn redox_open_v1(path_base: *const u8, path_len: usize, flags: u32, mode: u16) -> RawResult;
    fn redox_openat_v1(
        fd: usize,
        buf: *const u8,
        path_len: usize,
        flags: u32,
        fcntl_flags: u32,
    ) -> RawResult;
    fn redox_dup_v1(fd: usize, buf: *const u8, len: usize) -> RawResult;
    fn redox_dup2_v1(old_fd: usize, new_fd: usize, buf: *const u8, len: usize) -> RawResult;
    fn redox_read_v1(fd: usize, dst_base: *mut u8, dst_len: usize) -> RawResult;
    fn redox_write_v1(fd: usize, src_base: *const u8, src_len: usize) -> RawResult;
    fn redox_fchmod_v1(fd: usize, new_mode: u16) -> RawResult;
    fn redox_fchown_v1(fd: usize, new_uid: u32, new_gid: u32) -> RawResult;
    fn redox_getdents_v0(fd: usize, buf: *mut u8, buf_len: usize, opaque: u64) -> RawResult;
    fn redox_fstat_v1(fd: usize, dst: *mut data::Stat) -> RawResult;
    fn redox_fstatvfs_v1(fd: usize, dst: *mut data::StatVfs) -> RawResult;
    fn redox_fsync_v1(fd: usize) -> RawResult;
    fn redox_fdatasync_v1(fd: usize) -> RawResult;
    fn redox_ftruncate_v0(fd: usize, len: usize) -> RawResult;
    fn redox_futimens_v1(fd: usize, times: *const data::TimeSpec) -> RawResult;
    /* TODO: Support unlinkat using std_fs_call
    fn redox_unlinkat_v0(fd: usize, buf: *const u8, path_len: usize, flags: u32) -> RawResult;
    */
    fn redox_fpath_v1(fd: usize, dst_base: *mut u8, dst_len: usize) -> RawResult;
    fn redox_relpathat_v0(dirfd: usize, fd: usize, dst_base: *mut u8, dst_len: usize) -> RawResult;
    fn redox_close_v1(fd: usize) -> RawResult;

    // NOTE: While the Redox kernel currently doesn't distinguish between threads and processes,
    // the return value of this function is expected to be treated as a process ID and not a thread
    // ID.
    fn redox_get_pid_v1() -> RawResult;

    fn redox_get_euid_v1() -> RawResult;
    fn redox_get_ruid_v1() -> RawResult;
    fn redox_get_egid_v1() -> RawResult;
    fn redox_get_rgid_v1() -> RawResult;

    fn redox_get_ens_v0() -> RawResult;
    fn redox_get_ns_v0() -> RawResult;
    // This function is used to get the credentials, pid, euid, egid etc. of the process with the target pid.
    fn redox_get_proc_credentials_v1(cap_fd: usize, target_pid: usize, buf: &mut [u8])
        -> RawResult;

    fn redox_setrens_v1(rns: usize, ens: usize) -> RawResult;
    fn redox_mkns_v1(names: *const data::IoVec, num_names: usize, _flags: u32) -> RawResult;

    fn redox_kill_v1(pid: usize, signal: u32) -> RawResult;
    fn redox_waitpid_v1(pid: usize, status: *mut i32, options: u32) -> RawResult;

    fn redox_sigprocmask_v1(how: u32, new: *const u64, old: *mut u64) -> RawResult;
    fn redox_sigaction_v1(
        signal: u32,
        new: *const data::SigAction,
        old: *mut data::SigAction,
    ) -> RawResult;

    fn redox_clock_gettime_v1(clock: usize, ts: *mut data::TimeSpec) -> RawResult;

    fn redox_mmap_v1(
        addr: *mut (),
        unaligned_len: usize,
        prot: u32,
        flags: u32,
        fd: usize,
        offset: u64,
    ) -> RawResult;
    fn redox_munmap_v1(addr: *mut (), unaligned_len: usize) -> RawResult;

    fn redox_strerror_v1(dst: *mut u8, dst_len: *mut usize, error: u32) -> RawResult;

    fn redox_sys_call_v0(
        fd: usize,
        payload: *mut u8,
        payload_len: usize,
        flags: usize,
        metadata: *const u64,
        metadata_len: usize,
    ) -> RawResult;
    fn redox_get_socket_token_v0(fd: usize, payload: *mut u8, payload_len: usize) -> RawResult;

    fn redox_setns_v0(fd: usize) -> RawResult;

    fn redox_register_scheme_to_ns_v0(
        ns_fd: usize,
        name_base: *const u8,
        name_len: usize,
        cap_fd: usize,
    ) -> RawResult;
}

Block of extern "C" declarations for the redox kernel ABI: ~37 syscalls covering FDs (open, read, write, close, dup, stat), processes (waitpid, kill, pids, credentials), signals (sigaction, sigprocmask), namespaces (setns, mkns, register_scheme_to_ns), memory (mmap, munmap), and time (clock_gettime). Each wrapper below this block calls one of these symbols inside unsafe. Justifies uses-unsafe.

src/lib.rs, line 463-796

pub mod call {
    use core::mem::MaybeUninit;

    use super::*;

    /// flags and mode are binary compatible with libc
    #[inline]
    pub fn open(path: impl AsRef<str>, flags: i32, mode: u16) -> Result<usize> {
        let path = path.as_ref();
        Ok(Error::demux(unsafe {
            redox_open_v1(path.as_ptr(), path.len(), flags as u32, mode)
        })?)
    }
    #[inline]
    pub fn openat(
        fd: usize,
        path: impl AsRef<[u8]>,
        flags: i32,
        fcntl_flags: u32,
    ) -> Result<usize> {
        let path = path.as_ref();
        Ok(Error::demux(unsafe {
            redox_openat_v1(fd, path.as_ptr(), path.len(), flags as u32, fcntl_flags)
        })?)
    }
    #[inline]
    pub fn dup(fd: usize, buf: impl AsRef<[u8]>) -> Result<usize> {
        let buf = buf.as_ref();
        Ok(Error::demux(unsafe {
            redox_dup_v1(fd, buf.as_ptr(), buf.len())
        })?)
    }
    #[inline]
    pub fn dup2(old_fd: usize, new_fd: usize, buf: impl AsRef<[u8]>) -> Result<usize> {
        let buf = buf.as_ref();
        Ok(Error::demux(unsafe {
            redox_dup2_v1(old_fd, new_fd, buf.as_ptr(), buf.len())
        })?)
    }
    #[inline]
    pub fn read(raw_fd: usize, buf: &mut [u8]) -> Result<usize> {
        Ok(Error::demux(unsafe {
            redox_read_v1(raw_fd, buf.as_mut_ptr(), buf.len())
        })?)
    }
    #[inline]
    pub fn write(raw_fd: usize, buf: &[u8]) -> Result<usize> {
        Error::demux(unsafe { redox_write_v1(raw_fd, buf.as_ptr(), buf.len()) })
    }
    #[inline]
    pub fn fchmod(raw_fd: usize, new_mode: u16) -> Result<()> {
        Error::demux(unsafe { redox_fchmod_v1(raw_fd, new_mode) })?;
        Ok(())
    }
    #[inline]
    pub fn fchown(raw_fd: usize, new_uid: u32, new_gid: u32) -> Result<()> {
        Error::demux(unsafe { redox_fchown_v1(raw_fd, new_uid, new_gid) })?;
        Ok(())
    }
    #[inline]
    pub fn getdents(fd: usize, buf: &mut [u8], opaque: u64) -> Result<usize> {
        Error::demux(unsafe { redox_getdents_v0(fd, buf.as_mut_ptr(), buf.len(), opaque) })
    }
    #[inline]
    pub fn fstat(raw_fd: usize) -> Result<data::Stat> {
        unsafe {
            let mut ret = MaybeUninit::uninit();
            Error::demux(redox_fstat_v1(raw_fd, ret.as_mut_ptr()))?;
            Ok(ret.assume_init())
        }
    }
    #[inline]
    pub fn fstatvfs(raw_fd: usize) -> Result<data::StatVfs> {
        unsafe {
            let mut ret = MaybeUninit::uninit();
            Error::demux(redox_fstatvfs_v1(raw_fd, ret.as_mut_ptr()))?;
            Ok(ret.assume_init())
        }
    }
    #[inline]
    pub fn fsync(raw_fd: usize) -> Result<()> {
        Error::demux(unsafe { redox_fsync_v1(raw_fd) }).map(|_| ())
    }
    #[inline]
    pub fn fdatasync(raw_fd: usize) -> Result<()> {
        Error::demux(unsafe { redox_fdatasync_v1(raw_fd) }).map(|_| ())
    }
    #[inline]
    pub fn ftruncate(raw_fd: usize, new_size: usize) -> Result<()> {
        Error::demux(unsafe { redox_ftruncate_v0(raw_fd, new_size) }).map(|_| ())
    }
    #[inline]
    pub fn futimens(raw_fd: usize, times: &[data::TimeSpec; 2]) -> Result<()> {
        Error::demux(unsafe { redox_futimens_v1(raw_fd, times.as_ptr()) })?;
        Ok(())
    }
    /* TODO: Support unlinkat using std_fs_call
    #[inline]
    pub fn unlinkat(fd: usize, path: impl AsRef<[u8]>, flags: i32) -> Result<()> {
        let path = path.as_ref();
        Error::demux(unsafe { redox_unlinkat_v0(fd, path.as_ptr(), path.len(), flags as u32) })
            .map(|_| ())
    }
    */
    #[inline]
    pub fn fpath(raw_fd: usize, buf: &mut [u8]) -> Result<usize> {
        Error::demux(unsafe { redox_fpath_v1(raw_fd, buf.as_mut_ptr(), buf.len()) })
    }
    #[inline]
    pub fn relpathat(raw_fd: usize, fd: usize, buf: &mut [u8]) -> Result<usize> {
        Error::demux(unsafe { redox_relpathat_v0(raw_fd, fd, buf.as_mut_ptr(), buf.len()) })
    }
    #[inline]
    pub fn close(raw_fd: usize) -> Result<()> {
        Error::demux(unsafe { redox_close_v1(raw_fd) })?;
        Ok(())
    }
    #[cfg(feature = "redox_syscall")]
    #[inline]
    pub fn call_ro(
        fd: usize,
        payload: &mut [u8],
        flags: syscall::CallFlags,
        metadata: &[u64],
    ) -> Result<usize> {
        Ok(Error::demux(unsafe {
            redox_sys_call_v0(
                fd,
                payload.as_mut_ptr(),
                payload.len(),
                (flags | syscall::CallFlags::READ).bits(),
                metadata.as_ptr(),
                metadata.len(),
            )
        })?)
    }
    #[cfg(feature = "redox_syscall")]
    #[inline]
    pub fn call_wo(
        fd: usize,
        payload: &[u8],
        flags: syscall::CallFlags,
        metadata: &[u64],
    ) -> Result<usize> {
        Ok(Error::demux(unsafe {
            redox_sys_call_v0(
                fd,
                payload.as_ptr() as *mut u8,
                payload.len(),
                (flags | syscall::CallFlags::WRITE).bits(),
                metadata.as_ptr(),
                metadata.len(),
            )
        })?)
    }
    #[cfg(feature = "redox_syscall")]
    #[inline]
    pub fn call_rw(
        fd: usize,
        payload: &mut [u8],
        flags: syscall::CallFlags,
        metadata: &[u64],
    ) -> Result<usize> {
        Ok(Error::demux(unsafe {
            redox_sys_call_v0(
                fd,
                payload.as_mut_ptr(),
                payload.len(),
                (flags | syscall::CallFlags::READ | syscall::CallFlags::WRITE).bits(),
                metadata.as_ptr(),
                metadata.len(),
            )
        })?)
    }

    #[inline]
    pub fn geteuid() -> Result<usize> {
        Error::demux(unsafe { redox_get_euid_v1() })
    }

    #[inline]
    pub fn getruid() -> Result<usize> {
        Error::demux(unsafe { redox_get_ruid_v1() })
    }

    #[inline]
    pub fn getegid() -> Result<usize> {
        Error::demux(unsafe { redox_get_egid_v1() })
    }
    #[inline]
    pub fn getrgid() -> Result<usize> {
        Error::demux(unsafe { redox_get_rgid_v1() })
    }
    #[inline]
    pub fn getpid() -> Result<usize> {
        Error::demux(unsafe { redox_get_pid_v1() })
    }
    #[inline]
    pub fn getens() -> Result<usize> {
        Error::demux(unsafe { redox_get_ens_v0() })
    }

    #[inline]
    // [u8; size_of::<crate::protocol::ProcMeta>()]
    pub fn get_proc_credentials(cap_fd: usize, target_pid: usize, buf: &mut [u8]) -> Result<usize> {
        Error::demux(unsafe { redox_get_proc_credentials_v1(cap_fd, target_pid, buf) })
    }

    #[inline]
    pub fn setrens(rns: usize, ens: usize) -> Result<usize> {
        Error::demux(unsafe { redox_setrens_v1(rns, ens) })
    }
    #[inline]
    pub fn waitpid(pid: usize, status: &mut i32, options: i32) -> Result<usize> {
        Error::demux(unsafe { redox_waitpid_v1(pid, status as *mut i32, options as u32) })
    }
    #[inline]
    pub fn kill(pid: usize, signal: u32) -> Result<()> {
        Error::demux(unsafe { redox_kill_v1(pid, signal) }).map(|_| ())
    }
    #[inline]
    pub fn clock_gettime(clock: i32) -> Result<data::TimeSpec> {
        unsafe {
            let mut ret = MaybeUninit::uninit();
            Error::demux(redox_clock_gettime_v1(clock as usize, ret.as_mut_ptr()))?;
            Ok(ret.assume_init())
        }
    }
    #[inline]
    pub fn sigprocmask(
        how: i32,
        newmask: Option<&data::SigSet>,
        oldmask: Option<&mut data::SigSet>,
    ) -> Result<()> {
        Error::demux(unsafe {
            redox_sigprocmask_v1(
                how as u32,
                newmask.map_or(core::ptr::null(), |m| m),
                oldmask.map_or(core::ptr::null_mut(), |m| m),
            )
        })
        .map(|_| ())
    }
    #[inline]
    pub fn sigaction(
        signal: i32,
        newact: Option<&data::SigAction>,
        oldact: Option<&mut data::SigAction>,
    ) -> Result<()> {
        Error::demux(unsafe {
            redox_sigaction_v1(
                signal as u32,
                newact.map_or(core::ptr::null(), |m| m),
                oldact.map_or(core::ptr::null_mut(), |m| m),
            )
        })
        .map(|_| ())
    }

    #[derive(Clone, Copy, Debug)]
    pub struct MmapArgs {
        pub addr: *mut (),
        pub length: usize,
        pub prot: u32,
        pub flags: u32,
        pub fd: usize,
        pub offset: u64,
    }
    #[inline]
    pub unsafe fn mmap(args: MmapArgs) -> Result<*mut ()> {
        Error::demux(redox_mmap_v1(
            args.addr,
            args.length,
            args.prot,
            args.flags,
            args.fd,
            args.offset,
        ))
        .map(|addr| addr as *mut ())
    }
    #[inline]
    pub unsafe fn munmap(addr: *mut (), length: usize) -> Result<()> {
        Error::demux(redox_munmap_v1(addr, length)).map(|_| ())
    }

    #[inline]
    pub fn strerror(error: u16, desc: &mut [u8]) -> Option<(&str, usize)> {
        unsafe {
            let mut len_inout = desc.len();
            let copied_len = Error::demux(redox_strerror_v1(
                desc.as_mut_ptr(),
                &mut len_inout,
                error.into(),
            ))
            .ok()?;
            Some((
                core::str::from_utf8_unchecked(&desc[..copied_len]),
                len_inout,
            ))
        }
    }

    #[inline]
    #[cfg(feature = "mkns")]
    pub fn mkns(names: &[ioslice::IoSlice]) -> Result<usize> {
        // no-op
        let iovecs = ioslice::IoSlice::cast_to_raw_iovecs(names);

        unsafe { Error::demux(redox_mkns_v1(iovecs.as_ptr(), iovecs.len(), 0)) }
    }

    #[inline]
    pub fn get_socket_token(fd: usize, buf: &mut [u8]) -> Result<usize> {
        Error::demux(unsafe { redox_get_socket_token_v0(fd, buf.as_mut_ptr(), buf.len()) })
    }

    #[inline]
    pub fn setns(fd: usize) -> Result<usize> {
        Error::demux(unsafe { redox_setns_v0(fd) })
    }
    #[inline]
    pub fn getns() -> Result<usize> {
        Error::demux(unsafe { redox_get_ns_v0() })
    }

    #[inline]
    pub fn register_scheme_to_ns(ns_fd: usize, name: impl AsRef<str>, cap_fd: usize) -> Result<()> {
        let name = name.as_ref();
        Error::demux(unsafe {
            redox_register_scheme_to_ns_v0(ns_fd, name.as_ptr(), name.len(), cap_fd)
        })
        .map(|_| ())
    }
}

All wrappers in the call module are thin: each one forwards arguments to an extern function inside a single unsafe { ... } block, then runs the return value through Error::demux to split syscall returns into Ok/Err. The wrappers expose owned types (Fd, Stat, StatVfs, TimeSpec) over raw pointers but do not own kernel buffers. mmap/munmap are marked pub unsafe fn (their address arguments are caller-controlled). All others are safe wrappers.

src/lib.rs, line 200-225

    // TODO: Remove
    pub fn timespec_from_mut_bytes(bytes: &mut [u8]) -> &mut TimeSpec {
        assert!(bytes.len() >= core::mem::size_of::<TimeSpec>());
        assert_eq!(
            bytes.as_ptr() as usize % core::mem::align_of::<TimeSpec>(),
            0
        );
        unsafe { &mut *bytes.as_mut_ptr().cast() }
    }
    // TODO: Remove
    pub fn timespec_from_bytes(bytes: &[u8]) -> &TimeSpec {
        assert!(bytes.len() >= core::mem::size_of::<TimeSpec>());
        assert_eq!(
            bytes.as_ptr() as usize % core::mem::align_of::<TimeSpec>(),
            0
        );
        unsafe { &*bytes.as_ptr().cast() }
    }

    #[cfg(target_os = "redox")]
    pub use libc::sigset_t as SigSet;

    // TODO: Should libredox compile on non-Redox platforms?
    #[cfg(not(target_os = "redox"))]
    pub type SigSet = u64;
}

timespec_from_bytes / timespec_from_mut_bytes are safe-API wrappers around an unsafe reinterpretation of &[u8] / &mut [u8] as &TimeSpec / &mut TimeSpec. They assert size and alignment before the cast. Sound because libc::timespec is repr(C) with two integer fields and every bit pattern is valid for those fields, but neither function carries a safety comment explaining this. TODO comment above each marks them for removal. See FINDING-1.

src/lib.rs, line 748-763

    #[inline]
    pub fn strerror(error: u16, desc: &mut [u8]) -> Option<(&str, usize)> {
        unsafe {
            let mut len_inout = desc.len();
            let copied_len = Error::demux(redox_strerror_v1(
                desc.as_mut_ptr(),
                &mut len_inout,
                error.into(),
            ))
            .ok()?;
            Some((
                core::str::from_utf8_unchecked(&desc[..copied_len]),
                len_inout,
            ))
        }
    }

strerror uses core::str::from_utf8_unchecked on the buffer the kernel filled. Soundness depends on redox_strerror_v1 returning valid UTF-8; if the kernel returns invalid bytes the result is UB. The function is exposed as a safe API and the buffer is caller-supplied, so the unsoundness is contained to the trust boundary with the kernel. No safety comment justifies the unchecked conversion. See FINDING-2.

src/lib.rs, line 455-460

#[cfg(feature = "call")]
impl Drop for Fd {
    fn drop(&mut self) {
        let _ = unsafe { redox_close_v1(self.0) };
    }
}

Fd::drop calls redox_close_v1(self.0) and discards the result. Standard close-on-drop pattern; matches std::fs::File.

src/lib.rs, line 40-48

        pub fn demux(res: usize) -> Result<usize, Self> {
            if res > usize::wrapping_neg(4096) {
                Err(Self {
                    errno: res.wrapping_neg().try_into().expect("2^BITS - res < 4096"),
                })
            } else {
                Ok(res)
            }
        }

Error::demux maps the syscall convention (results in (MAX-4095, MAX] are negated errnos) into a Rust Result. The TODO at line 35 in mux notes the inherent ambiguity: a syscall that legitimately returns a usize in the error range would be mis-decoded. The .expect("2^BITS - res < 4096") is unreachable: the preceding bound check guarantees wrapping_neg(res) is in 1..=4095, fitting in u16.

src/lib.rs, line 817-817

    unsafe impl plain::Plain for ProcMeta {}

unsafe impl plain::Plain for ProcMeta (and for RtSigInfo at line 1024). Both are repr(C) and contain only u32/i32/usize fields, so every bit pattern is valid — plain::Plain's requirement is met.