Skip to content

Soundness: Out-of-bounds pointer arithmetic in CMSG_NXTHDR causes UB #189

@Manishearth

Description

@Manishearth

Note

This finding was identified during an agentic unsafe Rust code review performed by Gemini AI, followed by human review and verification.

The Issue

In src/lib.rs, the public unsafe function CMSG_NXTHDR performs out-of-bounds pointer arithmetic when iterating over socket control message headers in a control buffer:

linux-raw-sys/src/lib.rs

Lines 141 to 161 in 0e2918c

pub unsafe fn CMSG_NXTHDR(mhdr: *const msghdr, cmsg: *const cmsghdr) -> *mut cmsghdr {
// We convert from raw pointers to usize here, which may not be sound in a
// future version of Rust. Once the provenance rules are set in stone,
// it will be a good idea to give this function a once-over.
let cmsg_len = (*cmsg).cmsg_len;
let next_cmsg = (cmsg as *mut u8).add(CMSG_ALIGN(cmsg_len as _) as usize) as *mut cmsghdr;
let max = ((*mhdr).msg_control as usize) + ((*mhdr).msg_controllen as usize);
if cmsg_len < size_of::<cmsghdr>() as _ {
return ptr::null_mut();
}
if next_cmsg.add(1) as usize > max
|| next_cmsg as usize + CMSG_ALIGN((*next_cmsg).cmsg_len as _) as usize > max
{
return ptr::null_mut();
}
next_cmsg
}

Specifically, CMSG_NXTHDR computes a candidate pointer next_cmsg for the upcoming control header. It then validates whether this candidate header fits within the remaining control buffer (max) by performing pointer arithmetic on next_cmsg.

Under Rust pointer semantics for pointer::add(count), both the starting pointer and the resulting pointer must be either within bounds or at most one byte past the end of the same allocated object.

When iterating control messages in a received packet where the final cmsghdr ends exactly at the end of the allocated buffer (msg_control + msg_controllen), next_cmsg points exactly one byte past the end of the allocated object. On this terminating loop iteration, calling next_cmsg.add(1) advances this one-past-the-end pointer by size_of::<cmsghdr>() bytes (16 bytes on 64-bit platforms). Offsetting a pointer beyond one byte past the end of its underlying allocation violates the core validity conditions of pointer::add and triggers immediate Undefined Behavior.

Minimal Reproduction (Miri)
use linux_raw_sys::cmsg_macros::{CMSG_FIRSTHDR, CMSG_NXTHDR};
use linux_raw_sys::net::{cmsghdr, msghdr};
use core::mem::size_of;

fn main() {
    // Allocate a buffer representing socket control message buffer (`msg_control`).
    // We allocate exactly enough space for a single `cmsghdr` without trailing padding.
    let mut control_buf = [0u64; 2]; // 16 bytes on 64-bit platforms, 8-byte aligned

    // Initialize the `cmsghdr` at the start of the control buffer.
    // The length is set to exactly `size_of::<cmsghdr>()` (16 bytes).
    let hdr_ptr = control_buf.as_mut_ptr() as *mut cmsghdr;
    unsafe {
        (*hdr_ptr).cmsg_len = size_of::<cmsghdr>();
        (*hdr_ptr).cmsg_level = 0;
        (*hdr_ptr).cmsg_type = 0;
    }

    let mut mhdr: msghdr = unsafe { core::mem::zeroed() };
    mhdr.msg_control = control_buf.as_mut_ptr() as *mut core::ffi::c_void;
    mhdr.msg_controllen = size_of::<cmsghdr>();

    unsafe {
        // CMSG_FIRSTHDR returns a pointer to the first header.
        let first = CMSG_FIRSTHDR(&mhdr);
        assert!(!first.is_null());

        // CMSG_NXTHDR attempts to find the next header in the buffer.
        // Because the first header ends exactly at the buffer boundary, `next_cmsg`
        // points exactly 1 byte past the end of the `control_buf` allocation.
        // CMSG_NXTHDR then executes `next_cmsg.add(1)`, which advances a one-past-the-end
        // pointer by 16 bytes, triggering immediate out-of-bounds Undefined Behavior.
        let _next = CMSG_NXTHDR(&mhdr, first);
    }
}
error: Undefined Behavior: in-bounds pointer arithmetic failed: attempting to offset pointer by 16 bytes, but got alloc108+0x10 which is at or beyond the end of the allocation of size 16 bytes
   --> /usr/local/google/home/manishearth/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.12.1/src/lib.rs:154:12
    |
