Assertion failure: prop.isSome() in InitNamespaceOrSourceBinding via import { "*namespace*" as x }
Categories
(Core :: JavaScript Engine, defect, P2)
Tracking
()
| Tracking | Status | |
|---|---|---|
| firefox-esr115 | --- | unaffected |
| firefox-esr140 | --- | unaffected |
| firefox-esr153 | --- | unaffected |
| firefox152 | --- | unaffected |
| firefox153 | --- | unaffected |
| firefox154 | + | fixed |
People
(Reporter: bugmon, Assigned: allstars.chh)
References
(Blocks 1 open bug, Regression)
Details
(6 keywords)
Attachments
(3 files, 2 external files)
|
178 bytes,
text/plain
|
Details | |
|
1.79 KB,
patch
|
Details | Diff | Splinter Review | |
|
5.25 KB,
text/plain
|
Details |
SpiderMonkey represents the ECMAScript spec's out-of-band namespace sentinel (used for import * as foo) as the in-band well-known atom "namespace". Since ES2022, the import { "StringLiteral" as local } syntax allows arbitrary string literals as import names. When a module contains import { "*namespace*" as foo } from "x", the parser atomizes the user-supplied string to the same well-known atom used as the internal sentinel.
During parsing, the import is correctly treated as a named import: foo is recorded as BindingKind::Import (an indirect binding that has no slot on the ModuleEnvironmentObject). During linking, however, ModuleInitializeEnvironment compares importName == cx->names().star_namespace_star_ (Modules.cpp:1458) and mis-dispatches into the import * as foo branch. That branch calls InitNamespaceOrSourceBinding(env, "foo", ns), which looks up a direct property foo on the module environment. No such property exists, so env->lookup() returns Nothing() and the MOZ_ASSERT(prop.isSome()) fires.
In release builds the immediately following prop->slot() is guarded by MOZ_RELEASE_ASSERT(isSome()) inside mozilla::Maybe::operator->(), so the result is a safe controlled crash (content-process DoS) rather than an out-of-bounds slot write. The same sentinel confusion also affects other comparisons in Modules.cpp (lines 1003, 1415, 1472 for "source", 1517), so variants of this testcase may produce additional incorrect-binding behaviour beyond this specific assertion. The bug is reachable from web content via ordinary <script type="module"> — the shell helpers parseModule/registerModule/moduleLink merely drive the same engine paths.
Build Info
- Branch: main
- Revision: 3b0ec2e6ef5bd362c37420922deaddf9d5d5e175
- Timestamp: 2026-06-20T22:36:09+00:00
Affected Code
File: js/src/vm/CommonPropertyNames.h, line 468
MACRO_(star_namespace_star_, "*namespace*")
File: js/src/vm/Modules.cpp, line 1454-1471
localName = in.localName();
importName = in.importName();
// Step 7.b. If in.[[ImportName]] is ~namespace~, then:
if (importName == cx->names().star_namespace_star_) {
// Step 7.b.i. Let namespace be ? GetModuleNamespace(importedModule).
ModuleNamespaceObject* ns =
GetOrCreateModuleNamespace(cx, importedModule);
if (!ns) {
return false;
}
// Step 7.b.ii. Perform ! env.CreateImmutableBinding(in.[[LocalName]], true).
// This happens when the environment is created.
// Step 7.b.iii. Perform ! env.InitializeBinding(in.[[LocalName]], namespace).
InitNamespaceOrSourceBinding(cx, env, localName, ObjectValue(*ns));
}
File: js/src/vm/Modules.cpp, line 1217-1225
static void InitNamespaceOrSourceBinding(JSContext* cx,
ModuleEnvironmentObject* env,
JSAtom* name, const Value& obj) {
// The property already exists in the evironment but is not writable, so set
// the slot directly.
mozilla::Maybe<PropertyInfo> prop = env->lookup(cx, AtomToId(name));
MOZ_ASSERT(prop.isSome());
env->setSlot(prop->slot(), obj);
}
File: mfbt/Maybe.h, line 1004-1008
template <typename T>
constexpr T* Maybe<T>::operator->() {
MOZ_RELEASE_ASSERT(isSome());
return ptr();
}
The parser records the user string "namespace" as the ImportEntry's importName. At link time it is compared by atom identity against the internal namespace sentinel and matches, sending execution down the wrong branch where it tries to write into a non-existent environment slot.
Exploit Chain
- Attacker serves an ES module containing
import { "*namespace*" as foo } from "./dep.js";alongside any dependency moduledep.js. - Frontend parser records ImportEntry { importName: atom("namespace"), localName: atom("foo") } and gives
fooBindingKind::Import (indirect binding, no environment slot). - During JS::ModuleLink → InnerModuleLinking → ModuleInitializeEnvironment, the engine iterates importEntries and tests
importName == cx->names().star_namespace_star_. The user-supplied string atomized to the same well-known atom, so the test is true. - Engine enters the
import * as foobranch and calls InitNamespaceOrSourceBinding(env, atom("foo"), ObjectValue(*ns)). - env->lookup("foo") returns Nothing() because
foowas emitted as an indirect import binding with no slot on the ModuleEnvironmentObject. - Debug: MOZ_ASSERT(prop.isSome()) fires. Release/ASAN: prop->slot() invokes Maybe::operator->() which hits MOZ_RELEASE_ASSERT(isSome()), causing a controlled crash of the content process.
Steps to Reproduce
- Build SpiderMonkey JS shell with --enable-debug (or the ASAN fuzzing config).
- Save testcase.js:
var x = parseModule('export var v = 1;');
registerModule("x", x);
var m = parseModule('import { "namespace" as foo } from "x"; print(foo);');
moduleLink(m);
moduleEvaluate(m); - Run: obj-js-debug/dist/bin/js --fuzzing-safe testcase.js
- Observe: Assertion failure: prop.isSome(), at js/src/vm/Modules.cpp:1223
Security Impact
- Severity: Moderate
- Attacker capability: A web page can deterministically crash the content process (MOZ_RELEASE_ASSERT) by loading an ES module that imports the string literal "namespace" as a named binding. The release-assert in mozilla::Maybe::operator->() prevents the subsequent env->setSlot(uninitialized_index, ...) out-of-bounds write, limiting demonstrated impact to denial of service. The same in-band sentinel ("namespace", and analogously "source") is compared at several other points in Modules.cpp (ResolveExport, indirect-export handling), so related variants may yield additional binding-confusion behaviour worth auditing.
- Preconditions: None beyond loading attacker-controlled ES modules (e.g. visiting a web page with <script type="module">). No non-default prefs, no user interaction beyond navigation, no prior compromise required.
ASAN Report
[6619] Assertion failure: prop.isSome(), at js/src/vm/Modules.cpp:1223
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==6619==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x58cb13cf17d6 bp 0x7ffe70e02260 sp 0x7ffe70e02230 T6619)
==6619==The signal is caused by a WRITE memory access.
==6619==Hint: address points to the zero page.
#0 0x58cb13cf17d6 in MOZ_CrashSequence(void*, long) /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3
#1 0x58cb13cf17d6 in InitNamespaceOrSourceBinding(JSContext*, js::ModuleEnvironmentObject*, JSAtom*, JS::Value const&) /firefox/js/src/vm/Modules.cpp:1223:3
#2 0x58cb13cefec0 in ModuleInitializeEnvironment(JSContext*, JS::Handle<js::ModuleObject*>) /firefox/js/src/vm/Modules.cpp
#3 0x58cb13cef500 in InnerModuleLinking(JSContext*, JS::Handle<js::ModuleObject*>, JS::MutableHandle<JS::GCVector<js::ModuleObject*, 0ul, js::SystemAllocPolicy>>, unsigned long, unsigned long*) /firefox/js/src/vm/Modules.cpp:1972:8
#4 0x58cb13cd6742 in ModuleLink(JSContext*, JS::Handle<js::ModuleObject*>) /firefox/js/src/vm/Modules.cpp:1833:13
#5 0x58cb13cd6742 in JS::ModuleLink(JSContext*, JS::Handle<JSObject*>) /firefox/js/src/vm/Modules.cpp:421:10
#6 0x58cb123f96a6 in ModuleLoadResolved(JSContext*, JS::Handle<JS::Value>) /firefox/js/src/shell/js.cpp:6068:10
#7 0x58cb13cdbe92 in InnerModuleLoading(JSContext*, JS::Handle<js::GraphLoadingStateRecordObject*>, JS::Handle<js::ModuleObject*>, LoadType) /firefox/js/src/vm/Modules.cpp:1702:17
#8 0x58cb13cd6c6f in js::LoadRequestedModules(JSContext*, JS::Handle<js::ModuleObject*>, JS::Handle<JS::Value>, bool (*)(JSContext*, JS::Handle<JS::Value>), bool (*)(JSContext*, JS::Handle<JS::Value>, JS::Handle<JS::Value>)) /firefox/js/src/vm/Modules.cpp:1772:10
#9 0x58cb13cd6ae9 in JS::LoadRequestedModules(JSContext*, JS::Handle<JSObject*>, JS::Handle<JS::Value>, bool (*)(JSContext*, JS::Handle<JS::Value>), bool (*)(JSContext*, JS::Handle<JS::Value>, JS::Handle<JS::Value>)) /firefox/js/src/vm/Modules.cpp:432:10
#10 0x58cb123d8008 in ModuleLink(JSContext*, unsigned int, JS::Value*) /firefox/js/src/shell/js.cpp:6100:8
#11 0x58cb138e1334 in CallJSNative(JSContext*, bool (*)(JSContext*, unsigned int, JS::Value*), js::CallReason, JS::CallArgs const&) /firefox/js/src/vm/Interpreter.cpp:498:13
#12 0x58cb138bc445 in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:594:12
#13 0x58cb138cc2b7 in js::CallFromStack(JSContext*, JS::CallArgs const&, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:666:10
#14 0x58cb138cc2b7 in js::Interpret(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:3280:16
#15 0x58cb138bbe96 in MaybeEnterInterpreterTrampoline(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:392:10
#16 0x58cb138bbada in js::RunScript(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:468:13
#17 0x58cb138be920 in js::ExecuteKernel(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, js::AbstractFramePtr, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:858:10
#18 0x58cb138bed7e in js::Execute(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:888:10
#19 0x58cb13b7e821 in ExecuteScript(JSContext*, JS::Handle<JSObject*>, JS::Handle<JSScript*>, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/CompilationAndEvaluation.cpp:552:10
#20 0x58cb13b7ea08 in JS_ExecuteScript(JSContext*, JS::Handle<JSScript*>) /firefox/js/src/vm/CompilationAndEvaluation.cpp:576:10
#21 0x58cb12400d6d in RunFile(JSContext*, char const*, _IO_FILE*, CompileUtf8, bool, bool) /firefox/js/src/shell/js.cpp:1413:10
#22 0x58cb1240028b in Process(JSContext*, char const*, bool, FileKind) /firefox/js/src/shell/js.cpp
#23 0x58cb123b9299 in ProcessArgs(JSContext*, js::cli::OptionParser*) /firefox/js/src/shell/js.cpp:12138:10
#24 0x58cb123b9299 in Shell(JSContext*, js::cli::OptionParser*) /firefox/js/src/shell/js.cpp:12391:12
#25 0x58cb123b17f3 in main /firefox/js/src/shell/js.cpp:12801:12
SUMMARY: UndefinedBehaviorSanitizer: SEGV /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3 in MOZ_CrashSequence(void*, long)
==6619==ABORTING
| Reporter | ||
Comment 1•4 months ago
|
||
| Reporter | ||
Comment 2•4 months ago
|
||
| Reporter | ||
Comment 3•4 months ago
|
||
| Reporter | ||
Comment 4•4 months ago
|
||
Updated•4 months ago
|
Updated•4 months ago
|
| Assignee | ||
Updated•4 months ago
|
| Assignee | ||
Comment 6•4 months ago
|
||
| Assignee | ||
Comment 7•4 months ago
|
||
Comment 9•4 months ago
|
||
https://hg.mozilla.org/mozilla-central/rev/4da957f1e81e
https://hg.mozilla.org/mozilla-central/rev/fad980a70c8d
Comment 10•4 months ago
|
||
Please add a Beta uplift request for this.
| Assignee | ||
Comment 11•4 months ago
•
|
||
This is a regression from bug 2046502, which is also in Firefox 154.
Comment 12•4 months ago
|
||
For future reference, setting the appropriate bug fields will prevent people spending time chasing down uplifts that aren't actually needed :-)
Comment 13•3 months ago
|
||
Set release status flags based on info from the regressing bug 2046502
Updated•18 days ago
|
Description
•