Skip to content

parse() is quadratic in leading whitespace: 30k-space error message blocks the event loop for ~3s #49

Description

@theRizwan

parse() is quadratic in the length of a leading whitespace run, so a long run of spaces blocks the
event loop for seconds.

const {parse} = require('stacktrace-parser');

for (const n of [1000, 2000, 4000, 8000, 16000]) {
  const t = Date.now();
  parse(' '.repeat(n) + 'x');
  console.log(n, Date.now() - t, 'ms');
}
 1000     4.2 ms
 2000    14.6 ms
 4000    57.9 ms
 8000   231.3 ms
16000   924.0 ms

Time quadruples for each doubling of the input — clean O(n²).

Cause

Two patterns start with ^\s* followed immediately by a group that also matches whitespace, so a
leading run of spaces can be divided between them in n ways:

  • src/stack-trace-parser.js:74geckoRe: /^\s*(.*?)…/ (. matches a space)
  • src/stack-trace-parser.js:103javaScriptCoreRe: /^\s*(?:([^@]*)…/ ([^@] matches a space)

When the line does not match, the engine has to try every split before failing.

chromeRe, winjsRe and nodeRe are unaffected: each has a literal at after ^\s*, which
anchors the boundary.

Why it is reachable

err.stack embeds the message, and messages can contain attacker-influenced text:

const err = new Error('Invalid value: \n' + ' '.repeat(30000));
parse(err.stack);   // ~3.3 s of blocked event loop

Any service that parses stack traces from errors carrying user-supplied values — a validation
message, a filename, a parsed field — can be stalled by one request. Availability only; nothing is
disclosed or corrupted.

Fix

The leading \s* is redundant, because the group after it already matches whitespace. Removing it
makes the match linear. It was doing one useful thing — keeping leading whitespace out of the
captured methodName — so that is handled where the capture is read instead.

Measured after the change, same inputs: 8000 → 0.16 ms, 16000 → 0.37 ms, 32000 → 0.63 ms, i.e. ~2×
per doubling. The 30k case above drops from ~3.3 s to under a millisecond.

Happy to open a PR — I have one ready. All 35 existing fixtures pass unchanged, plus a timing
regression test that fails on current main.

Environment: stacktrace-parser@0.1.11, Node 24.16.0, macOS.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions