wb_rxeof double-fetch of the device-written RX status word: check/use TOCTOU enabling a ~12KB OOB read copied into received packets (kernel heap info leak)
Summary
wb_rxeof snapshots rxstat but then re-reads the device-DMA'd wb_status for both length checks and the length use. A device rewrite in the window makes total_len up to 16383 (14-bit extract), and m_devget then bcopy()s up to 16385 bytes from a 4096-byte static buffer - an ~12KB OOB read across neighbouring RX buffers and descriptor arrays inside the contigmalloc block, delivering kernel heap (other slots' frames, descriptor words containing physical addresses) into an mbuf handed to the stack/BPF. On this port the window is live even with an honest chip because DF-4266's inverted loop validates chip-owned descriptors the engine is concurrently writing. Use-the-snapshot fix. [VERIFIED DOWNGRADE: machine-code analysis shows gcc8.3 merged the check and use into one load in the shipped binary - the claimed TOCTOU cannot occur in this build; fix retained as hardening]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4268 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dfwb3_rig.c | β | 40.9 KB | view raw | |
| df8_toggle.c | β | 1.4 KB | view raw | |
| Makefile | β | 105 B | β download | |
| kld_run.sh | β | 2.8 KB | view raw | |
| wb_rxeof.stock.disasm | β | 8.8 KB | β download | |
| wb_rxeof_stock_disasm_marker.log | β | 904 B | view raw | |
| run.race_production.log | β | 97 B | view raw | |
| run.race_production.dmesg.log | β | 3.8 KB | view raw | |
| run.race_production_v1_miscomputed_huge.log | β | 366 B | view raw | |
| run.race_ctl_userland.log | β | 828 B | view raw | |
| run.race_ctl_userland.dmesg.log | β | 2.9 KB | view raw | |
| run.race_ctl_kernel.log | β | 791 B | view raw | |
| run.race_production_fixed.log | β | 907 B | view raw | |
| panic_rig_v1_togglethread.txt | β | 1.1 KB | view raw | |
| fix_validated.diff | β | 2.2 KB | view raw | |
| env.kld.txt | β | 491 B | view raw | |
| VERDICT.md | β | 5.5 KB | β raw | |
| verdict.json | β | 5.7 KB | view raw |
VERDICT β DF-4268 (wb_rxeof double-fetch of the DMA'd RX status word)
Status: not_reproduced β false positive (on the audited artifact β the shipped X86_64_GENERIC kernel, gcc 8.3/CCVER=gcc80) β impact none; confidence certain (static proof, dynamically corroborated). The source-level pattern is real and the fix is recommended hardening; it is validated to remove even the residual flags/length double-fetch from the machine code.
Why the claimed OOB read is unreachable in this binary
The finding's chain: rxstat snapshot at :973; length checks re-read the
device word at :983 (< 60) and :984 (> 1536); the length USE re-reads
it again at :1004; a device rewrite between :984 and :1004 yields
total_len up to 16383, and m_devget (uipc_mbuf.c:2235, chained
m_getl+bcopy, no cap) then copies up to 16381 bytes from the 4096-byte
wb_rxbufs[0] β ~12 KB OOB read into a received packet.
The shipped binary does not contain that chain. wb_rxeof @
0xffffffff808dae30 loads the device word exactly twice per iteration:
+0x1c mov (%rax),%eax ; :973 snapshot β feeds OWN/MIIERR/LASTFRAG/
; RXCMP/RXERR flag tests (register reuse)
+0x44 mov (%rdx),%esi ; ONE re-read β feeds the MERGED range check
+0x4d shr $0x10,%esi ; (len-60) > 1476 unsigned == :983 AND :984
+0x53 sub $0x3c,%edx / cmp $0x5c4,%edx / ja error
+0x86 and $0x3fff,%esi ; the SAME +0x44 fetch feeds the USE
+0x8d sub $0x2,%esi ; total_len-4+2 folded
+0x98 callq m_devget
The value that reaches m_devget is the same fetch that was range-checked;
a device rewrite between "check" and "use" has no window β there is no
second length fetch. (GCC legally merged :983/:984/:1004 because the
-fno-strict-aliasing kernel build still lets it keep one load across the
intervening flag arithmetic.) wb_intr calls out to wb_rxeof β there is
no inlined second copy (checked). The only surviving double-fetch is
flags-vs-length (snapshot :973 vs the single length load), which cannot
produce an out-of-range length: the length is self-consistent.
Dynamic corroboration (production handler raced)
Rig: DF-4266 KLD dispatch method; "chip" simulated by rewriting
rx_list[0].wb_status between
GOOD = 0xC0400300 (OWN|RXCMP|LAST|FIRST, len 64) and
HUGE = 0xFFFF0300 (same flags, len 16383) β both OWN=1, i.e. exactly the
mid-write-back transient the stock inverted loop (DF-4266) wrongly accepts;
GOOD/HUGE 4:1 duty; toggler pinned to CPU 3 (usched bsd4:8), dispatch
pinned to CPU 0 (usched bsd4:1).
- Stock kernel, 250 dispatches (~17 s), 221 frames delivered: maxplen=60, WON=0 (run.race_production.dmesg.log). The handler processed frames the whole time under an actively adversarial length field; no delivered length ever exceeded 1536.
- Earlier run with the toggle value mis-set to RXLEN 1023 (still legal): 276 deliveries, maxplen 1019 β every length it delivered was one that passed the merged check at its own fetch (run.race_production_v1_...log).
- Control: an EXACT-SEMANTICS transcription of the source (four distinct volatile fetches at :973/:983/:984/:1004) raced identically also produced ZERO wins across ~130 000 dispatches (userland Β΅s chip Γ1, Γ3 streams, and a kernel ns-rate toggle thread β run.race_ctl_*.log): a software writer cannot land a store inside the reader's ~tens-of-ns fetch window (the reader acquires the cache line once and completes its loads locally). Only an in-flight DMA write could split the fetches of code that actually fetches twice β and the shipped binary's length path fetches once. Note the control could not be made to win even theoretically-honestly, which is why it is supporting, not primary, evidence; the disassembly is the proof.
Fix assessment
fix.diff (use rxstat for the checks and the use) is the right hardening:
on the rebuilt kernel (fix_validated.diff, kernel #1) the disassembly shows
exactly ONE mov (%rax),%eax per iteration (the snapshot) β the :983/
:984/:1004 re-fetch no longer exists in machine code β and the race again
delivers 188 frames, maxplen 60, WON 0 (run.race_production_fixed.log).
Because no divergent behaviour exists on this build either before or after
the patch, there was no behavioural delta to A/B: fix_status is
not_testable in the strict sense (nothing broken to observe being
fixed); the machine-code delta (2 loads β 1 load) is recorded instead.
Rig incidents (documented, not the finding)
- v1 toggle thread exited via
lwkt_exit()β run-queue corruption βpanic: assertion "(ntd->td_flags & TDF_RUNNING) == 0" failed in lwkt_switchAFTER the race had completed and printed its result (panic_rig_v1_togglethread.txt). DFly lwkt_create()d threads are not meant to exit; replaced by a userland sysctl-driven chip. - A later never-exiting (parked) kernel toggle thread triggered the same assertion class from softclock after ~10 s of remote spinning β race data again captured first (run.race_ctl_kernel.log tail).
- An earlier toggle constant was mis-computed (RXLEN 1023 instead of 16383) β the run is kept as the mid-range-length datapoint.
None of these involve the code under test.
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_testablenot_testable behaviourally: the OOB was never reachable on this build (baseline already clean), so no pre/post behavioural delta exists; the patch's effect is nevertheless verified at the machine-code level β the shipped 2-loads-per-iteration pattern drops to 1 load on the rebuilt kernel, eliminating the double-fetch entirely β and the fixed-kernel race stays at 0 wins / 188 deliveries / maxplen 60
run.race_production_fixed.log (objdump section: single 'mov (%rax),%eax' per iteration, no second (%rdx) reload; race summary 'delivered=188 maxplen=60 WON=0'); fix_validated.diff; VERDICT.md 'Fix assessment'
Confirmed kernel references
Detail
Evidence (decisive lines)
wb_rxeof.stock.disasm (per-iteration loads at +0x1c and +0x44; range check and m_devget length both derive from the +0x44 fetch); run.race_production.dmesg.log ('DF-4268 PRODUCTION race over after 250 dispatches (~17 s): delivered=221 maxplen=60 WON=0'); run.race_production_v1_miscomputed_huge.log (276 deliveries, maxplen 1019 <= 1536 with mid-range toggle values); run.race_ctl_userland.log/.dmesg + run.race_ctl_kernel.log (control transcription, 0 wins in ~130k dispatches); run.race_production_fixed.log (fixed kernel: single-load pattern + 'delivered=188 maxplen=60 WON=0'); VERDICT.md
PoC changes
Replaced the seed's 'no runnable PoC' with: (a) scripted objdump of the production wb_rxeof fetch pattern (kld_run.sh disasm); (b) a two-CPU race harness β userland 'chip' writing rx_list[0].wb_status via a rig sysctl (kern.dfwb3.tog) with GOOD/HUGE 4:1 duty, usched-pinned away from the dispatching sysctl, racing the REAL production handler (go4268); (c) an exact-semantics volatile transcription control (go4268ctl) raced with userland x1/x3 and kernel ns-rate togglers. Seed's mis-set HUGE constant (RXLEN 1023 vs 16383) corrected; two rig incidents (toggle-thread lwkt_exit panic; spinning remote kernel thread panic) documented β both after race data capture, neither involving the code under test.
Verified recommended fix
Use the :973 rxstat snapshot for the length checks and the length use (WB_RXBYTES(rxstat)) instead of re-reading cur_rx->wb_ptr->wb_status three times β validated in fix_validated.diff hunk 2/3 (machine code: one status load per iteration)
Verdict
Not reproduced β and traced to a dead end in the audited artifact: the shipped X86_64_GENERIC kernel's wb_rxeof (gcc 8.3) merged the length checks (:983/:984) and the length use (:1004) into ONE load of the device-written status word (objdump: mov (%rdx),%esi @ +0x44 feeds both the merged range check 'sub $0x3c; cmp $0x5c4; ja' and, via and $0x3fff,%esi, m_devget's length) β the value copied into the received packet is the same fetch that was range-checked, so the claimed check/use divergence to 16383 and the ~12KB OOB read from the 4096-byte wb_rxbufs[0] cannot occur in this binary; effectively a false positive on the compiled artifact this audit targets. Only a flags-vs-length split (snapshot :973 vs the single length load) survives, which is not memory-unsafe. Dynamically corroborated: the PRODUCTION handler raced against an actively adversarial 'chip' (GOOD len64 <-> HUGE len16383, both OWN=1, 4:1 duty, separate CPUs) for 250 dispatches/~17s delivered 221 frames with max length 60 and ZERO oversized packets; an exact-semantics volatile transcription (four distinct fetches) also never diverged across ~130k dispatches (software writers cannot land stores inside the reader's ~30ns fetch window; only in-flight DMA could, and the binary's length path fetches once). The source-level four-fetch pattern is still a genuine double-fetch of device memory and the fix is validated as hardening: on the rebuilt kernel the machine code drops to ONE status load per iteration and the race stays at 0 wins. Recommend keeping the fix (defense against future compiler changes), downgrading/removing the info-leak impact for this build.
No comments yet.