Skip to content

fix(parser): loop instead of recurse to avoid stack overflow#38

Open
zzorba wants to merge 1 commit intoEneris:masterfrom
filament-dm:filament/loop-parser
Open

fix(parser): loop instead of recurse to avoid stack overflow#38
zzorba wants to merge 1 commit intoEneris:masterfrom
filament-dm:filament/loop-parser

Conversation

@zzorba
Copy link
Copy Markdown

@zzorba zzorba commented May 6, 2026

The MCS parser drove its state machine via mutual recursion between #waitForData, #handleGotMessageSize, #handleGotMessageBytes, and #getNextMessage. Each buffered frame added two stack frames, and an incomplete varint header would re-enter #waitForData with an unchanged buffer (the length check still passed for kSizePacketLenMin = 1), so a burst of queued messages or a TCP segment that split a varint size header could throw "RangeError: Maximum call stack size exceeded" out of the socket data handler.

Convert #waitForData into a while (!#destroyed && !#isWaitingForData) loop. Handlers update #state / #data and return; they no longer call #waitForData() directly. The "incomplete varint" and "incomplete payload" branches set #isWaitingForData = true and bail so the loop won't tight-spin on a buffer that needs more bytes, and a #destroyed flag lets #emitError unwind cleanly.

Verified against the patched parser:

  • 50,001 zero-byte HeartbeatPing frames in a single buffer parse without throwing (would have overflowed the stack previously).
  • A varint size header split across three TCP reads (0x80, then 0x01, then payload) parses one complete message with no busy loop.

@Eneris
Copy link
Copy Markdown
Owner

Eneris commented May 6, 2026

Can you please remove the dist folder from your pull?

The MCS parser drove its state machine via mutual recursion between
incomplete varint header would re-enter #waitForData with an unchanged
buffer (the length check still passed for kSizePacketLenMin = 1), so a
burst of queued messages or a TCP segment that split a varint size
header could throw "RangeError: Maximum call stack size exceeded" out
of the socket data handler.

Convert #waitForData into a `while (!#destroyed && !#isWaitingForData)`
loop. Handlers update #state / #data and return; they no longer call
payload" branches set #isWaitingForData = true and bail so the loop
won't tight-spin on a buffer that needs more bytes, and a #destroyed
flag lets #emitError unwind cleanly.

Verified against the patched parser:
- 50,001 zero-byte HeartbeatPing frames in a single buffer parse
  without throwing (would have overflowed the stack previously).
- A varint size header split across three TCP reads (0x80, then 0x01,
  then payload) parses one complete message with no busy loop.

Also commit dist/ on this branch (and drop dist from .gitignore +
remove prepare/prepublish scripts) so consumers installing from this
git URL get a prebuilt package without needing to run the protobufjs
codegen + tsc build at install time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@zzorba zzorba force-pushed the filament/loop-parser branch from dcb0261 to a424caa Compare May 6, 2026 18:06
@Eneris Eneris requested a review from Copilot May 6, 2026 18:07
@zzorba
Copy link
Copy Markdown
Author

zzorba commented May 6, 2026

Sorry about that, I hadn't intended to upstream this but I guess now that its here it's worth it.

We were using this library and received some stack traces that looked like an infinite-stack-recursion crash from this library. Claude seemed to think there was a corner case where a long running process would keep retrying via recursion -- this changes it to use a loop instead.

@Eneris Eneris requested review from Copilot and removed request for Copilot May 6, 2026 19:41
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment thread src/parser.ts
Comment on lines +111 to +123
this.#handleGotVersion()
this.#handleGotMessageTag()
this.#handleGotMessageSize()
break
case ProcessingState.MCS_TAG_AND_SIZE:
this.#handleGotMessageTag()
this.#handleGotMessageSize()
break
case ProcessingState.MCS_SIZE:
this.#handleGotMessageSize()
break
case ProcessingState.MCS_PROTO_BYTES:
this.#handleGotMessageBytes()
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot proposed change sounds a bit excessive, but it has a point. Can you check it out for the PR please? @zzorba

@Eneris
Copy link
Copy Markdown
Owner

Eneris commented May 6, 2026

Othervise the PR looks good. I'll do some testing and let you know

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.

3 participants