154 |         if next_cmsg.add(1) as usize > max
    |            ^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
    |
    = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
    = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
help: alloc108 was allocated here:
   --> src/bin/repro1.rs:8:9
    |
  8 |     let mut control_buf = [0u64; 2]; // 16 bytes on 64-bit platforms, 8...
    |         ^^^^^^^^^^^^^^^
    = note: stack backtrace:
            0: linux_raw_sys::cmsg_macros::CMSG_NXTHDR
                at /usr/local/google/home/manishearth/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/linux-raw-sys-0.12.1/src/lib.rs:154:12: 154:28
            1: main
                at src/bin/repro1.rs:33:21: 33:46

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

Note

The full audit report below also contains additional minor findings (such as missing safety comments or undocumented FFI assumptions) that are probably worth fixing as well but not the primary goal of this issue. The audit report has not been human-reviewed, it may contain misleading claims.

Full Gemini Codebase Audit Report Appendix

Unsafe Rust Review: linux_raw_sys (v0_12)

Overall Safety Assessment

linux_raw_sys provides generated FFI bindings and raw declarations for the Linux userspace API (UAPI), including kernel structs, unions, constants, and syscall numbers. The vast majority of the crate consists of offline bindgen-generated definitions separated by architecture and header feature modules (general, errno, net, ioctl, etc.).

In addition to the generated bindings, src/lib.rs contains human-authored macro helper modules (cmsg_macros, select_macros, signal_macros) that provide low-level manipulation of socket control messages, fd_set bitsets, and signal handler constants.

Audit of the human-authored codebase revealed a critical soundness vulnerability in src/lib.rs (cmsg_macros::CMSG_NXTHDR). The macro performs out-of-bounds pointer arithmetic (next_cmsg.add(1)) on *mut cmsghdr pointers when checking whether upcoming control headers fit within the socket control buffer. On terminating loop iterations where a control header ends exactly at the buffer boundary, this arithmetic offsets a pointer beyond one byte past the end of its allocated object, violating LLVM getelementptr inbounds rules and triggering immediate Undefined Behavior.

Furthermore, all human-authored pub unsafe fn declarations and internal unsafe calls in src/lib.rs completely lack # Safety documentation comments and // SAFETY: proof obligations.

Critical Findings

Out-of-Bounds inbounds Pointer Arithmetic in CMSG_NXTHDR (src/lib.rs:141-161) 🔴 🚨

  • Severity: 🔴 High

  • Threat Vector: 🚨 Untrusted Input

  • Bug Type: Out-of-Bounds Pointer Arithmetic

  • Location: src/lib.rs:141-161 (cmsg_macros::CMSG_NXTHDR)

  • Description: CMSG_NXTHDR advances a socket control message pointer (cmsg: *const cmsghdr) to the next header in a control buffer (mhdr: *const msghdr). It computes the candidate next header pointer next_cmsg via byte offset:

    let cmsg_len = (*cmsg).cmsg_len;
    let next_cmsg = (cmsg as *mut u8).add(CMSG_ALIGN(cmsg_len as _) as usize) as *mut cmsghdr;

    It then validates whether next_cmsg fits within the control buffer (max = msg_control + msg_controllen) by performing pointer arithmetic on next_cmsg:

    if next_cmsg.add(1) as usize > max
        || next_cmsg as usize + CMSG_ALIGN((*next_cmsg).cmsg_len as _) as usize > max
    {
        return ptr::null_mut();
    }
  • Soundness Violation: Under authoritative Rust pointer semantics and standard library contracts for pointer::add(count) (backed by LLVM getelementptr inbounds instructions), both the starting pointer and the resulting pointer must be either in bounds or at most one byte past the end of the same allocated object.
    In standard networking code, CMSG_NXTHDR is called repeatedly in a loop until it returns null. When the final cmsghdr in a received packet ends exactly at the end of the allocated control buffer (msg_control + msg_controllen), next_cmsg points exactly one byte past the end of the msg_control allocated object. On this final loop check, calling next_cmsg.add(1) offsets this one-past-the-end pointer by size_of::<cmsghdr>() bytes (16 bytes on 64-bit platforms). Offsetting a pointer beyond one byte past the end of its underlying allocation violates the validity conditions of pointer::add and triggers immediate Undefined Behavior.

  • Remediation: Replace pointer arithmetic next_cmsg.add(1) as usize with pure integer arithmetic (next_cmsg as usize) + size_of::<cmsghdr>() > max (or .wrapping_add(1) / byte slice calculations) to prevent generating out-of-bounds inbounds GEP instructions.

