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)
PoC verification
Evidence pack
findings/poc/DF-3315 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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-xon 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).
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
ripcbinfois initialized non-shared: raw_ip.c:115in_pcbinfo_init(&ripcbinfo, 0, FALSE)βinfotoken == NULL(in_pcb.c:201-207).GET_PCBINFO_TOKEN()is a no-op wheninfotoken == NULL(in_pcb.h:439-443) βin_pcblist_range(in_pcb.c:2452) holds nothing while walkingripcbinfo.pcblisthead.- The marker advance is three separate operations with no atomicity: read-next (in_pcb.c:2458), unlink (2459), relink-after (2460).
- The mutator,
in_pcbofflist()(in_pcb.c:255-270), runs fromrip_detachin netisr0 and frees the inpcb in the same dispatch viain_pcbdetach(in_pcb.c:1210-1226,kfree(inp, M_PCB)at 1225). - 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. - 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_prevpoints into the freed chunk; subsequent reads (inp->inp_flags2462,prison_xinpcb2464 β in_pcb.c:2392-2406 derefsinp->inp_socket->so_cred,bcopy2469) 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 exportsxso_so = soandxso_pcb = so->so_pcbraw pointers (same sysctl/sink, same fix scope as DF-0333). - DF-0334 (div0): unfixed β in_pcb.c:428
cut % lportand in_pcb.c:885karc4random() % countboth 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_testableConfirmed 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.
No comments yet.