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

ata_intel_chipinit treats any non-ENXIO return of ata_ahci_chipinit as AHCI success, leaving ctlr->setmode NULL β€” latent boot-time NULL function-pointer call (fragile error contract)

Summary

The '!= ENXIO' guard accepts error codes as success and returns 0, skipping the compat fallback that is the only remaining setmode assignment - the callee propagates reset errors verbatim and assigns setmode only after that point, so a future third error code yields an attached controller with setmode==NULL and the unconditional call in ata_pcichannel_setmode becomes a NULL fn-ptr panic at disk attach. Exhaustively verified dead on this tree (all four callee exits return {0,ENXIO}; the guest's PIIX3 boot path runs in-bounds every boot - live negative evidence in pack). == 0 fix to pin the contract.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4867 Β· 7 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
collect_live.sh β€” 668 B view raw
dmesg.txt β€” 237 B view raw
env.txt β€” 456 B view raw
fix.diff β€” 471 B view raw
verdict.json β€” 3.2 KB view raw

DF-4867 β€” latent NULL ctlr->setmode call: ata_intel_chipinit treats any non-ENXIO return of ata_ahci_chipinit as AHCI success

  • File: sys/dev/disk/nata/chipsets/ata-intel.c
  • Severity: Info (hardening / fragile error-contract), confidence: certain (shape) / latent (reachability)
  • Verdict: not_reproduced β€” traced dead on this tree (see VERDICT.md). Not a bug today; one refactor away from a boot-time NULL function-pointer call.

What the finding is

ata_intel_chipinit() (ata-intel.c:177-180) probes for AHCI mode:

if ((ctlr->chip->cfg1 == INTEL_AHCI) &&
    (pci_read_config(dev, 0x90, 1) & 0xc0) &&
    (ata_ahci_chipinit(dev) != ENXIO))
    return 0;

Any return value that is not exactly ENXIO β€” including any other error code β€” is treated as AHCI success and chipinit returns 0 immediately, skipping the compat-mode fallback at ata-intel.c:182-189 that is the only remaining place ctlr->setmode gets assigned on this path.

ata_ahci_chipinit() (ata-ahci.c:71-122) has three failure exits: - BAR(5) alloc failure β†’ ENXIO (ata-ahci.c:82) - interrupt setup failure β†’ ENXIO (ata-ahci.c:88) - ata_ahci_ctlr_reset() failure β†’ return (error) β€” propagated verbatim (ata-ahci.c:95-98)

All of ctlr->reset/dmainit/allocate/setmode and ctlr->channels are assigned only after that point (ata-ahci.c:101-108). So if ata_ahci_ctlr_reset() ever returns anything other than ENXIO (it currently returns only ENXIO/0, but its error is propagated as a variable), ata_intel_chipinit returns 0 with:

  • ctlr->setmode == NULL (compat block skipped, AHCI assignments never reached)
  • controller attach continues; channel attach then runs ata_pcichannel_setmode() (ata-pci.c:857-866) β†’ ctlr->setmode(dev, ATA_PIO_MAX) at ata-pci.c:863 β†’ NULL function-pointer call β†’ kernel panic during boot/attach (sink chain: ata-all.c:525 / ata-disk.c:436 β†’ ATA_SETMODE method).

Fix

Take the AHCI path only on explicit success β€” see fix.diff:

-    (ata_ahci_chipinit(dev) != ENXIO))
+    (ata_ahci_chipinit(dev) == 0))

Pack contents

  • VERDICT.md β€” full narrative + line-level trace + what was live-verified
  • dmesg.txt β€” guest boot evidence: the audited ata-intel path runs on every boot
  • env.txt β€” guest uname, PCI config dump of atapci0 (PIIX3), device id
  • collect_live.sh β€” exact commands to re-collect the evidence
  • fix.diff β€” git-apply-able one-line hardening
  • manifest.json / verdict.json

How to re-run the live evidence

dfbsd-qemu/vm.sh up          # if down
./collect_live.sh            # or: dfbsd-qemu/vm.sh run_root 'dmesg | grep -E "atapci0|ata[01]:"'

Expected: atapci0: <Intel PIIX3 WDMA2 controller> + ata0/ata1 channels β€” proving ata_intel_ident β†’ ata_intel_chipinit β†’ non-SATA branch β†’ ata_intel_new_setmode (timing tables, ata_mode2idx) executed in-bounds at boot with no anomaly.

VERDICT.md
↓ download raw

DF-4867 VERDICT β€” not_reproduced (latent; path traced dead on this tree)

Bottom line

The fragile error-contract at ata-intel.c:177-180 is real and certain in shape, but it is not reachable as a bug on this tree today: ata_ahci_chipinit() can only return 0 or ENXIO, so the != ENXIO test never mistakes a failure for success. No runtime manifestation is possible, hence not_reproduced / impact none. The finding is filed as hardening: the guard is one refactor away from a boot-time NULL function-pointer call, because ata_ahci_chipinit() propagates ata_ahci_ctlr_reset()'s error verbatim (return (error); at ata-ahci.c:97) and that callee's return set is not enforced by any contract.

