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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3203 Β· 32 files
FileTypeDescriptionSize
README.md β€” 4.6 KB ↓ raw
VERDICT.md β€” 4.9 KB ↓ raw
dfrig13_krpc.c β€” 10.8 KB view raw
attacker.c β€” 7.6 KB view raw
build.sh β€” 167 B view raw
run.sh β€” 376 B view raw
attrib.sh β€” 1.4 KB view raw
run.log β€” 4.1 KB view raw
run.2.log β€” 7.6 KB view raw
run.attrib.log β€” 10.0 KB view raw
run.couple.log β€” 1.7 KB view raw
couple.log β€” 650 B view raw
run.fixed.log β€” 3.1 KB view raw
canary_dump.txt β€” 1.4 KB view raw
leak_sample.txt β€” 1.5 KB view raw
primer_leak_sample.txt β€” 3.9 KB view raw
short_c3.log β€” 561 B view raw
short_5a.log β€” 561 B view raw
short_g2.log β€” 561 B view raw
short_p_5a5a5a5a_1.log β€” 650 B view raw
short_p_5a5a5a5a_2.log β€” 653 B view raw
short_p_5a5a5a5a_3.log β€” 653 B view raw
short_p_3cc33cc3_1.log β€” 653 B view raw
short_p_3cc33cc3_2.log β€” 653 B view raw
short_p_3cc33cc3_3.log β€” 653 B view raw
build_summary.log β€” 1.1 KB view raw
fix.diff β€” 943 B view raw
fix_all.diff β€” 2.3 KB view raw
env.txt β€” 4.3 KB view raw
manifest.json β€” 2.0 KB view raw
verdict.json β€” 4.8 KB view raw
run_fixed.sh β€” 2.8 KB view raw

DF-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

  1. printed to console/dmesg by the verbatim kprintf("rpc denied, status=%d\n") at :397, and
  2. 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 and continue.

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 P at offset 20..23 (their mbufs are freed with P in place), then sends the 16-byte short reply β€” the verbatim code reads and prints exactly P: pattern 0x5a5a5a5a β†’ rpc denied, status=1515870810 9/9; pattern 0x3cc33cc3 β†’ rpc denied, status=1019428035 9/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 0xc3 fill β†’ 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
VERDICT.md
↓ download raw

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_testable
↓ fix.diffper-fix-DF-3203

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