Closed Bug 2058668 Opened 3 months ago Closed 3 months ago

Null-pointer dereference in [@ js::jit::GetImportIRGenerator::tryAttachInitialized] after cyclic module link failure resets an initialized module to Unlinked

Categories

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

defect

Tracking

()

RESOLVED FIXED
155 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 --- wontfix
firefox-esr153 --- wontfix
firefox153 --- wontfix
firefox154 --- wontfix
firefox155 --- fixed

People

(Reporter: bugmon, Assigned: iain)

References

(Blocks 1 open bug)

Details

(Keywords: ai-involved, assertion, testcase)

Attachments

(3 files, 1 external file)

When linking a cyclic module group, InnerModuleLinking initializes module environments in DFS post-order. In the group {a, b}, module b's environment is fully initialized - import bindings inserted and function declarations instantiated as live closures (vm/Modules.cpp:1583) - before module a's InitializeEnvironment fails on an unresolvable import. ModuleLink's abrupt-completion path (vm/Modules.cpp:1850-1858) then resets every still-Linking module in the SCC, including the already-initialized b, back to Unlinked.

A function instantiated in b's environment remains callable; the testcase extracts it with the shell testing helper getModuleEnvironmentValue (js/src/shell/js.cpp:6200), which reads from the module's initial environment regardless of link status. When the function's JSOp::GetImport warms up into the baseline IC, GetImportIRGenerator::tryAttachInitialized (js/src/jit/CacheIR.cpp:16865-16872) calls GetModuleEnvironmentForScript(script_), which returns ModuleObject::environment(); that returns nullptr for any module with status() < Linked (js/src/builtin/ModuleObject.cpp:1138-1149). The only guard is a debug-only MOZ_ASSERT(env). The code then calls env->lookupImport(), which reads the MODULE_SLOT reserved slot through the null pointer (vm/EnvironmentObject.cpp:534,538,558), producing a SEGV READ at address 0x28 in release/ASAN builds and an assertion failure in debug builds. The same MOZ_ASSERT-only pattern exists in AddWarpGetImport (js/src/jit/WarpOracle.cpp:239-244) for Ion compilation.

The demonstrated impact is a deterministic read through a null pointer at a fixed small offset (0x28), i.e. a crash. The inconsistent engine state (a callable closure over the environment of an Unlinked module) is a genuine invariant violation, but the testcase reaches it only via shell testing functions available under --fuzzing-safe; no web-content path to extract such a closure was demonstrated.

Build Info

Affected Code

File: js/src/jit/CacheIR.cpp, line 16865-16872

