krpc_call reply validation is vacuous at chain level and reads rpc accept-status 8 bytes past the validated region β uninitialized kernel-heap read of rp_status, value printed to console and used for control flow
Summary
The pre-parse reply length gate is dead code: sbinit(&sio,1<<16) sets sb_cc=0 and soreceive counts sb_cc UP as data arrives (uipc_socket.c:1277), so 'len -= sio.sb_cc' at :366 yields the residual 65536-R, never < MIN_REPLY_HDR=16 for UDP (max datagram 65507). The only working gate is m->m_len >= 16 on the first mbuf (:372), but the accepted-status parser reads reply->rp_status at struct offset 20..23 (offsetof-verified: sizeof(rpc_reply)=24, rp_status@20) at :391 BEFORE the m_pullup(24) at :417. A spoofed 16-19-byte 'accepted' RPC reply during a diskless boot reads 4 bytes of stale/uninitialized mbuf heap past the datagram; the value is echoed to console/dmesg via kprintf (:392,:397) and used for control flow (stale==RPC_PROGMISMATCH takes the leak-exit; stale==0 falls into gotreply -> spurious ENOBUFS abort). Any LAN host during root-on-NFS boot (krpc reachable only via nfs_mountroot->md_mount; NFS_ROOT compiled into stock X86_64_GENERIC). Phase V not_testable without a root-on-NFS boot rig; arithmetic certain from struct offsets verified by guest-compiled offsetof program. Fix: len = sio.sb_cc and validate sizeof(struct rpc_reply) before field access. [VERIFIED pass2: reproduced as attacker-chosen uninit-heap disclosure - 16-byte reply passes the dead residual gate and rp_status is read 4 bytes past the datagram before the 24-byte pullup, verbatim kprintf prints the planted primer 0x5a5a5a5a 9 of 9 and 0x3cc33cc3 9 of 9, kernel-groom 0xc3 hit, stale values steer control flow incl. ENOBUFS abort and the DF-3204 leak exit; received-length plus pullup-first fix-validated zero prints]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3203 Β· 32 filesDF-3203 β krpc_call reads rpc accept-status 8 bytes past the validated region
File: sys/vfs/nfs/krpc_subr.c:356-373 (dead chain-level gate), :391
(pre-m_pullup rp_status read), :417 (the pullup that comes too late)
Severity: Low Β· CWE-125 Β· verified in VERIFY mode with the
verbatim-interposition kernel-compiled rig (dfrig13_krpc.c, sha256-gated
#include of the production krpc_subr.c; real UDP stack, real mbufs,
userland attacker attacker.c).
What was reproduced (baseline kernel #1)
A 16-byte "accepted" RPC reply (xid + direction + astatus only) passes every
gate β len < MIN_REPLY_HDR at :370 is dead code because sbinit() zeroes
sb_cc and soreceive counts the delivered bytes into it, so
len -= sio.sb_cc at :366 yields the residual 65536β16, never < 16 for any
UDP datagram β and reply->rp_status (offset 20..23 of a 24-byte struct)
is then read at :391 before the m_pullup(24) at :417: 4 bytes of
stale kernel-heap content beyond the 16-byte datagram, which is
- printed to console/dmesg by the verbatim
kprintf("rpc denied, status=%d\n")at :397, and - used for control flow:
0βgotreplyβm_pullup(24)fails on the 16-byte mbuf β spurious ENOBUFS abort (krpc_subr.c:417-419);2(RPC_PROGMISMATCH) β EBADRPC leak exit (the DF-3204 path); anything else β the console print andcontinue.
Attribution (the value is heap memory, not wire data)
- Userland-planted primer, 18/18 deterministic: the attacker sprays
24-byte datagrams to a dead port carrying pattern
Pat offset 20..23 (their mbufs are freed withPin place), then sends the 16-byte short reply β the verbatim code reads and prints exactlyP: pattern0x5a5a5a5aβrpc denied, status=15158708109/9; pattern0x3cc33cc3βrpc denied, status=10194280359/9 (run.attrib.log,primer_leak_sample.txt). Bytes 16..23 do not exist in a 16-byte datagram: no on-wire content can produce these values. - Kernel-side groom hit: uniform
0xc3fill βrpc denied, status=-1010580541= exactly(int)0xc3c3c3c3(run.2.log). - Raw residue across runs:
943337777/943337779(= ASCII"8:11"/"8:13"timestamp fragments),153789694,357452694(run.log,leak_sample.txt,canary_dump.txt) β uninitialized heap that varies run to run. - Stale==0 branch: multiple attempts abort with ENOBUFS when the slot previously held a successful reply (status 0 written at +20..23).
- Stale==RPC_PROGMISMATCH coupling: primer pattern
0x00000002β 3/3 calls return EBADRPC with exactly +3 leaked mbufs (run.couple.log) β DF-3203 steering execution into the DF-3204 leak exit on a value the attacker never put on the wire as a status.
No kernel panic occurs (the read stays inside the mbuf slab allocation),
hence no panic.txt; the disclosed artifact is canary_dump.txt.
Fix validation (kernel #2)
With fix.diff (len = received sb_cc, require the full 24-byte header,
m_pullup before any field read): the short reply is dropped at the gate,
zero verbatim rpc denied, status= lines appear (strict grep ^rpc
denied = 0 on the fixed boot; the only matches of the loose phrase are the
rig marker text), the call completes on the honest follow-up reply, and
honest replies are unaffected (6/6 in the DF-3205 suite).
Reproduce (on the DragonFly guest)
sh build.sh # gate + build + install; reboot sh attrib.sh # primer attribution (2 patterns x 3 rounds) sh run.sh # kernel-groom canaries + groom32 coupling # fix validation: sh build.sh af834f6e30225b1af68cc83cbcdaf25e29e0e39ca6deaec6f3c13663a661b886 ; reboot sh run_fixed.sh # (in DF-3205 pack) strict rpc-denied count stays 0
Files in this folder
| file | purpose |
|---|---|
dfrig13_krpc.c / attacker.c |
harness + userland attacker (modes short, short_p) |
build.sh / run.sh / attrib.sh |
exact commands |
run.log / run.2.log |
baseline kernel-groom runs (raw residue, c3 hit, ENOBUFS branch) |
run.attrib.log |
primer attribution 18/18 |
run.couple.log + couple.log |
stale==2 β EBADRPC + leaked mbufs (3/3, +3 mbufs) |
run.fixed.log |
fixed-kernel A/B (0 verbatim prints) |
canary_dump.txt / leak_sample.txt / primer_leak_sample.txt |
all stale values observed |
short_*.log, short_p_*.log |
attacker-side logs |
build_summary.log |
build gate excerpts (full logs in DF-3205 pack) |
fix.diff / fix_all.diff |
validated fix (single / combined) |
env.txt, VERDICT.md, manifest.json, verdict.json |
context + verdicts |
DF-3203 β VERDICT (verified 2026-10-20, VERIFY mode)
REPRODUCED (leak: 4 stale kernel-heap bytes per triggering reply, printed to console, steering control flow) β demonstrated through the verbatim production code on the guest (kernel #1; sha256-gated include of krpc_subr.c; real UDP stack on lo0; real mbufs; userland attacker).
Mechanism, line-accurate
- :356-357 len = 1 << 16; sbinit(&sio, len); β sbinit() zeroes
sb_cc and stores the limit (sys/sys/sockbuf.h:115-126).
- uipc_socket.c:1277 soreceive computes the budget from
sb_climit - sb_cc and appends the record into sio, counting the
DELIVERED bytes into sb_cc (sockbuf.h:83).
- :366 len -= sio.sb_cc; therefore yields 65536 - received β the
residual capacity, which for any UDP datagram (max 65507) is far
above MIN_REPLY_HDR=16, so the chain-level gate :370 is dead code.
- The only working gate is m->m_len >= 16 (:372), but the struct is
24 bytes: rp_astatus (8..11) is read in-bounds at :384, while
rp_status sits at offset 20..23 and is read at :391 BEFORE the
m_pullup(sizeof(struct rpc_reply)) that only happens at :417 after
gotreply. A 16..19-byte "accepted" reply makes :391 read 4..8
bytes past the datagram inside the mbuf data area β stale heap.
Observed manifestations (baseline kernel #1) 1. Console/dmesg disclosure: the verbatim kprintf at :397 printed stale values that vary run to run: 943337777 and 943337779 (0x383a3131 / 0x383a3133 = ASCII "8:11" / "8:13" β timestamp string fragments), 153789694, 357452694 (binary residue). 2. Deterministic attacker-planted value, 18/18: userland-only primer (24-byte datagrams to a dead port carry pattern P at offset 20..23; those mbuf slots are freed with P intact; the next short reply recycles one). P = 0x5a5a5a5a -> status=1515870810 9/9; P = 0x3cc33cc3 -> status=1019428035 9/9 (run.attrib.log). A 16-byte datagram cannot carry bytes 16..23, so the printed value cannot be wire data; the kernel-side groom canary 0xc3 also hit once (-1010580541 = (int)0xc3c3c3c3) in an earlier boot. 3. Control flow on stale memory: stale==0 attempts aborted with ENOBUFS (m_pullup(24) on the 16-byte mbuf fails at :417-419); stale==2 (primer pattern 0x00000002) returned EBADRPC and leaked exactly +3 mbufs in 3 attempts (run.couple.log) β the DF-3204 exit taken on an uninitialized value. No panic: the read stays inside the mbuf allocation, so INVARIANTS do not trip; the disclosed artifact is canary_dump.txt (no panic.txt by design of the bug class).
Impact / privesc assessment Low as filed: 4 bytes of uninitialized kernel heap per spoofed short reply, disclosed to console/dmesg (and boot serial logs), plus stale value steering boot-RPC control flow. Not a local unpriv->root vector: krpc runs only during diskless mountroot (no runtime syscall path reaches it on this disk-root guest). Reachability requires the boot-network attacker of DF-3205 (or a misbehaving server), which makes the short reply trivially deliverable β the two findings compound.
Fix validation (kernel #2 = baseline + fix_all.diff) fix.diff replaces the dead residual math with the received length (len = (int)sio.sb_cc), requires the full 24-byte header before any field access, and pulls the header up front (m_pullup before the direction/xid/astatus/status reads). A/B: on the fixed kernel the 16-byte short reply is dropped at the gate; strict grep for verbatim "^rpc denied, status=" over the whole fixed boot = 0 lines (the only loose-phrase matches are the rig marker text); the call completes on the honest follow-up; honest replies unaffected (6/6 in the DF-3205 T5 suite; PROGMISMATCH detection unchanged β DF-3204 T3 still returns EBADRPC 50/50). No regression observed.
Rig honesty notes - Attribution needed CPU pinning ("usched :1", both the groom and the attacker) because DragonFly objcache magazines are per-CPU; without pinning the stale value is whatever the slot last held (still proving the read, less crisp attribution). - The harness marker text prints the EXPECTED value with the words "verbatim console line is: ... (check dmesg)"; the authoritative observed value is the column-0 dmesg line captured by run.sh / attrib.sh β run.fixed.sh counts those strictly.
Artifacts: run.attrib.log (18/18), run.couple.log (stale==2 -> EBADRPC, +3 mbufs), run.log / run.2.log (residue + groom hit + ENOBUFS), canary_dump.txt / primer_leak_sample.txt / leak_sample.txt, run.fixed.log.
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_testableConfirmed kernel references
Detail
Exploit chain
boot-network attacker (DF-3205 delivery) sends a 16-byte accepted reply with the predicted xid; verbatim krpc_call reads 4 stale heap bytes past the datagram as rp_status; the value is printed to the console and serial boot log (kernel heap disclosure) and selects the control-flow branch (print+continue, ENOBUFS boot-RPC abort, or the DF-3204 mbuf-leak exit when the stale value is 2); with the primer spray the attacker chooses the exact printed value
Evidence (decisive lines)
run.attrib.log + primer_leak_sample.txt: rpc denied, status=1515870810 x9 and status=1019428035 x9, each exactly when its primer pattern was sprayed; run.couple.log: 3/3 err=EBADRPC with mbufs 7->10 for primer 0x00000002; run.2.log: -1010580541 (0xc3c3c3c3 groom hit) and ENOBUFS attempts; canary_dump.txt: all distinct stale values across runs; run.fixed.log + fixed-boot dmesg: strict grep ^rpc denied, status= returns 0
PoC changes
seed sketch assumed a root-on-NFS boot lab; replaced with the established verbatim-interposition kernel-compiled rig driving the real UDP stack with a userland attacker; attribution required usched :1 CPU pinning (per-CPU objcache magazines) and, for the deterministic 18/18 result, a userland primer spray (24-byte datagrams to a dead port carrying the pattern at offset 20..23) so the attacker alone plants the bytes the kernel later reads out of bounds; harness marker text prints the expected value while the authoritative observed value is the column-0 dmesg line captured by the scripts
Verified recommended fix
use the received length (len = (int)sio.sb_cc), require the full sizeof(struct rpc_reply) before any field access, and m_pullup the header before parsing (validated in fix.diff)
Verdict
The pre-pullup accept-status read at krpc_subr.c:391 was reproduced on the guest through the verbatim production code: a 16-byte accepted RPC reply (bytes 16..23 do not exist on the wire) passes the dead residual-length gate at :366-370 (sbinit zeroes sb_cc and soreceive counts delivered bytes into it, so the old math yields 65536-minus-received, never below 16 for UDP) and the m_len>=16 check at :372, after which the code reads rp_status at struct offset 20..23 before the m_pullup(24) at :417 -- 4 bytes of stale kernel-heap mbuf content. The verbatim kprintf at :397 prints that stale value to console and dmesg, and the value steers control flow. Attribution is deterministic: a userland-only primer planting 0x5a5a5a5a at mbuf offset 20..23 produced status=1515870810 on 9/9 attempts and 0x3cc33cc3 produced status=1019428035 on 9/9; a kernel-side 0xc3 groom hit once with exactly (int)0xc3c3c3c3; unplanted runs disclosed varying residue (943337777 = ASCII 8:11, 153789694, 357452694); stale==0 attempts aborted with ENOBUFS at :417-419; a primer of 0x00000002 made 3/3 calls take the RPC_PROGMISMATCH exit returning EBADRPC with exactly +3 leaked mbufs (the DF-3204 path driven purely by the uninitialized value). No panic (the read stays inside the mbuf allocation), so the disclosed artifact is canary_dump.txt. Not a local unpriv->root vector: krpc has no runtime callers on a disk-root guest; reachability needs the DF-3205 boot-network attacker or a misbehaving server. The validated fix (use the received sb_cc length, require the full 24-byte header, m_pullup before any field read) silenced all short-reply prints (0 verbatim rpc denied lines on the fixed boot) with no regression.
No comments yet.