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)
PoC verification
Evidence pack
findings/poc/DF-5718 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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):
sys/netgraph/netgraph/ng_parse.c:1572βstrcpy(p, p + 1)(dst = src-1)sys/netgraph/netgraph/ng_parse.c:1621βstrcpy(p, v)(v > p)sys/netgraph7/netgraph/ng_parse.c:1764β samesys/netgraph7/netgraph/ng_parse.c:1813β same
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 markedDEV:)wrestrict_shim.cβ the two call sites in isolation; gcc 8.3-Wrestrictdoes not diagnose them (compiler does not catch this)build.sh,run.shβ exact commandsbuild.log,run.logβ full untrimmed guest outputenv.txtβ guest uname / cc versionfix.diffβ git-apply-able fix (netgraph + netgraph7)VERDICT.md,manifest.json,verdict.json
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:
sys/netgraph/netgraph/ng_parse.c:1572strcpy(p, p + 1)β strips the backslashsys/netgraph/netgraph/ng_parse.c:1621strcpy(p, v)β shifts the tail left after decoding\xNN/\NNN- identical code at
sys/netgraph7/netgraph/ng_parse.c:1764,1813
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)
rig_intreeβ runs the verbatimng_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.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.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+1atng_parse.c:1559) placed at the end of a page followed by aPROT_NONEguard 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 akmallocobject into the adjacent heap).rig_fixedβ thefix.difftreatment (the two overlap sites switched tomemmove) produces byte-identical parser output.wrestrict_shim.cβ gcc 8.3-Wrestrictcompiles 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)atsys/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
strcpyfor an optimized/builtin implementation (the in-tree precedent issys/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 declarek < 2/k < 3but never incrementk(ng_parse.c:1601-1616netgraph,1773-1788netgraph7), so\xconsumes 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:1559allocatesstrlen(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 byrig_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_testablefix.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
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).
No comments yet.