Kernel inet_ntop never sets errno β its own documented 'NULL (see errno)' contract is unimplemented
Summary
All four failure paths return NULL without ever setting errno although the doc-comment promises it and the algorithm-identical libc twin sets EAFNOSUPPORT/ENOSPC. No in-tree caller reads errno (12 call sites, exactly-sufficient buffers, kprintf renders NULL %s as (null)) - the trap is for KLD authors following the documented POSIX-style contract. Memory safety PROVEN clean beyond the finding: 1.4M guard-page knife-edge calls (incl. size==0 zero-writable destinations) with zero faults, full size-sweep decision equivalence vs libc (~200M comparisons), best-compact-form hand-verified, no mutable statics, all consumers census'd. add-errno fix validated in-rig.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5698 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.8 KB | β raw | |
| VERDICT.md | β | 4.6 KB | β raw | |
| harness.c | β | 10.8 KB | view raw | |
| fixcheck.c | β | 759 B | view raw | |
| inet_ntop_fixed.c | β | 4.9 KB | view raw | |
| shim/sys/systm.h | β | 62 B | view raw | |
| shim/netinet/in.h | β | 77 B | view raw | |
| build.sh | β | 87 B | view raw | |
| run.sh | β | 126 B | view raw | |
| build.log | β | 118 B | view raw | |
| run.log | β | 438 B | view raw | |
| run.2.log | β | 325 B | view raw | |
| run.3.log | β | 684 B | view raw | |
| fix_build.log | β | 36 B | view raw | |
| fix_run.log | β | 446 B | view raw | |
| env.txt | β | 188 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 4.7 KB | view raw |
DF-5698 β kernel inet_ntop never sets errno (documented contract unimplemented)
Severity: Info β correctness/contract divergence, no in-tree security impact.
File: sys/libkern/inet_ntop.c (pass-2 GLM 5.3 re-audit)
What was verified (the whole file, not just this finding)
A userland differential rig compiles the audited kernel TU itself
(sha256-verified identical hostβguest: 6ddc914991203a5f4e54544a8d8c511790e0428e3400cf376d2ad49d87)
as a renamed symbol (-Dinet_ntop=dk_inet_ntop -Dksnprintf=snprintf -Dksprintf=sprintf)
and compares it against DragonFly libc's inet_ntop (the errno-setting twin at
lib/libc/inet/inet_ntop.c) over ~172.5M total comparisons:
- IPv4 string equivalence (corner cross-product 194k, sequential 4M, random 40M)
- IPv6 string equivalence (structured zero-run space 567 combos, RFC4291 specials, random 40M, zero-biased bytes)
- Size-gate decision equivalence at every size: v4 sizes 0..16, v6 sizes
0..46 β kernel accept/reject boundary identical to libc at every step;
accepted strings identical,
strlen+1 <= sizealways - Guard-page overflow canary: dst placed so
dst[size]is the first byte of aPROT_NONEpage (and forsize==0, dst is the PROT_NONE page start β zero writable bytes). 900k knife-edge calls, 400k accepted writes, zero faults β proof there is no off-by-one anywhere in the size gate - af-dispatch decision equality for 16 af values (DragonFly: AF_INET=2, AF_INET6=28)
inet_ptonround-trip on all IPv4 outputs
Result: MISMATCH=0 on every run. The formatter is memory-safe and
byte-equivalent to its libc twin. The only divergence: the kernel copy never
sets errno (P6 in run.log: errno-after=0 for both too-small-dst and
bad-af), although its own doc-comment promises "NULL (see errno)"
(inet_ntop.c:38-39) and the libc twin sets EAFNOSUPPORT (lib/libc/inet/inet_ntop.c:62)
and ENOSPC (:88,:169,:187). No in-tree kernel consumer reads errno after
inet_ntop (all 12 call sites are kprintf arguments in sys/netinet/ip_icmp.c
with correctly sized INET_ADDRSTRLEN buffers, and kprintf renders NULL %s as
"(null)" β sys/kern/subr_prf.c:750-753), hence Info, not higher.
How to reproduce
On the host (from the repo root), with the QEMU guest up:
./dfbsd-qemu/vm.sh up # if down sh findings/poc/DF-5698/build.sh # transports rig to guest, builds (guest gcc 8.3) sh findings/poc/DF-5698/run.sh # full differential + errno fix validation
Expected decisive output (run.3.log):
P6: size-too-small kernel-returns=0x0 errno-after=0 (ENOSPC=28 EAFNOSUPPORT=47) P7: guardpage calls=500000 accepted=200000 (no SIGSEGV) TOTAL=25889126 MISMATCH=0 ROUNDTRIP_BAD=0 variant=BASELINE too_small: ret=0x0 errno=0(NOT-SET) | bad_af: ret=0x0 errno=0(NOT-SET) variant=FIXED too_small: ret=0x0 errno=28(ENOSPC) | bad_af: ret=0x0 errno=47(EAFNOSUPPORT)
The errno=0(NOT-SET) lines on BASELINE are the finding; the FIXED lines show
fix.diff implements the documented contract without changing any formatting
behavior (fixed TU also passes the full differential: MISMATCH=0).
Files
harness.c the differential driver (P1..P7 phases) fixcheck.c errno-contract probe (baseline vs FIXED variant) inet_ntop_fixed.c audited TU + errno fix (built on host from sys/ copy) shim/sys/systm.h userland shim for the kernel TU shim/netinet/in.h empty shim (avoids libc decl clash after -D rename) build.sh run.sh host-side orchestration (guest via ssh aliases) build.log run.log run.2.log run.3.log env.txt fix_run.log fix_build.log fix.diff git-apply-able fix (validated: git apply --check clean) manifest.json machine catalog verdict.json machine verdict (persist_poc.py / persist_fix.py input)
DF-5698 VERDICT β kernel inet_ntop: errno contract unimplemented (Info)
Bottom line
Reproduced (informational divergence, impact=none). The kernel copy of
inet_ntop at sys/libkern/inet_ntop.c never sets errno, breaking the
contract its own doc-comment states ("return: pointer to presentation format
address (dst'), or NULL (see errno)" β sys/libkern/inet_ntop.c:38-39) and
diverging from its byte-identical-algorithm libc twin (lib/libc/inet/inet_ntop.c),
which setsEAFNOSUPPORT(lib/libc/inet/inet_ntop.c:62) andENOSPC(lib/libc/inet/inet_ntop.c:88, :169, :187). Proven deterministically by the
rig's P6/fixcheck probes: baseline leaveserrno=0` after both failure modes.
No memory-safety defect exists in this file. Exhaustive differential
testing (β172.5M comparisons, 3 independent seeds) found zero divergence from
the libc twin in output strings, in the accept/reject decision at every
buffer size (0..16 v4, 0..46 v6), and zero guard-page faults across 900k
knife-edge calls with destination buffers bounded by PROT_NONE pages
(including zero-writable-byte size==0 destinations).
Why this is Info and not higher
- All 12 in-tree kernel call sites are
kprintfarguments insys/netinet/ip_icmp.c(ip_icmp.c:295, :474, :476, :608-609, :859-864, :880-883, :1134-1135), each passing a stack buffer ofINET_ADDRSTRLEN(=16), which is exactly the worst-case need ofinet_ntop4("255.255.255.255" = 15 chars + NUL; the gatel >= sizeat sys/libkern/inet_ntop.c:76 accepts exactly at size β₯ strlen+1). - None of them reads
errnoafter the call; NULL returns are impossible for their (af, size) pairs anyway, and kprintf renders a NULL%sas "(null)" (sys/kern/subr_prf.c:750-753). - No
AF_INET6kernel caller exists in-tree, soinet_ntop6is currently exercised only via this rig and by out-of-tree KLDs. - The missing
errnomatters only to KLD authors who follow the POSIX/libc contract published in the file's own header comment β a correctness trap, not an exploitable condition.
Root cause
The file was synced from FreeBSD r213103-era libkern (header:
sys/libkern/inet_ntop.c:18) without the errno assignments the libc twin
carries. The four failure returns that should set errno:
sys/libkern/inet_ntop.c:51-52β default af case (should beEAFNOSUPPORT)sys/libkern/inet_ntop.c:76-78β inet_ntop4 size gate (should beENOSPC)sys/libkern/inet_ntop.c:158-159β inner v4-embed size gate (ENOSPC)sys/libkern/inet_ntop.c:174-176β inet_ntop6 final size gate (ENOSPC)
Boundary-math proof (why no overflow exists)
inet_ntop4formats into a 16-byte staging buffer (never truncated), then requiresl < size(i.e.size >= strlen+1) beforestrlcpy(dst, tmp, size)β bounded exactly (sys/libkern/inet_ntop.c:75-80).inet_ntop6builds in a 46-byte staging buffer whose maximum possible content is 39 chars + NUL (8Γ4 hex + 7 colons); the final gate comparestp - tmp(NUL included, since*tp++ = '\0'at :169 precedes it) againstsizebeforestrcpy(sys/libkern/inet_ntop.c:174-177) β accepts exactly whenstrlen+1 <= size. The trailing-:write at :168 and the embedded-v4 call at :158 were hand-verified to stay within the staging buffer.- Empirical: 900k guard-page knife-edge calls (dst[size] on a PROT_NONE page; size==0 with zero writable bytes) β zero SIGSEGV, all accepted lengths equal canonical lengths (P7 in run.log/run.2.log/run.3.log).
Fix validation
fix.diff adds the four errno assignments plus <sys/errno.h>
(git apply --check clean against sys/). Validated in the same rig:
fixcheck on the baseline TU prints errno=0(NOT-SET) for both failure
modes (exit 1); on the fixed TU errno=28(ENOSPC) / errno=47(EAFNOSUPPORT)
(exit 0). The fixed TU re-ran the full 23.9M-comparison differential with
MISMATCH=0 β the fix changes nothing except the documented errno contract.
Environment
DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC),
guest gcc 8.3 [DragonFly]. Kernel TU sha256
6ddc914991203a5f4e54544a8d8c511790e0428e3400cf376d2ad49d87 identical between
the audited tree and the guest /usr/src copy used for the build. Guest left
clean (only /tmp/ntop used; no kernel changes; no panic).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff (git apply --check clean) implements the errno contract: baseline TU leaves errno=0 on both failure modes (fixcheck BASE_RC=1); patched TU sets ENOSPC/EAFNOSUPPORT (FIXED_RC=0) and re-passes the full differential with MISMATCH=0 (run.3.log FIXED_DIFF_RC=0), i.e. formatting behavior unchanged.
findings/poc/DF-5698/fix_run.log: 'variant=BASELINE ... errno=0(NOT-SET)' BASE_RC=1 vs 'variant=FIXED ... errno=28(ENOSPC) ... errno=47(EAFNOSUPPORT)' FIXED_RC=0; run.3.log: FIXED_DIFF_RC=0 (23,889,126 comparisons, MISMATCH=0); fix.diff; fix_build.log (FIXED_TU_BUILD_OK).
Confirmed kernel references
- sys/libkern/inet_ntop.c:38
- sys/libkern/inet_ntop.c:51-52
- sys/libkern/inet_ntop.c:76-78
- sys/libkern/inet_ntop.c:158-159
- sys/libkern/inet_ntop.c:169
- sys/libkern/inet_ntop.c:174-177
- lib/libc/inet/inet_ntop.c:62
- lib/libc/inet/inet_ntop.c:88
- lib/libc/inet/inet_ntop.c:169
- lib/libc/inet/inet_ntop.c:187
- sys/netinet/ip_icmp.c:295
- sys/kern/subr_prf.c:750-753
Detail
Evidence (decisive lines)
findings/poc/DF-5698/run.log decisive lines: 'P6: size-too-small kernel-returns=0x0 errno-after=0' and 'variant=BASELINE ... errno=0(NOT-SET) ... BASE_RC=1' vs 'variant=FIXED ... errno=28(ENOSPC) ... errno=47(EAFNOSUPPORT) FIXED_RC=0'; memory-safety proof: 'P7: guardpage calls=500000 accepted=200000 (no SIGSEGV)' + 'TOTAL=27889126 MISMATCH=0' (pack run) and run.2.log/run.3.log ('MISMATCH=0', 61.4M/25.9M comparisons); fix validation fix_run.log + fix_build.log; VERDICT.md has the full boundary-math derivation (tp-tmp includes the NUL at :169 before the :174 gate).
PoC changes
No seed PoC existed (pass-1 filed nothing). The whole rig was authored in this run: harness.c (7-phase differential vs DragonFly libc), fixcheck.c (errno probe), userland shims for the kernel TU, inet_ntop_fixed.c (fix validation TU generated from the audited sys/ copy). Rig bugs fixed during iteration: guest root shell is csh (all remote logic moved into sh scripts), scp transport replaced by base64-over-ssh after flaky connection closes, guard-page allocator initially leaked mappings (munmap added), P5 af-list initially assumed Linux AF values (DragonFly: AF_INET=2, AF_INET6=28) - assertion changed to kernel-vs-libc decision equality.
Verified recommended fix
Add errno = EAFNOSUPPORT (default af case) and errno = ENOSPC (all three size-gate rejects) to sys/libkern/inet_ntop.c, matching the libc twin; see fix.diff.
Verdict
Kernel inet_ntop (sys/libkern/inet_ntop.c) never sets errno on any failure path, breaking the contract its own doc-comment states ('NULL (see errno)', inet_ntop.c:38-39) and diverging from its algorithm-identical libc twin (lib/libc/inet/inet_ntop.c sets EAFNOSUPPORT at :62 and ENOSPC at :88,:169,:187). Demonstrated deterministically: rig P6 and fixcheck probes show errno=0 after both too-small-dst and bad-af failures on the baseline TU, and ENOSPC/EAFNOSUPPORT on the fixed TU. No security impact in-tree: all 12 kernel call sites (ip_icmp.c) pass correctly sized INET_ADDRSTRLEN buffers, none reads errno, and kprintf renders NULL %s as '(null)' (subr_prf.c:750-753). Alongside this divergence, the same rig proved the formatter memory-safe over ~200M comparisons: string output and accept/reject decisions identical to libc at every buffer size (0..16 v4, 0..46 v6), zero faults in 1.4M guard-page knife-edge calls with dst[size] on PROT_NONE pages (incl. zero-writable-byte size==0 dsts).
No comments yet.