fix(#716): item 5 — bound >255-byte hostname in tcp_address_to_name#720
Conversation
NI_MAXHOST is 1025 (macOS, Linux) to accommodate non-DNS resolution sources (NIS, LDAP, /etc/hosts), but UserTalk's bigstring max payload is 255. A long PTR record -- or an attacker-controlled reverse lookup that returns a crafted hostname like "evil.com.<padding>...<more padding>.victim.com" -- would silently truncate via copyctopstring's post-#707 clamp, potentially leaving the prefix "evil.com" visible to the UserTalk-level identity check while concealing the actual intent of the lookup. The fix surfaces the truncation as an explicit log_warn and falls back to the dotted-decimal IP string -- the same fallback path used when getnameinfo() itself fails. Pattern matches frontier-cli/window_registry.c:116 and PR #714's post-#707 callsites. For testability, the hostname-to-bigstring decision was extracted from tcp_address_to_name into a new helper tcp_address_to_name_pack. The helper takes the resolved C string and the original address, so the truncation branch is testable without mocking getnameinfo. Two new behavioral unit tests in tests/tcp_phase1a_unit_tests.c: - address_to_name_pack_long_hostname_falls_back_to_ip: 300-byte synthetic hostname -> helper returns false, name_out is "192.168.1.1" - address_to_name_pack_short_hostname_preserved: "example.com" -> helper returns true, name_out is "example.com" verbatim Other tcpverbs.c copyctopstring callsites audited and verified bounded by construction: - tcp_set_error (line 387): char[256] bounded by snprintf - tcp_address_decode (line 666): INET_ADDRSTRLEN (16 bytes) - 9 string-literal sites (INACTIVE/OPEN/DATA/STOPPED/LISTENING/etc.) with payloads under 10 bytes WinSockNetEvents.c is Windows-only, not linked into headless build. Verified: - make build: clean - ./tools/run_headless_tests.sh: 591/591 pass (was 589, +2 new) - cd tests && make test-integration: 21 baseline failures (matches PR #717 baseline; no tcp_address_to_name failures) Closes item 5 of #716. Items 1, 3, 6 already closed; items 2, 4 closed as documentation-only. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…721 Security reviewer P2: add inline rationale to tcp_address_to_name_pack explaining (a) why dotted-decimal IP is the right fallback rather than empty / langerrormessage (UserTalk-caller identity-check safety, contract preservation), and (b) why the log_warn intentionally omits the truncated hostname (attacker-controlled, log-injection risk). Security reviewer P2: memset(name_out, 0, sizeof(name_out)) before each test call so a future regression that early-returns without writing surfaces as a deterministic empty string rather than reading stack garbage. Bar-raiser P2: WinSockNetEvents.c:1419 has the same bug pattern in the Windows path (not in headless build). Filed as #721 to track when/if the Windows path is revived. Bar-raiser P2 (comment placement) declined: the doc comment for tcp_address_to_name still reads correctly as "see the helper above for the truncation contract" -- moving it would create a different clarity tradeoff. Tests still 33/33. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
/auto SummaryWhat went well: This was the chain's payoff — a real bug with a clear attack model (attacker-controlled reverse DNS truncating past a UserTalk identity-check substring). Extracting What could improve: The chain's first three items (3, 4, 2) were doc-only because the audit targets pointed at legacy files not in the headless build. The audit script (the Plan-agent grep over Recommendations:
🤖 Generated with Claude Code /auto |
Summary
Closes item 5 of #716 — the only remaining open item. (Items 1, 3 already closed via PRs #718, #719; items 2, 4, 6 closed as documentation-only per audit reconciliation.)
The bug.
tcp_address_to_nameusesgetnameinfo()to do a reverse DNS lookup, writing into achar hostname[NI_MAXHOST]buffer.NI_MAXHOSTis 1025 on macOS/Linux to accommodate non-DNS resolution sources (NIS, LDAP,/etc/hosts). The resolved hostname is then handed tocopyctopstring(hostname, name_out)wherename_outis a 256-byte UserTalk bigstring. Per PR #707,copyctopstringclamps the payload to 255 bytes and returnsfalseon truncation — but the existing code attcpverbs.c:1273discarded that return value, silently writing a truncated hostname.A long PTR record — or an attacker-controlled reverse lookup result like
evil.com.<padding>...<more padding>.victim.com— could leave the prefixevil.comvisible to UserTalk-level identity checks while the actual lookup intent was concealed past the 255-byte cutoff.The fix. Check
copyctopstring's boolean return. On truncation, emit alog_warnand fall back to the dotted-decimal IP string — the same fallback path used whengetnameinfo()itself fails. Pattern matchesfrontier-cli/window_registry.c:116and PR #714's post-#707 callsites.Testability refactor. Extracted the post-
getnameinfohostname-to-bigstring decision into a new helpertcp_address_to_name_packso the truncation branch is exercisable without mockinggetnameinfo. The helper takes the resolved C string and the original address, writing the bigstring + fallback.Other tcpverbs.c callsites audited
Verified bounded by construction; no other fixes needed:
tcp_set_error(line 387)char[256]fromsnprintftcp_address_decode(line 666)INET_ADDRSTRLEN(16)WinSockNetEvents.cis Windows-only — not linked into headless build.Test plan
make build— clean./tools/run_headless_tests.sh— 591/591 pass (was 589; +2 new intcp_phase1a_unit_tests)./tests/tcp_phase1a_unit_tests— 33/33 (was 31; new tests at 3.8 and 3.9 exercise the helper directly)cd tests && make test-integration— in progress at PR open; will confirm matches 21-baseline before mergeBehavioral tests added
Long-hostname test: constructs a 300-byte synthetic C string, calls
tcp_address_to_name_pack, asserts:false(signaling fallback used)name_outcontains"192.168.1.1"(the dotted-decimal IP), NOT a 255-byte truncation of the synthetic hostnameShort-hostname test: passes
"example.com", asserts helper returnstrueandname_outcontains"example.com"verbatim.Scope
This PR closes the last code-touching item of #716. The audit-tail issue can be closed after this merges.
Discovery process
Original audit framing: "DNS-spec'd 253 bytes for hostnames, which is safely under 255". This turned out to be only half the story — DNS labels are bounded, but
getnameinforeturns intoNI_MAXHOST(1025) which can hold longer names from non-DNS sources. The actual truncation surface only appears when you trace from thecopyctopstringcall site upward through the buffer that feeds it, not just the spec for the most-common input source.🤖 Generated with Claude Code