Closed Bug 2022604 (CVE-2026-6748) Opened 6 months ago Closed 5 months ago

VideoFrame.copyTo() leaks uninitialized heap bytes due to stride mismatch in copyPlane

Categories

(Core :: Audio/Video: Web Codecs, defect, P2)

defect

Tracking

()

VERIFIED FIXED
151 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 150+ verified
firefox149 --- wontfix
firefox150 + verified
firefox151 + verified

People

(Reporter: y0un9sa, Assigned: chunmin)

References

Details

(4 keywords, Whiteboard: [adv-main150+][adv-esr140.10+])

Attachments

(7 files, 1 obsolete file)

User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:148.0) Gecko/20100101 Firefox/148.0

Steps to reproduce:

I was looking at how VideoFrame handles RGB surfaces internally and found
that copyTo() uses a hardcoded packed stride (width * 4) to walk the source
data, but the actual backing surface has a larger aligned stride. The gap
bytes are uninitialized heap, and they end up in the caller's ArrayBuffer.

The bug is in the copyPlane lambda inside Resource::CopyTo()
(VideoFrame.cpp:2825-2850). It advances the source pointer by
Stride(aPlane) on each row (line 2844). For RGB formats, Stride() returns
width * SampleBytes (line 2778) = width * 4 — the packed row size.

But copyPlane is called on a temp DataSourceSurface that CopyTo creates at
line 2925-2927:

Factory::CreateDataSourceSurfaceWithStride(size, f, map.GetStride())

This temp surface inherits the stride from the stored source surface, which
comes from AllocateBGRASurface (VideoFrame.cpp:211):

CreateDataSourceSurfaceWithStride(..., surfaceMap.GetStride())

The stored surface stride ultimately comes from wherever the snapshot was
created. For any DataSourceSurface, Init (SourceSurfaceRawData.cpp:74-103)
uses GetAlignedStride<16> by default, rounding up to the next 16-byte
boundary. For WebGPU canvases, the stride comes from the GPU readback
buffer alignment (CanvasManagerChild.cpp:257-272) and can be much larger
(e.g. 256 bytes).

So for a canvas with width=5 (packed row = 20 bytes):

  • GetAlignedStride<16>(20) = 32 for any 2D/WebGL canvas
  • GPU readback stride can be 256 for a WebGPU canvas
  • copyPlane advances by Stride(RGBA) = 20

copyPlane reads:
row 0 from offset 0 -- correct
row 1 from offset 20 -- wrong, actual row 1 is at offset 32 (or 256)
row 2 from offset 40 -- wrong, actual row 2 is at offset 64 (or 512)

Everything after row 0 drifts into the padding of earlier rows.

Both the AllocateBGRASurface and CopyTo temp surfaces are allocated with
aZero=false (default for B8G8R8A8), so the padding is whatever malloc
returned. SwizzleData writes only width*4 bytes per row — padding is never
touched.

The output goes directly into the caller's ArrayBuffer — no image encoder
in the path, no lossy step. Raw heap bytes.

Steps to reproduce:

  1. Firefox with WebCodecs enabled (dom.media.webcodecs.enabled = true)
  2. Serve the attached PoC over localhost or https (needs secure context)
  3. Click "Run"
  4. It creates a 5x16 canvas, draws solid green, constructs a VideoFrame
    from it, calls copyTo() into a Uint8Array, and checks whether all
    pixels match the expected green
  5. On a vulnerable build, rows after row 0 will contain corrupted data —
    a mix of real pixels and uninitialized padding bytes

The PoC uses a plain 2D canvas (stride gap = 12 bytes/row). WebGPU
canvases produce much larger gaps (up to 236 bytes/row for width=5) but
the root cause is the same: copyPlane uses Stride(aPlane) instead of the
temp surface's actual mapped stride.

Source analysis against mozilla-central at 150.0a1.
Tested on Firefox 148.0.2 (aarch64), macOS.

Actual results:

VideoFrame.copyTo() returns corrupted pixel data. Rows after the first
contain bytes from the surface padding instead of actual pixel data.

The chain:

new VideoFrame(canvas, {timestamp: 0})
-> SurfaceFromElement -> HTMLCanvasElement::GetSurfaceSnapshot()
-> returns DataSourceSurface with GetAlignedStride<16> stride
-> AllocateBGRASurface(surface)
-> CreateDataSourceSurfaceWithStride(size, B8G8R8A8, surfaceMap.GetStride())
-> SwizzleData, padding untouched

frame.copyTo(buffer)
-> Resource::CopyTo()
-> maps stored surface, gets map.GetStride() = 32 (for width=5)
-> creates temp surface with stride 32, aZero=false
-> SwizzleData into temp (pixel data correct, padding stale)
-> copyPlane(tempMap.GetData())
-> advances source by Stride(aPlane) = width * 4 = 20
-> reads padding bytes as pixel data
-> writes them into caller's ArrayBuffer

For width=5 with aligned stride 32:

  • packed row = 20 bytes, actual stride = 32
  • 12 bytes of uninitialized padding per row
  • copyPlane reads 20 bytes starting at offsets 0, 20, 40, 60, ...
  • but real rows start at offsets 0, 32, 64, 96, ...
  • row 1 output contains 12 bytes of row 0 padding + 8 bytes of row 1
  • each subsequent row drifts 12 bytes further

The caller gets a mix of real pixels and stale heap content in a plain
Uint8Array with no encoding or filtering step.

Expected results:

copyPlane should advance the source pointer by the actual mapped stride of
the surface it's reading from, not by the hardcoded Stride(aPlane).

The cleanest fix in Resource::CopyTo(): pass the temp surface's real stride
into copyPlane, or better yet skip the temp surface and swizzle directly
into the destination buffer using the caller's requested stride.

At minimum:

  • in copyPlane, use tempMap.GetStride() instead of Stride(aPlane) to
    advance aPlaneData
  • or allocate the temp surface with aZero=true so padding is at least
    zeroed (but the stride mismatch still corrupts the image)
Group: core-security → media-core-security
Severity: -- → S2
Status: UNCONFIRMED → NEW
Ever confirmed: true
Priority: -- → P2
Flags: needinfo?(cchang)
Assignee: nobody → cchang
Flags: needinfo?(cchang)
Flags: sec-bounty?
Attached file (secure)

gtest: CopyToBGRAAlignedStride creates a SourceSurfaceImage with stride 32
for width=5 BGRA (packed stride 20), fills padding with poison bytes, and
verifies CopyTo output. Detects the bug where copyPlane advances by packed
stride instead of actual surface stride.

WPT: videoFrame-copyTo-canvas.window.js creates a VideoFrame from a canvas
with stride-misaligned width and verifies pixel correctness. This test
catches the bug on platforms where the canvas backend produces surfaces with

4-byte stride alignment (e.g. macOS Metal/IOSurface), but passes on
Skia-only platforms where BGRA stride is always packed.

Attached file (secure)

The copyPlane lambda used Stride(aPlane), which returns the packed width
(width * bytesPerPixel), to advance the source pointer between rows. When
the underlying image has alignment padding (e.g. 16-byte aligned from
SourceSurfaceAlignedRawData), rows after the first drift into padding
bytes, producing corrupted output.

Pass the actual source stride into copyPlane. For RGB surfaces this is
tempMap.GetStride(); YUV call sites still pass Stride(aPlane) for now
(addressed separately).

See Also: → 2027167
Blocks: 2027167
See Also: 2027167
Attached file (secure)

videoFrame-copyTo-canvas.window.js creates a VideoFrame from a canvas with
stride-misaligned width and verifies pixel correctness. Catches the bug on
platforms where the canvas backend produces surfaces with >4-byte stride
alignment (e.g. macOS Metal/IOSurface), but passes on Skia-only platforms
where BGRA stride is always packed.

Comment on attachment 9560245 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: Moderate. The patch changes a lambda to accept an explicit source stride parameter instead of computing it from the pixel format. A reader familiar with stride alignment and surface memory layout could infer that the old code read from incorrect offsets, but the specific impact (reading stale data from padding bytes) is not directly apparent from the diff. The fix reads as a correctness improvement for pixel data handling.
  • 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 supported branches
  • If not all supported branches, which bug introduced the flaw?: N/A
  • Do you have backports for the affected branches?: No
  • If not, how different, hard to create, and risky will they be?: Backports are not yet prepared but should be straightforward. The fix is a small, self-contained change to one lambda in VideoFrame.cpp (adding a stride parameter and passing the correct value). The WebCodecs code has not diverged significantly across branches. Low risk.
  • How likely is this patch to cause regressions; how much testing does it need?: Low. The change is mechanical: the copyPlane lambda gains one parameter, and each call site passes the appropriate stride value. The RGB path now passes the actual mapped stride (which was already used to create the source surface). YUV call sites preserve existing behavior. All existing WebCodecs tests (gtests, mochitests, WPTs — over 5500 checks) pass with the fix.
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9560245 - Flags: sec-approval?

