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

kernel strcpy() invoked on overlapping src/dst by the netgraph text parser β€” restrict-contract violation whose correctness silently depends on strcpy.c's byte-forward implementation

Summary

The netgraph ASCII escape parser shifts its buffer left with strcpy(p, p+1) and strcpy(p, v) (duplicated verbatim in netgraph7) - UB per C11 (both prototypes __restrict). Works today only because strcpy.c is a forward byte loop; an instrumented rig with the verbatim algorithm counts 5 overlapping invocations in one parse, and a block-store strcpy model applied to the EXACT kernel allocation (kmalloc(strlen(s+start)), no +1, zero slack) SIGSEGVs on a guard page - an implementation-plausible strcpy swap becomes a kernel heap OOB write (in-tree precedent: hptrr osm.h defines strcpy to __builtin_strcpy). Root-gated (SYSCAP_RESTRICTEDROOT on ng sockets) - Info hardening with demonstrated latent hazard; memmove fix validated byte-identical. skpc.c CLEAN (byte-identical to FreeBSD 9.3, zero in-tree callers); strcat.c CLEAN (110-site census all bounded, the classic IPv6-log overflow already fixed).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5718 Β· 12 files
FileTypeDescriptionSize
overlap_rig.c β€” 10.9 KB view raw
wrestrict_shim.c β€” 570 B view raw
build.sh β€” 149 B view raw
run.sh β€” 90 B view raw
build.log β€” 291 B view raw
run.log β€” 889 B view raw
env.txt β€” 207 B view raw
fix.diff β€” 1.2 KB view raw
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 5.1 KB ↓ raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.9 KB view raw

DF-5718 β€” kernel strcpy invoked on overlapping regions by the netgraph text parser

What this is

Evidence pack for the pass-2 audit of sys/libkern/strcpy.c (batch with sys/libkern/skpc.c, sys/libkern/strcat.c). The unbounded-copy primitive itself is a correct implementation of the C contract; the actionable hazard found in the consumer census is that the netgraph ASCII message parser calls strcpy() with overlapping source and destination, which is undefined behavior under the restrict contract both prototypes declare (sys/sys/libkern.h:131), and works today only because the in-tree sys/libkern/strcpy.c:40 is a forward byte loop.

Overlapping call sites (all verified by reading and by the rig):

Severity: Info (hardening / latent). Netgraph control messages that reach this parser are gated behind caps_priv_check(SYSCAP_RESTRICTEDROOT) on DFly (sys/netgraph/socket/ng_socket.c:171-175), so there is no unprivileged-reachable bug in the shipped kernel.

How to reproduce (on the DragonFly guest)

# copy overlap_rig.c wrestrict_shim.c build.sh run.sh to /tmp/DF-5718/ on the guest
sh build.sh     # builds rig_intree rig_libc rig_block rig_fixed
sh run.sh

Expected decisive output

INTREE: parse correct with verbatim byte-forward strcpy: YES
INTREE: overlapping strcpy invocations (restrict violations at ng_parse.c:1572/1621): 5
LIBC  : divergence vs in-tree strcpy: none            <- negative control (DFly libc is byte-forward too)
BLOCK : child killed by SIGSEGV ... => equivalent to a kernel heap OOB write
RESULT : overlap/past-NUL reliance CONFIRMED - the in-tree byte-forward strcpy is load-bearing
FIXED  : memmove treatment output identical to in-tree semantics: YES

Rig modes

binary strcpy used proves
rig_intree verbatim sys/libkern/strcpy.c:36-42 loop, instrumented the shipped algorithm calls strcpy on overlapping regions (5x in one parse) and is correct with the byte-forward implementation
rig_libc libc / gcc builtin (-O2, no -fno-builtin) swap-scenario control; DFly's libc strcpy is also byte-forward -> no divergence on this platform
rig_block model of a vectorized block-store strcpy (16-byte block loads/stores, final partial block stored whole) a legal-in-practice optimized strcpy writes past the NUL and past the exact-size cbuf allocation (kmalloc(strlen(s + start)), no +1, ng_parse.c:1559) -> SIGSEGV on a guard page = OOB write
rig_fixed the two overlap sites switched to memmove (= fix.diff) fix preserves the parser's byte-exact semantics

Files

  • overlap_rig.c β€” the rig (parser body is the verbatim kernel algorithm; deviations marked DEV:)
  • wrestrict_shim.c β€” the two call sites in isolation; gcc 8.3 -Wrestrict does not diagnose them (compiler does not catch this)
  • build.sh, run.sh β€” exact commands
  • build.log, run.log β€” full untrimmed guest output
  • env.txt β€” guest uname / cc version
  • fix.diff β€” git-apply-able fix (netgraph + netgraph7)
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-5718 VERDICT

Status: reproduced (userland differential + guard-page rig). Impact: none (hardening). Confidence: certain.

What was claimed, what was proven

Pass-2 audit of sys/libkern/strcpy.c required a census of the unbounded-API hazard class. The census (all non-contrib strcpy/strcat call sites in kern/, net, netproto/, netgraph, vfs/, bus/, platform/, dev/, vm/, cpu/) found every sized-consumer path either exact-fit allocated, NUL-forced, or privilege-gated β€” with one class of genuine contract violation: the netgraph text parser calls strcpy() on overlapping regions:

Both prototypes declare __restrict (sys/sys/libkern.h:131), so per C11 Β§6.7.3.1 this is undefined behavior. It is correct today only because the in-tree sys/libkern/strcpy.c:40 is a forward byte loop (for (; (*to = *from) != 0; ++from, ++to);), which shifts left safely.

