Closed Bug 2009901 (CVE-2026-2801) Opened 6 months ago Closed 5 months ago

OOB access via IntrI8PrepareB()

Categories

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

defect

Tracking

()

RESOLVED FIXED
149 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 --- unaffected
firefox147 --- wontfix
firefox148 + fixed
firefox149 + fixed

People

(Reporter: canalun, Assigned: canalun)

Details

(Keywords: csectype-bounds, sec-moderate, Whiteboard: [client-bounty-form][adv-main148+])

Attachments

(4 files)

695 bytes, text/javascript
Details
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
Attached file poc.js

Summary

In IntrI8PrepareB(), boundary check is incorrect and it leads to OOB access.

The poc uses WebAssembly.mozIntGemm() and this API is available by default in addon context on Nightly.
So the impact is limited to the case in which a user has installed an addon on Nightly.

STR

  1. checkout 6ece603789f6751c3 on autoland (It's the commit used for the latest Nightly 149.0a1 (2026-01-12))
  2. run the poc with ASan-built SpiderMonkey (wasm_moz_intgemm is true by default)
    Here is mozconfig just in case.
$ cat mozconfig
ac_add_options --enable-project=js
ac_add_options --disable-unified-build
ac_add_options --enable-debug-symbols
ac_add_options --enable-address-sanitizer
mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/obj-debug-@CONFIG_GUESS@
  1. it shows the below log.
$ ./js poc.js
AddressSanitizer:DEADLYSIGNAL
=================================================================
==52068==ERROR: AddressSanitizer: BUS on unknown address (pc 0x00010426b0ac bp 0x00016dbd49d0 sp 0x00016dbd48b0 T0)
==52068==The signal is caused by a READ memory access.
==52068==Hint: this fault was caused by a dereference of a high value address (see register values below).  Disassemble the provided pc to learn which register was used.
    #0 0x00010426b0ac in gemmology::Engine<xsimd::neon64>::PrepareB(float const*, signed char*, float, unsigned long, unsigned long)+0x5d8 (js:arm64+0x1020470ac)
    #1 0x0001042679c4 in js::intgemm::IntrI8PrepareB(js::wasm::Instance*, unsigned int, float, float, unsigned int, unsigned int, unsigned int, unsigned char*)+0x3b8 (js:arm64+0x1020439c4)
    #2 0x107000090088  (<unknown module>)
    #3 0x1070000906a0  (<unknown module>)
    #4 0x000103f452f0 in js::wasm::Instance::callExport(JSContext*, unsigned int, JS::CallArgs const&, js::wasm::CoercionLevel)+0xa88 (js:arm64+0x101d212f0)
    #5 0x000103f44778 in WasmCall(JSContext*, unsigned int, JS::Value*)+0x170 (js:arm64+0x101d20778)
    #6 0x0001037f6fd8 in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason)+0x72c (js:arm64+0x1015d2fd8)
    #7 0x00010380baf4 in js::Interpret(JSContext*, js::RunState&)+0xcc0c (js:arm64+0x1015e7af4)
    #8 0x0001037f6088 in js::RunScript(JSContext*, js::RunState&)+0x470 (js:arm64+0x1015d2088)
    #9 0x0001037fa420 in js::ExecuteKernel(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, js::AbstractFramePtr, JS::MutableHandle<JS::Value>)+0x2a0 (js:arm64+0x1015d6420)
    #10 0x0001023ea554 in EvalKernel(JSContext*, JS::Handle<JS::Value>, EvalType, js::AbstractFramePtr, JS::Handle<JSObject*>, unsigned char*, JS::MutableHandle<JS::Value>)+0x1378 (js:arm64+0x1001c6554)
    #11 0x0001023e90dc in js::IndirectEval(JSContext*, unsigned int, JS::Value*)+0x188 (js:arm64+0x1001c50dc)
    #12 0x0001037f6fd8 in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason)+0x72c (js:arm64+0x1015d2fd8)
    #13 0x0001037f85c4 in js::Call(JSContext*, JS::Handle<JS::Value>, JS::Handle<JS::Value>, js::AnyInvokeArgs const&, JS::MutableHandle<JS::Value>, js::CallReason)+0x1dc (js:arm64+0x1015d45c4)
    #14 0x0001037d978c in js::ForwardingProxyHandler::call(JSContext*, JS::Handle<JSObject*>, JS::CallArgs const&) const+0x268 (js:arm64+0x1015b578c)
    #15 0x0001037b63e8 in js::CrossCompartmentWrapper::call(JSContext*, JS::Handle<JSObject*>, JS::CallArgs const&) const+0x334 (js:arm64+0x1015923e8)
    #16 0x0001037c5200 in js::Proxy::call(JSContext*, JS::Handle<JSObject*>, JS::CallArgs const&)+0x230 (js:arm64+0x1015a1200)
    #17 0x0001037f753c in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason)+0xc90 (js:arm64+0x1015d353c)
    #18 0x00010380baf4 in js::Interpret(JSContext*, js::RunState&)+0xcc0c (js:arm64+0x1015e7af4)
    #19 0x0001037f6088 in js::RunScript(JSContext*, js::RunState&)+0x470 (js:arm64+0x1015d2088)
    #20 0x0001037fa938 in js::Execute(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, JS::MutableHandle<JS::Value>)+0x394 (js:arm64+0x1015d6938)
    #21 0x0001038c9604 in JS_ExecuteScript(JSContext*, JS::Handle<JSScript*>)+0x190 (js:arm64+0x1016a5604)
    #22 0x0001022af3d0 in RunFile(JSContext*, char const*, __sFILE*, CompileUtf8, bool, bool)+0x3a0 (js:arm64+0x10008b3d0)
    #23 0x0001022aeab0 in Process(JSContext*, char const*, bool, FileKind)+0x1148 (js:arm64+0x10008aab0)
    #24 0x00010225a7a4 in Shell(JSContext*, js::cli::OptionParser*)+0xfec (js:arm64+0x1000367a4)
    #25 0x00010224eee4 in main+0x6fc (js:arm64+0x10002aee4)
    #26 0x000186ef5d50  (<unknown module>)

==52068==Register values:
 x[0] = 0x0000000300014030   x[1] = 0x00000003000040f0   x[2] = 0x0000000000000000   x[3] = 0x0000000000000018
 x[4] = 0x00000001065fcdc0   x[5] = 0x0000000300004000   x[6] = 0x0000000000000000   x[7] = 0x0000000000000018
 x[8] = 0x0000000000000000   x[9] = 0x0000000300014020  x[10] = 0x0000000000000000  x[11] = 0x0000000000000000
x[12] = 0x0000000060000820  x[13] = 0x00000000000000c0  x[14] = 0x0000000000000020  x[15] = 0x0000000300013000
x[16] = 0x0000000300004110  x[17] = 0x00000001f579eee0  x[18] = 0x0000000000000000  x[19] = 0x0000000300004100
x[20] = 0x000000006000081e  x[21] = 0x0000007000020000  x[22] = 0x00000003000040d0  x[23] = 0x0000000300004140
x[24] = 0x0000000060000810  x[25] = 0x000000006000081c  x[26] = 0x0000000300004120  x[27] = 0x0000000300004130
x[28] = 0x00000003000040e0     fp = 0x000000016dbd49d0     lr = 0x000000010426b564     sp = 0x000000016dbd48b0
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: BUS (js:arm64+0x1020470ac) in gemmology::Engine<xsimd::neon64>::PrepareB(float const*, signed char*, float, unsigned long, unsigned long)+0x5d8
==52068==ABORTING
zsh: abort      ./js poc.js

Root Cause

In js::intgemm::IntrI8PrepareB, boundary check is implemented like this.
https://searchfox.org/firefox-main/source/js/src/intgemm/IntegerGemmIntrinsic.cpp#157

  // Memory Bound and Alignment checks for matricies
  uint64_t sizeB = (uint64_t)rowsB * (uint64_t)colsB;
  size_t wasmBufferSize = GetWasmRawBufferLength(memBase);
  if (!CheckMatrixBoundAndAlignment(inputMatrixB, sizeB, wasmBufferSize) ||
      !CheckMatrixBoundAndAlignment(outputMatrixB, sizeB, wasmBufferSize)) {
    return -1;
  }
bool CheckMatrixBound(uint32_t input, uint64_t inputSize,
                      size_t wasmBufferSize) {
  mozilla::CheckedUint64 inputUpperLimit(inputSize);
  inputUpperLimit += input;

  // Bound check fails if size overflows or it spans outside the wasm memory
  return !(!inputUpperLimit.isValid() ||
           (inputUpperLimit.value() >= (uint64_t)wasmBufferSize));
}

However, the actual access is done in the form of float, so the necessary size is (uint64_t)rowsB * (uint64_t)colsB * 4.
So OOB access happens.

fyi

I would like to work on the fix also. So, I'll submit a patch soon. Let me take for a while.

Flags: sec-bounty?
Group: firefox-core-security → javascript-core-security
Component: Security → JavaScript: WebAssembly
Product: Firefox → Core
Attached file (secure)
Assignee: nobody → i.am.kanaru.sato
Status: NEW → ASSIGNED
Attached file (secure)

I've submitted the patches.

Severity: -- → S3
Type: defect → enhancement
Priority: -- → P3

This is the check we use to enable mozIntGemm [1] 'isSystemOrAddOnPrincipal'.

[1] https://searchfox.org/firefox-main/rev/5917a9f2af3294b27a325371c5c499e7dd9554fd/js/src/wasm/WasmFeatures.cpp#237.

Type: enhancement → defect

Thank you :)
Just in case, I'd like to point out that the API is available for any addons installed on Nightly, because 'isSystemOrAddOnPrincipal' allows an addon context such as content script and background script.

