Skip to content

netcode_generate_connect_token: bound num_server_addresses at runtime (#173) - #176

Merged
rowan-claude merged 2 commits into
mainfrom
fix/read-path-checks
Jul 26, 2026
Merged

netcode_generate_connect_token: bound num_server_addresses at runtime (#173)#176
rowan-claude merged 2 commits into
mainfrom
fix/read-path-checks

Conversation

@rowan-claude

Copy link
Copy Markdown
Contributor

Fixes #173.

The two bounds on num_server_addresses were netcode_assert only. Release builds define NDEBUG, those compile to ((void)0), and the parse loops write into parsed_public_server_addresses[NETCODE_MAX_SERVERS_PER_CONNECT] — a 32-element stack array — with no runtime bound.

This completes an existing decision, it does not introduce a policy

netcode.c:4101-4103 already states the rule outright:

an out of range value here must not get through in release builds where asserts compile out

…and the same assert+check pairing is on netcode_client_send_packet (3387), netcode_server_start (4104), netcode_server_send_packet (4998, 5010), and every client_index accessor. netcode_generate_connect_token is the one public entry point that was skipped. The tell is internal to the function: the same loop body already returns NETCODE_ERROR for a malformed address string.

Severity

Caller-controlled, not reachable from the wire — the count comes from whatever mints tokens. A precondition violation, not a remote vulnerability, which is why it was not filed as an advisory. The wire-facing path (netcode_read_packet, netcode_read_connect_token_private) validates before every write and is unaffected.

It cannot break a valid caller: anyone passing an out-of-range count was already in undefined behaviour, and this only turns that into a documented error return.

The test had to defeat the asserts to reach the release path

The asserts fire first by design, so test_generate_connect_token_out_of_range installs an assert handler that returns — the behaviour documented at netcode.h:362-366 for exactly this situation. It then:

  • checks all three out-of-range values return NETCODE_ERROR
  • checks the asserts still fired, so it cannot quietly pass by proving they were removed
  • checks an in-range call still returns NETCODE_OK, so it cannot pass by rejecting everything
  • restores &netcode_default_assert_handler rather than NULLnetcode_assert calls the pointer with no null guard, so NULL would turn the next failing assert anywhere in the suite into a crash

Verified to fail without the fix (suite exits 133) and pass with it.

Deliberately not changed

netcode_parse_address("[::1") still returns OK and yields ::1. I had this queued as a tightening and dropped it: that is permissive-on-receive and it is intended. #174 should close as by-design.

Likewise nothing was added to any write path — asserts there are the contract, and the caller owns not invoking UB.

🤖 Generated with Claude Code

…#173)

The two bounds were netcode_assert only. Release builds define NDEBUG, the asserts
compile to ((void)0), and the parse loops at 5296 and 5307 write into
parsed_public_server_addresses[NETCODE_MAX_SERVERS_PER_CONNECT] -- a 32-element
STACK array -- with no runtime bound. netcode_generate_connect_token_private
repeats the pattern into connect_token->server_addresses.

This completes a decision this file already made rather than introducing a new
policy. netcode.c:4101-4103 states it outright -- "an out of range value here must
not get through in release builds where asserts compile out" -- and the same
pairing is on netcode_client_send_packet (3387), netcode_server_start (4104),
netcode_server_send_packet (4998, 5010) and every client_index accessor.
netcode_generate_connect_token was the one entry point missed in that pass. The
tell is that the same loop body already returns NETCODE_ERROR for a malformed
address string.

Caller-controlled, not reachable from the wire: the count comes from whatever mints
tokens, so this is a precondition violation rather than a remote vulnerability. Not
filed as an advisory. It cannot break a valid caller either -- anyone passing an
out-of-range count was already in undefined behaviour, and this only turns that
into a documented error return.

The regression test has to defeat the asserts to reach the release path, so it
installs a handler that returns -- the behaviour netcode.h:362-366 documents for
exactly this. It also asserts that the asserts still fired, so it cannot quietly
pass by proving they were removed, and it checks an in-range call still succeeds so
it cannot pass by rejecting everything. Verified to FAIL without the fix (suite
exits 133) and pass with it.

Deliberately NOT changed: netcode_parse_address("[::1") still returns OK. That is
permissive-on-receive and it is intended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI caught this on every Release job. The test asserted that the asserts had fired,
which is right in a debug build -- they are the caller's debug aid and the test must
not silently prove they were removed -- but in a release build they compile to
((void)0) by design, so requiring them failed the exact configuration the fix exists
for.

My mistake was verifying only a debug build locally. Same shape as the lesson from
this morning: a green build under laxer flags than CI is a statement about my flags.
Both configurations are now verified locally, in both directions -- Release without
the fix still fails the test, Release with it passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rowan-claude
rowan-claude merged commit ecdba96 into main Jul 26, 2026
14 checks passed
@rowan-claude
rowan-claude deleted the fix/read-path-checks branch July 26, 2026 21:18
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.

netcode_generate_connect_token: server-address bounds are assert-only, so release builds write past a stack array

1 participant