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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4179 Β· 8 files
FileTypeDescriptionSize
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
VERDICT.md verdict verification verdict + full path:line trace
↓ download 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)

  1. twe_ioctl TWEIO_ADD_UNIT β†’ twe_add_unit(sc, td->td_unit) with td_unit a user-supplied int (twe.c:674-678, tweio.h:101-105).
  2. Bound check if (unit < 0 || unit > TWE_MAX_UNITS) (twe.c:212) β€” admits unit == TWE_MAX_UNITS == 16 (twereg.h:144), while struct twe_softc::twe_drive has exactly TWE_MAX_UNITS elements (twevar.h:136, indices 0..15). twe_del_unit next door already uses the correct >= (twe.c:291) β€” asymmetric, classic off-by-one.
  3. dr = &sc->twe_drive[unit] (twe.c:226) β€” one struct past the array. In struct twe_softc, twe_drive[] is immediately followed by twe_aen_queue[TWE_Q_LENGTH] (twevar.h:136-139), so the OOB struct overlays the AEN ring buffer.
  4. 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) writes dr->td_disk (an 8-byte kernel pointer, twe_freebsd.c:570) OOB and creates a ghost twedN child.
  5. The ghost disk's td_twe_unit == 16 is copied into the 4-bit cmd->io.unit field 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).
  6. Cleanup never happens: twe_shutdown iterates i < TWE_MAX_UNITS (twe_freebsd.c:471) and twe_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

inconclusive
baseline no→ patch + rebuild →patched clean

Compile-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.
↓ fix.diffper-fix-DF-4179

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.