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:74 — geckoRe: /^\s*(.*?)…/ (. matches a space)
src/stack-trace-parser.js:103 — javaScriptCoreRe: /^\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.
parse()is quadratic in the length of a leading whitespace run, so a long run of spaces blocks theevent loop for seconds.
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 aleading run of spaces can be divided between them in n ways:
src/stack-trace-parser.js:74—geckoRe:/^\s*(.*?)…/(.matches a space)src/stack-trace-parser.js:103—javaScriptCoreRe:/^\s*(?:([^@]*)…/([^@]matches a space)When the line does not match, the engine has to try every split before failing.
chromeRe,winjsReandnodeReare unaffected: each has a literalatafter^\s*, whichanchors the boundary.
Why it is reachable
err.stackembeds the message, and messages can contain attacker-influenced text: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 itmakes 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.