Null-deref crash [@ JSFunction::getOrCreateScript] via GenerateLcovInfo on self-hosted dummy function
Categories
(Core :: JavaScript Engine, defect, P3)
Tracking
()
People
(Reporter: bugmon, Unassigned)
References
(Blocks 1 open bug)
Details
(Keywords: ai-involved, assertion, testcase)
Attachments
(3 files)
|
131 bytes,
text/plain
|
Details | |
|
653 bytes,
patch
|
Details | Diff | Splinter Review | |
|
4.39 KB,
text/plain
|
Details |
GenerateLcovInfo() in js/src/vm/BytecodeUtil.cpp iterates all top-level scripts in a realm and, for each, walks script->gcthings() calling JSFunction::getOrCreateScript() on every JSFunction it finds. The code asserts (debug) and assumes (release) that any JSFunction stored in a script's gcthings is interpreted (has BASESCRIPT or SELFHOSTLAZY). This assumption is wrong for the per-realm self-hosted top-level script.
When self-hosted code in a realm needs a computed intrinsic (triggered here by Map iteration), GetComputedIntrinsic() calls CompilationStencil::instantiateSelfHostedTopLevelForRealm(). That function intentionally does NOT instantiate the thousands of inner self-hosted functions; instead it allocates a single native placeholder via NewNativeFunction(cx, SelfHostedDummyFunction, 0, nullptr) and stuffs that same native JSFunction into every function slot of the top-level script's gcthings. This placeholder has FunctionFlags == 0x0 — it is a native function with no BaseScript and no SelfHostedLazyScript.
When getLcovInfo() (or the JS_PUBLIC_API js::GetCodeCoverageSummary) is subsequently invoked on that realm, GenerateLcovInfo picks up the self-hosted top-level script (isTopLevel()==true, selfHosted()==true), iterates its gcthings, and passes the dummy native function to JSFunction::getOrCreateScript(). In debug builds this hits MOZ_ASSERT(fun->isInterpreted()). In release/ASAN builds it falls through to fun->baseScript()->hasBytecode(); baseScript() returns the nativeJitInfoOrInterpretedScript slot (nullptr for this native), and hasBytecode() reads sharedData_ at offset 0x48 from null, causing a SEGV. This is a deterministic null-pointer-plus-constant crash. It requires code coverage to be enabled and the coverage-summary API to be called; it is not reachable from web content in a default Firefox configuration, so severity is Low (DoS in testing/coverage tooling).
Build Info
- Branch: main
- Revision: d4a2cc2aa48ba5e56ce90680e90038843a7298c7
- Timestamp: 2026-07-01T00:30:50+00:00
Affected Code
File: js/src/vm/BytecodeUtil.cpp, line 2997-3017
auto gcthings = script->gcthings();
for (JS::GCCellPtr gcThing : mozilla::Reversed(gcthings)) {
if (!gcThing.is<JSObject>()) {
continue;
}
JSObject* obj = &gcThing.as<JSObject>();
if (!obj->is<JSFunction>()) {
continue;
}
fun = &obj->as<JSFunction>();
MOZ_ASSERT(fun->isInterpreted()); // <-- fires in debug; no runtime guard in release
// Queue the script in the list of script associated to the
// current source.
JSScript* childScript = JSFunction::getOrCreateScript(cx, fun); // <-- null-deref in release
if (!childScript || !queue.append(childScript)) {
return false;
}
}
File: js/src/vm/JSFunction.h, line 480-498
static JSScript* getOrCreateScript(JSContext* cx, js::HandleFunction fun) {
MOZ_ASSERT(fun->isInterpreted());
MOZ_ASSERT(cx);
if (fun->hasSelfHostedLazyScript()) {
if (!delazifySelfHostedLazyFunction(cx, fun)) {
return nullptr;
}
return fun->nonLazyScript();
}
MOZ_ASSERT(fun->hasBaseScript());
if (!fun->baseScript()->hasBytecode()) { // <-- baseScript() == nullptr for native dummy
if (!delazifyLazilyInterpretedFunction(cx, fun)) {
return nullptr;
}
}
return fun->nonLazyScript();
}
File: js/src/frontend/Stencil.cpp, line 2913-2944
JSScript* CompilationStencil::instantiateSelfHostedTopLevelForRealm(
JSContext* cx, CompilationInput& input) {
MOZ_ASSERT(isInitialStencil());
Rooted<CompilationGCOutput> gcOutput(cx);
gcOutput.get().sourceObject = SelfHostingScriptSourceObject(cx);
if (!gcOutput.get().sourceObject) {
return nullptr;
}
// The top-level script has ScriptIndex references in its gcthings list, but
// we do not want to instantiate those functions here since they are instead
// created on demand from the stencil. Create a dummy function and populate
// the functions array of the CompilationGCOutput with references to it.
RootedFunction dummy(
cx, NewNativeFunction(cx, SelfHostedDummyFunction, 0, nullptr)); // <-- native placeholder
if (!dummy) {
return nullptr;
}
if (!gcOutput.get().functions.allocateWith(dummy, scriptData.size())) {
ReportOutOfMemory(cx);
return nullptr;
}
if (!InstantiateTopLevel(cx, input, *this, gcOutput.get())) {
return nullptr;
}
return gcOutput.get().script;
}
File: js/src/vm/SelfHosting.cpp, line 2332-2352
static bool GetComputedIntrinsic(JSContext* cx, Handle<PropertyName*> name,
MutableHandleValue vp) {
// If the intrinsic was not in hardcoded set, run the top-level of the
// selfhosted script. ...
Rooted<NativeObject*> computedIntrinsicsHolder(
cx, cx->global()->getComputedIntrinsicsHolder());
if (!computedIntrinsicsHolder) {
...
// Instantiate a script in current realm from the shared Stencil.
JSRuntime* runtime = cx->runtime();
RootedScript script(
cx, runtime->selfHostStencil().instantiateSelfHostedTopLevelForRealm(
cx, runtime->selfHostStencilInput())); // <-- creates the top-level script in realm g
GenerateLcovInfo assumes every JSFunction in a top-level script's gcthings is interpreted and calls getOrCreateScript on it without checking. instantiateSelfHostedTopLevelForRealm deliberately places a native dummy function into those gcthings, violating that assumption.
Exploit Chain
- Enable code coverage for the process (--code-coverage shell flag / JS_CODE_COVERAGE_OUTPUT_DIR).
- Create a fresh global g and run JS in it that exercises a self-hosted builtin requiring a computed intrinsic (e.g. for-of over a Map, which delazifies MapIteratorNext and triggers GetComputedIntrinsic).
- GetComputedIntrinsic calls instantiateSelfHostedTopLevelForRealm, which materialises the self-hosted top-level JSScript in realm g and fills every inner-function gcthing slot with the native SelfHostedDummyFunction placeholder (FunctionFlags == 0x0).
- Call getLcovInfo(g) (fuzzing-safe testing function) or the JS_PUBLIC_API js::GetCodeCoverageSummary on realm g.
- GenerateLcovInfo enumerates realm g's scripts via IterateScripts, finds the self-hosted top-level script (isTopLevel()==true), and walks its gcthings.
- For each gcthing JSFunction (the native dummy), MOZ_ASSERT(fun->isInterpreted()) fires in debug. In release, JSFunction::getOrCreateScript proceeds to fun->baseScript()->hasBytecode(); baseScript() returns nullptr (nativeJitInfoOrInterpretedScript slot is null for a plain native), and reading sharedData_ at offset 0x48 SEGVs.
Steps to Reproduce
- Build the SpiderMonkey shell with the ASAN fuzzing mozconfig (or the debug mozconfig).
- Save testcase.js containing:
var g = newGlobal({sameZoneAs: this});
g.eval("for (var x of new Map([[1,2]])) {}");
getLcovInfo(g); - Run: obj-dir/dist/bin/js --fuzzing-safe --code-coverage testcase.js
- Observe SEGV at address 0x48 in JSFunction::getOrCreateScript (ASAN) or 'Assertion failure: fun->isInterpreted()' at BytecodeUtil.cpp:3009 (debug).
Security Impact
- Severity: Low
- Attacker capability: Denial of service via a deterministic near-null read (fixed offset 0x48 into the unmapped zero page). No attacker control over the faulting address, so this is not exploitable for memory corruption or information disclosure — it is a safe crash. Impact is limited to crashing SpiderMonkey embedders that enable LCov code coverage and invoke the js::GetCodeCoverageSummary API (or the getLcovInfo testing function) on a realm that has run self-hosted builtins.
- Preconditions: Process-wide code coverage must be enabled (coverage::IsLCovEnabled()), and the embedder/test harness must call js::GetCodeCoverageSummary / getLcovInfo on the affected realm. Not reachable from web content in a default Firefox configuration; primarily affects fuzzing, CI coverage runs, and JS shell testing.
ASAN Report
AddressSanitizer:DEADLYSIGNAL
=================================================================
==8125==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000048 (pc 0x5b8670fcebcf bp 0x7ffdf347e670 sp 0x7ffdf347e660 T0)
==8125==The signal is caused by a READ memory access.
==8125==Hint: address points to the zero page.
#0 0x5b8670fcebcf in RefPtr<js::SharedImmutableScriptData>::operator bool() const /firefox/obj-firefox-asan/dist/include/mozilla/RefPtr.h:335:45
#1 0x5b8670fcebcf in js::BaseScript::hasBytecode() const /firefox/js/src/vm/JSScript.h:1745:9
#2 0x5b8670fcebcf in JSFunction::getOrCreateScript(JSContext*, JS::Handle<JSFunction*>) /firefox/js/src/vm/JSFunction.h:493:29
#3 0x5b8672ed74a4 in GenerateLcovInfo(JSContext*, JS::Realm*, js::GenericPrinter&) /firefox/obj-firefox-asan/js/src/vm/./../../../../js/src/vm/BytecodeUtil.cpp:3013:31
#4 0x5b8672ed7b29 in js::GetCodeCoverageSummary(JSContext*, unsigned long*) /firefox/obj-firefox-asan/js/src/vm/./../../../../js/src/vm/BytecodeUtil.cpp:3049:8
#5 0x5b867141e64e in GetLcovInfo(JSContext*, unsigned int, JS::Value*) /firefox/obj-firefox-asan/js/src/builtin/./../../../../js/src/builtin/TestingFunctions.cpp:8745:15
#6 0x5b8672b8aaa5 in CallJSNative(JSContext*, bool (*)(JSContext*, unsigned int, JS::Value*), js::CallReason, JS::CallArgs const&) /firefox/js/src/vm/Interpreter.cpp:498:13
#7 0x5b8672b8aaa5 in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:594:12
#8 0x5b8672ba8416 in InternalCall(JSContext*, js::AnyInvokeArgs const&, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:661:10
#9 0x5b8672ba8416 in js::CallFromStack(JSContext*, JS::CallArgs const&, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:666:10
#10 0x5b8672ba8416 in js::Interpret(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:3280:16
#11 0x5b8672b89b46 in MaybeEnterInterpreterTrampoline(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:392:10
#12 0x5b8672b89b46 in js::RunScript(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:468:13
#13 0x5b8672b8e5f7 in js::ExecuteKernel(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, js::AbstractFramePtr, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:858:10
#14 0x5b8672f0b2f8 in ExecuteScript(JSContext*, JS::Handle<JSObject*>, JS::Handle<JSScript*>, JS::MutableHandle<JS::Value>) /firefox/obj-firefox-asan/js/src/vm/./../../../../js/src/vm/CompilationAndEvaluation.cpp:544:10
#15 0x5b8672f0b5f2 in JS_ExecuteScript(JSContext*, JS::Handle<JSScript*>) /firefox/obj-firefox-asan/js/src/vm/./../../../../js/src/vm/CompilationAndEvaluation.cpp:568:10
#16 0x5b8670fa9227 in RunFile(JSContext*, char const*, _IO_FILE*, CompileUtf8, bool, bool) /firefox/obj-firefox-asan/js/src/shell/./../../../../js/src/shell/js.cpp:1412:10
#17 0x5b8670fa88cb in Process(JSContext*, char const*, bool, FileKind) /firefox/obj-firefox-asan/js/src/shell/./../../../../js/src/shell/js.cpp
#18 0x5b8670f527e4 in ProcessArgs(JSContext*, js::cli::OptionParser*) /firefox/obj-firefox-asan/js/src/shell/./../../../../js/src/shell/js.cpp:12137:10
#19 0x5b8670f527e4 in Shell(JSContext*, js::cli::OptionParser*) /firefox/obj-firefox-asan/js/src/shell/./../../../../js/src/shell/js.cpp:12389:12
#20 0x5b8670f3f646 in main /firefox/obj-firefox-asan/js/src/shell/./../../../../js/src/shell/js.cpp:12801:12
#21 0x7b8b36a6f1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
#22 0x7b8b36a6f28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 8e9fd827446c24067541ac5390e6f527fb5947bb)
#23 0x5b8670e45928 in _start (/firefox/obj-firefox-asan/dist/bin/js+0x2dcd928) (BuildId: 1554468046c4c8c53fcf42c78d7d0571)
SUMMARY: AddressSanitizer: SEGV /firefox/obj-firefox-asan/dist/include/mozilla/RefPtr.h:335:45 in RefPtr<js::SharedImmutableScriptData>::operator bool() const
==8125==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
|
||
| Reporter | ||
Comment 5•4 months ago
|
||
| Reporter | ||
Comment 6•4 months ago
|
||
| Reporter | ||
Comment 7•4 months ago
|
||
Updated•4 months ago
|
Updated•4 months ago
|
Comment 9•2 months ago
|
||
Fixed by bug 2051478.
Description
•