VideoFrame.copyTo() leaks uninitialized heap bytes due to stride mismatch in copyPlane
Categories
(Core :: Audio/Video: Web Codecs, defect, P2)
Tracking
()
People
(Reporter: y0un9sa, Assigned: chunmin)
References
Details
(4 keywords, Whiteboard: [adv-main150+][adv-esr140.10+])
Attachments
(7 files, 1 obsolete file)
|
3.71 KB,
text/html
|
Details | |
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
tjr
:
sec-approval+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
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 |
|
238.57 KB,
image/jpeg
|
Details |
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:
- Firefox with WebCodecs enabled (dom.media.webcodecs.enabled = true)
- Serve the attached PoC over localhost or https (needs secure context)
- Click "Run"
- 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 - 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)
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
Updated•6 months ago
|
| Assignee | ||
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
| Assignee | ||
Comment 1•5 months ago
|
||
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.
| Assignee | ||
Comment 2•5 months ago
|
||
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).
| Assignee | ||
Updated•5 months ago
|
Updated•5 months ago
|
| Assignee | ||
Comment 3•5 months ago
|
||
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.
| Assignee | ||
Comment 4•5 months ago
|
||
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
Comment 5•5 months ago
|
||
Comment on attachment 9560245 [details]
(secure)
Please set affected release flags
| Assignee | ||
Comment 6•5 months ago
|
||
[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.
Updated•5 months ago
|
Comment 8•5 months ago
|
||
| Assignee | ||
Comment 9•5 months ago
|
||
(In reply to Sebastian Hengst [:aryx] (needinfo me if it's about an intermittent or backout) from comment #8)
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.
Comment 10•5 months ago
|
||
Comment 11•5 months ago
|
||
https://hg.mozilla.org/mozilla-central/rev/9933923586f2
https://hg.mozilla.org/mozilla-central/rev/8ce00411102f
Was there any question related to the needinfo request directed at me?
| Assignee | ||
Comment 13•5 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D290715
Updated•5 months ago
|
| Assignee | ||
Comment 14•5 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D290715
Updated•5 months ago
|
| Assignee | ||
Comment 15•5 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D290715
Updated•5 months ago
|
Comment 16•5 months ago
|
||
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
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
| Assignee | ||
Comment 18•5 months ago
|
||
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).
Comment 19•5 months ago
|
||
No, we already had it marked as unaffected and weren't tracking it for uplift. Thanks for doing the Beta and ESR140 requests!
Comment 20•5 months ago
|
||
| uplift | ||
Updated•5 months ago
|
Comment 21•5 months ago
|
||
| uplift | ||
Updated•5 months ago
|
Comment 22•5 months ago
|
||
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!
Updated•5 months ago
|
Comment 23•4 months ago
•
|
||
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.
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•3 months ago
|
Comment 24•29 days ago
|
||
Comment 25•29 days ago
|
||
| bugherder | ||
https://hg.mozilla.org/mozilla-central/rev/1a35181ab18d
https://hg.mozilla.org/mozilla-central/rev/7b6a5db6121c
https://hg.mozilla.org/mozilla-central/rev/a05b80235786
Updated•14 days ago
|
Description
•