Skip to content

fix: harden sequence matching and prevent resolver leaks - #710

Open
atwright147 wants to merge 2 commits into
codetheweb:masterfrom
atwright147:fix/sequence-matching-hardening
Open

atwright147 wants to merge 2 commits into
codetheweb:masterfrom
atwright147:fix/sequence-matching-hardening

Conversation

@atwright147

Copy link
Copy Markdown

The existing PR #709 fixes the forward-sync from received packets. However, on v3.4 devices the device numbers its replies from its own counter (does not echo ours), so replies can arrive on a different sequence number than registered or collide with other pending requests. This causes:

  1. A read to hang when a reply lands off-by-one/collided.
  2. A read to resolve with the wrong payload when a packet collides on the sequence number and hits the wrong resolver (observed as getState() returning all nulls).
  3. Every SET leaks its resolver forever because the CONTROL/CONTROL_NEW ack path returned early without draining it.

This PR hardens _packetHandler/_send:

  • Record each request's command byte on its resolver (_send now takes commandByte and stores resolver.commandByte) so replies can be matched by intent.
  • Match DP_QUERY/DP_QUERY_NEW replies by command byte (newest outstanding) when the exact sequence match fails or would be unsafe; this tolerates off-by-one/collisions without mis-resolving other requests.
  • Drain resolvers on CONTROL/CONTROL_NEW ack by command byte (prevents the per-SET leak).
  • Add guards so a query resolver is never resolved by an unrelated packet that merely collides on sequence number.
  • Add _newestResolverFor helper.

These are backwards compatible and only affect matching behaviour when sequence numbers drift/collide. They fix the rapid-fire SET->GET case we reproduced on real ECOSO Torr heaters.

The device advances its own sequence number for every packet it sends,
including unsolicited ones — e.g. the STATUS/DP_REFRESH push a v3.4 device
emits immediately after a SET. `_currentSequenceN` was only advanced when *we*
sent a packet, so after such a push the client's next request reused a sequence
number the device had already consumed. The device then answered with a
different number and the resolver registered for the request was never called,
leaving the pending get()/set() unresolved until the socket was recycled.

Reproduced against a Tuya v3.4 panel heater:

  connect -> _currentSequenceN = 53896
  SET     -> request seq 53897 ; device acks seq 53897
  (push)  -> device sends STATUS seq 53898 ; client counter stays 53897
  GET     -> request seq 53898 ; device replies seq 53899
             -> no resolver for 53899 -> get() hangs

Re-sync `_currentSequenceN` from every received packet in `_packetHandler()`.
Only moving it forward keeps it monotonic. This is safe for the session-key
negotiation: the SESS_KEY_NEG_RES branch that follows still rebases the counter
with `sequenceN - 1`.

Adds test/sequence.js covering the re-sync, a no-rewind guard, and the
end-to-end case where a request must still resolve after an unsolicited push.
- record request command byte on resolver so replies can be matched when sequence numbers drift
- match DP_QUERY/DP_QUERY_NEW replies by command byte (newest outstanding) to tolerate off-by-one/number collisions
- drain resolvers on CONTROL/CONTROL_NEW ack (prevents set() leak)
- guard exact-seq matching so a query resolver isn't mis-resolved by an unrelated packet
- add _newestResolverFor helper

These fix the case where the device doesn't echo our sequence numbers (replies use its own counter) and prevent stale resolvers from being triggered.

This branch has not been deployed

No deployments
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.

1 participant