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

bdg_netmap_rxsync: uint32 wrap of (reserved + avail) defeats the only sanity check on ring->reserved β€” k escapes the ring index space and the release loop never terminates while holding the kring q_lock exclusively

Summary

ring->reserved/avail are user-writable shared memory; the check 'resvd + ring->avail >= lim + 1' wraps uint32 for reserved=0xFFFFFFFF, avail=1 (sum 0 < 1024), reserved is kept, and k = cur + lim + 1 - resvd leaves [0..lim]; the release loop 'for (n=0; j!=k; n++) ... j=nm_next(j,lim)' can never terminate. VERIFIED in-kernel with the verbatim function (KLD): check defeated, k=1030 outside [0..1023], 1e6 iterations without termination; on the real subsystem there is no bail-out and the loop spins forever holding q_lock LK_EXCLUSIVE - unkillable NIOCRXSYNC, permanently locked ring. User with /dev/netmap access on any VALE port. Sibling of DF-3332 (same unvalidated-reserved class, different function/file/sink). Fix A/B-verified (reject resvd >= lim+1).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3369 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 3.1 KB ↓ raw
df3369_kmod.c β€” 8.5 KB view raw
Makefile β€” 145 B ↓ download
build.sh β€” 92 B view raw
run.sh β€” 183 B view raw
build.log β€” 913 B view raw
run.log β€” 1.5 KB view raw
env.txt β€” 221 B view raw
fix.diff β€” 692 B view raw
verdict.json β€” 3.3 KB view raw

DF-3369 β€” bdg_netmap_rxsync(): reserved+avail uint32 wrap defeats the sanity check β†’ non-terminating loop under exclusive q_lock

