.gitignore glob is catastrophically backtrackable (ReDoS)
shared/util/ignore.js's glob() (the IGNOGlob port behind be status/ls wt-scan) implements */** by recursing on EVERY suffix with no memoization, so a pattern like *a*a*a*a*az against a long non-matching path is exponential. .gitignore is untrusted — a checkout writes attacker-controlled content and load() walks UP to $HOME stacking every .gitignore — and match() runs once per worktree entry, so a hostile repo wedges the single-threaded runtime. Method: Issues.
shared/util/ignore.js:39 glob(pat,str) is the matcher.:51-53 ** branch: for (t=si..len) glob(subPat, str.slice(t)) — recurse
on every suffix, NO memo on (pi,si), NO iteration cap.
:56-62 * branch: same per-suffix recursion (stops past a /); two *segments in one pattern multiply → exponential on a non-match.
:52 :59 each step allocates fresh pat.slice/str.slice substrings,amplifying the constant factor on top of the exponential blowup.
:104 :109 :113 tryMatch calls glob per pattern; :113 retries atevery dir level for unanchored-with-slash, multiplying the call count.
load() :176 reads .gitignore per dir; :168-184
walks anchor→$HOME// stacking up to 64 files; match() :190-204 runs setDecideDeep→glob per scanned wt entry → one hostile pattern + one long path hangs be status/ls (cited GET).
glob() runs in time linear (or near-linear) in pat.length+str.length for
ALL patterns, including adversarial *a*a*…*az vs a long non-match.
fix returns no-match promptly, with identical match/no-match verdicts to native IGNOGlob on the existing parity corpus.
io.stat/io.mmap/io.getenv; no C, no dog. Byte-equivalent
verdicts to dog/git/IGNO.c (IGNOGlob/TryMatch) on the parity suite.
*=non-/, **=across-/, ?=one-char semantics exactly; do not
regress the dir/-prefix or negation rules. Scope: glob() (+ tiny helpers).
STILL "manual parsing" — abc.mkd forbids it; the JS analogue of the ragel rule is to TRANSLATE the glob to a RegExp and let the engine match. Landed: compile pat→ONE cached RegExp (*→[^/]*, **→.* swallowing one /, ?→[^/], else escaped literal; no [...] classes — the C glob has none).
str.length ** star-runs work budget skips a degenerate multi-* glob →no-match (fail-safe), since JS RegExp BACKTRACKS (it is not a ragel DFA).
shared/util/ignore.js (:39-70 is the recursive per-suffix matcher again).
test/ignore.js (*a*a*…*az bomb <2s + native-parity corpus).
glob() (ignore.js:39) recurses on every suffix for */** (:52,:59), no memo, no budget, per-step pat.slice/str.slice; no test/ignore.js exists. Presumably lost in the beagle-ext catch-up/JSQUE-016 reorg (027beac9+); still exploitable.
the be/ tree; see the 2026-07-07 audit note above. Design stands: RegExp translate, NOT a hand-rolled DP matcher — abc.mkd forbids manual parsing.)