Closed Bug 2053160 Opened 4 months ago Closed 2 months ago

Null-deref crash [@ JSFunction::getOrCreateScript] via GenerateLcovInfo on self-hosted dummy function

Categories

(Core :: JavaScript Engine, defect, P3)

defect

Tracking

()

RESOLVED DUPLICATE of bug 2051478

People

(Reporter: bugmon, Unassigned)

References

(Blocks 1 open bug)

Details

(Keywords: ai-involved, assertion, testcase)

Attachments

(3 files)

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

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

  1. Enable code coverage for the process (--code-coverage shell flag / JS_CODE_COVERAGE_OUTPUT_DIR).
  2. 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).
  3. 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).
  4. Call getLcovInfo(g) (fuzzing-safe testing function) or the JS_PUBLIC_API js::GetCodeCoverageSummary on realm g.
  5. GenerateLcovInfo enumerates realm g's scripts via IterateScripts, finds the self-hosted top-level script (isTopLevel()==true), and walks its gcthings.
  6. 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

  1. Build the SpiderMonkey shell with the ASAN fuzzing mozconfig (or the debug mozconfig).
  2. Save testcase.js containing:
    var g = newGlobal({sameZoneAs: this});
    g.eval("for (var x of new Map([[1,2]])) {}");
    getLcovInfo(g);
  3. Run: obj-dir/dist/bin/js --fuzzing-safe --code-coverage testcase.js
  4. 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
Attached file verifier-0.jsonl —
Attached file summarizer.jsonl —
Attached file patch.jsonl —
Attached file analyzer.jsonl —
Attached file testcase.js —
Attached patch fix.patch — — Splinter Review
Attached file crash_stack.txt —
Group: core-security → javascript-core-security
Blocks: sm-security
Severity: -- → S3
Priority: -- → P3

Fixed by bug 2051478.

Group: javascript-core-security
Status: UNCONFIRMED → RESOLVED
Closed: 2 months ago
Duplicate of bug: 2051478
Keywords: sec-other
Resolution: --- → DUPLICATE
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: