Closed Bug 2038465 (CVE-2026-12292) Opened 4 months ago Closed 4 months ago

Uninitialised heap disclosure via decodeAudioData when FLAC frames vary channel count

Categories

(Core :: Web Audio, defect)

Firefox 152
defect

Tracking

()

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

People

(Reporter: zzjas98, Assigned: chunmin)

References

Details

(Keywords: csectype-bounds, reporter-external, sec-high, Whiteboard: [adv-main152+][adv-esr140.12+])

Attachments

(6 files, 2 obsolete files)

1.05 KB, text/html
Details
21.52 KB, audio/flac
Details
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
Attached file poc.html

Steps to reproduce:

  1. Serve the attached html & flac: python -m http.server -p 8991
  2. Load http://localhost:8991/poc.html in Firefox

Actual results:

getChannelData(1) on the decoded AudioBuffer returns uninitialised heap.

With an ASan build where mozjemalloc shows poisoned allocation, the page shows:

[POC] ch1[0..4095] non-zero: 4096/4096
[POC] ch1[0]=0xe4e4e4e4
ch1[1]=0xe4e4e4e4
ch1[2]=0xe4e4e4e4
ch1[3]=0xe4e4e4e4
ch1[4]=0xe4e4e4e4
...

Tested on commit 1a4668757b5f.

Expected results:

No uninitialized memory should be exposed.

Chrome and Safari both reject decoding the poc audio file:

Chrome:
ERROR: IndexSizeError: Failed to execute 'getChannelData' on 'AudioBuffer': channel index (1) exceeds number of channels (1)

// Safari
ERROR: EncodingError: Decoding failed

Analysis

poc_input.flac is a crafted FLAC stream whose STREAMINFO declares 1 channel, but whose frames alternate channel_assignment between 0 (1 channel) and 1 (2 channels).

Parser code: STREAMINFO channel count, per-frame channel assignment.

ffvpx's libavcodec/flacdec.c::decode_frame honours each frame's per-frame channel count, so the decoder emits AudioData packets with mChannels = 1, 2, 1, 2, ...

MediaDecodeTask::FinishDecode allocates a 2-channel output buffer (because mMediaInfo.mAudio.mChannels was last set to 2 by OnAudioDecodeCompleted).

The buffer uses js_pod_malloc<float> — uninitialised.

When processing mono packets, the write loop iterates for (i = 0; i < audioData->mChannels; ++i) which is only i < 1, so channel 1 in the mono-frame ranges is never written.

getChannelData(1) exposes those uninit bytes to JS by: AudioBuffer::GetChannelData -> AudioBuffer::RestoreJSChannelData.

This was found while analyzing commit 63376b3.

Please let me know if I can provide any more information, thanks!

Attached audio poc_input.flac
Group: core-security → media-core-security
Keywords: sec-high

It looks like this is more like a Firefox issue rather than FFmpeg's one. MediaDecodeTask::FinishDecode allocates the output buffer using the last observed per-packet channel count (because OnAudioDecodeCompleted unconditionally overwrites mMediaInfo.mAudio.mChannels on every sample), then writes each queued packet with a loop bounded by the per-packet channel count. If allocation ends up wider than a queued packet (no symmetric down-mix branch exists for that direction for the channelCount < audioData->mChannels branch), every position in the surplus channel for that packet's frame range is left uninitialised.

Assignee: nobody → cchang
Attached file (secure)
Attached file (secure) (obsolete) —

Web Audio's decode path used to derive the output buffer width from
mMediaInfo.mAudio.mChannels, which OnAudioDecodeCompleted overwrites
to the last decoded packet's mChannels per sample. For a stream
whose per-packet channel count varies, this produced a buffer whose
width matched the final packet but mismatched earlier packets in the
queue: the per-packet write loop in FinishDecode iterates
i < audioData->mChannels, so any channels in [audioData->mChannels,
channelCount) for that packet's frame range went unwritten. Because
ThreadSharedFloatArrayBufferList::Create allocated channels with
js_pod_malloc<float>, those gaps were script-visible via
AudioBuffer.getChannelData.

