fix: remove stale data listener explicitly on socket error#3209
fix: remove stale data listener explicitly on socket error#3209Chrisp1tv wants to merge 2 commits into
Conversation
|
Hi, I’m Jit, a friendly security platform designed to help developers build secure applications from day zero with an MVS (Minimal viable security) mindset. In case there are security findings, they will be communicated to you as a comment inside the PR. Hope you’ll enjoy using Jit. Questions? Comments? Want to learn more? Get in touch with us. |
|
Hey, sorry I didn't have time to introduce a reliable test for this yet! That would be great if a maintainer could add a test or give some guidance. Thanks 🙏 |
|
@Chrisp1tv thanks for this PR, i will take a look |
nkaradzhov
left a comment
There was a problem hiding this comment.
@Chrisp1tv, thanks for the careful writeup — the race is real. Traced in current master: on a socket error, RedisSocket.#onSocketError emits 'error', the client's error handler flushes and calls resetDecoder(), but the old net socket is still referenced and keeps its data listener, so any buffered bytes delivered afterward reach the freshly-reset decoder and can be matched to a new command. Removing the data listener and destroying the old socket inside #onSocketError is the right fix at the right layer, and the this.#socket?.destroy() guard in the #connect catch is a correct companion, since #socket can now become undefined mid-initiator.
Two things needed before merge:
-
Please rebase onto current
master.#onSocketErrorhas since gainedpublish(CHANNELS.ERROR, …)before theemit('error')and awasReady-guardedpublish(CHANNELS.CONNECTION_CLOSED, …)after it. Re-integrate the socket-capture/null/remove-listeners/destroy sequence without dropping those publishes, keeping ordering intact (capture + null → publish ERROR → emit → remove data listeners + destroy → CONNECTION_CLOSED → reconnect decision). -
Add a regression test for the interleaving. A fake socket that emits a
datachunk after theerrorevent, with an assertion that the reset decoder receives no stale bytes and a subsequent command's reply is not corrupted, would lock this in. Happy to point you at the fake-socket helpers in the client test-utils if useful.
Once rebased with that test, this is good to merge.

Description
Fix a race condition in
RedisSocketwhere buffereddataevents from a broken socket could be emitted after a reconnect had already started, causing stale responses to be matched to new commands.This PR tries to provide a fix for the bug reported in:
I must give some credits to @nicklvsa for the reproduction script I've been using to test the issue and my fixes. Thanks for this 🙏
Root cause (written by Claude):
RedisSocketattaches a persistentdatalistener to each TCP socket it creates. When a socket error occurs,#onSocketErrorflushes the command queue and resets the RESP decoder — but the old socket was not destroyed immediately, leaving itsdatalistener alive. Any responses already buffered in the kernel for the old socket would then fire, get parsed by the reset decoder, and be matched against new commands in the queue. This causes silent data corruption:GET key1could resolve with the value ofkey2.This fix:
this.#socketbefore emittingerror, so the reference is atomically transferred to a local variable before any listener runssocket.removeAllListeners('data')beforesocket.destroy(), ensuring no buffereddataevents from the old socket reach the decoder after teardownsocket.destroy()before the reconnectChecklist
npm testpass with this change (including linting)?Note
Medium Risk
Touches core socket error/reconnect handling; while small, it can affect connection teardown timing and event ordering during failures/reconnects.
Overview
Fixes a race in
RedisSocketteardown on errors by atomically clearingthis.#socketbefore emitting theerrorevent, and then explicitly removing the old socket’sdatalisteners before destroying it.Also hardens the initiator error path by using
this.#socket?.destroy()to avoid crashing if the socket reference has already been cleared.Written by Cursor Bugbot for commit c6b6288. This will update automatically on new commits. Configure here.