Skip to content

fix: recover from parser errors instead of crashing the process#2138

Open
GiHoon1123 wants to merge 1 commit into
redis:mainfrom
GiHoon1123:fix-2108-parser-recursion-crash
Open

fix: recover from parser errors instead of crashing the process#2138
GiHoon1123 wants to merge 1 commit into
redis:mainfrom
GiHoon1123:fix-2108-parser-recursion-crash

Conversation

@GiHoon1123

@GiHoon1123 GiHoon1123 commented Jul 14, 2026

Copy link
Copy Markdown

Summary

redis-parser recursively parses RESP arrays without a depth limit.

A deeply nested reply can overflow the JS call stack and throw a synchronous RangeError.

DataHandler currently calls parser.execute(data) directly from the "data" event, so the exception escapes and crashes the process.

Fix

Wrap parser.execute(data) in try/catch and forward unexpected errors to the existing returnFatalError -> recoverFromFatalError path.

This PR only handles the ioredis side. Adding a recursion limit belongs in redis-parser.

Test

  • Added a regression test.
  • Verified the test fails without the fix.
  • Verified the client reconnects instead of crashing.
  • Ran test:js. One pre-existing flaky test (unrelated to this change) also fails on unmodified main.

Test plan

  • Added regression test
  • Confirmed regression test fails without the fix
  • Full local test suite passes (aside from one pre-existing, unrelated flake)
  • Maintainer review

Fixes #2108


Note

Medium Risk
Touches the hot path for every inbound Redis byte and changes failure handling for parse errors; behavior is aligned with existing fatal recovery but affects connection lifecycle under attack or corruption.

Overview
Prevents process crashes when the Redis stream delivers a deeply nested RESP2 reply that makes redis-parser throw a synchronous RangeError (stack overflow) instead of using its normal fatal-parser callback.

In DataHandler, the stream "data" handler now wraps parser.execute in try/catch and forwards caught errors through the existing returnFatalErrorrecoverFromFatalError path with { offlineQueue: false }, matching behavior for other fatal protocol errors (flush in-flight commands, preserve offline queue, reconnect).

A unit regression test feeds a 50k-level nested array buffer and asserts no throw, a single recoverFromFatalError call, and the expected error message/options.

Reviewed by Cursor Bugbot for commit ac38e15. Bugbot is set up for automated code reviews on this repo. Configure here.

redis-parser recurses once per level of RESP array nesting with no
depth limit, so a sufficiently deeply nested reply throws a
synchronous RangeError instead of a reportable parser error. Because
DataHandler's stream "data" listener had no try/catch around
parser.execute(), this RangeError escaped as an uncaught exception,
crashing the process.

Wrap parser.execute() in try/catch and route failures through the
existing returnFatalError -> recoverFromFatalError recovery path,
which already handles parser-detected fatal errors the same way:
flush the in-flight command queue, keep the not-yet-sent offline
queue intact, and reconnect.

Fixes redis#2108
@PavelPashov

Copy link
Copy Markdown
Contributor

@GiHoon1123 thank you for this PR, I'll take a look, however please check #2127 as it might make sense to adapt the change for that PR.

@GiHoon1123

Copy link
Copy Markdown
Author

Thanks for the pointer! I took a look at #2127 — its DataHandler.ts already wraps decoder.write() in a try/catch that routes into returnFatalError the same way, so this exact crash looks like it's already handled there for the new RESP3 decoder path.

Since #2127 is still in progress, this PR still seemed worth having as a fix for the currently released behavior - main still calls redis-parser directly with no such guard. Happy to close this in favor of #2127 once it lands, or adjust the scope here if you'd prefer a different approach - whichever is more useful, just let me 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.

Deeply nested RESP aggregates can trigger uncaught RangeError and crash the process

2 participants