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)
Tracking
()
People
(Reporter: bugmon, Assigned: iain)
References
(Blocks 1 open bug)
Details
(Keywords: ai-involved, assertion, testcase)
Attachments
(3 files, 1 external file)
|
1.55 KB,
text/plain
|
Details | |
|
1.50 KB,
patch
|
Details | Diff | Splinter Review | |
|
4.08 KB,
text/plain
|
Details |
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
- Branch: main
- Revision: 306b3786de3f6229f58cb71597e2ac73a090c3d4
- Timestamp: 2026-07-25T16:17:03+00:00
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
- 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.
- 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'.
- 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.
- 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.
- Call f() in a loop until the JSOp::GetImport site warms up and DoGetImportFallback runs GetImportIRGenerator::tryAttachInitialized (CacheIR.cpp:16865).
- 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
- Build the SpiderMonkey shell with /app/mozconfigs/mozconfig.linux.asan.fuzzing (or /app/mozconfigs/mozconfig.js.linux.debug for the assertion variant).
- Run the testcase with <objdir>/dist/bin/js --fuzzing-safe testcase.js (no other flags needed).
- ASAN build: AddressSanitizer reports SEGV on address 0x28 (READ) in ModuleEnvironmentObject::module() reached from GetImportIRGenerator::tryAttachInitialized.
- 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
| Reporter | ||
Comment 1•3 months ago
|
||
| Reporter | ||
Comment 2•3 months ago
|
||
| Reporter | ||
Comment 3•3 months ago
|
||
Comment 4•3 months ago
|
||
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.
Updated•3 months ago
|
Updated•3 months ago
|
| Assignee | ||
Comment 5•3 months ago
|
||
- 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.
| Assignee | ||
Comment 6•3 months ago
|
||
I based the module->status() < ModuleStatus::Linked check on the check in ModuleObject::environment().
Updated•3 months ago
|
Comment 7•3 months ago
|
||
: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?
| Assignee | ||
Updated•3 months ago
|
| Assignee | ||
Comment 10•3 months ago
|
||
This isn't security-sensitive: it's just a testing function that gives more power than it should (and crashes safely anyway).
Updated•3 months ago
|
Comment 11•3 months ago
|
||
Comment 12•3 months ago
|
||
| bugherder | ||
Updated•2 months ago
|
Description
•