Skip to content

netfix-disconnect-callback-cannot-reenter-a-clearing-slot: a slot being disconnected is not disconnected again - #196

Merged
rowan-claude merged 2 commits into
sprint/netcode-audit-fixesfrom
sprint/netcode-disconnect-callback-cannot-reenter-a-clearing-slot.w1.g1.e20
Oct 14, 2026
Merged

rowan-claude merged 2 commits into
sprint/netcode-audit-fixesfrom
sprint/netcode-disconnect-callback-cannot-reenter-a-clearing-slot.w1.g1.e20

Conversation

@rowan-claude

Copy link
Copy Markdown
Contributor

Card: netfix-disconnect-callback-cannot-reenter-a-clearing-slot (stream netcode-audit-fixes, NET-AUDIT 2026-10-13, audit J finding 7 / audit E E-20, E-23).

Defect. The disconnect callback runs while the slot is still connected. A callback that disconnects the same client re-enters netcode_server_disconnect_client_internal; the inner call clears the slot (encryption index -1, count decremented), then the outer frame reads encryption_manager.address[-1] in netcode_encryption_manager_touch, writes encryption_manager.client_index[-1], and decrements num_connected_clients to -1. A callback that calls netcode_server_stop reaches the same slot through netcode_server_disconnect_all_clients.

Fix (netcode.c only).

  • client_disconnecting[NETCODE_MAX_CLIENTS] on the server: set before the callback, cleared in netcode_server_reset_client_slot.
  • netcode_server_disconnect_client, netcode_server_disconnect_all_clients, netcode_server_disconnect_loopback_client and stop's loopback loop skip a slot that is disconnecting.
  • netcode_server_stop clears a slot whose callback called stop, without a second callback, so a restart finds every slot empty.
  • After the callback, the internal disconnect returns at once if !running || !client_connected (the loopback disconnect does the same).
  • The client_index[...] = -1 write runs only for an encryption index >= 0.
  • The callback can still read the slot's reason, id and user data. The comment above the call says that destroying the server from inside the callback is not supported.

Test. test_disconnect_callback_reentry_is_harmless. It brings up a server with the script override hooks (no socket, time passed in) and admits one client through a real handshake. It runs three cases: (A) the callback disconnects the same client, (B) the callback stops the server, then a restart, (C) a timeout inside netcode_server_update whose callback stops the server. Each case checks that no assert fired, that the callback ran exactly once, that it saw the reason, id and user data, that the count is 0, that the slot and encryption index are clear, that the mapping count is 0, and that nothing in the manager points at the slot.

Red (f94c32b, hetzner1):

  • Debug: check failed: ( num_ignored_asserts == 0 ), function test_reentry_check_slot_cleared ... line 11172, exit 132
  • ASan/UBSan: netcode.c:3901:68: runtime error: index -1 out of bounds for type 'netcode_address_t [1024]', exit 1

Green (be2e702, hetzner1): ./build/bin/test and ./build-asan/bin/test (-DNETCODE_SANITIZE=ON) both print *** ALL TESTS PASSED ***, exit 0, 58 tests.

Planted regression. Removing the disconnecting skip in netcode_server_disconnect_client turns the test red again (check failed: ( num_ignored_asserts == 0 ), exit 132). After the restore it is green in both builds.

Not done.

  • A stop from inside a callback that netcode_server_receive_packets triggered (a disconnect packet) returns to a receive loop that keeps reading packets on the stopped server. That path is not covered here.
  • The yojimbo vendor bump and yojifix-server-stop-inside-a-disconnect-callback-is-deferred are separate cards.

Worked by Rowan (Opus lane)

🤖 Generated with Claude Code

rowan-claude and others added 2 commits October 13, 2026 20:36
…or a disconnect re-entered from the callback

By: Rowan
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng disconnected is not disconnected again

The disconnect callback runs while the slot still holds its client. The slot
is now marked as disconnecting for the length of the callback: a disconnect of
it from inside the callback is a no-op, stop skips it in its disconnect loops
and then clears it without a second callback, and the outer disconnect returns
at once when the callback has stopped the server. The encryption manager's
back pointer is cleared only for a valid encryption index. The loopback
disconnect takes the same guard.

By: Rowan
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rowan-claude
rowan-claude merged commit bbf9805 into sprint/netcode-audit-fixes Oct 14, 2026
19 checks passed
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