This patch:

  • Adds a dedicated MediaDecodeTask::mMaxChannels member tracked
    separately from the demuxer-declared mMediaInfo.mAudio.mChannels.
  • Adds a UpdateMaxChannels helper shared between OnAudioDecodeCompleted
    and OnAudioDrainCompleted; the helper logs at Debug level whenever
    the max grows.
  • Allocates FinishDecode's output buffer with mMaxChannels and logs at
    Warning level when mMaxChannels diverges from the demuxer-declared
    channel count (i.e. the stream is internally inconsistent).
  • Replaces the mid-loop reallocation/upmix branch with a single
    MOZ_DIAGNOSTIC_ASSERT(audioData->mChannels <= channelCount) at the
    per-packet consumer site. Deletes the now-unreachable
    UpmixPreviousData helper.
  • Adds a small per-packet Upmix helper that mirrors channel 0's
    just-written range into channels [audioData->mChannels,
    channelCount) at the same offset, so narrower packets render as
    duplicated mono in the surplus channels rather than as silence.
  • Switches ThreadSharedFloatArrayBufferList::Create from
    js_pod_malloc<float> to js_pod_calloc<float> as a defence-in-depth
    floor: if a future producer/consumer mismatch ever leaves channel
    ranges untouched, the worst case is silence rather than a heap
    disclosure.

Behaviour change for streams whose demuxer-declared channel count
differs from every emitted packet: the produced AudioBuffer's
numberOfChannels now reflects the maximum per-packet count rather
than the last-seen per-packet count. For streams whose declared
channel count is wider than every packet (e.g. STREAMINFO declares 4
but every per-frame channel_assignment encodes 2), the AudioBuffer is
allocated for the max observed packet width (2), not the header
width. Bug 1905287's user-visible decode-succeeds behaviour for
HE-AACv2 mid-stream parametric-stereo discovery is preserved: the
buffer is allocated wide enough to hold the widest seen packet, and
narrower packets render with duplicated-mono content in the surplus
channels via the per-packet Upmix helper, matching what the deleted
UpmixPreviousData applied to the prefix when the upmix-on-realloc
branch fired.

Attached file (secure) (obsolete) —

FinishDecode initialises the speex resampler once with
mMediaInfo.mAudio.mRate, so any per-packet rate change would silently
produce incorrect output (the resampler holds state for the first
configured rate, and resampledFrames is computed once from the
last-seen rate). No codec/container in tree currently emits varying
rates within a single decode job, but the warning surfaces the case
in MOZ_LOG output if a future producer ever does so. Mirrors the
channel-count divergence warning added in the previous commit.

See Also: 20142921905287
See Also: → CVE-2026-2773
Blocks: 2039651
Attached file (secure)

Switch ThreadSharedFloatArrayBufferList::Create from js_pod_malloc<float>
to js_pod_calloc<float> so that channel arrays are zero-initialised at
allocation time. Any range of a channel buffer that a downstream
producer fails to write is therefore silent rather than uninitialised
heap, closing the script-visible heap-disclosure path through
AudioBuffer.getChannelData regardless of producer/consumer mismatches in
callers of Create.

Document the zero-init at the method declaration in AudioNodeEngine.h.

Comment on attachment 9585787 [details]
(secure)

Revision D300185 was moved to bug 2039651. Setting attachment 9585787 [details] to obsolete.

Attachment #9585787 - Attachment is obsolete: true

Comment on attachment 9585788 [details]
(secure)

Revision D300186 was moved to bug 2039651. Setting attachment 9585788 [details] to obsolete.

Attachment #9585788 - Attachment is obsolete: true