Target: sys/net/netmap/netmap_vale.c:1502-1528 (bdg_netmap_rxsync, the nm_rxsync of every VALE port β€” netmap_vale.c:1551 β€” reached from NIOCRXSYNC on the port's mmap()ed ring).

The bug

ring->reserved and ring->avail are user-writable shared-memory fields. The only sanity check is

    if (resvd > 0) {
        if (resvd + ring->avail >= lim + 1) {   /* netmap_vale.c:1503 */
            ring->reserved = resvd = 0;
        }
        k = (k >= resvd) ? k - resvd : k + lim + 1 - resvd;

resvd + ring->avail wraps uint32: reserved=0xFFFFFFFF, avail=1 β†’ sum 0, check false, reserved kept; then k = cur + lim + 1 - resvd = cur + 1025 (mod 2^32) β€” outside [0..lim]. The release loop for (n = 0; j != k; n++) { ...; j = nm_next(j, lim); } (netmap_vale.c:1515) can never terminate because j only ever takes values in [0..lim]. The loop runs with kring->q_lock held LK_EXCLUSIVE (acquired netmap_vale.c:1493): the NIOCRXSYNC thread spins forever, never returns to user mode (unkillable), and the kring lock is never released.

Sibling of DF-3332 (netmap.c netmap_grab_packets, same class of unvalidated ring->reserved, different function/file/sink β€” that one k += n sign error; this one the wrap in bdg_netmap_rxsync).

Platform note

Netmap does not compile on this tree (established platform fact); verified via verbatim-function KLD: df3369_kmod.c copies bdg_netmap_rxsync verbatim (real lockmgr/q_lock; stubs for BDG_NMB/reinit that cannot influence the control flow; a clearly-marked bail-out counter replaces the infinite spin so the guest survives the demonstration).

Build / run (guest, root)

cd /root/df3369 && make && make fixed
./run.sh vuln 0    # control reserved=4: terminates in 1 iteration
./run.sh vuln 1    # reserved=0xFFFFFFFF: NON-TERMINATING (1e6 iters, k=1030 outside ring)
./run.sh fixed 1   # reserved rejected ("XXX invalid reserve/avail"), terminates in 5

Expected results

See run.log β€” exact predicted arithmetic observed: k = 1030 for cur=5, lim=1023, reserved=0xFFFFFFFF.

VERDICT.md
↓ download raw

DF-3369 β€” VERDICT

Status: reproduced (non-terminating kernel loop proven in-kernel with the exact predicted arithmetic; impact: unkillable NIOCRXSYNC holding the kring q_lock exclusively β€” local DoS).

What was proven, on the guest

Verbatim bdg_netmap_rxsync (netmap_vale.c:1485-1535) in a KLD on stock kernel #0, driven with the exact ring state an mmap()ed user thread writes:

  • control (reserved=4): terminates in 1 iteration (run.log).
  • attack (cur=5, reserved=0xFFFFFFFF, avail=1, 1024-slot ring):
  • the only sanity check resvd + ring->avail >= lim + 1 (netmap_vale.c:1503) prints 0x00000000 >= 1024 ? FALSE (wrap defeats check) β€” 0xFFFFFFFF+1 wrapped to 0;
  • k = 5 + 1024 + 1 = 1030, reported by the module as k = 1030 is OUTSIDE [0..1023];
  • the verbatim release loop (for (n = 0; j != k; n++) … j = nm_next(j, lim), netmap_vale.c:1515) ran 1,000,000 iterations without j reaching k β€” j only ever cycles [0..1023]. The module's bail-out (INSTRUMENTATION, marked) is the only reason it returned; the in-tree code has none.

On the real subsystem the spinning thread holds kring->q_lock LK_EXCLUSIVE (netmap_vale.c:1493), never returns to user mode (unkillable), and the port's ring is locked forever.

Root cause, path:line

netmap_vale.c:1502-1508: ring->reserved (user-writable shared memory) is only validated by the wrap-able expression resvd + ring->avail >= lim + 1; a reserved value β‰₯ ring size must be rejected outright. The derived k then leaves the ring index space and the j != k loop cannot terminate. (Same class as DF-3332 β€” unvalidated ring->reserved in netmap.c's netmap_grab_packets β€” but a different function, file, and failure mode.)

Why DoS and not more

The loop body only clears NS_BUF_CHANGED in slot flags and advances j β€” no write primitive beyond the (already user-writable) shared ring. Impact ceiling = permanent kernel-thread wedge + ring lock, i.e. local DoS.

PoC changes vs the seeded sketch

None (authored in this pass). Bail-out counter and the mode sysctl are the marked INSTRUMENTATION additions; loop control flow untouched.

Fix validation (A/B, same module)

-DDD3369_FIXED adds resvd >= lim + 1 || to the check (tree patch in fix.diff): attack input now prints the XXX invalid reserve/avail 4294967295 1 diagnostic, resets reserved, and terminates in 5 iterations (run.log). Full-subsystem kernel build impossible on this platform (netmap build rot β€” platform note), so verbatim A/B is the maximal validation.

Kernel refs: sys/net/netmap/netmap_vale.c:1485-1493 (lock), :1502-1508 (wrap + k computation), :1510-1528 (release loop), :1551 (nm_rxsync wiring), :914-918 (num_slots bound at port creation); reachability: NIOCRXSYNC β†’ nm_rxsync on any vale port.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff adds 'resvd >= lim + 1 ||' to the sanity check. Validated A/B with the -DDD3369_FIXED variant: the 0xFFFFFFFF reserve is now rejected ('XXX invalid reserve/avail 4294967295 1'), k stays in range, loop terminates in 5 iterations on the same input.

run.log fixed-variant section; fix.diff
↓ fix.diffn/a (verbatim-function KLD A/B on stock kernel #0; netmap subsystem does not compile on this platform)

Confirmed kernel references

Detail

Exploit chain

open /dev/netmap -> NIOCREGIF vale port -> mmap ring -> write ring->cur=5, ring->reserved=0xFFFFFFFF, ring->avail=1 -> ioctl(NIOCRXSYNC) -> bdg_netmap_rxsync spins forever under q_lock. Impact ceiling DoS (loop body only clears NS_BUF_CHANGED in shared memory; no corruption primitive).

Evidence (decisive lines)

run.log: 'check (resvd+avail >= lim+1): 0x00000000 >= 1024 ? FALSE (wrap defeats check)'; 'RESULT NON-TERMINATING: 1000000 iterations without j reaching k (k = 1030 is OUTSIDE [0..1023])'; control terminates (1 iter); fixed: '[D] XXX invalid reserve/avail 4294967295 1' then 'terminated after 5 iterations'.

PoC changes

finding authored this pass; fresh verbatim-KLD harness. Marked bail-out counter is the only addition to the loop.

Verified recommended fix

Reject resvd >= ring size as well (see fix.diff): 'if (resvd >= lim + 1 || resvd + ring->avail >= lim + 1)'.

Verdict

bdg_netmap_rxsync (netmap_vale.c:1485-1535, nm_rxsync of every VALE port) validates the user-writable ring->reserved only via 'resvd + ring->avail >= lim + 1' (:1503), which wraps uint32: reserved=0xFFFFFFFF + avail=1 -> sum 0, check false; k = cur + lim + 1 - resvd = cur+1025 (mod 2^32) escapes [0..lim] and the release loop 'for (n=0; j!=k; n++) ... j=nm_next(j,lim)' (:1515) can never terminate. Proven in-kernel with the verbatim function in a KLD on the stock kernel: module reports the check defeated ('0x00000000 >= 1024 ? FALSE (wrap defeats check)'), k=1030 OUTSIDE [0..1023], 1,000,000 iterations without termination; on the real subsystem the loop spins forever holding kring->q_lock LK_EXCLUSIVE with no bail-out - unkillable NIOCRXSYNC, permanently locked ring (local DoS). Fixed variant (reject resvd >= lim+1) terminates in 5 iterations on the same input.