Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/10176-regex-no-work-cap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- **RegExp operations are no longer capped by work** (#10164). Valid programs that Node completes threw `RangeError: Regular expression work limit exceeded`: a 32,000-unit non-ASCII `split`, a 60,000-unit global `replace`, and linear splits and replaces of 11–15 million units. No finite allowance separates valid programs from pathological ones, so a catastrophic pattern now runs as long as it does in Node. Matching still yields to the collector and to cancellation, and the memory limits are unchanged.
2 changes: 2 additions & 0 deletions crates/perry-runtime/src/gc/tests/runtime_roots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ mod perex_reuse;
mod perex_split;
#[cfg(feature = "regex-engine")]
mod perex_strings;
#[cfg(feature = "regex-engine")]
mod perex_work_policy;
mod prototype_addr_cache;
mod regexp_last_index;
mod segment_record_keys;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! RegExp operations are not capped by work (#10164): a valid program whose
//! matching charges more than the former 100,000,000-unit allowance completes.
use super::*;
use crate::regex::perex_api as api;
use crate::regex::perex_memory::MemoryBudget;
use crate::regex::RegExpHeader;
use crate::string::StringHeader;
use perex::Budget;

const FORMER_WORK_LIMIT: usize = 100_000_000;

fn text<'s>(scope: &'s RuntimeHandleScope, bytes: &[u8]) -> RuntimeHandle<'s> {
scope.root_string_ptr(crate::string::js_string_from_bytes(
bytes.as_ptr(),
bytes.len() as u32,
))
}

#[test]
fn perex_operation_allowance_admits_linear_work_beyond_the_former_limit() {
let _guard = CopyingNurseryTestGuard::new(0);
let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers();
super::perex_public::register_host_roots();
let scope = RuntimeHandleScope::new();
// One search that is linear but heavy: at every position the lookahead
// reads 32 units and then fails on a character class the subject never
// contains, so it matches nothing and allocates no results. The pattern has
// no required literal on purpose; a literal lets admission reject the whole
// search in one pass without doing the per-position work. Measured at about
// 220 work units per subject unit, so 540,000 units charge about 1.2e8.
let pattern = text(&scope, br"\w(?=[\w,; ]{32}[^\w,; ])");
let flags = text(&scope, b"");
let receiver = scope.root_raw_mut_ptr(pattern.with_const_ptr::<StringHeader, _>(|pattern| {
flags.with_const_ptr::<StringHeader, _>(|flags| crate::regex::js_regexp_new(pattern, flags))
}));
let input = text(&scope, "ab12,cd345;ef6 ".repeat(36_000).as_bytes());
let memory = MemoryBudget::new(api::SCRATCH_BYTES);
let mut budget = Budget::new(api::WORK);
let found = receiver.with_mut_ptr::<RegExpHeader, _>(|receiver| {
input.with_const_ptr::<StringHeader, _>(|input| {
api::execute_with_resources(
receiver,
input,
false,
&mut budget,
&memory,
&mut || Ok(()),
None,
)
})
});
let charged = api::WORK - budget.remaining();
assert!(
matches!(found, Ok(None)),
"a valid search must complete without a work-limit error"
);
assert!(
charged > FORMER_WORK_LIMIT,
"the witness must charge more than the former limit to prove anything; \
charged {charged}, so a Perex change may have made this pattern cheaper"
);
}
13 changes: 12 additions & 1 deletion crates/perry-runtime/src/regex/perex_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,18 @@ use perex::executor::ExecError;
use perex::{span::Span, Budget};

// One explicit host policy; no retained scratch cache or alternate engine.
pub(crate) const WORK: usize = 100_000_000;
/// A RegExp operation's work allowance: effectively unlimited (#10164).
///
/// JavaScript engines never abort regex matching for doing too much work, and
/// no finite allowance separates valid programs from pathological ones: Perex's
/// charge per subject unit depends on the program (about 1 for `/x/`, 60 for
/// `/([a-z]+)([0-9]+)/g`, over 200 for a 32-unit lookahead), so any cap throws
/// on some large linear input Node completes. The former 100,000,000 did, as
/// `RangeError: Regular expression work limit exceeded`. Searches still run in
/// `QUANTUM` slices with a GC poll between them, so collection and cancellation
/// keep working; a catastrophic pattern runs as long as it does in Node. The
/// memory limits below are unchanged.
pub(crate) const WORK: usize = usize::MAX;
pub(crate) const SCRATCH_BYTES: usize = 64 * 1024 * 1024;
pub(crate) const PROGRAM_BYTES: usize = 32 * 1024 * 1024;
pub(crate) const QUANTUM: usize = 4096;
Expand Down
Loading