WASM Bounds Check Elimination (BCE) out-of-bounds read/write (when custom page sizes is enabled)
Categories
(Core :: JavaScript: WebAssembly, defect, P1)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox-esr115 | --- | unaffected |
| firefox-esr140 | --- | unaffected |
| firefox147 | --- | disabled |
| firefox148 | --- | disabled |
| firefox149 | --- | fixed |
People
(Reporter: pwning.me, Assigned: bvisness)
References
(Blocks 2 open bugs)
Details
(4 keywords)
Attachments
(1 file, 1 external file)
|
3.40 KB,
text/javascript
|
Details |
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
Steps to reproduce:
Details
The popConstMemoryAccess function in the WebAssembly Baseline compiler incorrectly determines when to eliminate bounds checks. The vulnerable code compares only the effective address against the memory limit, without accounting for the access size.
Vulnerable Code
// js/src/wasm/WasmBCMemory.cpp:164
uint64_t ea = uint64_t(addr) + uint64_t(access->offset32());
uint64_t limit = codeMeta_.memories[access->memoryIndex()].initialLength() +
offsetGuardLimit;
check->omitBoundsCheck = ea < limit; // BUG: ignores access->byteSize()
Correct Fix
check->omitBoundsCheck = (ea + access->byteSize()) <= limit;
Root Cause
The Bounds Check Elimination (BCE) optimization skips runtime bounds verification when the compiler can statically prove an access is safe. The bug is a classic off-by-size error:
- Bug Logic: Skip bounds check if
ea < limit - Correct Logic: Skip bounds check if
ea + byteSize <= limit
When ea < limit but ea + byteSize > limit, the optimization incorrectly eliminates the bounds check, allowing OOB access.
Triggering Conditions
- Custom page sizes enabled (
wasm_custom_page_sizes=true)- Custom page sizes set
offsetGuardLimit = 0(no guard pages) - This is required for the bug window to exist
- Custom page sizes set
- Constant address in WASM (triggers
popConstMemoryAccess) - Access near memory boundary where
ea < limit < ea + byteSize
Trigger Path
- Enable custom page sizes pref
- Create WASM memory with custom page size (e.g., 1-byte pages)
- Use
i32.constto push a constant address near memory limit - Execute
i64.load/i64.store(8-byte access) that crosses boundary - BCE incorrectly skips bounds check -> OOB access occurs
Exploitation
Primitive Gained
- OOB Read: Information leak from memory beyond WASM buffer
- OOB Write: Memory corruption beyond WASM buffer
Exploitation Window
- Maximum OOB:
byteSize - 1bytes (up to 15 bytes for v128) - The OOB access occurs within the system page-aligned allocation
Actual results:
anya@subagent ~/w/f/o/j/d/bin> ./js --setpref wasm_custom_page_sizes=true /tmp/poc.js main?
Valid read at offset 4088: 0x4847464544434241
Expected: 0x4847464544434241
Status: PASS
OOB read at offset 4095:
Result: 0x5806c455d24048
Status: BOUNDS CHECK BYPASSED - Read succeeded!
Byte Analysis:
Byte 0 (offset 4095): 0x48 [valid] = 'H' (our data)
Byte 1 (offset 4096): 0x40 [OOB LEAK]
Byte 2 (offset 4097): 0xd2 [OOB LEAK]
Byte 3 (offset 4098): 0x55 [OOB LEAK]
Byte 4 (offset 4099): 0xc4 [OOB LEAK]
Byte 5 (offset 4100): 0x06 [OOB LEAK]
Byte 6 (offset 4101): 0x58 [OOB LEAK]
Byte 7 (offset 4102): 0x00 [OOB LEAK]
TEST 2: Information Leak Statistical Analysis
----------------------------------------------------------------------
Samples collected: 100
Non-zero leaks: 5 (5.0%)
Pointer-like values: 0
Unique values: 5
leaked ptrs:
0x0070870ba8b280
0xda001020010324
0x0070870a91a000
0x006c870a638000
0x006c87091f5000
Updated•10 months ago
|
Updated•10 months ago
|
Updated•10 months ago
|
Comment 1•10 months ago
|
||
This check has been around a long time, much longer than custom page sizes. Is it just easier to abuse with small pages, or are standard pages actually safe for some other reason?
Updated•10 months ago
|
Comment 2•10 months ago
|
||
Preliminary thought: before custom page sizes, we could rely on access beyond the end to trap, at least within a page or so (https://searchfox.org/firefox-main/source/js/src/wasm/WasmMemory.cpp#289-292). But with custom page sizes, this is no longer the case. It does seem like a real bug, at first glance.
| Assignee | ||
Comment 3•10 months ago
|
||
Yes, we generally rely on guard pages to catch accesses beyond the end of memory, including the byte size of the access. But indeed that is different with custom page sizes, since with one-byte “pages” the only thing to do is a proper bounds check. I will audit things this morning, since it’s possible that there are some other cases where this could trigger a problem, but I think this will likely be unique to custom page sizes.
| Assignee | ||
Comment 4•10 months ago
|
||
This is legit but low-impact due to several mitigating factors:
- It requires custom page sizes, which are disabled by default.
- You can only read or write at most 15 bytes past the end of memory, since 16 bytes is the largest access size in wasm.
- The memory size must be a multiple of the system page size (or close to it) in order to access any meaningful data. This is because we round the actual allocation for the wasm memory up to the system page size so it is compatible with mmap/VirtualAlloc/etc.
If for example the memory size was 4000 bytes instead of 4096 (as in the poc), we would still allocate 4096 bytes for the memory. You could then do a load from address 3999, incorrectly accessing bytes 4000-4014, but yielding no interesting data because it is still part of the wasm memory allocation.
Additionally, it does not seem that any other aspects of bounds checking are affected. All other bounds checking paths in Baseline seem correct, and custom page sizes are not enabled yet in Ion.
Because this seems low-severity we will just put up a patch to fix this.
| Assignee | ||
Comment 5•10 months ago
|
||
Access size was not accounted for, allowing for small reads off the end
of memory under specific circumstances, and only with custom page sizes
enabled.
Updated•10 months ago
|
Comment 7•9 months ago
|
||
Updated•8 months ago
|
Updated•8 months ago
|
Comment 8•8 months ago
|
||
Unfortunately this bug is not eligible for our bounty program because it requires changing hidden prefs. Features that are enabled by default in "Nightly" and not "Release" can be eligible for the bounty, but if they are not even enabled on nightly then they are unfinished and untested and off-limits for the bounty program.
Updated•3 months ago
|
Description
•