Thank you :)
Just in case, I'd like to point out that the API is available for any addons installed on Nightly, because 'isSystemOrAddOnPrincipal' allows an addon context such as content script and background script.

Yes that seems to be the case. I just mostly wrote that to have a reference for later.

We probably should restrict this to be just available in the system principal. I think we needed this to be available in add-ons when firefox translations was an add-on, but I don't believe that's the case anymore.

Just to confirm, this does look like a valid bounds check by-pass. We are indeed not taking the element size into account when computing the end offset for a bounds check.

I'm not sure how easily it can be exploited, as while the end address for the matrix operation can be outside of wasm memory, the base address still needs to be in wasm memory. And there are guard pages in-between that the matrix operations will need to go through and lead to segfaults.

Flags: sec-bounty?
Group: javascript-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 5 months ago
Resolution: --- → FIXED
Target Milestone: --- → 149 Branch

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

For more information, please visit BugBot documentation.

Flags: needinfo?(i.am.kanaru.sato)

I don't think it has to be uplifted, because the fixed API is available only on Nightly.

:rhunt, I don't have a permission to change the status. Could you please handle it? Thanks.

Flags: needinfo?(i.am.kanaru.sato) → needinfo?(rhunt)

I'm pretty sure the mozIntGemm API is available in beta and release (bug 1876406). I'm still not quite sure if this is worth uplifting to beta due to the low chances of it being exploitable (see comment 6), but it's also a small safe change to make. I'll conservatively request uplift.

Flags: needinfo?(rhunt)

firefox-beta Uplift Approval Request

  • User impact if declined: Malicious add-on could bypass bounds checks and corrupt memory. It's unclear if this is exploitable or if it will always result in a crash from touching guard pages (see comment 6).
  • 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: Simple patch that is tested.
  • String changes made/needed: None
  • Is Android affected?: no
Attachment #9542796 - Flags: approval-mozilla-beta?
Attachment #9542796 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c149/b148]
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main148+]
Alias: CVE-2026-2801

:rhunt

I noticed the patch to add the case has not been landed although it's accepted.
Because I don't have a permission to land a patch, could you please land it instead of me?
Thank you in advance.

Flags: needinfo?(rhunt)
Flags: needinfo?(rhunt)
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: