Out-of-bounds descriptor write in wb_encap when TX chain fills all 16 fragments under minimum frame length
- File:
sys/dev/netif/wb/if_wb.c - Lines: 1221, 1248, 1274, 1275, 1276, 1277, 1279, 1284, 1285
- Severity: High
- CVSS:
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H - CWE: CWE-787 Out-of-bounds Write
- Confidence: likely
Summary
wb_encap() packs an outgoing mbuf chain into a fixed 16-slot wb_frag[]
array. The for-loop at line 1221 caps frag at WB_MAXFRAGS (16) via an early
break, but the WB_MIN_FRAMELEN padding branch at line 1274 then indexes
wb_frag[frag] without re-checking frag, so when exactly 16 non-empty mbufs
were consumed (loop exits with frag==16, m==NULL, coalesce branch skipped)
AND total_len < 60, the code writes 12 bytes (wb_status/wb_ctl/wb_data)
plus a read-modify-write of wb_ctl (line 1285 via WB_TXCTL macro at
wb_lastdesc==16) into wb_frag[16], one full wb_txdesc (256 bytes) past
c->wb_ptr.
Root cause
wb_encap() at sys/dev/netif/wb/if_wb.c:1221-1238 fills wb_frag[] until
if (frag == WB_MAXFRAGS) break; (line 1223).
For a chain of exactly 16 non-empty mbufs whose m_len sum is < WB_MIN_FRAMELEN
(60), the loop exits naturally with m==NULL and frag==16, so the coalesce
fallback at line 1248 (if (m != NULL)) is NOT taken.
Control then falls into the padding branch at line 1274:
if (total_len < WB_MIN_FRAMELEN) {
f = &c->wb_ptr->wb_frag[frag]; /* frag == 16, OOB */
...
}
with frag==16. wb_frag is declared
struct wb_desc wb_frag[WB_MAXFRAGS] (WB_MAXFRAGS==16) at if_wbreg.h:291,
so wb_frag[16] is one-past-end.
Concretely the driver writes:
f->wb_ctl = WB_MIN_FRAMELEN - total_len(line 1276)f->wb_data = vtophys(&sc->wb_cdata.wb_pad)(line 1277)f->wb_status = WB_TXSTAT_OWN(line 1279)frag++β 17 (line 1280)c->wb_lastdesc = frag - 1= 16 (line 1284)WB_TXCTL(c) |= WB_TXCTL_LASTFRAG(line 1285) expands (viaif_wbreg.h:296) toc->wb_ptr->wb_frag[16].wb_ctl |= WB_TXCTL_LASTFRAGβ a further OOB read-modify-write.
WB_MAXFRAGS is 16 (if_wbreg.h:271). The next wb_txdesc in wb_tx_list[]
begins exactly at the wb_frag[16] offset (sizeof(struct wb_txdesc) == 16*sizeof(struct wb_desc) == 256),
so the OOB either corrupts the next in-flight TX descriptor (common case) or,
when c->wb_ptr == &wb_ldata->wb_tx_list[127] (last descriptor), writes past
the contigmalloc region β which was sized only
sizeof(struct wb_list_data) + 8 (line 779), so the f->wb_data write at OOB
offset 8 is a 4-byte write into the kernel heap.
Threat
Local unprivileged user on a system with a wb(4) interface (Winbond
W89C840F / Compex RL100-ATX NIC, or the device exposed to a VM) that can
construct an outgoing packet whose mbuf chain has exactly 16 non-empty mbufs
totaling < 60 bytes.
The 4-12 byte OOB write of partially-controlled data (a kernel physical address
from vtophys(&wb_pad), plus fixed flags WB_TXSTAT_OWN=0x80000000 and
WB_TXCTL_TLINK|WB_TXCTL_LASTFRAG) can corrupt a sibling TX descriptor the chip
is concurrently DMA-ing from (packet corruption / chip-follows-stale-wb_next /
panic) or, with TX-ring grooming that drives wb_tx_free to chain[127], land a
4-byte heap write past the contigmalloc allocation.
With heap grooming the write can be directed at an adjacent kernel object to escalate to arbitrary kernel write.
Even absent escalation, the chip DMAing from a corrupted descriptor is a reliable kernel panic (A:H).
wb_start also ignores wb_encap's return value (line 1327), so a failing
encap still queues the descriptor, compounding the corruption.
Exploit / PoC
PoC plan (drop into findings/poc/DF-1487/):
trigger.cβ openAF_INET SOCK_DGRAMsocket bound to awbinterface address; build a 16-entryiovviasendmsg()with eachiov_basepointing at a 1-byte buffer (iov_len=1) for a 16-byte user payload; target a destination on thewbsegment soether_outputfires.
If the local UDP/IP prepend produces a 17th mbuf, fall back to:
trigger_bpf.c β open /dev/bpfN on the wb interface and write a
hand-crafted 16-mbuf chain via a kernel helper, OR trigger_raw.c β use
AF_INET SOCK_RAW with IP_HDRINCL and craft the IP header inline in the
first iov so the network layer does not prepend.
The decisive condition is exactly 16 mbufs with sum(m_len) < 60 in the
chain handed to wb_start.
- Build on DragonFlyBSD:
cc -O2 -o trigger trigger.c. - Run:
./triggerβ observe panic indmesg(panic comes from the chip following a corruptedwb_nextpointer or from a corruptedwb_dataphysical address) or, with KASAN/KMSAN enabled, a clear OOB write report atif_wb.c:1275-1279. - For the heap-OOB variant (
c->wb_ptr == wb_tx_list[127]), first flood the interface with 127 small UDP packets to advancewb_tx_freetochain[127], pause to let the chip drain (or block TX viaifconfig down/up), then fire the 16-fragment trigger so its descriptor lands atchain[127]; KASAN will report the write past thecontigmallocallocation.
Success criterion: kernel panic with stack through wb_encap+wb_start, or
KASAN OOB write at the cited lines.
Recommended fix
Validate frag in the padding branch and refuse (or coalesce) instead of
overflowing. Minimal diff against sys/dev/netif/wb/if_wb.c:
--- a/sys/dev/netif/wb/if_wb.c
+++ b/sys/dev/netif/wb/if_wb.c
@@ -1271,6 +1271,17 @@ wb_encap(struct wb_softc *sc, struct wb_chain *c, struct mbuf *m_head)
frag = 1;
}
+ /*
+ * We can only fit WB_MAXFRAGS fragments per super-descriptor. If the
+ * chain filled every slot (frag == WB_MAXFRAGS) and still needs a
+ * runt-padding fragment, the fragment array cannot hold it -- bail out
+ * so wb_start can drop the packet instead of writing past wb_frag[].
+ */
+ if (frag >= WB_MAXFRAGS && total_len < WB_MIN_FRAMELEN) {
+ m_freem(m_head);
+ return (1);
+ }
+
if (total_len < WB_MIN_FRAMELEN) {
f = &c->wb_ptr->wb_frag[frag];
f->wb_ctl = WB_MIN_FRAMELEN - total_len;
@@ -1324,7 +1335,15 @@ wb_start(struct ifnet *ifp, struct ifaltq_subque *ifsq)
sc->wb_cdata.wb_tx_free = cur_tx->wb_nextdesc;
/* Pack the data into the descriptor. */
- wb_encap(sc, cur_tx, m_head);
+ if (wb_encap(sc, cur_tx, m_head) != 0) {
+ /*
+ * Encap failed (e.g. fragment array would overflow).
+ * cur_tx was not consumed; m_head was already freed by
+ * wb_encap on this error path. Stop dequeuing.
+ */
+ ifp->if_oerrors++;
+ break;
+ }
if (cur_tx != start_tx)
WB_TXOWN(cur_tx) = WB_TXSTAT_OWN;
The first hunk makes wb_encap return failure (1) when it would otherwise
index wb_frag[16]; the second makes wb_start honor that return value (it
currently ignores it) so the corrupt descriptor is never queued to the chip.
A more aggressive fix would also extend the if (m != NULL) coalesce branch at
line 1248 to trigger on (frag == WB_MAXFRAGS) regardless of m, so the packet
is still transmitted via a single coalesced cluster instead of being dropped β
but the minimal diff above is sufficient to close the memory-safety hole.
Note on RX path (deliberately NOT a finding)
The RX length handling in wb_rxeof (lines 982β986) DOES enforce
WB_RXBYTES <= 1536 AND >= WB_MIN_FRAMELEN, and the buffer is
WB_BUFBYTES = 4096 bytes. So the missing-MCLBYTES pattern (DF-1410/DF-1478/
DF-1481) does NOT apply to this driver β only the TX-side wb_encap OOB write
above is a security issue in if_wb.c.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1487 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | reproduces wb_encap fill-loop + padding branch OOB | 5.2 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 65 B | view raw |
| run.sh | run-script | ./harness | 41 B | view raw |
| build.log | build-log | in-guest build, BUILD_EXIT=0 | 13 B | view raw |
| run.log | run-log | decisive run; 3/6 configs OOB | 808 B | view raw |
| env.txt | environment | uname + guest PCI inventory (no wb) | 543 B | view raw |
| fix.diff | suggested-fix | refuse encap when frag>=WB_MAXFRAGS && total_len<WB_MIN_FRAMELEN | 673 B | view raw |
| fix_build.log | fix-build-log | patched nativekernel, rc=0 | 5.6 MB | β download |
| VERDICT.md | verdict | full narrative | 3.6 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1487 β wb_encap wb_frag[16] OOB write
Verdict
REPRODUCED (source-level harness). The bug is real; impact ceiling is
kernel heap corruption (12 bytes of fresh write + 4-byte RMW past the end of
struct wb_txdesc) on any local user who can route a 16-fragment sub-60-byte
mbuf chain to a wb(4) (Winbond W89C840F) NIC. The guest has no Winbond NIC
(pciconf -lv lists only virtio + PIIX3), so the kernel code path cannot be
exercised end-to-end; the harness demonstrates the OOB indexing using the
genuine loop/padding logic from if_wb.c:1221-1285. fix.diff applies
cleanly and nativekernel succeeds (rc=0).
Mechanism (sys/dev/netif/wb/if_wb.c)
- Lines 1221-1238:
for (m = m_head, frag = 0; m != NULL; m = m->m_next)β fillswb_frag[frag]whilefrag < WB_MAXFRAGS(=16). - If exactly 16 non-empty mbufs are supplied, the loop exits with
frag == 16andm == NULL(becausem->m_nextwas NULL). - Line 1248:
if (m != NULL)coalesce branch is then skipped, so no re-pack into a cluster resetsfrag. - Line 1274:
if (total_len < WB_MIN_FRAMELEN)(=60) is true for any 16-fragment chain summing to < 60 bytes (e.g. 16 Γ 1-byte mbufs). - Line 1275:
f = &c->wb_ptr->wb_frag[frag]=wb_frag[16]β one past thewb_frag[WB_MAXFRAGS]array declared atif_wbreg.h:291. - Lines 1276-1280: write
f->wb_ctl,f->wb_data,f->wb_statusβ 12 bytes of fresh OOB write.frag++makes it 17. - Line 1284:
c->wb_lastdesc = frag - 1= 16. - Line 1285:
WB_TXCTL(c) |= WB_TXCTL_LASTFRAGexpands towb_frag[c->wb_lastdesc].wb_ctl |= ...=wb_frag[16].wb_ctl |= ...β a 4-byte OOB read-modify-write. - Compounding:
wb_startat line 1327 ignores the return value ofwb_encap, so even when encap fails the half-corrupted state is published.
Trigger: sendmsg() with a 16-entry iov of total length < 60 to a UDP
socket bound to the wb NIC, OR AF_INET SOCK_RAW with IP_HDRINCL and a
short IP header.
Harness proof (harness.c)
Reproduces the genuine fill-loop and padding logic and reports the OOB index:
config total_len coalesce? result 16x1 16 no OOB! 16x2 32 no OOB! 16x3 48 no OOB! 16x4 64 no in-bounds 15x1 15 no in-bounds 17x1 17 yes in-bounds Buggy configs: 3/6 write past wb_frag[15]
Exploit-chain note
This is a write-capable local primitive on real hardware. QEMU has no
Winbond NIC, so the kernel-side chain cannot be exercised here. On a real
wb(4) system the corruption lands in adjacent heap (or the next contiguous
wb_txdesc when c->wb_ptr == wb_tx_list[127]), so a successful trigger
yields reliable heap corruption / DoS at minimum, with heap-grooming β
controlled write a credible escalation path.
PoC changes
- Original PoC folder had no source.
- Added harness.c, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.
Fix
fix.diff adds if (frag >= WB_MAXFRAGS) return(1); at the top of the
padding branch, refusing the encap when the chain consumed all descriptors
and still needs padding. Matches the finding markdown proposal
("refuse encap when frag>=16 && total_len<60").
Fix-validation
patch -p1 --forward succeeds (hunk #1 at line 1271). nativekernel
completes with rc=0 (fix_build.log). No run-time exercise is possible
because no Winbond NIC is present on the guest β fix_status:
"not_testable". Diff applies and compiles; changed logic closes the OOB.
Fix verification
not_testablenot_testable because no Winbond W89C840F NIC on the audit guest; validated that fix.diff applies cleanly (hunk #1 at line 1271) and single-fix nativekernel compiles rc=0 (fix_build.log).
baseline (harness): Buggy configs: 3/6 write past wb_frag[15] patched kernel build: === NK_DONE rc=0 ===
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- w
- b
- /
- i
- f
- _
- w
- b
- .
- c
- :
- 1
- 2
- 2
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- w
- b
- /
- i
- f
- _
- w
- b
- .
- c
- :
- 1
- 2
- 4
- 8
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- w
- b
- /
- i
- f
- _
- w
- b
- .
- c
- :
- 1
- 2
- 7
- 4
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- w
- b
- /
- i
- f
- _
- w
- b
- .
- c
- :
- 1
- 2
- 7
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- w
- b
- /
- i
- f
- _
- w
- b
- .
- c
- :
- 1
- 2
- 8
- 4
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- w
- b
- /
- i
- f
- _
- w
- b
- .
- c
- :
- 1
- 2
- 8
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- w
- b
- /
- i
- f
- _
- w
- b
- r
- e
- g
- .
- h
- :
- 2
- 7
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- w
- b
- /
- i
- f
- _
- w
- b
- r
- e
- g
- .
- h
- :
- 2
- 9
- 1
Detail
Exploit chain
none (HW-gated): no Winbond NIC in QEMU. Primitive characterized via harness: 12 bytes of fresh OOB write + 4-byte OOB RMW past struct wb_txdesc into adjacent kernel heap (or the next contiguous wb_txdesc when wb_ptr==wb_tx_list[127]). Realistic ceiling on real HW: local heap corruption -> DoS at minimum, heap-grooming -> controlled write a credible escalation.
Evidence (decisive lines)
config total_len coalesce? result 16x1 16 no OOB! 16x2 32 no OOB! 16x3 48 no OOB! 16x4 64 no in-bounds 15x1 15 no in-bounds 17x1 17 yes in-bounds Buggy configs: 3/6 write past wb_frag[15]
PoC changes
Original folder had no source. Added harness.c replicating wb_encap fill-loop + padding branch, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.
Verified recommended fix
fix.diff adds if (frag >= WB_MAXFRAGS) return(1); at the top of the padding branch, refusing encap when the chain consumed all 16 descriptors and still needs padding. Matches finding markdown proposal.
Verdict
REPRODUCED at the source-logic level. if_wb.c:1221-1238 wb_encap fill loop fills wb_frag[0..15] when supplied 16 non-empty mbufs, exiting with frag==16 and m==NULL (chain end). Line 1248 coalesce branch (if(m!=NULL)) is skipped. Line 1274 padding branch (total_len<60) fires for chains summing to <60 bytes (e.g. 16x1-byte mbufs). Line 1275 f = &wb_frag[frag=16] is one past the wb_frag[16] array (if_wbreg.h:291); lines 1276-1280 write 12 bytes OOB; line 1284 c->wb_lastdesc=16; line 1285 WB_TXCTL(c) |= LASTFRAG = wb_frag[16].wb_ctl |= ... (4-byte OOB RMW). Harness confirms 3/6 configs (16x1,16x2,16x3) overflow; 16x4/15x1/17x1 are in-bounds. No Winbond W89C840F NIC on guest; harness proof only.
No comments yet.