Comment on attachment 9560245 [details]
(secure)

Please set affected release flags

Attachment #9560245 - Flags: sec-approval? → sec-approval+

[Tracking Requested - why for this release]:

The vulnerabilities are exposed in Nightly, Beta, Release, and ESR 140. Will need to ask uplift once it successfully land on Nightly.

Pushed by cchang@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/71cd3b8ff623 https://hg.mozilla.org/integration/autoland/rev/2ae12734b463 Fix VideoFrame.copyTo() using incorrect stride for RGB surfaces. r=media-playback-reviewers,padenot
Group: media-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 5 months ago
Resolution: --- → FIXED
Target Milestone: --- → 151 Branch

(In reply to Sebastian Hengst [:aryx] (needinfo me if it's about an intermittent or backout) from comment #8)

https://hg.mozilla.org/mozilla-central/rev/2ae12734b463

https://bugzilla.mozilla.org/show_bug.cgi?id=2025883#c18 reverts the fix for VideoFrame: https://hg-edge.mozilla.org/integration/autoland/rev/6d57830ae50a. I will need to reland the patches.

Status: RESOLVED → REOPENED
Flags: needinfo?(aryx.bugmail)
Resolution: FIXED → ---
Status: REOPENED → RESOLVED
Closed: 5 months ago5 months ago
Flags: needinfo?(aryx.bugmail)
Resolution: --- → FIXED

NI for uplift requests.

Flags: needinfo?(cchang)
Attached file (secure)
Attachment #9569427 - Flags: approval-mozilla-beta?
Attached file (secure)
Attachment #9569432 - Flags: approval-mozilla-esr140?
Attached file (secure) (obsolete) —
Attachment #9569439 - Flags: approval-mozilla-release?

firefox-beta Uplift Approval Request

  • User impact if declined/Reason for urgency: Leak uninitialized heap memory that might contain sensitive data
  • Code covered by automated testing?: yes
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: yes
  • Steps to reproduce for manual QE testing: Test with Bug 2025883
  • Risk associated with taking this patch: low
  • Explanation of risk level: These code exists for years, no it's not likely to cause merge conflicts, and should not cause regression since it's verified in Nightly and pass all the web-platform-tests that check it behaves as w3c' spec.
  • String changes made/needed?: no
  • Is Android affected?: yes
Flags: qe-verify+
Attachment #9569427 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9569432 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Attachment #9569439 - Attachment is obsolete: true
Attachment #9569439 - Flags: approval-mozilla-release?
Duplicate of this bug: 2027167

Hi Ryan, I've filed uplift requests for beta, release and ESR 140. The webcodecs pref is disabled in ESR 115: https://searchfox.org/firefox-esr115/rev/8f92a41f3658b21ce04a80d10000b9aa125dcbf5/modules/libpref/init/StaticPrefList.yaml#3182, does it need to uplift ESR115 in this case? User would only run into this issue if they manually enable the webcodecs (In ESR 115, there are only partial webcodecs implementations, so not much things users could do even they enable it).

Flags: needinfo?(cchang) → needinfo?(ryanvm)

No, we already had it marked as unaffected and weren't tracking it for uplift. Thanks for doing the Beta and ESR140 requests!

Flags: needinfo?(ryanvm)
QA Whiteboard: [uplift][qa-ver-needed-c151/b150][sec]
Attached image 2022604.jpg

Verified on Firefox for Android Nightly 151.0a1, and Firefox for Android Beta 150.0b9, with an Oppo Find N2 Flip (Android 15), and the results after tapping on "Run" are the ones reflected on the attached screenshot.
Let us know if there is anything else we should test on Android side.
Thank you!

Flags: qe-verify+

Verified fixed on Nightly 151.0a1 (2026-04-14), Firefox 150.0b10 and Firefox ESR 140.10.0 using macOS 13.2.1. Running the attached POC HTML (poc_videoframe_copyto_padding.html) yields the same output as the screenshot in Comment 22.

QA Whiteboard: [uplift][qa-ver-needed-c151/b150][sec] → [uplift][qa-ver-done-c151/b150][sec]
QA Contact: rpopovici
Flags: sec-bounty? → sec-bounty+
Whiteboard: [adv-main150+]
Whiteboard: [adv-main150+] → [adv-main150+][adv-esr140.10+]
Alias: CVE-2026-6748
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: