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)
Tracking
()
People
(Reporter: tomoya2517, Assigned: kinetik)
References
Details
(5 keywords, Whiteboard: [client-bounty-form][adv-main153+][adv-ESR140.13+][adv-ESR115.38+])
Attachments
(8 files)
|
3.24 KB,
text/x-patch
|
Details | |
|
592 bytes,
text/html
|
Details | |
|
20.52 KB,
application/octet-stream
|
Details | |
|
44 bytes,
text/x-github-pull-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
tjr
:
sec-approval+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-esr140+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-esr115+
|
Details | Review |
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:
-
Apply the attached patch to a Firefox source checkout:
git apply /absolute/path/to/repro.patch -
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-onlyBuild command:
MOZCONFIG=mozconfig-asan ./mach build -
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.htmlReplace
/absolute/path/to/trigger.htmlwith 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:
- Normal compromised-content-process reachability
The content process obtains its AudioIPC connection through the normal PContent::CreateAudioIPCConnection path.
async CreateAudioIPCConnection() returns (FileDescOrError fd);
mozilla::ipc::IPCResult ContentParent::RecvCreateAudioIPCConnection(
CreateAudioIPCConnectionResolver&& aResolver) {
FileDescriptor fd = CubebUtils::CreateAudioIPCConnection(OtherPid());
...
}
rawFD = audioipc2::audioipc2_server_new_client(
sServerHandle,
aRemotePid,
sAudioIPCShmAreaSize);
- 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.
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,
...
});
}
- 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.
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 storedoutput_frame_sizeStreamInit: a 1-channel output stream, causing the real backend output buffer to be sized for 1 channel
- The callback builds a Rust slice using the stale
StreamCreatesize
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.
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
}
- 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.
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.
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()))
}
}
}
| Reporter | ||
Comment 1•4 months ago
|
||
| Reporter | ||
Comment 2•4 months ago
|
||
Updated•4 months ago
|
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Comment 3•4 months ago
|
||
Comment 4•4 months ago
|
||
r+ there, sorry for the delay.
Comment 5•4 months ago
|
||
The severity field is not set for this bug.
:karlt, could you have a look please?
For more information, please visit BugBot documentation.
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Comment 6•4 months ago
|
||
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Comment 7•4 months ago
|
||
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
Updated•4 months ago
|
Updated•4 months ago
|
Comment 9•4 months ago
|
||
Comment 10•4 months ago
|
||
Please add Beta, ESR140, and ESR115 uplift requests when you get a chance.
Comment 11•3 months ago
|
||
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
| Assignee | ||
Comment 12•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D305421
| Assignee | ||
Comment 13•3 months ago
|
||
Updated•3 months ago
|
Comment 14•3 months ago
|
||
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
| Assignee | ||
Comment 15•3 months ago
|
||
Updated•3 months ago
|
Comment 16•3 months ago
|
||
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
| Assignee | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 17•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Comment 18•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
Updated•3 months ago
|
Comment 20•3 months ago
|
||
| uplift | ||
Updated•2 months ago
|
Updated•2 months ago
|
Updated•2 months ago
|
Updated•26 days ago
|
Description
•