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)
Tracking
()
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
- Branch: main
- Revision: bf9757d91e2e53b8ab836f2a08bf4c0275fdcd23
- Timestamp: 2026-10-02T21:18:24+00:00
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
- The shell (or embedding) starts with the startup pref
experimental.legacy_regexpset to false in a build whereNIGHTLY_BUILDandDEBUGare defined. - Script creates an ordinary RegExp literal;
RegExpAllocleavesLegacyFeaturesEnabledBitclear because the pref is off (js/src/vm/RegExpObject.cpp:82-101). - Script calls
"a,b".split(/,/);RegExpSplitfinds the RegExp optimizable and reads its flags slot (js/src/builtin/RegExp.js:951-959). - The self-hosted assert on
REGEXP_LEGACY_FEATURES_ENABLED_FLAGfails (RegExp.js:961) andintrinsic_AssertionFailedexecutesMOZ_ASSERT(false)(js/src/vm/SelfHosting.cpp:388), aborting the process. - In a non-debug build the assert is absent; the same call returns
["a","b"]with no sanitizer report.
Steps to Reproduce
- Build the SpiderMonkey debug shell from an unmodified tree with /app/mozconfigs/mozconfig.js.linux.debug (no source patches).
- Save
"a,b".split(/,/);as testcase.js. - 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).
- Observe
Self-hosted JavaScript assertion info: ./../js/src/builtin/RegExp.js:962: Legacy features must be enabled in optimized pathfollowed byAssertion failure: false, at js/src/vm/SelfHosting.cpp:388. - 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
| Reporter | ||
Comment 1•1 month ago
|
||
| Reporter | ||
Comment 2•1 month ago
|
||
| Reporter | ||
Comment 3•1 month ago
|
||
| Reporter | ||
Comment 4•1 month ago
|
||
| Reporter | ||
Comment 5•1 month ago
|
||
| Reporter | ||
Comment 6•1 month 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 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.
Updated•1 month ago
|
Updated•1 month ago
|
Comment 7•1 month ago
|
||
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.
Description
•