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)
PoC verification
Evidence pack
findings/poc/DF-3369 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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) prints0x00000000 >= 1024 ? FALSE (wrap defeats check)β 0xFFFFFFFF+1 wrapped to 0; k = 5 + 1024 + 1 = 1030, reported by the module ask = 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
fixedfix.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
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.
No comments yet.