AttachDecision GetImportIRGenerator::tryAttachInitialized() {
  ModuleEnvironmentObject* env = GetModuleEnvironmentForScript(script_);
  MOZ_ASSERT(env);

  jsid id = NameToId(script_->getName(pc_));
  ModuleEnvironmentObject* holderEnv;
  Maybe<PropertyInfo> prop;
  MOZ_ALWAYS_TRUE(env->lookupImport(id, &holderEnv, &prop));

File: js/src/builtin/ModuleObject.cpp, line 1138-1149

ModuleEnvironmentObject* ModuleObject::environment() const {
  // Note that this it's valid to call this even if there was an error
  // evaluating the module.

  // According to the spec the environment record is created during linking, but
  // we create it earlier than that.
  if (status() < ModuleStatus::Linked) {
    return nullptr;
  }

  return &initialEnvironment();
}

File: js/src/vm/EnvironmentObject.cpp, line 533-539

ModuleObject& ModuleEnvironmentObject::module() const {
  return getReservedSlot(MODULE_SLOT).toObject().as<ModuleObject>();  // <- null 'this': read at offset 0x28
}

IndirectBindingMap& ModuleEnvironmentObject::importBindings() const {
  return module().importBindings();
}

File: js/src/vm/Modules.cpp, line 1850-1858

  if (!ok) {
    // Step 4.a. For each Cyclic Module Record m of stack, do:
    for (ModuleObject* m : stack) {
      // Step 4.a.i. Assert: m.[[Status]] is linking.
      MOZ_ASSERT(m->status() == ModuleStatus::Linking);
      // Step 4.a.ii. Set m.[[Status]] to unlinked.
      m->setStatus(ModuleStatus::Unlinked);
      m->clearDfsAncestorIndex();
    }

File: js/src/jit/WarpOracle.cpp, line 235-244

[[nodiscard]] static bool AddWarpGetImport(TempAllocator& alloc,
                                           WarpOpSnapshotList& snapshots,
                                           uint32_t offset, JSScript* script,
                                           PropertyName* name) {
  ModuleEnvironmentObject* env = GetModuleEnvironmentForScript(script);
  MOZ_ASSERT(env);

  mozilla::Maybe<PropertyInfo> prop;
  ModuleEnvironmentObject* targetEnv;
  MOZ_ALWAYS_TRUE(env->lookupImport(NameToId(name), &targetEnv, &prop));

The IC generator trusts GetModuleEnvironmentForScript to be non-null with only a debug assert; ModuleObject::environment() returns nullptr for unlinked modules; ModuleLink's failure rollback creates the unlinked-but-initialized state; WarpOracle has the identical unguarded pattern.

Exploit Chain

  1. Register two mutually-importing modules: a imports a non-existent export 'nope' from b; b imports 'x' from a and declares an exported function f() that reads the imported x via JSOp::GetImport.
  2. Call moduleLink(a). InnerModuleLinking initializes b's environment first (import bindings created, f instantiated as a closure over b's ModuleEnvironmentObject via Modules.cpp:1583), then fails resolving a's import 'nope'.
  3. ModuleLink's error path (Modules.cpp:1850-1858) resets both modules to Unlinked, leaving b in an inconsistent state: initialized environment and a live closure, but status Unlinked.
  4. Extract f from b's initial environment with the shell helper getModuleEnvironmentValue(B, 'f') (js/src/shell/js.cpp:6200), which does not check link status.
  5. Call f() in a loop until the JSOp::GetImport site warms up and DoGetImportFallback runs GetImportIRGenerator::tryAttachInitialized (CacheIR.cpp:16865).
  6. GetModuleEnvironmentForScript returns nullptr because b.status() < Linked (ModuleObject.cpp:1144); the MOZ_ASSERT(env) guard compiles out in release builds, and env->lookupImport() reads the MODULE_SLOT reserved slot through the null pointer - SEGV READ at 0x28.

Steps to Reproduce

  1. Build the SpiderMonkey shell with /app/mozconfigs/mozconfig.linux.asan.fuzzing (or /app/mozconfigs/mozconfig.js.linux.debug for the assertion variant).
  2. Run the testcase with <objdir>/dist/bin/js --fuzzing-safe testcase.js (no other flags needed).
  3. ASAN build: AddressSanitizer reports SEGV on address 0x28 (READ) in ModuleEnvironmentObject::module() reached from GetImportIRGenerator::tryAttachInitialized.
  4. Debug build: 'Assertion failure: env, at js/src/jit/CacheIR.cpp:16867'.

Security Impact

  • Severity: Low
  • Attacker capability: Deterministic crash of the JS engine process via a read through a null pointer at fixed offset 0x28 (an unmapped page). No attacker control over the dereferenced address was demonstrated; the crash occurs before any IC stub is emitted from the bad state.
  • Preconditions: Requires the SpiderMonkey shell's module testing functions (parseModule, registerModule, moduleLink, getModuleEnvironmentValue), which are available under --fuzzing-safe. getModuleEnvironmentValue is the only demonstrated way to obtain a callable function from a module that was rolled back to Unlinked; no web-content-reachable path to that state was demonstrated.

ASAN Report

==214967==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000028 (pc 0x5b36d13e3541 bp 0x7ffda2e5f130 sp 0x7ffda2e5f110 T0)
==214967==The signal is caused by a READ memory access.
==214967==Hint: address points to the zero page.
    #0 0x5b36d13e3541 in JSObject* JS::Value::unboxGCPointer<JSObject, (JSValueTag)131068>() const /firefox/obj-firefox-asan/dist/include/js/Value.h:763:51
    #1 0x5b36d13e3541 in JS::Value::toObject() const /firefox/obj-firefox-asan/dist/include/js/Value.h:965:13
    #2 0x5b36d13e3541 in js::ModuleEnvironmentObject::module() const /firefox/js/src/vm/EnvironmentObject.cpp:534:39
    #3 0x5b36d13e3541 in js::ModuleEnvironmentObject::importBindings() const /firefox/js/src/vm/EnvironmentObject.cpp:538:10
    #4 0x5b36d13e3541 in js::ModuleEnvironmentObject::lookupImport(JS::PropertyKey, js::ModuleEnvironmentObject**, mozilla::Maybe<js::PropertyInfoBase<unsigned int>>*) /firefox/js/src/vm/EnvironmentObject.cpp:558:10
    #5 0x5b36d07d8960 in js::jit::GetImportIRGenerator::tryAttachInitialized() /firefox/js/src/jit/CacheIR.cpp:16872:3
    #6 0x5b36d07d8fcc in js::jit::GetImportIRGenerator::tryAttachStub() /firefox/js/src/jit/CacheIR.cpp:16889:3
    #7 0x5b36d0248be8 in void js::jit::TryAttachStub<js::jit::GetImportIRGenerator>(char const*, JSContext*, js::jit::BaselineFrame*, js::jit::ICFallbackStub*) /firefox/js/src/jit/BaselineIC.cpp:523:17
    #8 0x5b36d0248be8 in js::jit::DoGetImportFallback(JSContext*, js::jit::BaselineFrame*, js::jit::ICFallbackStub*, JS::MutableHandle<JS::Value>) /firefox/js/src/jit/BaselineIC.cpp:2701:3
    #9 0x3f2463bfca7b  (<unknown module>)
SUMMARY: AddressSanitizer: SEGV /firefox/obj-firefox-asan/dist/include/js/Value.h:763:51 in JSObject* JS::Value::unboxGCPointer<JSObject, (JSValueTag)131068>() const
==214967==ABORTING

Debug build corroboration (obj-js-debug/dist/bin/js --fuzzing-safe, same testcase):
[215336] Assertion failure: env, at js/src/jit/CacheIR.cpp:16867
    #1 js::jit::GetImportIRGenerator::tryAttachInitialized() /firefox/js/src/jit/CacheIR.cpp:16867:3
    #2 js::jit::GetImportIRGenerator::tryAttachStub() /firefox/js/src/jit/CacheIR.cpp:16889:3
    #3 js::jit::TryAttachStub<js::jit::GetImportIRGenerator> /firefox/js/src/jit/BaselineIC.cpp:523:17
    #4 js::jit::DoGetImportFallback /firefox/js/src/jit/BaselineIC.cpp:2701:3
Attached file testcase.js —
Attached patch iter_4.patch — — Splinter Review
Attached file crash_stack.txt —

This bug was filed automatically by an AI-powered security testing tool. The analysis, testcase, and any suggested patches were generated by an AI model and have NOT been verified by a human.

Please review all details carefully before triaging. The crash is real (confirmed by ASAN), but the root cause analysis and suggested fix may contain errors.

WARNING: A blocking issue was auto-patched in the source used for this analysis. Please flag if you believe the bug was introduced by that patch.

Group: core-security → javascript-core-security
Blocks: sm-security
Severity: -- → S3
Priority: -- → P3
  1. Extract f from b's initial environment with the shell helper getModuleEnvironmentValue(B, 'f') (js/src/shell/js.cpp:6200), which does not check link status.

It seems to me that the right fix here is to check link status in the shell helper.

I based the module->status() < ModuleStatus::Linked check on the check in ModuleObject::environment().

Assignee: nobody → iireland
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true

:iain, please update all release status flags to reflect the affected/unaffected/disabled state correctly. Which branches (beta, release, and/or ESR) are affected by this flaw?

Flags: needinfo?(iireland)
Pushed by iireland@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/36bb0f76c9fb https://hg.mozilla.org/integration/autoland/rev/c0e3bad064ce Fix getModuleEnvironmentValue for unlinked modules r=spidermonkey-reviewers,jonco
Pushed by smolnar@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/d64e4551315b https://hg.mozilla.org/integration/autoland/rev/ec3d2c14a630 Revert "Bug 2058668: Fix getModuleEnvironmentValue for unlinked modules r=spidermonkey-reviewers,jonco" for causint spider-monkey failures.
Group: javascript-core-security
Keywords: sec-other

This isn't security-sensitive: it's just a testing function that gives more power than it should (and crashes safely anyway).

Attachment #9623422 - Attachment description: (secure) → Bug 2058668: Fix getModuleEnvironmentValue for unlinked modules r=#spidermonkey-reviewers
Pushed by iireland@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/ac2c159f0267 https://hg.mozilla.org/integration/autoland/rev/aa5d2a4ffca3 Fix getModuleEnvironmentValue for unlinked modules r=spidermonkey-reviewers,jonco
Status: ASSIGNED → RESOLVED
Closed: 3 months ago
Resolution: --- → FIXED
Target Milestone: --- → 155 Branch
QA Whiteboard: [qa-triage-done-c156/b155]
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: