netcode_generate_connect_token: bound num_server_addresses at runtime (#173) - #176
Merged
Conversation
…#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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #173.
The two bounds on
num_server_addresseswerenetcode_assertonly. Release builds defineNDEBUG, those compile to((void)0), and the parse loops write intoparsed_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-4103already states the rule outright:…and the same assert+check pairing is on
netcode_client_send_packet(3387),netcode_server_start(4104),netcode_server_send_packet(4998, 5010), and everyclient_indexaccessor.netcode_generate_connect_tokenis the one public entry point that was skipped. The tell is internal to the function: the same loop body already returnsNETCODE_ERRORfor 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_rangeinstalls an assert handler that returns — the behaviour documented atnetcode.h:362-366for exactly this situation. It then:NETCODE_ERRORNETCODE_OK, so it cannot pass by rejecting everything&netcode_default_assert_handlerrather thanNULL—netcode_assertcalls the pointer with no null guard, soNULLwould turn the next failing assert anywhere in the suite into a crashVerified 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