Skip to content

Prevent ACK ID collisions in synchronous and asynchronous clients - #514

Open
utrumo wants to merge 1 commit into
1c3t3a:mainfrom
utrumo:fix/ack-id-collisions
Open

utrumo wants to merge 1 commit into
1c3t3a:mainfrom
utrumo:fix/ack-id-collisions

Conversation

@utrumo

@utrumo utrumo commented Oct 6, 2026 •

Copy link
Copy Markdown

Problem

Both clients choose an ACK ID using thread_rng().gen_range(0..999) without
checking outstanding requests. Two pending requests can therefore share an ID.
In the asynchronous client, handle_ack records matching vector indices and
then removes them in ascending order. Earlier removals invalidate later
indices: pending IDs [7, 8, 7] produce indices [0, 2], and removing index 2
after index 0 panics with removal index (is 2) should be < len (is 2).
The receive task stops, so subsequent events stop reaching the application.

The synchronous client on main already removes one matching entry safely and
calls its callback outside the mutex. It does not have that index-removal
panic, but collisions can still associate a reply with the wrong request and
leave another request with the same ID outstanding.

Change

  • Select an unused ID from the full nonnegative i32 range, advancing past
    occupied IDs and wrapping from i32::MAX to zero.
  • Hold the existing registry lock through ID selection and insertion, so
    concurrent callers (including cloned clients) cannot reserve the same ID.
  • Extract all matching ACKs without retaining indices across removals.
  • Release the registry lock before invoking callbacks or awaiting async
    callbacks. Timeout checks and text/binary payload handling are preserved.
  • Add deterministic in-memory regression tests for collision resolution,
    wraparound, removing duplicate ACKs, preserving unrelated requests, and
    ignoring an already handled ID. Both clients call the tested registry
    functions in production; no socket, process, network, or mock implementation
    of the registry is involved.

This is scoped to outstanding ACK IDs. It does not change timeout cleanup,
reconnection, or failed-send cleanup, and makes no claim about late replies
after an ID has been retired.

Reproduction

The panic follows directly from handle_ack (see Problem above); the added
regression tests cover it and pass with this change:

cargo test -p rust_socketio --all-features --lib regression_tests

Validation

Following CONTRIBUTING.md and the Makefile's feature settings:

cargo test -p rust_socketio --all-features --lib regression_tests
cargo test -p rust_socketio --no-default-features --lib regression_tests
cargo test --workspace --all-features --lib packet
cargo build --verbose --all-features
cargo clippy --verbose --all-features
cargo fmt --all -- --check

Results: 4/4 ACK regressions with all features, 2/2 synchronous regressions
without async, 7/7 engineio packet tests and 6/6 socketio packet tests. Build,
clippy and formatting passed. Clippy reports 76 engineio and 75 socketio
warnings on both clean upstream main and the patched tree; warnings were not
promoted to errors because the upstream Makefile does not do so.

The network integration suite (make test-all, which starts Docker servers)
was not run. Callback lock release was reviewed in the production code rather
than exercised with a connected client.

Reserve an unused ACK ID under the registration lock in both clients. Extract matching ACKs without stale indices and release the registry lock before invoking callbacks. Add deterministic, network-free regression tests.
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