Skip to content

netcode 1.4.7: missing override and loopback callbacks are refused at setup; the simulator and address_to_string are hardened - #192

Merged
rowan-claude merged 1 commit into
mainfrom
rowan/release-1.4.7
Sep 13, 2026
Merged

netcode 1.4.7: missing override and loopback callbacks are refused at setup; the simulator and address_to_string are hardened#192
rowan-claude merged 1 commit into
mainfrom
rowan/release-1.4.7

Conversation

@rowan-claude

@rowan-claude rowan-claude commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

A missing callback is refused where it is configured, instead of being called as a null pointer.

A client or server config with override_send_and_receive set and either send_packet_override or receive_packet_override NULL is refused: netcode_client_create and netcode_server_create (and their _dual forms) log the reason and return NULL. This is a behaviour change, and it is the reason for two new create error codes, NETCODE_CLIENT_CREATE_ERROR_MISSING_OVERRIDE_CALLBACK (7) and NETCODE_SERVER_CREATE_ERROR_MISSING_OVERRIDE_CALLBACK (8), reported by netcode_client_create_error and netcode_server_create_error. The receive override is called on the update path with no null check, so a config missing it was a crash on the first update, and a config missing only the send override dropped sends silently; both are now a configuration error the create can name. Both codes are appended; no existing create error code changes value, and an integration that sets both overrides, or neither, sees no change.

netcode_client_connect_loopback and netcode_server_connect_loopback_client refuse the loopback connection, in release builds as well as debug, when send_loopback_packet_callback is unset, and log the reason. The loopback send path is entered only through those two functions, so it is reached only with a callback to call.

The network simulator allocates a packet before it touches its ring slot. If the allocator fails, the simulator logs, drops that packet, and leaves the entry already queued in the slot intact.

netcode_address_to_string states its buffer contract in the public header. NETCODE_MAX_ADDRESS_STRING_LENGTH sits in netcode.h beside NETCODE_MAX_PACKET_SIZE, and the declaration carries the contract: the buffer must be at least that many bytes, the result is null terminated, and a longer address is truncated to fit rather than overflowing. The signature is unchanged, so ports that mirror it are unaffected, and a caller that includes only netcode.h can size the buffer it is required to pass.

Console platforms need an RNG of their own. On __ORBIS__ and __PROSPERO__ the vendored libsodium's randombytes_sysrandom_buf has an empty body and does not write the buffer, and netcode draws every key and nonce from randombytes_buf. sodium/NOTES.md and IMPLEMENTERS.md now say so, for this library and for the ports that mirror it: a console port supplies a system RNG before it ships.

The wire format does not change and no function signature changes. The four code changes are not reachable from unauthenticated network input; the console RNG note is different in kind, because every connect token key is drawn from that RNG, so a console port without a system RNG has predictable keys, which is why the note is in the shipped docs. Integrations that link the library take this release by relinking; a caller that wants the new header constant recompiles against netcode.h. Integrations that vendor the source re-vendor: yojimbo vendors 1.4.5, so its hop to 1.4.7 also carries 1.4.6's slack fix, which is reachable from packet input. The C# port does not yet carry the two new create error codes (netcode.cs#7). The findings come from an outside security review.

Every version site the repo carries: project(netcode VERSION) in
CMakeLists.txt, and NETCODE_VERSION_FULL and NETCODE_VERSION_PATCH in
netcode.h. The release-check workflow requires all three to agree with
the tag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rowan-claude rowan-claude changed the title netcode 1.4.7: the security#24 fixes netcode 1.4.7: a missing callback is refused where it is configured, and the simulator, the address contract and the console RNG note are hardened Sep 13, 2026
@rowan-claude rowan-claude changed the title netcode 1.4.7: a missing callback is refused where it is configured, and the simulator, the address contract and the console RNG note are hardened netcode 1.4.7: missing override and loopback callbacks are refused at setup; the simulator and address_to_string are hardened Sep 13, 2026
@gafferongames

Copy link
Copy Markdown
Contributor

READ (Alex, security seat, at c6cfe36): APPROVE — every item is one of my findings, stated accurately, and the two behaviour changes are the correct ones to flag.

The mapping to my audit (security#24): the missing-callback refusal (my trivia note on half-configured overrides — now a create-time refusal with two new error codes, the right fix: name the configuration error where it is configured rather than crash on first update); the loopback callback refusal (my reading of the loopback path — release-checked now, correctly, because a missing callback there is the same class); the simulator allocation guard (the reliable.c finding's sibling in netcode's simulator — log, drop, keep the queue intact); the address_to_string buffer contract (my finding 2 exactly: the contract now stated in the header beside the constant, signature unchanged, truncation named — a consumer can now size the buffer from the header alone); the console RNG note (my deployment note, now in NOTES.md and IMPLEMENTERS.md where the ports will read it).

The two things a consumer must know, both said: (1) the two new error codes 7 and 8 — appended, no existing code changes value, an integration that sets both overrides or neither sees no change; (2) none of it is reachable from unauthenticated network input, so the threat model sentence is honest. The wire format unchanged. Existing integrations relink; only a caller wanting the new constant recompiles.

One note for the record, not a hold: the notes attribute to 'an outside security review' — my report also carried the two historical advisories and the verification that they are fixed, which is the context this release continues. Nothing missing from my seat. Cut 1.4.7.

@rowan-claude

Copy link
Copy Markdown
Contributor Author

Fable cold read (cold child) at c6cfe36: APPROVE

MEDIUM — "The overrides are called on the update path with no null check" is true of one half only — only receive_packet_override is called unguarded (netcode.c:3282 client, :5080 server); send_packet_override is already behind else if ( send_packet_override ) at :2777, and #191's own body says "Its twin is checked". So "rather than a crash on the first update" holds for a missing receive override; a config missing only the send override used to drop sends silently and is now refused. One-word fix in the body before the tag (the body is the release body): "The receive override is called on the update path with no null check". No new commit needed.
LOW — "refuse ... in release builds as well as debug ... and log the reason": in a debug build the netcode_assert at :3654/:5398 fires first and the default handler traps and exits, so the logged refusal is the release behaviour; debug asserts. Refused either way, so the sentence is true in effect.
LOW — netcode.h:190-196: the buffer-contract comment sits after netcode_address_to_string and directly above netcode_address_equal, so it reads as the latter's comment; the note says "the declaration carries the contract". In the merged tree, not this PR; a follow-up, not a hold.
LOW — the title (the house release name) is two clauses on a semicolon where v1.4.6's is one; optional.

Verified:

  • Version sites: CMakeLists.txt:14 and netcode.h:35/:38, the same two files netcode 1.4.6: payload packets carry the reader's eight bytes of slack #189 bumped for 1.4.6; release-check.yml checks exactly these and they agree at 1.4.7; no "1.4.6" string anywhere in the head tree (SECURITY.md names only 1.4.5 and 1.4.0).
  • Codes: NETCODE_CLIENT_CREATE_ERROR_MISSING_OVERRIDE_CALLBACK 7, NETCODE_SERVER_CREATE_ERROR_MISSING_OVERRIDE_CALLBACK 8, appended after 6 and 7; no existing value moves.
  • Loopback: client->loopback = 1 only at :3666 and client_loopback[i] = 1 only at :5421, both behind the new guard with a log line; the sends at :3553/:5296 are reached only under those flags.
  • Simulator: allocate into a local first, log and return on NULL, slot untouched (:2620-2640).
  • Header: NETCODE_MAX_ADDRESS_STRING_LENGTH 256 beside NETCODE_MAX_PACKET_SIZE, removed from netcode.c; the body writes through inet_ntop with the full 256 (> INET6_ADDRSTRLEN, always fits) and snprintf elsewhere, so terminated and truncating as stated.
  • Console RNG: sodium.c:1407 __ORBIS__ || __PROSPERO__, randombytes_sysrandom_buf body empty at :1421; netcode's keys and nonces all go through randombytes_buf (:1040/:1046/:1053), no other source; randombytes_set_implementation is in the vendored slice (sodium.h:1182).
  • Range v1.4.6...main is 4 commits, all Robustness batch from the 2026-09-13 audit (closes #190) #191 and its two read fixes, touching IMPLEMENTERS.md, netcode.c, netcode.h, sodium/NOTES.md; every change is in the notes.
  • Voice: present tense, what the user gets, no line numbers or issue pointers, no private-repo pointer. CI: 18 of 18 checks SUCCESS at this head.

@rowan-claude

Copy link
Copy Markdown
Contributor Author

Opus cold read (cold child) at c6cfe36: APPROVE

Consumer seat (yojimbo vendor, netcode.cs mirror). The code is right and CI is 18/18; all three findings are release-body text, editable before the tag.

HIGH — "Existing integrations take this release by relinking" is false for the vendoring consumer. yojimbo vendors the source (netcode/netcode.h there is NETCODE_VERSION_FULL "1.4.5") and pins it in dependencies.manifest (netcode 1.4.5 v1.4.5 1.4.5); that tag column drives the -DYOJIMBO_SYSTEM_DEPS job, so even the system-deps build installs v1.4.5. Taking 1.4.7 is a source re-vendor plus a manifest bump, not a relink — and the hop owed is 1.4.5 → 1.4.7, so it carries 1.4.6's eight-bytes-of-slack fix, which is reached from network payload input. Name the re-vendor and its span.

HIGH — "none of this is reachable from unauthenticated network input" does not hold for item 5 the way a consumer reads it. The same range adds IMPLEMENTERS.md finding 4: "Exploitability: total, on an affected build — the connect token keys are predictable". The stub is not reached by packet input, true — but its output is the key that defends against packet input, so on an __ORBIS__/__PROSPERO__ build an unauthenticated attacker gets everything. Carve item 5 out of the sentence (scope it to the four code items, and say the console note is a porting trap whose consequence is remote), or a console porter reading only the notes defers it.

MEDIUM — no consumer action named for the C# port. netcode.cs#7 is open, so NETCODE_CLIENT_CREATE_ERROR_MISSING_OVERRIDE_CALLBACK 7 and NETCODE_SERVER_CREATE_ERROR_MISSING_OVERRIDE_CALLBACK 8 exist in C and not in C# the moment 1.4.7 tags — two enums that mirror position for position, diverging on public values. #191 tracked it ("C# mirror owed: netcode.cs#7"); the release body drops it. v1.4.6's notes named their consumer; this one names none.

Verified from my seat:

  • No break for yojimbo on re-vendor: both overrides are set together (yojimbo_client.cpp:344-346, yojimbo_server.cpp:78-80) and send_loopback_packet_callback is set unconditionally before create (:340, :74), so "sets both overrides, or neither, sees no change" holds; both loopback entry points (yojimbo_client.cpp:289, yojimbo_server.cpp:257) are reached with the callback already in place.
  • Codes right and appended: netcode.h:124 7 after ALLOCATE_CLIENT_FAILED 6, netcode.h:134 8 after ALLOCATE_SERVER_FAILED 7; no existing value moves.
  • The notes name netcode_client_create / netcode_server_create; the refusal is in _dual (netcode.c:2925, :4205), reached from the singles at :3027 / :4312. _dual is public too and also refuses — worth naming, since the ports mirror _dual.
  • release-check.yml asserts exactly the CMakeLists project version, NETCODE_VERSION_FULL, MAJOR/MINOR/PATCH, and the tag. Those two files are the only bumped sites and they agree at 1.4.7; no other 1.4.6 in the head tree. It runs on tag push only, so nothing gates the bump on this PR — by design, and the values are right.
  • Range v1.4.6...main is 4 commits over 4 files, all Robustness batch from the 2026-09-13 audit (closes #190) #191 and its two read fixes; every hunk lands in one of the five items. Nothing unstated.

@rowan-claude
rowan-claude merged commit 7458bda into main Sep 13, 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.

2 participants