Closed Bug 2042033 (CVE-2026-16350) Opened 4 months ago Closed 4 months ago

Compromised content process can trigger a parent-process out-of-bounds write in the AudioIPC server by sending mismatched StreamCreate and StreamInit output parameters

Categories

(Core :: Audio/Video: cubeb, defect, P2)

defect

Tracking

()

RESOLVED FIXED
154 Branch
Tracking Status
firefox-esr115 153+ fixed
firefox-esr140 153+ fixed
firefox151 --- wontfix
firefox152 --- wontfix
firefox153 + fixed
firefox154 + fixed

People

(Reporter: tomoya2517, Assigned: kinetik)

References

Details

(5 keywords, Whiteboard: [client-bounty-form][adv-main153+][adv-ESR140.13+][adv-ESR115.38+])

Attachments

(8 files)

Attached file repro.patch —

Platform: Desktop
Version: Firefox Nightly 153.0a1 (commit 1f7030c8de8f2b349c7d91d7b5a3253c109a1cc1)

Summary:

A compromised content process can make Firefox's parent-process AudioIPC server accept inconsistent audio stream setup data. The server then sizes an internal audio copy as if the stream had many more output channels than the real backend buffer was allocated for. When audio playback starts, the server copies bytes controlled by the content process from AudioIPC shared memory past the end of that backend buffer, causing a parent-process out-of-bounds write.

Steps to Reproduce:

  1. Apply the attached patch to a Firefox source checkout:

    git apply /absolute/path/to/repro.patch

  2. Build Firefox with ASan. The tested mozconfig was:

    mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/objdir-ff-asan
    ac_add_options --enable-address-sanitizer
    ac_add_options --disable-jemalloc
    ac_add_options --disable-crashreporter
    ac_add_options --disable-elf-hack
    ac_add_options --disable-install-strip
    ac_add_options --enable-debug-symbols=-gline-tables-only
    

    Build command:

    MOZCONFIG=mozconfig-asan ./mach build

  3. Run the patched ASan build with the repro hook enabled:

    MOZCONFIG=mozconfig-asan PULSE_MEMPOOL_DISABLE=1 MOZ_AUDIOIPC_STREAM_CREATE_MISMATCH_REPRO=1 ./mach run --temp-profile -- --headless file:///absolute/path/to/trigger.html

    Replace /absolute/path/to/trigger.html with the absolute path to the attached trigger page.

PULSE_MEMPOOL_DISABLE=1 is used only to make the bug easier for ASan to observe on Linux/PulseAudio. It makes PulseAudio use a normal heap-backed output buffer with ASan redzones, so the out-of-bounds write is reported deterministically by ASan.

The vulnerability does not depend on PULSE_MEMPOOL_DISABLE=1. The content process still sends inconsistent AudioIPC stream parameters, and the parent-side AudioIPC server still computes an oversized copy length and writes past the true end of the backend output buffer. Without PULSE_MEMPOOL_DISABLE=1, PulseAudio may place that backend buffer in its shared-memory/mempool allocator instead of a normal heap allocation, so this local ASan build may not report the overwrite even though the same out-of-bounds write path is reached.

Security Impact:

A compromised content process can turn the normal AudioIPC connection into a parent-process out-of-bounds write.

With PulseAudio's default mempool enabled (without PULSE_MEMPOOL_DISABLE=1), the destination may be PulseAudio-managed shared-memory/mempool storage rather than a normal malloc allocation. Corrupting adjacent PulseAudio mempool or memblock metadata could increase exploitability and, when combined with another bug or a suitable memory disclosure/control primitive, could be used as part of an arbitrary address write chain.

Technical Analysis:

  1. Normal compromised-content-process reachability

The content process obtains its AudioIPC connection through the normal PContent::CreateAudioIPCConnection path.

https://searchfox.org/firefox-main/rev/1f7030c8de8f2b349c7d91d7b5a3253c109a1cc1/dom/ipc/PContent.ipdl#1170

async CreateAudioIPCConnection() returns (FileDescOrError fd);

https://searchfox.org/firefox-main/rev/1f7030c8de8f2b349c7d91d7b5a3253c109a1cc1/dom/ipc/ContentParent.cpp#5131-5134

mozilla::ipc::IPCResult ContentParent::RecvCreateAudioIPCConnection(
    CreateAudioIPCConnectionResolver&& aResolver) {
  FileDescriptor fd = CubebUtils::CreateAudioIPCConnection(OtherPid());
  ...
}

https://searchfox.org/firefox-main/rev/1f7030c8de8f2b349c7d91d7b5a3253c109a1cc1/dom/media/CubebUtils.cpp#603-617