Fishy Findings

1. Raw Pointer Address Casts for Strict Bounds Checks in CMSG_NXTHDR (src/lib.rs:142-148) 🟡 🤦

  • Severity: 🟡 Low

  • Threat Vector: 🤦 Accidental Misuse

  • Bug Type: Pointer Provenance

  • Location: src/lib.rs:142-148 (cmsg_macros::CMSG_NXTHDR)

  • Description: The author includes an explicit inline comment acknowledging uncertainty surrounding pointer-to-integer casts:

    // We convert from raw pointers to usize here, which may not be sound in a
    // future version of Rust. Once the provenance rules are set in stone,
    // it will be a good idea to give this function a once-over.
  • Analysis: While casting pointers to usize via as and comparing integer addresses via > is valid under Rust's current operational semantics (expose_provenance / ptr::addr()), relying on raw integer comparisons across distinct pointer allocations rather than safe slice APIs or pointer offset methods is fragile under strict pointer provenance models.

2. Constructing Dangling Function Pointers via Transmute in sig_ign (src/lib.rs:208-214) 🟡 🤦

  • Severity: 🟡 Low
  • Threat Vector: 🤦 Accidental Misuse
  • Bug Type: Invalid Transmute
  • Location: src/lib.rs:208-214 (signal_macros::sig_ign)
  • Description: To represent C's SIG_IGN macro (((__sighandler_t) 1)), sig_ign() transmutes the literal integer 1 into an Option<unsafe extern "C" fn(c_int)>.
  • Analysis: Under Rust's validity invariants for function pointers, function pointer types must be non-null. Because Option<fn()> uses niche optimization for None (0), any non-null integer address (such as 0x1) is a structurally valid bit pattern. However, constructing a fake dangling function pointer to an unmapped address relies entirely on OS kernel syscall conventions intercepting the literal value 1 during signal or sigaction registration. While sound in this FFI context, it represents an unusual boundary pattern.

Missing Safety Comments

