Version
v24.20.0 and v26.10.0. Also reproduced on v24.7.0, v24.9.0, v24.14.1, and on v22.14.0 with --js-regexp-modifiers.
Platform
Subsystem
deps/v8 (irregexp)
What steps will reproduce the bug?
Save as modifier-case.js and run node modifier-case.js. No dependencies; it also runs under d8.
const log = typeof print === "function" ? print : console.log;
const first = (source, flags, input) => new RegExp(source, flags).exec(input)?.[0] ?? null;
const show = (label, got, want) => log(`${got === want ? "ok " : "FAIL"} ${label}: got ${JSON.stringify(got)}, expected ${JSON.stringify(want)}`);
// 1. Fresh process, short pattern. The failing atom count depends on the V8 version.
for (const n of [24, 49]) {
const x = "x?".repeat(n);
show(`${n} x? then (?i:a?) on "A"`, first(x + "(?i:a?)", "", "A"), "A");
show(`${n} x? then (?-i:a?) with /i on "A"`, first(x + "(?-i:a?)", "i", "A"), "");
}
// 2. Any pattern whose source is longer than 20,480 characters.
show(`x?(?i:a?) padded to 20,481 chars on "A"`, first("x?(?i:a?)" + "(?:)".repeat(5118), "", "A"), "A");
// 3. A long-running process: once enough regex code has been generated,
// a modifier pattern compiled for the first time is wrong from then on.
show(`/x?(?i:a?)/ before the workload`, first("x?(?i:a?)(?:q{0,0})", "", "A"), "A");
const filler = Array.from({ length: 32 }, (_, i) => "abcdefgh"[i % 8] + "{0,3}").join("");
for (let k = 0; k < 2000; k++) {
const re = new RegExp(filler + "z{0," + k + "}");
re.exec("ab");
re.exec("ab");
}
show(`/x?(?i:a?)/ after 2,000 unrelated regexes`, first("x?(?i:a?)(?:q{0,1})", "", "A"), "A");
How often does it reproduce? Is there a required condition?
Every time. It needs a letter inside a (?i:…) or (?-i:…) group, plus one of three conditions:
- a certain number of preceding optional atoms: 49–51, 102–104 … on V8 13.6, and 24–26, 52–54 … on V8 14.6;
- a source longer than 20,480 characters;
- a process that has already generated enough regex code.
The third makes it a practical problem. In a long-running server or test run, any modifier pattern first compiled after that point matches wrongly for the rest of the process, while the same pattern is correct in a fresh process.
What is the expected behavior? Why is that the expected behavior?
Every line prints ok, as it does on d8 15.6.62. Per the RegExp modifiers semantics (ES2025), (?i:a?) matches "A", and (?-i:a?) inside /…/i does not.
What do you see instead?
v24.20.0 (V8 13.6.233.17-node.53):
ok 24 x? then (?i:a?) on "A": got "A", expected "A"
ok 24 x? then (?-i:a?) with /i on "A": got "", expected ""
FAIL 49 x? then (?i:a?) on "A": got "", expected "A"
FAIL 49 x? then (?-i:a?) with /i on "A": got "A", expected ""
FAIL x?(?i:a?) padded to 20,481 chars on "A": got "", expected "A"
ok /x?(?i:a?)/ before the workload: got "A", expected "A"
FAIL /x?(?i:a?)/ after 2,000 unrelated regexes: got "", expected "A"
v26.10.0 (V8 14.6.202.34-node.34):
FAIL 24 x? then (?i:a?) on "A": got "", expected "A"
FAIL 24 x? then (?-i:a?) with /i on "A": got "A", expected ""
ok 49 x? then (?i:a?) on "A": got "A", expected "A"
ok 49 x? then (?-i:a?) with /i on "A": got "", expected ""
FAIL x?(?i:a?) padded to 20,481 chars on "A": got "", expected "A"
ok /x?(?i:a?)/ before the workload: got "A", expected "A"
FAIL /x?(?i:a?)/ after 2,000 unrelated regexes: got "", expected "A"
d8 15.6.62 (via jsvu): all seven lines ok.
Additional information
Upstream fix. V8 4cc98031 (CL 8260876, "[regexp] Store flags on RegExpNode and remove ActionNode::MODIFY_FLAGS", 2026-08-28) landed during V8 15.4 development. Its message describes this mechanism: the old design stored modifier flags as mutable state on the compiler, and "out-of-order emission via worklists" let that state leak across branches. The three conditions above all force that kind of emission.
I haven't bisected to that exact commit. What I have verified is that V8 15.2 (Chromium 152) fails and d8 15.6.62 passes. The CL is a redesign across src/regexp/ (10 files, including tests), so it may not apply cleanly to V8 13.6 or 14.6. It builds on the earlier, narrower d315af3d (CL 8257209, "[regexp] Reset compiler flags between word boundary branches"), which a backport may also need.
Related. Chromium issue 560182133 reports the long-running case with /(?i:(a)|(b))\2/d on "BB" against Node v26.5.1. It was closed as obsolete on the assumption that V8 main was already fixed. That pattern also returns null on v24.20.0 and v26.10.0 after the 2,000-regex workload above, and for a source over 20,480 characters.
Workarounds. Both of these are correct on the affected versions:
- put
i on the whole pattern rather than using a modifier group;
- spell the letter as a character class, such as
[aA] or (?i:[a]).
How it was found. Differential fuzzing of regex-partial-match, which uses the running V8 as its oracle. The results drifted part way through long runs.
Request. Please cherry-pick 4cc98031 (with d315af3d if needed) into v24.x and v26.x. v22.x is affected too, but only with --js-regexp-modifiers.
Version
v24.20.0 and v26.10.0. Also reproduced on v24.7.0, v24.9.0, v24.14.1, and on v22.14.0 with
--js-regexp-modifiers.Platform
Subsystem
deps/v8 (irregexp)
What steps will reproduce the bug?
Save as
modifier-case.jsand runnode modifier-case.js. No dependencies; it also runs under d8.How often does it reproduce? Is there a required condition?
Every time. It needs a letter inside a
(?i:…)or(?-i:…)group, plus one of three conditions:The third makes it a practical problem. In a long-running server or test run, any modifier pattern first compiled after that point matches wrongly for the rest of the process, while the same pattern is correct in a fresh process.
What is the expected behavior? Why is that the expected behavior?
Every line prints
ok, as it does on d8 15.6.62. Per the RegExp modifiers semantics (ES2025),(?i:a?)matches"A", and(?-i:a?)inside/…/idoes not.What do you see instead?
v24.20.0 (V8 13.6.233.17-node.53):
v26.10.0 (V8 14.6.202.34-node.34):
d8 15.6.62 (via jsvu): all seven lines
ok.Additional information
Upstream fix. V8 4cc98031 (CL 8260876, "[regexp] Store flags on RegExpNode and remove ActionNode::MODIFY_FLAGS", 2026-08-28) landed during V8 15.4 development. Its message describes this mechanism: the old design stored modifier flags as mutable state on the compiler, and "out-of-order emission via worklists" let that state leak across branches. The three conditions above all force that kind of emission.
I haven't bisected to that exact commit. What I have verified is that V8 15.2 (Chromium 152) fails and d8 15.6.62 passes. The CL is a redesign across
src/regexp/(10 files, including tests), so it may not apply cleanly to V8 13.6 or 14.6. It builds on the earlier, narrower d315af3d (CL 8257209, "[regexp] Reset compiler flags between word boundary branches"), which a backport may also need.Related. Chromium issue 560182133 reports the long-running case with
/(?i:(a)|(b))\2/don"BB"against Node v26.5.1. It was closed as obsolete on the assumption that V8 main was already fixed. That pattern also returnsnullon v24.20.0 and v26.10.0 after the 2,000-regex workload above, and for a source over 20,480 characters.Workarounds. Both of these are correct on the affected versions:
ion the whole pattern rather than using a modifier group;[aA]or(?i:[a]).How it was found. Differential fuzzing of regex-partial-match, which uses the running V8 as its oracle. The results drifted part way through long runs.
Request. Please cherry-pick 4cc98031 (with d315af3d if needed) into v24.x and v26.x. v22.x is affected too, but only with
--js-regexp-modifiers.