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

sc_invalid written through the rt2560 union alias misses on rt2860-family devices: rt2860_softc dropped sc_tx_timer from the documented common prefix, so the glue writes sc_debug (offset 7404) and rt2860_softc.sc_invalid (7400) is never set β€” card-eject guards permanently dead

Summary

The union-alias prefix contract drifted (rt2860var.h dropped sc_tx_timer from the prefix, shifting sc_invalid down 4) - every sc_invalid write lands on rt2860 sc_debug for the default-dispatch family while the real sc_invalid has no writer: the eject guard can never trip and the watchdog keeps MMIO-ing after surprise removal. PROVEN with live compiled offsets on guest (alias 7404 == rt2860 sc_debug; sc_st/sc_sh coincide which is why it went unnoticed). Restore-prefix+CTASSERT fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4997 Β· 7 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 4.0 KB ↓ raw
run.log β€” 708 B view raw
build.sh β€” 134 B view raw
run.sh β€” 121 B view raw
env.txt β€” 376 B view raw
fix.diff β€” 1.6 KB view raw

DF-4997 β€” if_ral_pci.c writes sc_invalid through the rt2560 union alias; for rt2860-family devices the offsets diverge and the write lands on rt2860_softc.sc_debug (sc_invalid stays permanently 0)

Where

sys/dev/netif/ral/if_ral_pci.c:205,249-251,296,308 (union ral_pci_softc at :143-155) vs the softc prefixes in rt2560var.h:106-122, rt2661var.h:99-115, rt2860var.h:117-132.

What

The glue uses struct rt2560_softc *sc = &psc->u.sc_rt2560; as a universal alias and writes sc->sc_invalid (attach: 1 then 0; detach: !bus_child_present). The three child structs documented a common prefix ("The same in both up to here", rt2860var.h:133-135), but rt2860_softc drifted from it: sc_snd was moved before sc_mtx and sc_tx_timer was dropped from the prefix (it lives at rt2860var.h:160). Consequence for the default dispatch (all rt2860-family PCI IDs, 24+ of 33 entries in ral_pci_ids):

  • rt2560_softc.sc_invalid (offset 7404) == rt2860_softc.sc_debug (7404) β†’ the glue writes sc_debug, not sc_invalid;
  • rt2860_softc.sc_invalid (7400) is never set β€” the "card ejected" guards (rt2860.c:2080 watchdog; the whole point of the flag) can never fire, and the "not-yet-initialized" latch during attach is dead.

rt2661 keeps the rt2560 prefix (both at 7404) and is unaffected; sc_st/sc_sh coincide in all three (7360/7368 β€” the mtx/snd swap is offset-neutral since both are 8-aligned blocks), so the bustag/handle setup is correct and impact is confined to sc_invalid/sc_debug.

Reproduce (build/run on the audit DragonFly guest)

See ../DF-4996/ β€” the same module prints the compiled offsets at MOD_LOAD (df4996.c, df4997_report_offsets()). Copies of the decisive output are in run.log.

kldload ./df4996.ko
dmesg | grep DF4997

Expected output (observed 2026-09-25)

DF4997: rt2560_softc size=7920: ... sc_tx_timer=7400 sc_invalid=7404 sc_debug=7408
DF4997: rt2661_softc size=8040: sc_invalid=7404 sc_debug=7408
DF4997: rt2860_softc size=21536: ... sc_invalid=7400 sc_debug=7404 sc_tx_timer=21224
DF4997: VERDICT rt2560.sc_invalid alias == rt2860.sc_debug: glue writes MISS sc_invalid and hit sc_debug on rt2860 (claim TRUE)
DF4997: sc_st/sc_sh offsets DO coincide (...)
VERDICT.md
↓ download raw

DF-4997 β€” VERDICT

Finding: if_ral_pci.c drives the three child drivers through one union (struct ral_pci_softc.u) and accesses the per-child softc via the sc_rt2560 member alias. sc_invalid writes through that alias miss their target for rt2860-family devices.

Reproduced? β€” YES, at the level the bug exists (compiled layout), live on the guest

The bug is a layout divergence; it is fully demonstrated by the real compiled offsets, printed from the actual kernel headers on the audit guest (module df4996.c, df4997_report_offsets(), captured in run.log):

field rt2560 rt2661 rt2860
sc_st 7360 β€” 7360
sc_sh 7368 β€” 7368
watchdog_ch 7376 β€” 7376
sc_tx_timer 7400 β€” (absent; at 21224)
sc_invalid 7404 7404 7400
sc_debug 7408 7408 7404

Root cause chain:

  1. rt2560var.h:106-122 prefix: sc_ic, sc_mtx, sc_snd, sc_dev, sc_st, sc_sh, watchdog_ch, sc_tx_timer, sc_invalid, sc_debug.
  2. rt2860var.h:117-132 prefix: sc_ic, sc_snd, sc_mtx, sc_dev, sc_st, sc_sh, watchdog_ch, sc_invalid, sc_debug β€” sc_snd swapped ahead of sc_mtx (offset-neutral: sizeof(struct lock)=32, sizeof(struct mbufq)=24, both 8-aligned, so all following offsets coincide) and sc_tx_timer dropped from the prefix (it survives at :160), which shifts sc_invalid/sc_debug down by 4 relative to rt2560/rt2661. The comment at rt2860var.h:133-135 ("The same in both up to here") is therefore false for the rt2860 prefix.
  3. if_ral_pci.c:205 aliases &psc->u.sc_rt2560; :251 sc->sc_invalid=1, :296 sc->sc_invalid=0, :308 sc->sc_invalid=!bus_child_present(dev) all write at offset 7404 β€” rt2860's sc_debug.
  4. Dispatch (if_ral_pci.c:227-239): default β‡’ rt2860 ops for every rt2860-family ID (0x0601/0x0681/0x0701/0x0781/0x3060/…/0x539f and all Edimax/MSI/AWT rebrands β€” the overwhelming majority of ral_pci_ids). Only 0x0201β†’rt2560 and 0x0301/0x0302/0x0401β†’rt2661 use the aligned layouts.

Effects (rt2860 family)

  • rt2860_softc.sc_invalid is never set by the glue: the card-ejected guard if (sc->sc_invalid) return; in rt2860_watchdog (rt2860.c:2080) can never trip after surprise removal (the flag's entire purpose); the "device not ready" latch during attach is equally dead. No other writer of sc_invalid exists (checked rt2560.c/rt2661.c/rt2860.c).
  • rt2860_softc.sc_debug receives 1 at pre-attach (transiently enables DPRINTF until rt2860.c:242 resets it to 0) and !bus_child_present() at detach (softc is dying; the RAL_DEBUG sysctl at rt2860.c:369 may print debug spam during teardown on an ejected card).
  • sc_st/sc_sh writes land correctly (7360/7368 in both), so the device still works β€” this is why the regression went unnoticed.

Impact

Robustness/correctness regression with a narrow security edge: dead surprise-removal guard on CardBus/PCIe hot-unplug of rt2860-family cards (watchdog keeps driving rt2860_stop_locked/rt2860_init_locked against unmapped MMIO after ejection). Requires physical removal/failure β€” Low severity, no unprivileged trigger.

Fix

Root-cause fix in fix.diff: restore the documented common prefix in rt2860_softc (move sc_mtx ahead of sc_snd, move sc_tx_timer into the prefix β€” private struct, no persistence/ABI impact) and add compile-time assertions in if_ral_pci.c so any future drift of the aliased fields (sc_st, sc_sh, sc_invalid) fails the build instead of silently mis-writing. Not kernel-built on the guest (no ral hardware; a plain kernel build would only prove it compiles) β€” layout math and the live offset table above carry the proof.

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_testable
baseline reproduced→ patch + rebuild →patched clean

fix.diff generated mechanically from the real tree and verified with patch --dry-run (APPLY-OK). Not kernel-built: no ral hardware to observe the restored eject-guard behavior, and the CTASSERTs make any residual drift a compile-time error by construction. The reorder moves only members of a driver-private struct (no persistence, no ABI).

findings/poc/DF-4997/fix.diff (patch -p1 --dry-run: OK)
↓ fix.diffper-fix-DF-4997

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (live offsetof table printed from the real kernel headers on the guest); VERDICT.md (offset table + root-cause chain + scope analysis); fix.diff (restores the common prefix in rt2860_softc + CTASSERT guards on the aliased fields, patch --dry-run verified)

PoC changes

No seed PoC. Offsets demonstrated via the DF-4996 module's MOD_LOAD reporting (df4997_report_offsets), which compiles the actual rt2560var.h/rt2661var.h/rt2860var.h.

Verified recommended fix

Restore the documented common prefix in rt2860_softc (sc_mtx before sc_snd; move sc_tx_timer into the prefix) and add CTASSERTs in if_ral_pci.c pinning the aliased field offsets

Verdict

The layout divergence - which IS the bug - was reproduced live on the audit guest by printing the compiled offsets of the real kernel structs (module df4996.c): rt2560_softc.sc_invalid=7404/sc_debug=7408, rt2661_softc.sc_invalid=7404 (unaffected), rt2860_softc.sc_invalid=7400/sc_debug=7404. if_ral_pci.c writes sc_invalid through the rt2560 union alias (if_ral_pci.c:205,251,296,308), i.e. at offset 7404, which on rt2860-family devices (the default dispatch at if_ral_pci.c:227-239 - 24+ of 33 PCI IDs) is rt2860_softc.sc_debug; rt2860_softc.sc_invalid (7400) is never written anywhere and stays 0, permanently defeating the card-eject guard rt2860.c:2080 and the attach-time not-ready latch; the stray writes land on sc_debug (transiently 1 until rt2860.c:242 resets it, then !bus_child_present at detach). sc_st/sc_sh offsets coincide (7360/7368 in both - sizeof(lock)=32 and sizeof(mbufq)=24, both 8-aligned, so the mtx/snd swap is offset-neutral), which is why the driver still functions and the regression went unnoticed; the actual shift comes from sc_tx_timer being dropped from the rt2860 prefix (now at rt2860var.h:160) despite the comment at rt2860var.h:133 claiming 'The same in both up to here'. Runtime impact ceiling: dead surprise-removal guard (watchdog keeps programming an ejected CardBus/PCIe rt2860) - requires physical removal, no unprivileged trigger; impact=none for runtime escalation purposes, severity Low.