rawFD = audioipc2::audioipc2_server_new_client(
    sServerHandle,
    aRemotePid,
    sAudioIPCShmAreaSize);
  1. The server stores output frame size from the first client-controlled message

The AudioIPC server receives stream format information in two different RPC messages. CubebServer::process_stream_create() first stores output_frame_size in ServerStreamCallbacks from StreamCreateParams, which are supplied by the remote AudioIPC client.

Because StreamCreateParams come from the remote AudioIPC client, a compromised content process can make this first message describe a large output channel count. The value is stored before the real cubeb stream is initialized and is later used to size the parent-process copy.

https://searchfox.org/firefox-main/rev/1f7030c8de8f2b349c7d91d7b5a3253c109a1cc1/third_party/rust/audioipc2-server/src/server.rs#728-775

fn process_stream_create(&mut self, params: &StreamCreateParams) -> Result<ClientMessage> {
    let output_frame_size = params
        .output_stream_params
        .as_ref()
        .map(StreamParams::frame_size_in_bytes);

    ...

    let cbs = Box::new(ServerStreamCallbacks {
        input_frame_size,
        output_frame_size,
        shm,
        ...
    });
}
  1. The actual backend stream is created from a second message

CubebServer::process_stream_init() validates the later StreamInitParams, including channels <= 64, but it does not compare those validated parameters with the earlier StreamCreateParams. The actual cubeb/PulseAudio stream is then created from the later StreamInitParams. In the repro, the real backend stream is initialized from the smaller 1-channel StreamInitParams.

https://searchfox.org/firefox-main/rev/1f7030c8de8f2b349c7d91d7b5a3253c109a1cc1/third_party/rust/audioipc2-server/src/server.rs#795-876

fn process_stream_init(..., params: &StreamInitParams) -> Result<ClientMessage> {
    fn valid_stream_params(p: &StreamParams) -> bool {
        const MAX_CHANNELS: u32 = 64;
        ...
        p.channels >= 1 && p.channels <= MAX_CHANNELS && ...
    }

    for p in params.input_stream_params.iter().chain(params.output_stream_params.iter()) {
        if !valid_stream_params(p) {
            ...
        }
    }

    let stream = context.stream_init(
        stream_name,
        input_device,
        input_stream_params,
        output_device,
        output_stream_params,
        latency,
        Some(data_cb_c),
        Some(state_cb_c),
        user_ptr,
    );
}

Because the two messages are not checked for consistency, a compromised content process can send:

  • StreamCreate: a 128-channel output stream, causing a large stored output_frame_size
  • StreamInit: a 1-channel output stream, causing the real backend output buffer to be sized for 1 channel
  1. The callback builds a Rust slice using the stale StreamCreate size

When the audio backend invokes the server callback, data_cb_c() receives the actual backend output_buffer, but it constructs the Rust output slice length from ServerStreamCallbacks.output_frame_size, which came from the earlier StreamCreateParams. The backend-provided output_buffer is from the stream created with StreamInitParams, but the slice length is computed using the older StreamCreate-derived size.

https://searchfox.org/firefox-main/rev/1f7030c8de8f2b349c7d91d7b5a3253c109a1cc1/third_party/rust/audioipc2-server/src/server.rs#902-923

unsafe extern "C" fn data_cb_c(..., output_buffer: *mut c_void, nframes: c_long) -> c_long {
    let cbs = &mut *(user_ptr as *mut ServerStreamCallbacks);
    ...
    let output: &mut [u8] = if output_buffer.is_null() {
        &mut []
    } else {
        let nbytes = nframes * cbs.output_frame_size.unwrap_or(0) as c_long;
        slice::from_raw_parts_mut(output_buffer as *mut u8, nbytes as usize)
    };
    cbs.data_callback(input, output, nframes as isize) as c_long
}
  1. The final copy checks the wrong destination size and copies bytes controlled by the content process

ServerStreamCallbacks::data_callback() checks nbytes against output.len() and the AudioIPC shared-memory size. However, output.len() is already based on the stale, client-controlled output_frame_size; it is not the true length of the backend-provided output buffer. Therefore this check does not prove that the backend buffer is actually that large. The destination is the backend-provided parent-process buffer, and the source bytes come from AudioIPC shared memory mapped for the stream.

https://searchfox.org/firefox-main/rev/1f7030c8de8f2b349c7d91d7b5a3253c109a1cc1/third_party/rust/audioipc2-server/src/server.rs#281-298

