fix: walk the group chain iteratively so deep nesting can't overflow the stack - #42
Open
BrianWillows wants to merge 1 commit into
Open
fix: walk the group chain iteratively so deep nesting can't overflow the stack#42BrianWillows wants to merge 1 commit into
BrianWillows wants to merge 1 commit into
Conversation
…the stack
RTF groups nest via `{`, and get/getFont/getColor/getStyle each recursed
up the parent chain on every lookup. A document that nests deeply enough
overflows the stack:
RangeError: Maximum call stack size exceeded
at RTFGroup.getStyle (rtf-group.js:21:12)
at RTFGroup.getStyle (rtf-group.js:22:53) ...
That error is raised inside a stream callback rather than passed to the
rtf.string(str, cb) error argument, so it is not catchable through the
documented API and takes the process down. A ~39KB file was enough.
Walk the chain with a loop instead. The walk still terminates at
RTFDocument, which overrides each of these to return directly, so
resolution order is unchanged.
getStyle() with no name also collected the chain and merged it root-first
in one pass, rather than allocating a fresh object at every level of every
lookup - about 5.7x faster on deeply nested input.
The repo has no test suite, so correctness was checked with a differential
harness: 34 documents (fonts, colour tables, bold/italic/underline,
alignment, super/subscript, \plain, escaped and unicode characters,
paragraphs, sibling groups, and nesting from 1 to 250 deep) serialise
byte-for-byte identically before and after.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
RTF groups nest via
{, andget/getFont/getColor/getStyleeach recurseup the parent chain on every lookup:
A document that nests deeply enough overflows the stack:
The important part: that error is raised inside a stream callback, not passed to
the
rtf.string(str, cb)error argument — so it can't be caught through thedocumented API and takes the process down. A ~39 KB file (20,000 nested groups) is
enough. For anything parsing untrusted
.rtf— mail/attachment processing, documentconverters, indexing pipelines — that's a remote crash.
Fix
Walk the chain with a loop instead of recursing. The walk still terminates at
RTFDocument, which overrides each of these to return directly, so resolution orderis unchanged.
getStyle()with no name now also collects the chain and merges it root-first in asingle pass, instead of allocating a fresh object at every level of every lookup.
Results
The crash is gone — 20,000 nested groups now parses normally instead of killing the
process — and deep documents get meaningfully faster:
Being straight about what this doesn't fix: the cost is still quadratic in nesting
depth, just with a much smaller constant. Each
addContentresolves the effectivestyle by walking the whole chain, so N nested groups still do O(N) work N times.
Making that O(1) means caching a group's effective style, which needs invalidating
wherever the interpreter does
group.style.x = ...— that's a broader change and Ididn't want to guess at it inside someone else's parser. Happy to follow up with it
if you'd like, or to add a nesting-depth limit instead.
Realistic documents are unaffected either way: 2,000 × 3-deep formatting groups
(29 KB) parses in ~52 ms. This only bites on deliberately-nested input.
Verification
The repo has no test suite (
test/isn't present, sonpm testcan't run), so Ichecked correctness with a differential harness against the current implementation:
34 documents serialise byte-for-byte identically, covering font tables, colour
tables, bold/italic/underline, alignment, super/subscript,
\plain, escaped (\'e9)and unicode (
\u233?) characters, paragraph handling, repeated sibling groups, andnesting from 1 to 250 deep.
Found and fixed with AI assistance (Claude). Happy to add the harness as a test file
if that would be useful — it would give the repo a starting suite.