Comment on attachment 9586516 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: Moderate. The fix is a one-line swap of js_pod_malloc<float> for js_pod_calloc<float> in a generic Web Audio channel-buffer allocator (ThreadSharedFloatArrayBufferList::Create) plus a doc-comment update reflecting the new init semantics. The diff signals that some prior caller of this helper had been leaving channel ranges untouched, but does not point at which decoder, which codec, or what crafted input is required to reach an unwritten range. Reproducing the leak still requires independently identifying MediaDecodeTask::FinishDecode as the vulnerable consumer and producing an audio stream whose per-packet channel count varies in a way the per-branch handling does not symmetrically cover.
  • Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No. The fix commit message and the only touched doc comment were rewritten before this request to drop all security-revealing language; no inline source comment or identifier reveals the security nature of the change. The regression mochitest (D300184) is deliberately held out of this landing per the standard test-landing policy and will land at least 4 weeks after the fix reaches release branches.
  • 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 currently supported branches contain the vulnerable js_pod_malloc<float> allocation in ThreadSharedFloatArrayBufferList::Create. The known POC produces a script-visible heap disclosure on Nightly 152, Beta 152, Release 151, ESR 140, and ESR 128. ESR 128 in particular has no mid-stream channel-count check at all in MediaDecodeTask::FinishDecode, so on top of the narrowing leak its per-packet write loop can also reach ChannelDataForWrite indices beyond the initial mChannelData length, risking an out-of-bounds heap write if the packet ordering widens past the buffer. ESR 115 currently carries a backported widening-rejection check (Bug 2014832, commit c80e0050731a, applied to ESR 115 in Feb 2026) which rejects the POC's mono->stereo transition with InvalidContent before the buffer reaches JS; the narrowing branch on ESR 115 still leaves surplus channels uninitialised, so a narrowing-only stream is theoretically affected, but no such trigger has been constructed. The bug currently carries no status-firefoxN flags; setting Nightly/Beta/Release/ESR140/ESR128 to affected and ESR115 to affected (defence-in-depth, no demonstrated POC) reflects the analysis above.
  • If not all supported branches, which bug introduced the flaw?: Not a single-commit regression. The js_pod_malloc<float> allocation in ThreadSharedFloatArrayBufferList::Create has been present since Bug 1199559 (Aug 2015), and the per-packet write loop in MediaDecodeTask::FinishDecode has iterated audioData->mChannels writes against an mMediaInfo.mAudio.mChannels-sized (last-seen) buffer for years. Bug 1905287 (commit 35fd6b28e789, landed 2025-03-20, first shipped in Firefox 138 on 2025-04-29) added a reallocate-and-upmix-on-widening branch that addressed the widening case but did not address the narrowing-leak case; Bug 2014832 (commit c80e0050731a, applied to ESR 115 in Feb 2026) added a stricter "fail decode on widening" check that incidentally rejects the known POC on ESR 115 only.
  • Do you have backports for the affected branches?: Not yet. Backports for Beta, Release, ESR 140, and ESR 128 will be prepared after sec-approval; an ESR 115 backport will be included as defence-in-depth.
  • If not, how different, hard to create, and risky will they be?: Trivial. The fix is a one-line edit to dom/media/webaudio/AudioNodeEngine.cpp (js_pod_malloc<float> -> js_pod_calloc<float>) plus a doc-comment update in AudioNodeEngine.h. The callsite has been identical across mozilla-central, Beta, Release, ESR 140, ESR 128, and ESR 115 for years; cherry-picking is mechanical with no expected merge conflict and no behavioural divergence between branches.
  • How likely is this patch to cause regressions; how much testing does it need?: Low. The change is a one-line swap from malloc to calloc on a single helper that allocates per-channel float arrays. The only behavioural change is that newly allocated channel data starts at 0.0f instead of uninitialised; no caller is expected to rely on uninitialised contents. Performance impact is negligible (on Linux/macOS calloc typically returns zero-filled pages from the allocator without an explicit memset for large requests; on Windows the cost is one memset of a buffer that is about to be fully written). Existing Web Audio mochitests exercise this allocator path extensively and continue to pass locally.
  • Is the patch ready to land after security approval is given?: Yes. Both D300184 (regression test, deferred landing) and D300628 (fix) are currently Accepted on Phabricator with sanitized commit metadata, and the fix is ready to land on autoland after sec-approval is granted.
  • Is Android affected?: Yes. ThreadSharedFloatArrayBufferList lives in dom/media/webaudio/, which is compiled into all Gecko-based platforms including Firefox for Android (GeckoView). The Web Audio decodeAudioData path is reachable from web content on Android.

Drafted with the assistance of Claude Code — reviewed and approved by the patch author.

