Skip to content

ReDoS: _normalize_ts_import_types hangs forever on some .ts files (catastrophic backtracking in _TS_IMPORT_TYPE_CALL_RE) #3341

Description

@paunescumihai

Summary

_normalize_ts_import_types (graphify/extract.py) can hang forever on a single ordinary TypeScript file. _TS_IMPORT_TYPE_CALL_RE has two nested lazy quantifiers inside a + group, which produces catastrophic regex backtracking when a line contains several import(...) occurrences without a following >( that lets the overall match succeed.

Introduced with the #3154 normalizer; still present in 0.9.53. During graphify update . one worker pegs a full core at ~100% CPU and the AST-extraction counter stops advancing (looks like a pool deadlock on a many-core box, because one pinned core reads as ~3% aggregate CPU).

The regex

_TS_IMPORT_TYPE_CALL_RE = re.compile(
    rb"<((?:[^;{}]*?\bimport\s*\([^()]+\)[^;{}]*?)+)>(?=\s*\()"
)

The filler [^;{}]*? appears both before and after import(...) inside a +-repeated group. When the trailing >( lookahead fails, the engine can partition the filler between group iterations in ~2^k ways for k import( tokens, so it backtracks exponentially before giving up on the line.

Minimal repro (pure re, no graphify needed)

import re, time
OLD = re.compile(rb"<((?:[^;{}]*?\bimport\s*\([^()]+\)[^;{}]*?)+)>(?=\s*\()")
for k in (3, 5, 7, 9, 11):
    s = b"x < " + b" ".join(b'import("m%d")' % i for i in range(k)) + b" and more text no close"
    t = time.time(); OLD.search(s); print(f"imports={k}: {time.time()-t:.3f}s")
imports=3: 0.000s
imports=5: 0.005s
imports=7: 0.156s
imports=9: 4.914s
imports=11: (does not finish)

Real-world trigger was a 13 KB .ts file mixing <, several import("...") type references and generic calls; _normalize_ts_import_types(source) never returned.

Suggested fix

Forbid import (and </>) inside the filler so the decomposition is unique — same matches, linear time:

_TS_IMPORT_TYPE_CALL_RE = re.compile(
    rb"<((?:(?!import)[^;{}<>])*"
    rb"(?:\bimport\s*\([^()]+\)(?:(?!import)[^;{}<>])*)+)>(?=\s*\()"
)

Verified parity on the intended positives and negatives:

input old new
f<typeof import("mod")>() typeof import("mod") same
f<import("mod").Foo>() import("mod").Foo same
g<import("a").A, import("b").B>() import("a").A, import("b").B same
a < b && c > (d) (no match) (no match)

With the patched pattern the previously-hanging file returns in ~0 ms.

Environment

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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