The human-authored helper modules in src/lib.rs lack safety documentation (/// # Safety) on public unsafe fn items and internal // SAFETY: proof comments on unsafe operations.

(Note: We do not flag auto-generated unsafe blocks in bindgen architecture modules.)

1. src/lib.rs:116 (CMSG_ALIGN) 🔴

  • Missing Documentation: pub const unsafe fn CMSG_ALIGN(len: c_uint) -> c_uint

  • Proposed Documentation:

    /// # Safety
    /// This function performs pure integer arithmetic and has no memory safety preconditions.
    /// It is marked `unsafe` solely for FFI macro parity with C headers.
    

2. src/lib.rs:121-123 (CMSG_DATA) 🔴

  • Missing Documentation & Comment: pub const unsafe fn CMSG_DATA and raw pointer .add(...).

  • Proposed Proof:

    /// # Safety
    /// `cmsg` must be a valid pointer to an allocated `cmsghdr` object containing at least
    /// `size_of::<cmsghdr>()` initialized bytes.
    pub const unsafe fn CMSG_DATA(cmsg: *const cmsghdr) -> *mut c_uchar {
        // SAFETY:
        // By caller contract, `cmsg` points to an allocated object of at least `size_of::<cmsghdr>()`
        // bytes. Offsetting by `size_of::<cmsghdr>()` remains within or exactly one byte past the end
        // of the allocated object and does not overflow `isize`.
        (cmsg as *mut c_uchar).add(size_of::<cmsghdr>())
    }

3. src/lib.rs:125-127 (CMSG_SPACE) 🔴

  • Missing Documentation & Comment: pub const unsafe fn CMSG_SPACE and call to CMSG_ALIGN.

  • Proposed Proof:

    /// # Safety
    /// This function performs pure integer arithmetic with no memory safety preconditions.
    pub const unsafe fn CMSG_SPACE(len: c_uint) -> c_uint {
        // SAFETY: `CMSG_ALIGN` performs pure arithmetic with no safety preconditions.
        size_of::<cmsghdr>() as c_uint + CMSG_ALIGN(len)
    }

4. src/lib.rs:129-131 (CMSG_LEN) 🔴

  • Missing Documentation: pub const unsafe fn CMSG_LEN

  • Proposed Documentation:

    /// # Safety
    /// This function performs pure integer arithmetic with no memory safety preconditions.
    

5. src/lib.rs:133-139 (CMSG_FIRSTHDR) 🔴

  • Missing Documentation & Comment: pub const unsafe fn CMSG_FIRSTHDR and raw pointer dereference *mhdr.

  • Proposed Proof:

    /// # Safety
    /// `mhdr` must be a valid, aligned pointer valid for reads of `msghdr`.
    pub const unsafe fn CMSG_FIRSTHDR(mhdr: *const msghdr) -> *mut cmsghdr {
        // SAFETY: By caller contract, `mhdr` is non-null, aligned, and valid for reads of `msghdr`.
        if (*mhdr).msg_controllen < size_of::<cmsghdr>() as _ {

6. src/lib.rs:141-161 (CMSG_NXTHDR) 🔴

  • Missing Documentation & Comments: pub unsafe fn CMSG_NXTHDR, raw pointer dereferences (*cmsg, *mhdr, *next_cmsg), and pointer .add(...) calls.

  • Proposed Proof:

    /// # Safety
    /// `mhdr` must be a valid pointer to a readable `msghdr` whose `msg_control` buffer is valid
    /// for `msg_controllen` bytes. `cmsg` must point to a valid `cmsghdr` within that buffer.
    pub unsafe fn CMSG_NXTHDR(mhdr: *const msghdr, cmsg: *const cmsghdr) -> *mut cmsghdr {
        // SAFETY: By caller contract, `cmsg` is valid for reads of `cmsghdr`.
        let cmsg_len = (*cmsg).cmsg_len;
        // SAFETY: `cmsg` points into `msg_control`. Assuming `cmsg_len` is uncorrupted, the offset
        // remains within the allocated control buffer object.
        let next_cmsg = (cmsg as *mut u8).add(CMSG_ALIGN(cmsg_len as _) as usize) as *mut cmsghdr;
        // SAFETY: By caller contract, `mhdr` is valid for reads of `msghdr`.
        let max = ((*mhdr).msg_control as usize) + ((*mhdr).msg_controllen as usize);

7. src/lib.rs:170-175 (FD_CLR) 🔴

  • Missing Documentation & Comment: pub unsafe fn FD_CLR and raw pointer arithmetic/dereference.

  • Proposed Proof:

    /// # Safety
    /// `fd` must satisfy `0 <= fd < FD_SETSIZE` (typically 1024), and `set` must be a valid,
    /// aligned pointer to a mutable `__kernel_fd_set` allocation.
    pub unsafe fn FD_CLR(fd: c_int, set: *mut __kernel_fd_set) {
        let bytes = set as *mut u8;
        if fd >= 0 {
            // SAFETY:
            // By caller contract, `fd < 1024`, so byte index `fd / 8 < 128`. `set` points to an
            // allocated `__kernel_fd_set` (128 bytes). Offsetting and mutating within these bounds
            // is valid and aligned for `u8`.
            *bytes.add((fd / 8) as usize) &= !(1 << (fd % 8));
        }
    }

8. src/lib.rs:177-182 (FD_SET) 🔴

  • Missing Documentation & Comment: pub unsafe fn FD_SET and raw pointer arithmetic/dereference.
  • Proposed Proof: (Identical safety contract and proof obligations as FD_CLR).

9. src/lib.rs:184-191 (FD_ISSET) 🔴

  • Missing Documentation & Comment: pub unsafe fn FD_ISSET and raw pointer arithmetic/dereference.
  • Proposed Proof: (Identical safety contract and proof obligations as FD_CLR).

10. src/lib.rs:193-196 (FD_ZERO) 🔴

  • Missing Documentation & Comment: pub unsafe fn FD_ZERO and call to ptr::write_bytes.

  • Proposed Proof:

    /// # Safety
    /// `set` must be a valid, aligned pointer valid for writes of `size_of::<__kernel_fd_set>()` bytes.
    pub unsafe fn FD_ZERO(set: *mut __kernel_fd_set) {
        let bytes = set as *mut u8;
        // SAFETY: By caller contract, `bytes` is valid for writes of `size_of::<__kernel_fd_set>()` bytes.
        core::ptr::write_bytes(bytes, 0, size_of::<__kernel_fd_set>());
    }

11. src/lib.rs:209-213 (sig_ign) 🟡

  • Improper Comment Formatting: sig_ign() contains an informal // Safety: comment.

  • Proposed Proof:

    // SAFETY:
    // Constructing an arbitrary non-null pointer address (`0x1`) via `transmute` satisfies the
    // non-null validity invariant of function pointers (`Option<fn()>` uses null pointer optimization).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions