Open Bug 2078806 Opened 1 month ago Updated 1 month ago

Assertion failure: "Legacy features must be enabled in optimized path" in self-hosted RegExpSplit/RegExpMatchAll when javascript.options.experimental.legacy_regexp is false

Categories

(Core :: JavaScript: Standard Library, defect, P3)

defect

Tracking

()

UNCONFIRMED

People

(Reporter: bugmon, Unassigned)

References

(Blocks 1 open bug)

Details

(Keywords: assertion, testcase)

Attachments

(5 files)

The self-hosted RegExpSplit and RegExpMatchAll functions in js/src/builtin/RegExp.js assert, under #ifdef NIGHTLY_BUILD, that every RegExp passing IsOptimizableRegExpObject has REGEXP_LEGACY_FEATURES_ENABLED_FLAG set in its flags slot. That assumption only holds while the experimental.legacy_regexp pref is true: with the pref false, RegExpAlloc never sets the bit, yet the object still has the default shape and takes the optimized path. The assert therefore fails for any ordinary RegExp and intrinsic_AssertionFailed hits MOZ_ASSERT(false).

A script running in a debug, Nightly-configured SpiderMonkey build with the legacy-RegExp pref disabled can abort the process by calling String.prototype.split or String.prototype.matchAll with an ordinary RegExp. The demonstrated effect is a debug-only assertion abort in the JS shell. No memory corruption was observed: a non-debug ASAN shell given the same testcase and flag exits 0 with correct split and matchAll results. The pref defaults to true on Nightly builds and the assert is not compiled outside NIGHTLY_BUILD, so a non-default pref value is a precondition. Reachability from web content in the browser was not tested.

Deduplication found that it reproduces the same underlying defect as bug 2059115, even though the fix is present in the analyzed build. The cause is not established.

Build Info

Affected Code

File: js/src/builtin/RegExp.js, line 956-964

  var flags, unicodeMatching, splitter;
  if (optimizable) {
    // Step 5.
    flags = UnsafeGetInt32FromReservedSlot(rx, REGEXP_FLAGS_SLOT);
    #ifdef NIGHTLY_BUILD
    assert(!!(flags & REGEXP_LEGACY_FEATURES_ENABLED_FLAG),
           "Legacy features must be enabled in optimized path");
    #endif

File: js/src/builtin/RegExp.js, line 1225-1232

  if (IsOptimizableRegExpObject(rx) && C === builtinCtor) {
    // Step 5, 9-12.
    source = UnsafeGetStringFromReservedSlot(rx, REGEXP_SOURCE_SLOT);
    flags = UnsafeGetInt32FromReservedSlot(rx, REGEXP_FLAGS_SLOT);
    #ifdef NIGHTLY_BUILD
    assert(!!(flags & REGEXP_LEGACY_FEATURES_ENABLED_FLAG),
    "Legacy features must be enabled in optimized path");
    #endif

File: js/src/vm/RegExpObject.cpp, line 82-101

  bool legacyFeaturesEnabled = false;
  if (JS::Prefs::experimental_legacy_regexp()) {
    JS::Realm* thisRealm = cx->realm();

    JSObject* thisRealmRegExp =
        &thisRealm->maybeGlobal()->getConstructor(JSProto_RegExp);

    legacyFeaturesEnabled = (!newTarget || newTarget == thisRealmRegExp);
    if (!legacyFeaturesEnabled &&
        !JSObject::setLegacyFeaturesDisabled(cx, regexp)) {
      return nullptr;
    }
  }
  regexp->setLegacyFeaturesEnabled(legacyFeaturesEnabled);

File: js/src/builtin/RegExp.cpp, line 2355-2367

bool js::IsOptimizableRegExpObject(JSObject* obj, JSContext* cx) {
  bool optimizable =
      obj->shape() == cx->global()->maybeRegExpShapeWithDefaultProto() &&
      cx->realm()->realmFuses.optimizeRegExpPrototypeFuse.intact();
  MOZ_ASSERT_IF(optimizable,
                obj->is<RegExpObject>() &&
                    obj->as<RegExpObject>().realm() == cx->realm());
  return optimizable;
}

File: js/src/vm/SelfHosting.cpp, line 388

  MOZ_ASSERT(false);

The assert at RegExp.js:961 (and its sibling at 1230) requires the legacy-features bit on every optimizable RegExp. RegExpAlloc only sets that bit when the pref is on; with the pref off legacyFeaturesEnabled stays false and neither the bit nor the LegacyFeaturesDisabled object flag is set, so the RegExp keeps the default shape that IsOptimizableRegExpObject accepts. The failing self-hosted assert calls intrinsic_AssertionFailed, which executes MOZ_ASSERT(false). The pref is a startup pref with default @IS_NIGHTLY_BUILD@ (modules/libpref/init/StaticPrefList.yaml, javascript.options.experimental.legacy_regexp).

Exploit Chain

  1. The shell (or embedding) starts with the startup pref experimental.legacy_regexp set to false in a build where NIGHTLY_BUILD and DEBUG are defined.
  2. Script creates an ordinary RegExp literal; RegExpAlloc leaves LegacyFeaturesEnabledBit clear because the pref is off (js/src/vm/RegExpObject.cpp:82-101).
  3. Script calls "a,b".split(/,/); RegExpSplit finds the RegExp optimizable and reads its flags slot (js/src/builtin/RegExp.js:951-959).
  4. The self-hosted assert on REGEXP_LEGACY_FEATURES_ENABLED_FLAG fails (RegExp.js:961) and intrinsic_AssertionFailed executes MOZ_ASSERT(false) (js/src/vm/SelfHosting.cpp:388), aborting the process.
  5. In a non-debug build the assert is absent; the same call returns ["a","b"] with no sanitizer report.

Steps to Reproduce

  1. Build the SpiderMonkey debug shell from an unmodified tree with /app/mozconfigs/mozconfig.js.linux.debug (no source patches).
  2. Save "a,b".split(/,/); as testcase.js.
  3. Run it with js_shell_evaluator: js_binary=/firefox/obj-js-debug/dist/bin/js, flags=["--setpref=experimental.legacy_regexp=false"] (the evaluator adds --fuzzing-safe).
  4. Observe Self-hosted JavaScript assertion info: ./../js/src/builtin/RegExp.js:962: Legacy features must be enabled in optimized path followed by Assertion failure: false, at js/src/vm/SelfHosting.cpp:388.
  5. Control: the same testcase without the flag exits 0. Variant: "a,b".matchAll(/,/g); with the flag hits the sibling assert at RegExp.js:1231.

Security Impact

  • Severity: Low
  • Attacker capability: Abort a debug Nightly-configured SpiderMonkey process through a failing debug-only assertion. No memory corruption or incorrect result was demonstrated; the non-debug ASAN shell runs the same input with the same flag to completion with correct output.
  • Preconditions: Debug build with NIGHTLY_BUILD defined, and the startup pref javascript.options.experimental.legacy_regexp set to false (non-default on Nightly; set in the shell via --setpref=experimental.legacy_regexp=false).

ASAN Report

Self-hosted JavaScript assertion info: ./../js/src/builtin/RegExp.js:962: Legacy features must be enabled in optimized path
[2222] Assertion failure: false, at js/src/vm/SelfHosting.cpp:388
==2222==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x55a64358b3f1 bp 0x7ec8b1d25060 sp 0x7ec8b1d25020 T2222)
==2222==The signal is caused by a READ memory access.
==2222==Hint: address points to the zero page.
    #0 0x55a64358b3f1 in MOZ_CrashSequence(void*, long) /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3
    #1 0x55a64358b3f1 in intrinsic_AssertionFailed(JSContext*, unsigned int, JS::Value*) /firefox/obj-js-debug/js/src/vm/./../../../../js/src/vm/SelfHosting.cpp:388:3
    #2 0x55a643494c94 in CallJSNative(JSContext*, bool (*)(JSContext*, unsigned int, JS::Value*), js::CallReason, JS::CallArgs const&) /firefox/js/src/vm/Interpreter.cpp:528:13
    #3 0x55a6434716a7 in js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:624:12
    #4 0x55a643480107 in js::CallFromStack(JSContext*, JS::CallArgs const&, js::CallReason) /firefox/js/src/vm/Interpreter.cpp:696:10
    #5 0x55a643480107 in js::Interpret(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:3278:16
    #6 0x55a643471106 in MaybeEnterInterpreterTrampoline(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:420:10
    #7 0x55a643470d48 in js::RunScript(JSContext*, js::RunState&) /firefox/js/src/vm/Interpreter.cpp:498:13
    #8 0x55a6434737e0 in js::ExecuteKernel(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, js::AbstractFramePtr, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:888:10
    #9 0x55a643473c3e in js::Execute(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, JS::MutableHandle<JS::Value>) /firefox/js/src/vm/Interpreter.cpp:918:10
    #10 0x55a643735ad1 in ExecuteScript(JSContext*, JS::Handle<JSObject*>, JS::Handle<JSScript*>, JS::MutableHandle<JS::Value>) /firefox/obj-js-debug/js/src/vm/./../../../../js/src/vm/CompilationAndEvaluation.cpp:544:10
    #11 0x55a643735cb8 in JS_ExecuteScript(JSContext*, JS::Handle<JSScript*>) /firefox/obj-js-debug/js/src/vm/./../../../../js/src/vm/CompilationAndEvaluation.cpp:568:10
    #12 0x55a641f5901c in RunFile(JSContext*, char const*, _IO_FILE*, CompileUtf8, bool, bool) /firefox/obj-js-debug/js/src/shell/./../../../../js/src/shell/js.cpp:1430:10
    #13 0x55a641f58542 in Process(JSContext*, char const*, bool, FileKind) /firefox/obj-js-debug/js/src/shell/./../../../../js/src/shell/js.cpp
    #14 0x55a641f11268 in ProcessArgs(JSContext*, js::cli::OptionParser*) /firefox/obj-js-debug/js/src/shell/./../../../../js/src/shell/js.cpp:12390:10
    #15 0x55a641f11268 in Shell(JSContext*, js::cli::OptionParser*) /firefox/obj-js-debug/js/src/shell/./../../../../js/src/shell/js.cpp:12641:12
    #16 0x55a641f09766 in main /firefox/obj-js-debug/js/src/shell/./../../../../js/src/shell/js.cpp:13061:12
    #17 0x7ef6878ad1c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9)
    #18 0x7ef6878ad28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a)
    #19 0x55a641ecef28 in _start (/firefox/obj-js-debug/dist/bin/js+0x2765f28)

SUMMARY: UndefinedBehaviorSanitizer: SEGV /firefox/obj-js-debug/dist/include/mozilla/Assertions.h:261:3 in MOZ_CrashSequence(void*, long)
==2222==ABORTING
Attached patch fix.patch — — Splinter Review
Attached file crash_stack.txt —
Attached file testcase.js —
Attached file README.md —
Attached file flags.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 issue was confirmed by an independent verifier, but for non-ASAN issues, the verifier has been made less strict. Additionally, the root cause analysis and suggested fix may contain errors.

Group: core-security → javascript-core-security
Blocks: sm-security
Severity: -- → S3
Priority: -- → P3

This is just an overzealous assertion, largely caused by the difficulty of reading prefs from self-hosted code. It doesn't have any impact in non-debug builds, and will go away once we actually ship this feature.

It's not security-sensitive.

Group: javascript-core-security
Keywords: sec-other
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: