Closed Bug 2015959 (CVE-2026-6773) Opened 7 months ago Closed 6 months ago

WebGPU: Buffer bounds checks can be bypassed via integer overflow, and the parent process can be crashed through multiple unhandled panics

Categories

(Core :: Graphics: WebGPU, defect, P1)

defect

Tracking

()

RESOLVED FIXED
150 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 --- disabled
firefox148 --- wontfix
firefox149 --- wontfix
firefox150 --- fixed

People

(Reporter: kerberoasting, Assigned: ErichDonGubler)

References

Details

(5 keywords, Whiteboard: [fixed in 2017645][client-bounty-form][adv-main150+])

Attachments

(2 files)

Two related issues in Firefox's WebGPU implementation that compound each other.

The first is that wgpu-core validates buffer operations using plain integer addition (e.g. source_offset + size > buffer.size), but Firefox's release profile compiles Rust with overflow-checks disabled. A compromised content process can send crafted offset+size pairs via PWebGPU::Messages() that wrap around u64, causing the bounds check to pass when it shouldn't. The GPU driver then receives a copy/write command with an absurd size. This affects copy_buffer_to_buffer (transfer.rs:985), queue_write_buffer (queue.rs:663), and get_mapped_range (resource.rs:706). Notably, buffer_map_async already has an overflow guard (start > end check at resource.rs:610), the other functions just don't.

The second is that PWebGPU batches all commands into a single bincode blob, and the parent-side Rust code has 8+ paths that panic on malformed input — .unwrap() on bincode deserialization (server.rs:2417), unchecked array indexing (server.rs:2606), split_at on insufficient data (command.rs:770), from_utf8 on attacker-controlled bytes (command.rs:895), and MOZ_RELEASE_ASSERT on duplicate IDs (WebGPUParent.cpp:1622). Since Firefox compiles with panic=abort, each of these kills the parent process.

Together: the overflow-checks=false setting turns what should be a caught arithmetic error into a silent bypass, and the panic=abort setting turns what should be a handled error into a process kill. On Linux, where the compositor runs in the parent process with no GPU sandbox, both issues directly affect the main browser process.

This is the same vulnerability class as CVE-2025-13021 through CVE-2025-13026 (FF145), but these specific code paths were not patched — they are still present on current trunk.

Attached: full write-up (submission_final.md) and a standalone Rust PoC (crash-harness/) that reproduces the validation logic with Firefox's build profile and demonstrates 3/3 bounds checks being bypassed. The PoC operates in isolation rather than end-to-end, since triggering the overflow requires raw bincode from a compromised content process — the JS API caps values at Number precision (2^53), below the u64 overflow threshold.

=== How discovered ===
Static source code analysis.

=== Attachments ===

  1. submission_final.md (full write-up with line references)
  2. crash-harness/ (Rust PoC — cargo build --release && ./target/release/webgpu-overflow-poc)
Flags: sec-bounty?
Attached file submission_final.md
Attached file crash-harness.tar.gz
Group: firefox-core-security → gfx-core-security
Component: Security → Graphics: WebGPU
Product: Firefox → Core
Duplicate of this bug: 2015958

Tentatively setting P1 and S2, and investigating a fix for the overflows. I'll leave the panics in server.rs for another day.

Severity: -- → S2
Priority: -- → P1

I have a tentative fix committed (but not filed for PR) at erichdongubler-mozilla/wgpu:23440e28f076347a755bb86d5a1c6dd0563e1029. I already see refinements I'd like to make before submitting a PR, but I believe it will work (pending testing).

Erich: did the bounds issues lead to an overflow (read or write) in your testing or analysis?

Assignee: nobody → egubler
Flags: needinfo?(egubler)

:dveditz: I haven't reproduced an overflow yet, but as the description says, this requires a compromised content process that circumvents JS' typical capping of integer values to 53 bits. I'll see if I can make a PoC that simulates a compromised content process, and get back to you.

Leaving the NI open until I have an answer.

The usual way to simulate a compromised content process is to edit the code that is running in the content process to inject bad values in the way that you want.

(In reply to Erich Gubler [:ErichDonGubler] (he/him) from comment #8)

:dveditz: I haven't reproduced an overflow yet, but as the description says, this requires a compromised content process that circumvents JS' typical capping of integer values to 53 bits. I'll see if I can make a PoC that simulates a compromised content process, and get back to you.

Nothing in wgpu-core should be assuming that integer values have been limited to 2**53. wgpu-core's validation should assume that all input values are valid values of their Rust type, but otherwise come straight from an attacker. So if there's a u64 or NonZeroU64, we should assume it could be any value from 0..2**64-1 or 1..2**64-1.

Just spot-checking the commits:

The change in wgpu-core/src/command/transfer.rs looks good to me. It does seem like that is the point at which validation of these copy parameters is being done, so any arithmetic there must be very careful, and assume that values are chosen by the attacker. In particular, that addition source_offset + size is a bug, because it could overflow.

Often times these sorts of checks can be staged such that arithmetic operations are only performed on previously checked values: operations are placed after the validation that shows that they would not overflow. For example, in the case of a source_offset + size addition, with buffer size of buffer_size (retrieved from the buffer itself, and therefore trusted):

  1. You would first check that source_offset <= buffer_size. This makes no assumptions, so it's always safe.
  2. You would then check that size <= buffer_size - source_offset. Since we've already done the check in 1), buffer_size - source_offset is a subtraction of two trusted values, so we know that it will not underflow. And then, as before, the comparison is always safe. Note that the algebraically equivalent source_offset + size <= buffer_size would not be safe, since we haven't done anything to validate size yet.
  3. Then you'd compute source_offset + size, if you really need it.

"Why not just use checked_add for everything?" Using checked_add suggests to the reader that the values being operated on are untrusted, whereas our job in wgpu-core is to establish clear transitions from untrusted to trusted values. So when the checks performed by functions like checked_add match the validation the WebGPU spec requires, that's great, use them. But more often, the validation we need is against the actual size of something, not u64::MAX or what-have-you, and then once we've done the validation that the spec requires, the arithmetic itself can be trusted, as demonstrated in the previous comment.

But more often, the validation we need is against the actual size of something, not u64::MAX or what-have-you

In fact, this is exactly the problem you're getting at in your "TODO" comment:

// TODO: Is this the right error to report?

The checked_add is reporting that something went wrong, but it doesn't have a direct relationship to a requirement found in the spec, and it's not clear what error to report.

The advantage of the approach I've described above is that you're using the validation steps from the spec to establish the safety of arithmetic operations in the implementation, so failures relate directly to something in the spec.

I think this should always be possible: if we come across a situation where our implementation has to do some validation absent from the spec in order to make something safe, that seems like it would point out a loophole in the spec.

See Also: → 1981270

Dveditz comment above wasn't answered. Tentatively giving sec-high.

Status: UNCONFIRMED → NEW
Ever confirmed: true

:dveditz: It's definitely possible to trigger this overflow from usage of the wgpu-core crate, and Firefox exposes those operations to JS. That's something to fix, but whether this arises to sec-high due to exposure through Firefox, I can't say yet, and I've already spent a few hours trying to figure out the exact threat model. I personally am going to prioritize plugging the code in question over spending more time trying to figure out the answer.

I'll tag in :jimb for further analysis of this issue while I work on a fix.

Flags: needinfo?(egubler) → needinfo?(jimb)

Tentative fix PR, currently in draft: wgpu#9073

PR has left draft, and I've requested Jim as a reviewer. There is outstanding work to test things, but that formality can be (fast) follow-up work if need be with confirmation of fix from Richard Belisle.

PR has been merged, and is awaiting a wgpu revendor. I'll roll it into bug 2017645.

Depends on: 2017645

Richard, the instances of problems mentioned in this bug should be fixed now, as of the latest Nightly builds. Can you please confirm?

Flags: needinfo?(kerberoasting)

Hey Erich. The overflow is indeed fixed in Nightly with what looks like size > buffer.size -offset

Additionally, all 5 overflows in the PoC are now rejected. I appreciate y'all. - Richard

Flags: needinfo?(kerberoasting)
Status: NEW → RESOLVED
Closed: 6 months ago
Resolution: --- → FIXED

The overflows try to happen, but Rust does panic on those, right? Beyond crashing the GPU process (or parent in some cases) how could this be turned into an exploit?

See Also: → CVE-2025-13021
Whiteboard: [client-bounty-form] → [fixed in 2017645][client-bounty-form]
Group: gfx-core-security → core-security-release
Flags: sec-bounty? → sec-bounty-

(In reply to Daniel Veditz [:dveditz] from comment #21)

The overflows try to happen, but Rust does panic on those, right? Beyond crashing the GPU process (or parent in some cases) how could this be turned into an exploit?

I'm actually unfamiliar with our Rust configuration for bounds checking in release builds. If we do keep them, then I don't think an exploit is possible. I'm not sure, though.

Howdy. From my review, Firefox's Cargo.toml sets debug-assertions = false in [profile.release] and doesn't specify overflow-checks, which defaults to match, so integer overflow wraps silently in release builds rather than panicking. That said, Rust's bounds checking on slice/array access is always enforced regardless, so wrapped values shouldn't easily lead to OOB memory access through safe code. Theoretically perhaps possible to exploit in other Rust crates but probably not practical.

QA Whiteboard: [sec] [qa-triage-done-c151/b150]
Whiteboard: [fixed in 2017645][client-bounty-form] → [fixed in 2017645][client-bounty-form][adv-main150+]

I don't think this bug needs further help from me.

Flags: needinfo?(jimb)
Alias: CVE-2026-6773
Flags: sec-bounty-hof+
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: