Skip to content

fix(c, cpp): bound the type token run in function declarations - #4462

Merged
joshgoebel merged 2 commits into
highlightjs:mainfrom
Jaybhade:fix/4362-function-declaration-backtracking
Aug 9, 2026
Merged

fix(c, cpp): bound the type token run in function declarations#4462
joshgoebel merged 2 commits into
highlightjs:mainfrom
Jaybhade:fix/4362-function-declaration-backtracking

Conversation

@Jaybhade

@Jaybhade Jaybhade commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #4362

Changes

FUNCTION_DECLARATION in c.js / cpp.js (and so arduino) matches a run of type tokens followed by a function name:

begin: '(' + FUNCTION_TYPE_RE + '[\\*&\\s]+)+' + FUNCTION_TITLE

The outer quantifier is unbounded, so on text that never reaches a function name the group first consumes the whole run of words, and the engine then retries FUNCTION_TITLE at every token boundary of that run before giving up. The scan repeats that at every start offset, so the cost grows with the square of the document size.

Worth noting the ambiguity is not between [\*&\s]+ and the outer +, as the issue guesses — FUNCTION_TYPE_RE can't start with whitespace, so each token/separator split is forced and shortening [\*&\s]+ always leaves the next iteration on a separator, where it fails immediately. The quadratic term is purely O(start offsets) × O(iterations to backtrack), which is why the fix has to bound the iteration count: for any (A)+B, a backtracking engine retries B once per iteration when B fails, so limiting how far the group can reach is the only way to make the work per start offset constant.

Bounding it to 12 tokens leaves plenty of headroom — static const volatile unsigned long long int * const * restrict fn(void) is 11 — and puts highlighting back to linear time.

Why not [\*&\s]*

On the suggestion in the thread (comment) to relax the inner + to *: that one makes things considerably worse. With *, FUNCTION_TYPE_RE can repeat with no separator in between, and (ident)+ is ambiguous for any run of word characters — so it turns polynomial backtracking into exponential. 'a'.repeat(n), no function name in sight:

identifier length 20 22 24 26 28
time 62ms 254ms 1.1s 3.6s 13.3s

That's ~4× per 2 characters, so a 40-character identifier is already hours. It's also rejected by our own regex suite — with * in place, ONLY_LANG=c npx mocha test/regex fails both checks:

1) c should not cause exponential backtracking:
   The quantifier `((decltype\(auto\)|...)[\*&\s]*)+` ambiguous for all words "AA".repeat(n) for any n>1.
2) c should not cause polynomial backtracking:
   By repeating any character that matches /[A-Z_]/i, an attack string can be created.

The current + version passes those checks, which is presumably why this survived: the exponential test only looks at A vs A{2,} disjointness, and the polynomial test only compares adjacent single-character quantifiers, so neither can see the (A sep)+ B shape this issue is about.

Verification

hljs.highlight('a '.repeat(n), { language, ignoreIllegals: true }), node 22, same machine:

c 4 KB 8 KB 16 KB 32 KB 64 KB 128 KB 256 KB
before 368ms 1538ms 5631ms 18545ms (~4× per doubling)
after 11ms 17ms 29ms 75ms 116ms 173ms 278ms

cpp and arduino behave the same; arduino was the worst before, at 22.6s for 32 KB.

Highlighting is unchanged. I hashed the rendered HTML of a corpus before and after — all 230 C headers and 192 libc++ headers in the macOS SDK, plus every c/cpp/arduino fixture under test/, each run through all three grammars (2442 outputs, 18.5 MiB) — and all 2442 hashes are identical. As a side effect that corpus now highlights in 8.5s instead of 22.8s, since real headers contain plenty of comment prose that was hitting the backtracking path.

npm test is green (1597 passing), npm run lint and npm run lint-languages clean.

Not fixed here

A single long identifier ('a'.repeat(n), no spaces) is also quadratic, but that one isn't specific to this grammar — javascript and java show the same curve, and it comes from \w* being re-scanned at every start offset rather than from this quantifier. Unchanged by this PR; happy to open a separate issue if that's useful.

Checklist

  • Added markup tests, or they don't apply here because... a markup test can't express this — the rendered output is deliberately identical (see the 2442-hash differential above). Instead test/parser/function-declaration-backtracking.js asserts a 32 KB adversarial payload highlights well inside a loose 1s budget for all three languages, and that a declaration with 11 type tokens still gets its title highlighted. Reverting just the grammar change fails the three timing assertions (18.4s / 16.9s / 16.6s against the 1s budget) while the title assertion keeps passing.
  • Updated the changelog at CHANGES.md

The `FUNCTION_DECLARATION` matcher looked for `(TYPE[*&\s]+)+NAME(`, with an
unbounded outer quantifier. On text that never reaches a function name the
group first consumes the whole run of words, and the engine then retries the
function name at every token boundary of that run before giving up. Since the
scan repeats this at every start offset, highlighting cost grows with the
square of the document size: 16 KB of `'a '` took ~7s and 64 KB took ~2min.

Bound the repetition instead. Real declarations use a handful of type tokens,
so 12 leaves plenty of room, and it makes the work per start offset constant,
which puts highlighting back to linear time. Output is unchanged.

Fixes highlightjs#4362
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

Build Size Report

Changes to minified artifacts in /build, after gzip compression.

9 files changed

Total change +30 B

View Changes
file base pr diff
es/core.min.js 8.24 KB 8.24 KB +1 B
es/highlight.min.js 8.24 KB 8.24 KB +1 B
es/languages/arduino.min.js 4.63 KB 4.63 KB +5 B
es/languages/c.min.js 2.1 KB 2.1 KB +5 B
es/languages/cpp.min.js 2.6 KB 2.61 KB +4 B
highlight.min.js 8.28 KB 8.28 KB +1 B
languages/arduino.min.js 4.64 KB 4.64 KB +5 B
languages/c.min.js 2.11 KB 2.11 KB +4 B
languages/cpp.min.js 2.61 KB 2.61 KB +4 B

@joshgoebel
joshgoebel merged commit 4d4268d into highlightjs:main Aug 9, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: Quadratic ReDoS in C/C++/Arduino FUNCTION_DECLARATION regex (v11.11.1)

2 participants