Proposal: Regular Expression Linear-Time Flag

Hi,

I’m currently preparing a proposal for a RegExp Linear-Time Flag (/l) and am seeking a Champion to present it at committee meetings.

This topic comes up regularly, as vulnerable patterns in npm packages trigger ReDoS alerts for many developers. I’d be happy to discuss the issue and find someone who can help advance the proposal.

Summary

The /l flag enables linear-time matching using a finite automaton engine that explores all possible matches simultaneously, eliminating catastrophic backtracking.
A read-only Boolean property RegExp.prototype.linearTime is provided to indicate whether a regular expression was constructed with /l.

Motivation

Regular expressions in ECMAScript are currently implemented using backtracking engines. While very fast for most patterns, some patterns with nested quantifiers, alternations, or backreferences may trigger catastrophic backtracking, causing exponential runtime (O(2^n)). This behavior has been widely documented as a source of Regular Expression Denial of Service (ReDoS) vulnerabilities when processing untrusted input.

Example of a vulnerable pattern:

/(a+)*b/.exec('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); // Exponential runtime

Here, the nested quantifiers (a+)* combined with the absence of b in the input can cause the engine to explore an exponential number of backtracking paths.

Existing mitigations are inadequate:

  • Linters and static analysis tools may detect obvious risky patterns, but cannot prevent exponential backtracking at runtime.
  • Try/catch or timeouts do not help: the RegEx engine runs synchronously, so the code cannot catch a hanging match in time.
  • Third-party libraries can provide safety but may break feature compatibility or require extra dependencies.
  • Worker threads isolate long-running regex, but do not reduce its execution time; they only prevent blocking the main thread, and require complex coordination.

ECMAScript currently has no language-level guarantee of safe linear-time regular expression matching for all input.

Proposed solution

The /l flag is opt-in and provides a guarantee of linear-time matching (O(pattern.length × input.length)).
Regular expressions without the flag continue to use the standard backtracking engine, ensuring full backward compatibility.

When the /l flag is used:

  1. The regular expression MUST be executed using an algorithm that guarantees linear time complexity relative to the input length.

  2. If the pattern contains features that cannot be implemented with such an algorithm (e.g., backreferences, lookahead, lookbehind), a SyntaxError is thrown at construction time.

  3. This validation occurs at construction, not at execution time, ensuring that developers can safely detect unsupported patterns during development.

This design ensures that:

  • All /l regular expressions are safe from ReDoS attacks.
  • Behavior is consistent across all conforming implementations.
  • Developers have a simple way to write secure regular expressions.
  • Existing code continues to work unchanged.

Use cases

Literal notation

const pattern = /abc+/l;
const match = re.exec('abccc');

console.log(pattern.linearTime); // true

Constructor notation

const pattern = new RegExp('abc+', 'l');
console.log(pattern.linearTime); // true

Unsupported pattern triggers SyntaxError:

try {
  const pattern = /(a+)*b/l; // SyntaxError: pattern unsupported for linear matching
}
catch (error) {
  console.log('Pattern not allowed with /l');
}

Syntax

/l // Linear-time flag
  • Applies only to newly constructed regular expressions.
  • Patterns using unsupported features (backreferences, lookaheads, lookbehinds) throw at construction.
  • Existing regular expressions without /l continue using the standard backtracking engine.
  • RegExp.prototype.linearTime is read-only and reflects whether /l was used.

Comparison

Some programming languages provide linear-time regular expression engines with built-in protection against catastrophic backtracking:

  • Go – linear-time engine is part of the standard library. All regexes run in O(pattern × input), making ReDoS impossible.
  • C# – standard library supports a non-backtracking mode for most patterns (RegexOptions.NonBacktracking), preventing exponential backtracking.

Implementations

V8 provides an experimental linear-time regular expression engine accessible via command-line flags (--enable-experimental-regexp-engine). This engine supports most JavaScript RegExp features except backreferences and lookarounds and enforces linear-time guarantees for all /l patterns.

node -e '/(a*)*b/l.test("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac")' --enable-experimental-regexp-engine

Note: The engine only checks for unsupported patterns when run with the experimental flag. This demonstrates that while linear-time regex execution is possible in JavaScript, an opt-in mechanism is required to ensure safety without breaking existing code.

Does this proposal affect ECMAScript lexing?

No. The /l flag only affects user-constructed regular expressions. ECMAScript’s built-in lexer uses fixed patterns for tokenization, none of which rely on the /l flag or unsupported features. Therefore, existing lexing behavior remains unchanged: all code that parsed correctly before this proposal will continue to parse correctly afterward.

Q&A

Q: Why is the proposal this way?
A: The /l flag provides a simple, opt-in way to guarantee linear-time regular expression matching. Without it, developers must rely on external libraries or manual pattern restrictions, which is error-prone and inconsistent across platforms.

Q: Why does this need to be built-in, instead of being implemented in JavaScript?
A: Linear-time guarantees for all patterns cannot be reliably implemented in user-space using standard JavaScript RegExp. Polyfills can only check patterns or reimplement engines partially, often with significant performance overhead. A native implementation ensures consistent behavior and performance across browsers and Node.js.

Specification

RegExp.prototype.linearTime

I have linear time regex implemented in JS userspace if you need an implementation for polyfill purposes.

Another way of describing a linear-time regex is "non-backtracking" or "streaming" since such an engine can be used to find matches in a stream of input. My implementation shows that working as well.

I think you really do need runtime support for this. In your example the lack of runtime support has making + repetition a syntax error sometimes, but that just means that the end user has to learn that sometimes + works and sometimes it doesn't.

If the standards give away + to be a syntax error in linear regexes, then we'll need new standards when + works fine in linear regex, which it will as soon as there is an implementation available

In fact your work to make the l flag generate errors would already create a conflict with the flagged V8 streaming implementation which uses the flag to indicate runtime support...!!!

Thanks for your reply. An exception is thrown only if the engine does not support the /l flag or cannot handle the pattern (for example, backreferences or lookarounds).
Go and V8 also do not throw errors for nested quantifiers — they simply execute them in linear time.
In V8, this behavior is officially stated:

RegExps constructed with this flag are always eagerly executed with the new engine; Irregexp is not involved at all. If the new RegExp engine can’t handle the pattern of an l-RegExp, then an exception is thrown at construction.

PS: I just realized that I used an incorrect formulation. In fact, it should be like this:

try {
  const pattern = /(a+)*b/l; // SyntaxError: Invalid regular expression flags
} catch {}

You can't have some engines handle /(a+)*b/l by throwing while others produce a result. Chromium handles it by producing a result. You propose the spec say it should (could) be handled by throwing.

I can only conclude if you propose an incompatible implementation that you are proposing that they remove their implementation and replace it with yours. If that is not the case, how are you suggesting that one syntax can have two different spec-compliant behaviors? Are library authors supposed to wrap every /pat/l in a try/catch to figure out which behavior the current runtime environment actually has?

By the way, welcome! I don't mean to sound like I'm ungrateful that you're here and that you're willing to do some work to carry this idea of linear regex matching forward! I'm totally with you that it would be a great ability to have inside JS, and I'm willing to work with you to get it there. I just want to make sure we're offering developers access to the actual functionality, like the Chromium flag is doing.

I'm NOT suggesting that /(a+)*b/l should throw in some engines and not throw in others.

Engines that do NOT implement the /l flag at all:

/(a+)*b/l  // → SyntaxError: Invalid regular expression flags

This is exactly like any other unsupported flag. If an engine hasn't implemented the linear-time flag, it's simply an invalid flag. This is non-controversial and matches how all ES features work.

Engines that DO implement the /l flag:

/(a+)*b/l  // → Behavior is defined by that implementation

Once an engine commits to implementing /l , it accepts all syntactically valid patterns with that flag.

Simple feature detection:

const isLinearRegexFlagSupported = () => {
    try { 
          new RegExp('', 'l'); 

          return true; 
    }
    catch (error /* SyntaxError: Invalid flags supplied to RegExp constructor */) { 
         return false;  
    }
};

So my proposal is actually the opposite of what you're concerned about: one syntax, one behavior in supporting engines , and a clear, predictable error in non-supporting engines. That's the only way to ensure backward compatibility while moving forward.

Once an engine commits to implementing /l , it accepts all syntactically valid patterns with that flag.

OK, but now you're saying something that directly contradicts the examples you are showing, specifically in this example:

const pattern = /(a+)*b/l; // SyntaxError: pattern unsupported for linear matching

This example shows a runtime error in an engine which supports the /l flag on a pattern that is otherwise syntactically valid.

Yeah, I see, but I can’t edit that comment — it’s locked :slightly_smiling_face:

OK, just to clarify you're saying that /(a+)*b/l would not ever be a syntax error unless the engine does not implement the /l flag.

Exactly!

So I think that your next step in pursuing the standards track would be to figure out how this is going to be implemented by Babel and CoreJS before real support is widespread and for engines which never add native support.

That's what I'm saying I could help you with.

1 Like

That would be great! Your VM implementation looks very interesting and could help address the polyfill story for this proposal.