Attachment #9586516 - Flags: sec-approval?

Comment on attachment 9586516 [details]
(secure)

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 currently supported branches contain the vulnerable js_pod_malloc<float> allocation in ThreadSharedFloatArrayBufferList::Create. The known POC produces a script-visible heap disclosure on Nightly 152, Beta 152, Release 151, ESR 140, and ESR 128. ESR 128 in particular has no mid-stream channel-count check at all in MediaDecodeTask::FinishDecode, so on top of the narrowing leak its per-packet write loop can also reach ChannelDataForWrite indices beyond the initial mChannelData length, risking an out-of-bounds heap write if the packet ordering widens past the buffer. ESR 115 currently carries a backported widening-rejection check (Bug 2014832, commit c80e0050731a, applied to ESR 115 in Feb 2026) which rejects the POC's mono->stereo transition with InvalidContent before the buffer reaches JS; the narrowing branch on ESR 115 still leaves surplus channels uninitialised, so a narrowing-only stream is theoretically affected, but no such trigger has been constructed. The bug currently carries no status-firefoxN flags; setting Nightly/Beta/Release/ESR140/ESR128 to affected and ESR115 to affected (defence-in-depth, no demonstrated POC) reflects the analysis above.

That's a lot of words for "All but 115"

Attachment #9586516 - Flags: sec-approval? → sec-approval+
Flags: sec-bounty?
Group: media-core-security → core-security-release
Status: UNCONFIRMED → RESOLVED
Closed: 4 months ago
Resolution: --- → FIXED
Target Milestone: --- → 153 Branch

Please submit Beta and ESR140 uplift requests.

Flags: needinfo?(cchang)
Attached file (secure)
Attachment #9588720 - Flags: approval-mozilla-beta?

(In reply to Ryan VanderMeulen [:RyanVM] from comment #12)

Please submit Beta and ESR140 uplift requests.

I've asked the uplifts via https://lando.moz.tools/D300628/

Flags: needinfo?(cchang)

firefox-beta Uplift Approval Request

  • User impact if declined/Reason for urgency: Heap memory disclosure. A crafted audio stream decoded via Web Audio's decodeAudioData() can expose uninitialised heap bytes to web content via AudioBuffer.getChannelData().
  • Code covered by automated testing?: no
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing: N/A
  • Risk associated with taking this patch: low
  • Explanation of risk level: Three-line change in one allocator: js_pod_malloc<float> is replaced with js_pod_calloc<float> in ThreadSharedFloatArrayBufferList::Create, and the doc comment is updated to reflect the new contract. On the normal path callers fully overwrite the buffer before reading, so the only observable change is that any previously-uninitialised region now reads as zero. No API or ABI changes; shipping the same defence-in-depth zero-init on Beta and ESR 140 as on Nightly.
  • String changes made/needed?: None
  • Is Android affected?: yes

firefox-esr140 Uplift Approval Request

  • User impact if declined/Reason for urgency: Heap memory disclosure. A crafted audio stream decoded via Web Audio's decodeAudioData() can expose uninitialised heap bytes to web content via AudioBuffer.getChannelData().
  • Code covered by automated testing?: no
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing: N/A
  • Risk associated with taking this patch: low
  • Explanation of risk level: Three-line change in one allocator: js_pod_malloc<float> is replaced with js_pod_calloc<float> in ThreadSharedFloatArrayBufferList::Create, and the doc comment is updated to reflect the new contract. On the normal path callers fully overwrite the buffer before reading, so the only observable change is that any previously-uninitialised region now reads as zero. No API or ABI changes; shipping the same defence-in-depth zero-init on Beta and ESR 140 as on Nightly.
  • String changes made/needed?: None
  • Is Android affected?: yes
Attachment #9588722 - Flags: approval-mozilla-esr140?
Attached file (secure)
Attachment #9588720 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9588722 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c153/b152]
Whiteboard: [adv-main152+]
Whiteboard: [adv-main152+] → [adv-main152+][adv-esr140.12+]
Flags: sec-bounty? → sec-bounty+
Alias: CVE-2026-12292
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: