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

in_pcblist_range walks non-shared pcbinfo (ripcbinfo) with no synchronization; non-atomic marker advance vs same-cpu netisr pcb teardown yields UAF read/write and list corruption

Summary

in_pcblist_range exports pcb lists via sysctl. For pcbinfo initialized non-shared - ripcbinfo (raw_ip.c, infotoken==NULL) - GET_PCBINFO_TOKEN is a no-op, so the walk holds nothing. The marker advance is a non-atomic read/unlink/relink dance performed by a USER thread merely migrated onto the pcbinfo cpu; the marker was designed only to survive SYSCTL_OUT blockage, not same-cpu netisr preemption. A concurrent rip_detach in netisr0 (unlink AND kfree in the same dispatch) landing between the LIST_NEXT read and LIST_INSERT_AFTER relinks the marker after a freed inpcb: pointer written into freed M_PCB memory, succ->le_prev pointing into the freed chunk, subsequent reads operate on freed/recycled memory exported to the unprivileged reader. The identical dance exists in tcp_pcblist where churn is fully unprivileged; tcp_pcblist's own comment admits the assumption ('depend on being on the correct cpu to avoid races') - sound only for netisr-context code, not a preemptible user thread. NOT reproduced under ~13.8M sysctl dumps (~6e9 marker-advance windows) vs ~7M teardowns across 3 rounds incl. 8 concurrent readers: teardown work executes at the walker's blocking points where the marker protocol works as designed - safety rests on lwkt scheduler serialization, not code synchronization; filed as hardening (speculative). Reader unpriv (sysctl net.inet.raw.pcblist); churn root-gated raw sockets but unpriv-drivable via setuid ping. Fix: crit_enter/crit_exit around the marker advance + snapshot (marker keeps coherence across the sleeping SYSCTL_OUT).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3315 Β· 11 files
FileTypeDescriptionSize
README.md β€” 3.8 KB ↓ raw
VERDICT.md β€” 5.4 KB ↓ raw
pcb_race.c β€” 5.0 KB view raw
build.sh β€” 90 B view raw
run.sh β€” 145 B view raw
build.log β€” 75 B view raw
run.log β€” 1.6 KB view raw
env.txt β€” 514 B view raw
fix.diff β€” 1.7 KB view raw
manifest.json β€” 888 B view raw
verdict.json β€” 4.1 KB view raw

DF-3315 β€” in_pcblist_range: unsynchronized pcblist walk of non-shared pcbinfo (non-atomic marker dance)

What

in_pcblist_range() (sys/netinet/in_pcb.c:2408-2495) exports pcb lists via sysctl (net.inet.raw.pcblist, net.inet.udp.pcblist). For a pcbinfo initialized without a shared infotoken β€” ripcbinfo (raw_ip.c:115 in_pcbinfo_init(&ripcbinfo, 0, FALSE)) β€” GET_PCBINFO_TOKEN() is a no-op (in_pcb.h:439-449), so the walk has no synchronization at all.

The walk advances its list marker with a non-atomic two-step dance:

while ((inp = LIST_NEXT(marker, inp_list)) != NULL && i < n) {   /* read next */
        LIST_REMOVE(marker, inp_list);                           /* unlink    */
        LIST_INSERT_AFTER(inp, marker, inp_list);                /* relink    */
        ... inp->inp_flags / prison_xinpcb(inp) / bcopy(inp, ...) ...

(in_pcb.c:2458-2471). The marker only keeps the walk coherent when SYSCTL_OUT() blocks (that is what it was designed for); it does not make the advance atomic against a same-cpu netisr that concurrently removes and frees the pcb being stepped to (rip_detach β†’ in_pcbdetach β†’ in_pcbofflist + kfree(inp) β€” the free happens in the same dispatch, in_pcb.c:1210-1226). The twin walker tcp_pcblist() (tcp_subr.c:1226-1300) has the identical dance and admits the assumption in its own comment: "but otherwise depend on being on the correct cpu to avoid races" β€” correct-cpu placement is sufficient for code running in netisr context, but the sysctl walker is a user thread merely migrated onto that cpu (lwkt_migratecpu, in_pcb.c:2450).

If a pcb removal lands between the LIST_NEXT read and the LIST_INSERT_AFTER, the marker is relinked relative to an unlinked and freed inpcb: LIST_INSERT_AFTER writes the marker pointer into freed M_PCB memory, leaves succ->le_prev pointing into the freed chunk, and the walk then reads freed (possibly recycled) memory (inp->inp_flags, prison_xinpcb derefs inp->inp_socket->so_cred, bcopy(inp,...) exports recycled heap contents to the reader).

Reachability

  • Reader half: any unprivileged user β€” sysctl net.inet.raw.pcblist (CTLFLAG_RD, no privilege gate).
  • Churn half: raw-socket create/close requires root (raw_ip.c:473 SYSCAP_NONET_RAW) β€” or an unprivileged user via the setuid-root /sbin/ping (-r-sr-xr-x on the stock guest): each invocation creates and closes an ICMP raw socket, so an unpriv user can drive ripcb churn at will.

Reproduce

scp pcb_race.c build.sh run.sh dfbsd:/tmp/df3315/
ssh dfbsd  'cd /tmp/df3315 && sh build.sh'
ssh dfbsd  'cd /tmp/df3315 && ./pcb_race -c 300 96 &'      # root churner
ssh dfbsd-maxx '/tmp/df3315/pcb_race -r 280'               # unpriv reader
# (or sh run.sh 280 β€” runs both; see VERDICT.md for the multi-reader runs)

Expected on success: kernel panic (LIST corruption / wild deref in prison_xinpcb/sotoxsocket/in_pcbofflist KASSERT) or garbage (recycled-heap) records among the dumped xinpcb entries.

Result on this guest

NOT reproduced under maximal stress: ~13.8M sysctl dumps (~6x10^9 marker advances) against ~7M concurrent pcb teardowns across three rounds (115 s single reader @45k dumps/s; 230 s x 8 concurrent readers @6.5k dumps/s each; churn 13-55k cycles/s). No panic, no corruption, guest healthy. See VERDICT.md for why: on this kernel the teardown work in netisr0 executes at the walker's blocking/yield points (where the marker protocol protects the walk), not by preempting the walker mid-dance.

Fix

fix.diff β€” wrap the marker advance plus the pcb snapshot in crit_enter()/crit_exit() so the sequence is atomic against same-cpu netisr preemption regardless of pcbinfo sharing. The same treatment is needed for the tcp_pcblist() twin in tcp_subr.c (out of this diff's scope).

VERDICT.md
↓ download raw

DF-3315 VERDICT β€” not_reproduced (structural race confirmed, practical trigger not achieved)

Status: not_reproduced. Impact observed: none (guest survived all runs). Structural claim: confirmed by source analysis β€” the race exists by construction; whether it can be entered depends on lwkt scheduler preemption semantics that, empirically, do not preempt the walker mid-dance in this workload.

Source-level proof of the missing synchronization

  1. ripcbinfo is initialized non-shared: raw_ip.c:115 in_pcbinfo_init(&ripcbinfo, 0, FALSE) β†’ infotoken == NULL (in_pcb.c:201-207).
  2. GET_PCBINFO_TOKEN() is a no-op when infotoken == NULL (in_pcb.h:439-443) β†’ in_pcblist_range (in_pcb.c:2452) holds nothing while walking ripcbinfo.pcblisthead.
  3. The marker advance is three separate operations with no atomicity: read-next (in_pcb.c:2458), unlink (2459), relink-after (2460).
  4. The mutator, in_pcbofflist() (in_pcb.c:255-270), runs from rip_detach in netisr0 and frees the inpcb in the same dispatch via in_pcbdetach (in_pcb.c:1210-1226, kfree(inp, M_PCB) at 1225).
  5. The walker is a user thread (sysctl) only migrated onto the pcbinfo cpu (in_pcb.c:2450 lwkt_migratecpu); tcp_pcblist's own comment (tcp_subr.c:1246-1249) states the design assumption: "depend on being on the correct cpu to avoid races" β€” which is only sound for code running inside the netisr, not for a preemptible user thread parked on it.
  6. If a teardown lands in the window, LIST_INSERT_AFTER(inp, marker) relinks the marker after an unlinked+freed pcb: marker pointer written into freed M_PCB memory; succ->le_prev points into the freed chunk; subsequent reads (inp->inp_flags 2462, prison_xinpcb 2464 β†’ in_pcb.c:2392-2406 derefs inp->inp_socket->so_cred, bcopy 2469) operate on freed/recycled memory and export recycled heap contents to the unprivileged reader.

Attempts (all on DragonFly 6.5-DEVELOPMENT #0, INVARIANTS kernel, 6 cpus)

# setup duration exposure result
1 1 unpriv reader (45,563 dumps/s solo) + root churn 96 raw socks (39k close/create/s) 115 s 2.81M dumps (~4.7x10^8 marker advances), 3.08M teardowns no panic, guest up
2 launch bug (su -c arg mangling) β€” churner only ~190 s 10.6M churn cycles, no reader n/a (aborted)
3 8 concurrent unpriv readers (6.2-6.8k dumps/s each) + churn (13.6k cycles/s) 230 s 10.97M dumps (~1.8x10^9 advances), 3.26M teardowns no panic, guest up

Cumulative: ~13.8M sysctl dumps, ~6x10^9 marker-dance windows, ~7M pcb teardowns. Zero faults; no garbage observed in dumped records (all records were well-formed xinpcb entries of live raw sockets + the expected zero-padding tail).

Why it does not fire (analysis)

The teardown work (rip_detach messages) executes in the netisr0 thread; in this workload it is processed while the walker is blocked or yielding β€” SYSCTL_OUT copyout (1.1 GB/s across the readers) provides constant blocking points, and at those points the marker is linked and the protocol works exactly as designed (removal of the element before the marker correctly re-links the marker; in_pcb.c:1316-1331 comment). The lwkt scheduler did not preempt the walker user thread inside the ~15 ns advance window often enough (or at all) to intersect one of the ~260/s removals of the exact element being stepped to. The safety therefore rests on scheduler serialization behavior, not on any synchronization in the code β€” a fragility (any future preemption change, or a pcb teardown that runs in true interrupt context, re-opens the window), which is why this is filed as a hardening (Low/speculative) finding rather than a demonstrable DoS.

Exploit chain (if it ever fired)

unpriv sysctl net.inet.raw.pcblist loop + unpriv setuid-ping churn loop β†’ marker relinked after freed inpcb β†’ UAF write (pointer into freed M_PCB chunk) + UAF read (recycled heap contents exported to user) β†’ list corruption β†’ panic; recycled-chunk contents could in principle include other pcbs' data (info leak). Not demonstrated.

Fix validation

not_testable β€” the baseline never reproduced the fault, so "patched kernel no longer faults" is vacuous. fix.diff (crit_enter/crit_exit around advance+snapshot) applies cleanly to sys/netinet/in_pcb.c (patch -p1 --dry-run verified) and is defense-in-depth making the walk atomic by construction instead of by scheduler behavior. The identical dance in tcp_subr.c tcp_pcblist() (fully unprivileged churn β€” TCP connect/close storm) needs the same treatment; recommend fixing both.

Known-findings fix status checked during this pass

  • DF-0333 (xinpcb pointer leak): unfixed β€” in_pcb.c:2469 still bcopys the raw inpcb; sotoxsocket (uipc_socket2.c:827) additionally exports xso_so = so and xso_pcb = so->so_pcb raw pointers (same sysctl/sink, same fix scope as DF-0333).
  • DF-0334 (div0): unfixed β€” in_pcb.c:428 cut % lport and in_pcb.c:885 karc4random() % count both intact.
  • DF-0335 (portrange inversion): unfixed β€” in_pcb.c:2532-2555 intact.

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

Confirmed kernel references

Detail

Exploit chain

if the window were entered: unpriv sysctl net.inet.raw.pcblist loop + unpriv setuid-ping churn -> LIST_INSERT_AFTER on freed inpcb -> pointer write into freed M_PCB chunk + succ->le_prev into freed chunk -> list corruption -> panic; freed-recycled inpcb contents bcopy'd to user (info leak). Not demonstrated.

Evidence (decisive lines)

VERDICT.md (full analysis + attempts table); run.log (3 rounds, ~13.8M dumps / ~7M teardowns / 0 panics); pcb_race.c (reader+churn harness); fix.diff (crit_enter protection, patch -p1 --dry-run verified)

PoC changes

Seed sketch (none existed - new finding): wrote pcb_race.c with -r reader (unpriv sysctl hammer, dynamic sizing+data passes) and -c churner (root, holds 96 raw ICMP sockets, close+recreate round-robin); run.sh wrapper plus multi-reader stress.sh variant used in round 3.

Verified recommended fix

Wrap the in_pcblist_range marker advance + pcb snapshot in crit_enter()/crit_exit() (see fix.diff); apply the same to tcp_pcblist() in tcp_subr.c.

Verdict

Structural race confirmed in source: in_pcblist_range (sys/netinet/in_pcb.c:2456-2476) walks non-shared pcbinfo lists (ripcbinfo, infotoken==NULL) with zero synchronization and advances its marker via a non-atomic read/unlink/relink dance; a same-cpu netisr teardown (rip_detach -> in_pcbdetach: in_pcbofflist + kfree, in_pcb.c:1210-1226) landing inside the window would relink the marker after a freed inpcb (UAF write of a pointer into freed M_PCB memory) and read/export freed-recycled memory (UAF read). Reader half is fully unprivileged (sysctl net.inet.raw.pcblist); churn half is root-gated raw sockets but unpriv-drivable via setuid /sbin/ping. NOT reproduced under maximal stress: ~13.8M sysctl dumps (~6e9 marker advances) vs ~7M concurrent teardowns across 3 rounds (115s single reader @45k dumps/s, 230s x 8 concurrent readers) - no panic, no corruption, guest healthy. On this kernel the netisr0 teardown work executes at the walker's blocking points where the marker protocol works as designed; safety rests on scheduler serialization, not on synchronization in the code. Filed as Low/speculative hardening; fix.diff makes the advance+snapshot atomic via crit_enter/crit_exit. Twin non-atomic dance in tcp_subr.c tcp_pcblist (fully unpriv churn) needs the same fix.