twe_add_unit off-by-one: TWEIO_ADD_UNIT with unit==16 passes `unit > TWE_MAX_UNITS` and writes one struct past twe_drive[16] (OOB write into softc + ghost disk with unit truncated to 0)
Summary
The bound check uses > instead of >=, admitting unit==16 while twe_drive has exactly 16 elements. dr=&twe_drive[16] overlays sc->twe_aen_queue[]; if the unit-summary byte reports online, controller-derived geometry and an 8-byte device_t are written OOB and a ghost twed attaches. Its unit==16 copies into the 4-bit cmd->io.unit field on every I/O - truncating to 0 and directing the ghost disk's I/O at unit 0 (silent cross-unit data corruption). twe_shutdown only iterates 0..15 so the ghost child is never cleaned. One-character >= fix (validated -Werror).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4179 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | verification verdict + full path:line trace | 3.0 KB | β raw |
| twe_addunit16.c | trigger | TWEIO_ADD_UNIT unit=16 trigger | 2.3 KB | view raw |
| build.sh | build | build command | 99 B | view raw |
| run.sh | run | run command | 155 B | view raw |
| run.log | runlog | guest run: open /dev/twe0 ENOENT (no 3ware HW) | 98 B | view raw |
| env.txt | env | guest environment | 621 B | view raw |
| fix.diff | suggested-fix | one-line bound fix (git-apply-able) | 860 B | view raw |
| verdict.json | verdict | machine verdict | 2.6 KB | view raw |
DF-4179 β Verification Verdict
Status: inconclusive (hardware-gated: no 3ware controller in the QEMU guest) Impact: none on this guest (OOB struct write into the softc + cross-unit I/O confusion on affected hardware) Confidence: certain (pure bounds bug, provable from source)
Mechanism (source trace)
twe_ioctlTWEIO_ADD_UNIT βtwe_add_unit(sc, td->td_unit)withtd_unita user-suppliedint(twe.c:674-678, tweio.h:101-105).- Bound check
if (unit < 0 || unit > TWE_MAX_UNITS)(twe.c:212) β admits unit == TWE_MAX_UNITS == 16 (twereg.h:144), whilestruct twe_softc::twe_drivehas exactlyTWE_MAX_UNITSelements (twevar.h:136, indices 0..15).twe_del_unitnext door already uses the correct>=(twe.c:291) β asymmetric, classic off-by-one. dr = &sc->twe_drive[unit](twe.c:226) β one struct past the array. Instruct twe_softc,twe_drive[]is immediately followed bytwe_aen_queue[TWE_Q_LENGTH](twevar.h:136-139), so the OOB struct overlays the AEN ring buffer.drives->data[unit](twe.c:228) reads byte 16 of the unit-summary parameter (the array logically has 16 entries; the underlying 512-byte DMA buffer makes the read itself stay in-bounds β the value is controller-controlled). If the online bit is set: -twe_get_param_4(..., &dr->td_size)(twe.c:236) β OOB 4-byte write, -td_state(twe.c:242),td_type(twe.c:261),td_heads,td_sectors,td_cylinders,td_twe_unit = unit(twe.c:264-272) β OOB writes of controller-derived/computed values, -twe_attach_drive(sc, dr)writesdr->td_disk(an 8-byte kernel pointer, twe_freebsd.c:570) OOB and creates a ghost twedN child.- The ghost disk's
td_twe_unit == 16is copied into the 4-bitcmd->io.unitfield on every I/O (twe.c:448, twereg.h:187) β truncates to 0 β the ghost disk's reads/writes are directed at unit 0 (silent cross-unit data corruption). - Cleanup never happens:
twe_shutdowniteratesi < TWE_MAX_UNITS(twe_freebsd.c:471) andtwe_del_unit(16)is rejected by the correct>=β the ghost child outlives the driver state it references.
Why it cannot be reproduced on this guest
No 3ware PCI device (vendor 0x13c1) and no /dev/twe* node exist in the
QEMU guest; twe_addunit16 builds and runs, open("/dev/twe0") fails
with ENOENT (run.log). Control node is additionally 0600 root:operator
(twe_freebsd.c:339-342).
Fix validation
One-line > β >= (twe.c:212); applies cleanly and batch-compiled into
X86_64_GENERIC with the rest of this batch (fix_build.log). Behavior on
hardware: TWEIO_ADD_UNIT(16) now returns EINVAL β the exact success
criterion encoded in twe_addunit16.c.
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
inconclusiveCompile-validated only: one-line fix applied cleanly and batch-built into X86_64_GENERIC with -Werror, rc=0 (fix_build.log). On hardware the PoC's success criterion flips from ioctl()==0 to -EINVAL; could not be behaviorally tested on this guest (no 3ware controller).
fix_build.log excerpt; grep 'unit >= TWE_MAX_UNITS' verified present at twe.c:212 in the patched tree before the build.
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (path:line trace incl. the >= twin in twe_del_unit twe.c:291 proving intent); twe_addunit16.c (trigger: TWEIO_ADD_UNIT 16); run.log (hardware absent, build OK); fix.diff + fix_build.log
PoC changes
n/a (first-pass PoC authored in this run)
Verified recommended fix
Change the bound at twe.c:212 from 'unit > TWE_MAX_UNITS' to 'unit >= TWE_MAX_UNITS'.
Verdict
Pure source-level off-by-one: twe_add_unit checks 'unit > TWE_MAX_UNITS' (twe.c:212) instead of '>=', so TWEIO_ADD_UNIT with td_unit==16 passes and dr=&sc->twe_drive[16] (twe.c:226) is one struct twe_drive past the 16-element array (twevar.h:136) - it overlays sc->twe_aen_queue[]. If the controller's unit-summary byte at data[16] (twe.c:228) reports online, controller-derived geometry is written out of bounds (td_size twe.c:236, td_state/td_type/heads/sectors/cylinders/td_twe_unit twe.c:242-272) and twe_attach_drive writes an 8-byte device_t OOB and creates a ghost twed unit whose td_twe_unit==16 is truncated into the 4-bit cmd->io.unit field (twe.c:448), directing that disk's I/O at unit 0. Not exercisable on this guest: no 3ware controller, open('/dev/twe0') = ENOENT (run.log); control node 0600 root:operator.
No comments yet.