Line-level trace

  1. sys/dev/disk/nata/chipsets/ata-intel.c:177-180 c if ((ctlr->chip->cfg1 == INTEL_AHCI) && (pci_read_config(dev, 0x90, 1) & 0xc0) && (ata_ahci_chipinit(dev) != ENXIO)) return 0; Entering the SATA branch requires chip->max_dma >= ATA_SA150 (ata-intel.c:159/166), i.e. cfg1==INTEL_AHCI entries only (ICH6+ AHCI/RAID SKUs, ata-intel.c:71-112). Requires config 0x90 bits 0xc0 set (BIOS AHCI/RAID mode).

  2. ata_ahci_chipinit() return paths (sys/dev/disk/nata/chipsets/ata-ahci.c): - :80-82 BAR(5) MEMORY alloc fails β†’ ENXIO - :85-89 ata_setup_interrupt fails β†’ ENXIO - :95-98 ata_ahci_ctlr_reset() != 0 β†’ return (error) β€” any error code propagates - :121 success β†’ 0 (only here are ctlr->channels/:105-108 setmode etc. assigned) ata_ahci_ctlr_reset() (:124-159) currently returns only ENXIO (:141) or 0 (:159).

  3. If a non-ENXIO error ever propagated: ata_intel_chipinit returns 0 at :180 with ctlr->setmode == NULL β€” the compat block at :182-189 (ctlr->setmode = ata_intel_sata_setmode or ata_sata_setmode) is skipped, and the AHCI block never assigned it.

  4. Sink: ata_pcichannel_setmode() ata-pci.c:857-866 calls ctlr->setmode(dev, ATA_PIO_MAX) (:863) unconditionally via the ata_setmode bus method, invoked from ata-all.c:525 and ata-disk.c:436 during channel/device attach β†’ NULL function pointer call β†’ panic while attaching the disk subsystem.

Why not reproducible on this guest / tree

  • Hardware path: requires an ICH6+ Intel SATA controller with cfg1==INTEL_AHCI, BIOS mode bits 0x90&0xc0, and an AHCI controller-reset failure returning non-ENXIO.
  • Source path: with the current tree, ata_ahci_chipinit ∈ {0, ENXIO} at every exit (verified by exhaustively reading all four return sites), so the != ENXIO guard never misfires. There is no way to make ata_ahci_ctlr_reset return a third value without editing sys/ (read-only for this audit).
  • Guest (QEMU PIIX3, see env.txt) has no AHCI-capable Intel SATA part at all: the boot path is the non-SATA branch (ata-intel.c:159-163), which always assigns ctlr->setmode = ata_intel_new_setmode.

What WAS live-verified (negative evidence for this file's boot path)

The guest boots on atapci0: <Intel PIIX3 WDMA2 controller> (chip 0x70108086 = ATA_I82371SB, ata-intel.c:51 β†’ ata_intel_chipinit β†’ non-SATA branch, channels=2, ata_intel_allocate, ata_intel_new_setmode):

  • dmesg.txt: atapci0, ata0, ata1, acd0 ... WDMA2 β€” the setmode callback ran at attach and indexed timings[ata_mode2idx(WDMA2)] = timings[7] (in-bounds, table has 15 entries) with no anomaly.
  • env.txt: PCI config dword at 0x54 = 0x00000400 β€” exactly the vendor-specific bit reg54 |= 0x0400 written by ata_intel_new_setmode (ata-intel.c:310). Direct register-level proof the audited function executed on this boot.
  • Guest healthy throughout; no panic; guest not dirtied.

Fix validation

Not applicable (nothing to make fail on the baseline). The one-line fix.diff (!= ENXIO β†’ == 0) is trivially semantics-preserving for every return value the callee can produce today and closes the contract hole for any future error code.

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 no→ patch + rebuild →patched clean

Fix validation N/A: the latent path cannot be made to fail on the baseline tree without modifying sys/ (read-only). The == 0 guard is semantics-preserving for every return value producible today and eliminates the contract hole.

findings/poc/DF-4867/fix.diff
↓ fix.diffper-fix-DF-4867

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (full path:line trace and reachability analysis); dmesg.txt (PIIX3 boot through ata_intel_chipinit non-SATA branch + ata_intel_new_setmode at attach); env.txt (uname, pciconf 0x40:0x5f dump showing 0x54=0400 from ata-intel.c:310); fix.diff (one-line guard fix)

PoC changes

No PoC needed (Info hardening, latent). Replaced trigger with a live negative-evidence collector (collect_live.sh) proving the audited chip-init/setmode path executes in-bounds on this guest at every boot.

Verified recommended fix

Only take the AHCI path on explicit success: ata_ahci_chipinit(dev) == 0 (not != ENXIO) in ata_intel_chipinit

Verdict

Latent-by-analysis: ata_intel_chipinit (ata-intel.c:177-180) accepts ANY non-ENXIO return of ata_ahci_chipinit as AHCI success and returns 0 without assigning ctlr->setmode (compat fallback at :182-189 skipped; AHCI assigns it only on the success path at ata-ahci.c:108). A third error code β€” e.g. any future value propagated verbatim by return(error) at ata-ahci.c:97 from ata_ahci_ctlr_reset β€” would leave ctlr->setmode NULL and panic the kernel at boot via the unconditional ctlr->setmode(dev, ATA_PIO_MAX) call in ata_pcichannel_setmode (ata-pci.c:863, invoked from ata-all.c:525 / ata-disk.c:436). Not reachable on this tree: exhaustively verified all four exits of ata_ahci_chipinit return only {0, ENXIO}, and the guest (PIIX3, non-SATA branch) has no AHCI-capable Intel SATA hardware. Live-verified instead that the audited boot path runs cleanly every boot: dmesg shows atapci0 + ata0/ata1 + acd0 WDMA2, and PCI cfg 0x54=0x0400 is the exact vendor bit written by ata_intel_new_setmode (ata-intel.c:310). Hardening fix: == 0 instead of != ENXIO.