JavaScript: a backreference takes fuel for each 16 units before it compares them

This commit is contained in:
dotCooCoo
2026-11-07 20:26:40 -08:00
parent fab3425f54
commit 971a0d6626
2 changed files with 39 additions and 4 deletions
+12 -4
View File
@@ -5,7 +5,8 @@
//! pattern with a loop and a stack of open groups, and the matcher keeps its choice points and the
//! values it must restore on its own stack, so neither recurses however deeply the groups nest.
//! Quantifiers count their iterations in registers instead of repeating their atom's code. Every
//! instruction run, every capture slot cleared and every choice point taken back costs fuel, and
//! instruction run, every capture slot cleared, every choice point taken back and every 16 units a
//! backreference compares cost fuel, and
//! the compiler's work, the matcher's state and its stack are held against the heap limit, so a
//! pattern that backtracks without end stops at a limit.
@@ -1294,12 +1295,19 @@ impl<'r> Matcher<'r> {
true
} else {
let len = e - s;
let hit = pos + len <= input.len()
&& (0..len).all(|k| match (input.get(s + k), input.get(pos + k)) {
let mut hit = pos + len <= input.len();
// The units are compared 16 at a time, and each 16, or fewer at the end,
// count as a step taken before they are compared.
let mut k = 0;
while hit && k < len {
let end = len.min(k + 16);
self.step(interp)?;
hit = (k..end).all(|i| match (input.get(s + i), input.get(pos + i)) {
(Some(&a), Some(&b)) => re.canon(a) == re.canon(b),
_ => false,
});
self.steps += len as u64 / 16;
k = end;
}
if hit {
pos += len;
}
+27
View File
@@ -447,6 +447,33 @@ fn the_matcher_state_counts_against_the_heap() {
assert_eq!(interp.heap.bytes, live);
}
#[test]
fn a_backreference_takes_fuel_for_the_units_it_compares() {
let mut realm = Realm::with_options(Options::default(), Limits::default());
let interp = realm.interp();
let mut fuel_for = |pattern: &str, input: &str, want: bool| {
let input: Vec<u16> = input.encode_utf16().collect();
let Ok(re) = compile_within(pattern, usize::MAX, u64::MAX).0 else { panic!("the pattern compiles") };
let Ok(mut m) = crate::regexp_vm::Matcher::new(&re, interp) else { panic!("the state fits") };
interp.fuel = 1 << 40;
assert!(matches!(m.match_at(interp, &input, 0), Ok(hit) if hit == want), "{pattern}");
(1u64 << 40) - interp.fuel
};
// The same 20,000 iterations, each running a one-unit backreference instead of a character:
// each comparison takes a step besides the instruction's own.
let a = "a".repeat(20_001);
let (by_char, by_backref) = (fuel_for("(a)(?:a)*$", &a, true), fuel_for("(a)(?:\\1)*$", &a, true));
assert!(by_backref >= by_char + 20_000, "{by_backref} < {by_char} + 20000");
// A long comparison takes a step for each 16 units it compares.
let long = fuel_for("^(a{10000})\\1", &a, true);
let plain = fuel_for("^(a{10000})", &a, true);
assert!(long >= plain + 625, "{long} < {plain} + 625");
// One that fails on its first unit takes a step for those 16 units only.
let ab = format!("{}{}", "a".repeat(10_000), "b".repeat(10_000));
let (by_backref, by_char) = (fuel_for("^(a{10000})\\1", &ab, false), fuel_for("^(a{10000})c", &ab, false));
assert!(by_backref <= by_char + 2, "{by_backref} > {by_char} + 2");
}
#[test]
fn clearing_matcher_state_takes_fuel() {
let limits = Limits { fuel: 500_000, ..Limits::default() };