let Some(nbytes) = frames.checked_mul(output_frame_size) else {
    return cubeb::ffi::CUBEB_ERROR.try_into().unwrap();
};

if nbytes > output.len() || nbytes > self.shm.get_size() {
    return cubeb::ffi::CUBEB_ERROR.try_into().unwrap();
}

unsafe {
    output[..nbytes].copy_from_slice(self.shm.get_slice(nbytes).unwrap());
}

SharedMemView::get_slice() bounds only the shared-memory source mapping. It does not validate the true size of the backend output buffer.

https://searchfox.org/firefox-main/rev/1f7030c8de8f2b349c7d91d7b5a3253c109a1cc1/third_party/rust/audioipc2/src/shm.rs#231-240

impl SharedMemView {
    pub unsafe fn get_slice(&self, size: usize) -> Result<&[u8]> {
        let map = slice::from_raw_parts(self.ptr as _, self.size);
        if size <= self.size {
            Ok(&map[..size])
        } else {
            Err(Error::Other("mmap size".into()))
        }
    }
}
Flags: sec-bounty?
Attached file trigger.html —
Attached file asan.log —
Group: firefox-core-security → media-core-security
Component: Security → Audio/Video: cubeb
Product: Firefox → Core
Flags: needinfo?(kinetik)
Assignee: nobody → kinetik
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Flags: needinfo?(kinetik)
Attached file GitHub Pull Request —
Attachment #9590011 - Flags: review?(padenot)

r+ there, sorry for the delay.

The severity field is not set for this bug.
:karlt, could you have a look please?

For more information, please visit BugBot documentation.

Flags: needinfo?(karlt)
Severity: -- → S2
Flags: needinfo?(karlt)
OS: Unspecified → All
Priority: -- → P2
Hardware: Unspecified → All
Version: unspecified → Trunk
Attached file (secure) —
Attachment #9590011 - Flags: review?(padenot)

Comment on attachment 9595348 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: Requires chaining an existing exploit in the content process
  • Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
  • Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: all, yes
  • If not all supported branches, which bug introduced the flaw?: None
  • Do you have backports for the affected branches?: No
  • If not, how different, hard to create, and risky will they be?: Fairly simple
  • How likely is this patch to cause regressions; how much testing does it need?: Simple change, low chance of regressions
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9595348 - Flags: sec-approval?
Attachment #9595348 - Flags: sec-approval? → sec-approval+
Pushed by mgregan@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/2419f7747a23 https://hg.mozilla.org/integration/autoland/rev/20bd899032c1 Update audioipc to c9dcbaee1624daad2fa6b1445fd047efde4f1ec0. r=cubeb-reviewers,pehrsons
Group: media-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 4 months ago
Resolution: --- → FIXED
Target Milestone: --- → 154 Branch

Please add Beta, ESR140, and ESR115 uplift requests when you get a chance.

Flags: needinfo?(kinetik)

firefox-beta Uplift Approval Request

  • User impact if declined/Reason for urgency: sec-high fix: out-of-bounds write in parent process can be triggered from a compromised/attacker-controlled content process; potential sandbox escape
  • Code covered by automated testing?: yes
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: Very low, simple refactor of init to avoid TOCTOU.
  • String changes made/needed?: N/A
  • Is Android affected?: yes
Attachment #9598573 - Flags: approval-mozilla-beta?
Attached file (secure) —
Attached file (secure) —
Attachment #9598580 - Flags: approval-mozilla-esr140?

firefox-esr140 Uplift Approval Request

  • User impact if declined/Reason for urgency: sec-high fix: out-of-bounds write in parent process can be triggered from a compromised/attacker-controlled content process; potential sandbox escape
  • Code covered by automated testing?: yes
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: Very low, simple refactor of init to avoid TOCTOU.
  • String changes made/needed?: N/A
  • Is Android affected?: yes
Attached file (secure) —
Attachment #9598582 - Flags: approval-mozilla-esr115?

firefox-esr115 Uplift Approval Request

  • User impact if declined/Reason for urgency: sec-high fix: out-of-bounds write in parent process can be triggered from a compromised/attacker-controlled content process; potential sandbox escape
  • Code covered by automated testing?: yes
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: Very low, simple refactor of init to avoid TOCTOU.
  • String changes made/needed?: N/A
  • Is Android affected?: yes
Flags: needinfo?(kinetik)
Attachment #9598573 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c154/b153]
Attachment #9598580 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Duplicate of this bug: 2048138
Attachment #9598582 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main153+][adv-ESR140.13+][adv-ESR115.38+]
Alias: CVE-2026-16350
Flags: sec-bounty? → sec-bounty+
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: