β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5698

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5698 Β· 19 files
FileTypeDescriptionSize
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 <= size always
  • Guard-page overflow canary: dst placed so dst[size] is the first byte of a PROT_NONE page (and for size==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_pton round-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)
VERDICT.md
↓ download raw

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 kprintf arguments in sys/netinet/ip_icmp.c (ip_icmp.c:295, :474, :476, :608-609, :859-864, :880-883, :1134-1135), each passing a stack buffer of INET_ADDRSTRLEN (=16), which is exactly the worst-case need of inet_ntop4 ("255.255.255.255" = 15 chars + NUL; the gate l >= size at sys/libkern/inet_ntop.c:76 accepts exactly at size β‰₯ strlen+1).
  • None of them reads errno after the call; NULL returns are impossible for their (af, size) pairs anyway, and kprintf renders a NULL %s as "(null)" (sys/kern/subr_prf.c:750-753).
  • No AF_INET6 kernel caller exists in-tree, so inet_ntop6 is currently exercised only via this rig and by out-of-tree KLDs.
  • The missing errno matters 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:

Boundary-math proof (why no overflow exists)

  • inet_ntop4 formats into a 16-byte staging buffer (never truncated), then requires l < size (i.e. size >= strlen+1) before strlcpy(dst, tmp, size) β€” bounded exactly (sys/libkern/inet_ntop.c:75-80).
  • inet_ntop6 builds in a 46-byte staging buffer whose maximum possible content is 39 chars + NUL (8Γ—4 hex + 7 colons); the final gate compares tp - tmp (NUL included, since *tp++ = '\0' at :169 precedes it) against size before strcpy (sys/libkern/inet_ntop.c:174-177) β€” accepts exactly when strlen+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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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).
↓ fix.diffn/a (userland differential rig against the fixed TU; errno divergence requires no kernel rebuild to validate)

Confirmed kernel references

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).