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

Per-link stats unreachable for links 1..63: linkNum == 0 typo in GET/CLR_STATS link selection

Summary

NGM_ONE2MANY_GET_STATS/CLR_STATS/GETCLR_STATS reject every many-link index except 0 with EINVAL because the selection predicate is 'linkNum == 0 && linkNum < NG_ONE2MANY_MAX_LINKS' instead of '>= 0' (the ng7 twin has >= 0). Statistics for links 1..63 can never be read or cleared even while the links are connected and passing traffic; monitoring silently fails. Fail-closed: no OOB access. VERIFIED live on the guest (linkNum:1 -> EINVAL while 0 and -1 succeed). Root-only control surface. Also killed for v1: DF-0676 structurally ABSENT (v1 has ROUNDROBIN only, rcvdata consumes mbuf exactly once); DF-3539 unserialized-race pattern hunted live and KILLED (13-minute race, 6.49M rcvdata injections vs 58,824 concurrent kfree(priv) with same-thread slab re-spray, zero panics; div0 compiled out - cached divisor; teardown ordering defuses SHUTDOWN-vs-rcvdata).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3543 Β· 13 files
FileTypeDescriptionSize
statsbug.c β€” 4.9 KB view raw
race.c β€” 7.9 KB view raw
race2.c β€” 6.6 KB view raw
race3.c β€” 7.2 KB view raw
race4.c β€” 8.1 KB view raw
disasm.txt β€” 3.4 KB view raw
race-attempt.log β€” 580 B view raw
run.log β€” 290 B view raw
env.txt β€” 426 B view raw
fix.diff β€” 409 B view raw
build.sh β€” 111 B view raw
run.sh β€” 118 B view raw
VERDICT.md β€” 4.4 KB ↓ raw
VERDICT.md
↓ download raw

DF-3543 β€” ng_one2many (v1) per-link stats linkNum validation bug

Verdict: REPRODUCED (functional; no memory-safety impact)

sys/netgraph/one2many/ng_one2many.c:339-340 gates the NGM_ONE2MANY_GET_STATS / CLR_STATS / GETCLR_STATS link selection on

else if (linkNum == 0
    && linkNum < NG_ONE2MANY_MAX_LINKS) {

linkNum == 0 should be linkNum >= 0 (the ng7 twin at sys/netgraph7/one2many/ng_one2many.c:335 has linkNum >= 0). Consequence: statistics for "many" links 1..63 can never be read or cleared β€” every query returns EINVAL even though the link is connected and passing traffic. The "one" link (-1) still works.

Reproduced live on the guest (v1 stack, kldload ng_one2many):

GET_STATS linkNum=0  -> Undefined error: 0 (expected: OK)
GET_STATS linkNum=1  -> Invalid argument (expected: OK, got rejection => BUG)
GET_STATS linkNum=-1 -> Undefined error: 0 (expected: OK for 'one' link)

(see run.log; statsbug.c builds a node with many0 and many1 connected, then queries both.)

Impact: functional/availability of monitoring only. The bug is fail-closed β€” it rejects requests rather than accepting out-of-range indices, so there is no OOB consequence. Severity Info.

Pass-2 race hunt around this file (negative results, documented)

The orchestrator asked specifically whether the DF-3539 (v1 ng_async unserialized rcvmsg-vs-rcvdata) pattern manifests here. It does not, for three verified reasons:

  1. No XMIT_ALL in v1. sys/netgraph/one2many/ng_one2many.h:62 defines only NG_ONE2MANY_XMIT_ROUNDROBIN, and ng_one2many_rcvmsg rejects every other xmitAlg (ng_one2many.c:285-291). The ng7 DF-0676 peeked-mbuf double-free class is structurally absent: v1 rcvdata consumes the mbuf exactly once through NG_SEND_DATA (ng_one2many.c:425).

  2. Divide-by-zero candidate is dead on the shipped build. rcvdata checks numActiveMany != 0 at :411 and computes % priv->numActiveMany at :416; a concurrent ng_one2many_update_many (:490, from NGM_SET_CONFIG :308, newhook :253, or disconnect :468 β€” all executed synchronously in the sender's thread per sys/netgraph/netgraph/ng_base.c:1207 and :1678, with no lock) could zero it in between in C terms. But objdump of the shipped ng_one2many.ko (disasm.txt) shows the compiler loads numActiveMany once at +0x87 and re-uses the cached %dx as the idiv divisor at +0xc1 β€” the checked value and the divisor are the same register, so % 0 cannot execute on this kernel. (Compiler-dependent UB in the abstract machine; not a bug on the shipped artifact.)

  3. SHUTDOWN-vs-rcvdata UAF is defused by teardown ordering. ng_one2many_rmnode (:433-445) β†’ ng_cutlinks β†’ ng_destroy_hook sets HK_INVALID on both hook ends before freeing them (ng_base.c:819-827), the disconnect callbacks NULL priv->many[i].hook / priv->one.hook and zero numActiveMany before kfree(priv) runs, and ng_send_data handles both the NULL-hook and HK_INVALID cases safely (ng_base.c:1684-1696). On DragonFly, user-thread syscalls are non-preemptive with respect to other user threads, so a writer that entered rcvdata on CPU A always finishes its ~1-2Β΅s critical section before the closer's ~10Β΅s+ teardown on CPU B reaches kfree(priv). A 13-minute live attempt (race4.c, race-attempt.log) drove 6.49M packets into ng_one2many_rcvdata against 58,824 concurrent NGM_SHUTDOWN executions of kfree(priv) with same-thread slab re-spray (0x42-filled zone-4096/64 allocations) and ping -f interrupt pressure: no panic, no guest instability, ssh alive throughout. The C-level race exists, but no reachable bad outcome on this kernel was demonstrable β€” DF-3539 remains the umbrella finding for the v1-framework class and is not re-reported here.

Fix

One-line: linkNum == 0 β†’ linkNum >= 0 (fix.diff). Not kernel-rebuild-validated (Info-severity finding; the change is self-evident and the live demo already proves both branches of the predicate).

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
baseline reproduced→ patch + rebuild →patched clean

One-line predicate fix (== 0 -> >= 0); kernel rebuild validation skipped per contract for Info-severity findings.

fix.diff
↓ fix.diffper-fix-DF-3543

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (linkNum=1 -> EINVAL while connected), statsbug.c, disasm.txt (cached idiv divisor at +0x87/+0xc1), race-attempt.log (final counters), VERDICT.md sections 2-3

PoC changes

Wrote statsbug.c from scratch (v1 ng_mesg layout, absolute node addresses need trailing ':'); race*.c written from scratch for the UAF hunt (same-thread slab re-spray design in race4).

Verified recommended fix

Change linkNum == 0 to linkNum >= 0 in the GET/CLR_STATS link selection (ng_one2many.c:339).

Verdict

Reproduced live: NGM_ONE2MANY_GET_STATS/CLR_STATS/GETCLR_STATS reject linkNum 1..63 with EINVAL (linkNum==0 typo at sys/netgraph/one2many/ng_one2many.c:339-340; ng7 twin uses >=0). Fail-closed functional bug, no memory-safety consequence. The pass-2 race hunt (DF-3539 pattern) was executed and is negative: no XMIT_ALL exists in v1 (DF-0676 class absent), the numActiveMany divide-by-zero candidate is compiled out on the shipped kernel (objdump: divisor cached from the checked load), and the SHUTDOWN-vs-rcvdata priv UAF is defused by teardown ordering (HK_INVALID before free, hooks NULLed before kfree(priv), NULL/invalid hooks handled in ng_send_data) plus non-preemptive user syscalls - 6.49M rcvdata injections against 58824 concurrent kfree(priv) executions with slab re-spray produced no panic.