Evidence (see run.log)

  1. rig_intree β€” runs the verbatim ng_get_string_token() algorithm (ng_parse.c:1548-1625) with the verbatim in-tree strcpy, instrumented only to count range-intersecting invocations: parse output correct, 5 overlapping strcpy invocations in a single parse of "A\tB\x41C\"D\\E". The restrict violation is real and exercised on every escape sequence parsed.
  2. rig_libc β€” negative control: with libc/builtin strcpy the parse is byte-identical on this guest (DragonFly's libc strcpy is also byte-forward). The hazard is not with today's libc.
  3. rig_block β€” a model of a vectorized/block-store strcpy (16-byte block loads and stores; the final partial block is stored whole β€” i.e. bytes past the NUL are written, the documented behavior of SSE/AVX implementations) applied to the same parser with the exact kernel allocation size (kmalloc(strlen(s + start)), note the missing +1 at ng_parse.c:1559) placed at the end of a page followed by a PROT_NONE guard page: the forked child dies from SIGSEGV β€” the past-the-NUL store is an out-of-bounds write past the exact-size allocation (kernel-equivalent: write past a kmalloc object into the adjacent heap).
  4. rig_fixed β€” the fix.diff treatment (the two overlap sites switched to memmove) produces byte-identical parser output.
  5. wrestrict_shim.c β€” gcc 8.3 -Wrestrict compiles the two call sites silently: the compiler does not diagnose this pattern, so there is no build-time guard against the fragility.

Why Info and not higher

  • The parser input arrives through netgraph control messages, gated behind caps_priv_check(SYSCAP_RESTRICTEDROOT) at sys/netgraph/socket/ng_socket.c:171-175 β€” root-only on DragonFly. No unprivileged-reachable bug exists in the shipped kernel.
  • The in-tree strcpy and DFly libc strcpy are both byte-forward, so the shipped binaries behave correctly. The finding is latent fragility: any future swap of strcpy for an optimized/builtin implementation (the in-tree precedent is sys/dev/raid/hptrr/osm.h:168 #define strcpy(dst, src) __builtin_strcpy((dst), (src))) silently becomes either wrong parsing or a heap overwrite.

Adjacent observations (not findings)

  • ng_parse.c's hex/octal escape loops declare k < 2 / k < 3 but never increment k (ng_parse.c:1601-1616 netgraph, 1773-1788 netgraph7), so \x consumes all consecutive hex digits and truncates to one byte. Bounded by the closing quote; harmless memory-safety-wise; FreeBSD- inherited; the rig's expected string encodes this real behavior.
  • ng_parse.c:1559 allocates strlen(s + start) with no +1; the copied string is one byte shorter than that (the leading quote), so it is an exact fit β€” but it leaves zero slack for any strcpy variant that writes past the NUL (demonstrated by rig_block).

Fix validation

fix.diff (netgraph + netgraph7, strcpy -> memmove at the four overlap sites) was validated at the algorithm level by rig_fixed (byte-identical output). A full patched-kernel build/reboot was not performed: the finding is Info/hardening with no kernel-reachable bug to baseline, so fix_status is recorded as not_testable at the kernel level with the rig-level validation as fix_evidence.

Bottom line

sys/libkern/strcpy.c is a correct unbounded strcpy; the pass-2 value here is the census that pinned down its one live contract violation β€” the netgraph parser's overlapping use β€” and proved with a guard-page rig that the byte-forward implementation is load-bearing. Apply fix.diff to make the parser independent of strcpy implementation details.

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

not_testable

fix.diff authored (netgraph + netgraph7, strcpy->memmove at the four overlap sites) and validated at the algorithm level: rig_fixed produces byte-identical parser output to the in-tree semantics (run.log). A full patched-kernel build/reboot validation was not performed because the finding is Info/hardening with no kernel-reachable defect to baseline (input path is root-gated by caps_priv_check SYSCAP_RESTRICTEDROOT); recorded as not_testable at the kernel level.

findings/poc/DF-5718/run.log line 'FIXED  : memmove treatment output identical to in-tree semantics: YES'; fix.diff
↓ fix.diffper-fix-DF-5718

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-5718/run.log: 'INTREE: overlapping strcpy invocations ... : 5'; 'BLOCK : child killed by SIGSEGV ... => equivalent to a kernel heap OOB write'; 'FIXED : memmove treatment output identical to in-tree semantics: YES'; env.txt for guest/toolchain; VERDICT.md for the full narrative

PoC changes

The seed had no PoC; authored from scratch. The rig embeds the VERBATIM ng_get_string_token algorithm and the VERBATIM in-tree strcpy; three iterations were needed: (1) nested block-comment syntax error; (2) non-numeric MODE preprocessor tokens enabled all arms at once; (3) initial 'expected' string was wrong because the kernel's hex-escape loop never increments k (consumes ALL consecutive hex digits, ng_parse.c:1601-1616) - fixed expected to the real kernel semantics and fixed the overlap counter's range check. Added MODE_FIXED (fix.diff treatment) and a hexdump printer so invisible bytes are visible.

Verified recommended fix

Replace the four overlapping strcpy calls with overlap-safe memmove in both netgraph stacks (see fix.diff): memmove(p, p+1, strlen(p+1)+1) and memmove(p, v, strlen(v)+1).

Verdict

Info/hardening finding from the pass-2 census of sys/libkern/strcpy.c: the netgraph text parser calls strcpy() on overlapping src/dst (ng_parse.c:1572 strcpy(p,p+1), :1621 strcpy(p,v); netgraph7 ng_parse.c:1764/1813), violating the __restrict contract both prototypes declare (sys/sys/libkern.h:131). Runtime proof: the verbatim kernel algorithm executed with the verbatim in-tree strcpy counts 5 overlapping invocations in a single parse and parses correctly (rig_intree). A block-store strcpy model (vectorized implementations store the final partial block whole, i.e. write past the NUL) applied to the exact kernel allocation (kmalloc(strlen(s+start)) with no +1, ng_parse.c:1559) SIGSEGVs on a guard page - i.e. an implementation-plausible strcpy swap turns this into a kernel heap OOB write (rig_block). DFly's own libc strcpy is byte-forward, so rig_libc (the swap control) shows no divergence on this platform, and the input path is root-gated (caps_priv_check SYSCAP_RESTRICTEDROOT, ng_socket.c:171-175): no unprivileged-reachable bug exists in the shipped kernel. The memmove treatment (fix.diff) is byte-identical (rig_fixed). gcc 8.3 -Wrestrict does not diagnose the call sites (wrestrict_shim.c).