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

Out-of-bounds array access in amd_Reselect when reselecting-ID byte decodes to no valid target

Summary

amd_Reselect decodes reselecting-target ID from SCSI FIFO as ffs(xored)-1 with no check ffs() returned non-zero. If target drives reselection byte that XORs with HostID_Bit to zero ffs() returns 0 cur_target becomes -1 subsequent tinfo[] untagged_srbs[][] disc_count[][] accesses including write index out of bounds in amd_softc. cur_target=-1 accesses disc_count[-1][lun] overlap tail of tinfo[] sync_period_reg/offset/disc_tag bytes. Multi-bit garbage byte decodes via ffs to wrong-but-in-range target ID producing confused-deputy. Attacker: malicious SCSI target device drives HostID_Bit during reselection. Impact: kernel memory corruption wild active_srb pointer panic or exploitable. [VERIFIED pass2: reproduced with offsetof-verified aliasing - cur_target -1 makes tinfo -1 span softc 556-567, untagged_srbs -1 reads and NULL-writes 8 bytes BEFORE the softc, the OOB read becomes the kernel active_srb pointer and MSG IN wrote SRB_DISCONNECT through it into a victim object, uncontained variant Fatal trap 12 supervisor write at amd_MsgInPhase0+0xae; byte 0xA8 decodes to wrong in-range target 3 binding target-3 SRBs under an attacker nexus - confused deputy; upgrade Medium to High - groomable arbitrary-write ceiling plausible peripheral to uid0, though hardware-gated AV:P with no software-only trigger; power-of-two decode validation fix-validated]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2427 Β· 25 files
FileTypeDescriptionSize
README.md β€” 3.1 KB ↓ raw
VERDICT.md β€” 8.2 KB ↓ raw
df_pre.c β€” 3.6 KB view raw
df_post.c β€” 26.3 KB view raw
gen.sh β€” 1.6 KB view raw
build.sh β€” 338 B view raw
run.sh β€” 479 B view raw
make_fixed.sh β€” 3.1 KB view raw
run_fixed.sh β€” 960 B view raw
fix.diff β€” 1009 B view raw
build.log β€” 620.2 KB ↓ download
run.log β€” 7.3 KB view raw
run_battery1_clean.log β€” 5.6 KB view raw
run_battery1_dmesg.log β€” 28.8 KB view raw
run_battery1_dmesg.iter1.log β€” 19.0 KB view raw
run_battery1.log β€” 29 B view raw
run_battery2.log β€” 23 B view raw
panic_2427.txt β€” 3.9 KB view raw
run_fixed_dmesg.log β€” 24.9 KB view raw
run_fixed_battery.log β€” 29 B view raw
fix_build.log β€” 616.1 KB ↓ download
fix_installkernel.log β€” 80.4 KB view raw
env.txt β€” 611 B view raw
manifest.json β€” 2.3 KB view raw
verdict.json β€” 6.2 KB view raw

DF-2427 PoC β€” OOB array access in amd_Reselect() on garbage reselect ID (amd.c:1829-1844)

  • File under test: sys/dev/disk/amd/amd.c (AMD53c974 / Tekram DC-390(T) SCSI HBA driver)
  • Bug: amd_Reselect() decodes the reselecting-target ID from the SCSI FIFO as ffs(raw ^ HostID_Bit) - 1 with no check that ffs() was non-zero (amd.c:1831). A reselection byte that XORs with HostID_Bit to zero yields cur_target = -1 (cur_target is a signed int, amd.h:204); the subsequent tinfo[-1], untagged_srbs[-1][lun], disc_count[-1][lun] accesses (amd.c:1833-1835, 1844-1845) read and WRITE outside the per-target arrays in struct amd_softc. A multi-bit garbage byte decodes to a WRONG but in-range target (confused deputy: the reselection nexus binds to another target's SRB).
  • Attacker: a malicious (or signal-garbled) SCSI TARGET device on the bus of this HBA drives the reselection phase. No software-only path exists; the trigger is register/FIFO-level wire input.

Harness (shared with DF-2426)

Kernel-compiled verbatim interposition: df_pre.c + <verbatim amd.c> + df_post.c compiled into X86_64_GENERIC as dev/disk/amd/ dfamd_harness.c (sha256-gated). Register access executes the real inline bus_space code against a fabricated memory-mapped AMD53c974 register block + SCSI FIFO; the scenarios enter through the REAL amd_intr() (RESELECTED interrupt) β†’ amd_Reselect().

Build / run

cd findings/poc/DF-2426 && ./build.sh     # shared baseline kernel
cd ../DF-2427 && ./run.sh                 # battery=1 contained, battery=2 fault
cd ../DF-2426 && ./make_fixed.sh          # apply both fix.diffs, rebuild
cd ../DF-2427 && ./run_fixed.sh           # all scenarios rejected, no damage

Expected on the verbatim kernel:

  • battery=1, scenario A (poison byte 0x80 = HostID_Bit, lun 0): cur_target = -1; untagged_srbs[-1][0] is softc-8 β€” the OOB READ loads a wild active_srb from the 8 bytes before the softc, the OOB WRITE NULLs those bytes; disc_count[-1][0]-- corrupts 16 bits of the tinfo tail (tinfo[6].goal); chip config registers are loaded from tinfo[-1] (OOB bytes of untagged_srbs[7][6..7]); a follow-on MSG IN writes SRB_DISCONNECT through the wild active_srb into a victim object.
  • battery=1, scenario A2 (lun 6): untagged_srbs[-1][6] aliases &cur_target/cur_lun β€” wild non-canonical pointer + both fields zeroed.
  • battery=1, scenario B (byte 0xA8, attacker at ID 5): decodes target 3; target-3's SRB is bound as active_srb (SRB/nexus confusion).
  • battery=1, scenario C (byte 0xA0): correct in-bounds binding (control).
  • battery=2: scenario A2 then lets the real MSG IN path dereference the non-canonical active_srb β†’ kernel fault captured on the serial console.

Expected on the fixed kernel (reject non-single-bit XOR remainder, reset bus): every scenario prints FIXED behavior, canary rings and tinfo tail untouched, victim object untouched, no fault.

Fix

Validate the decoded target before any array use β€” see fix.diff (reject zero/multi-bit remainder of raw ^ HostID_Bit, reset the bus).

VERDICT.md
↓ download raw

DF-2427 VERDICT β€” OOB array access in amd_Reselect on garbage reselect ID (amd.c:1829-1844)

Bottom line

REPRODUCED β€” full kernel memory-corruption chain, mechanically demonstrated through the REAL amd_intr() -> amd_Reselect() path of the byte-verbatim driver, ending in a supervisor write at an attacker-influenced wild address (Fatal trap 12) and, in the contained variant, write-what-where into a victim object.

amd_Reselect() decodes the reselecting target from the SCSI FIFO (amd.c:1829-1831):

amd->cur_target = amd_read8(amd, SCSIFIFOREG);   /* wire byte */
amd->cur_target ^= amd->HostID_Bit;
amd->cur_target = ffs(amd->cur_target) - 1;      /* no ffs()==0 check */

cur_target is a signed int (amd.h:204). A reselection byte that XORs with HostID_Bit to zero (e.g. 0x80 with AdaptSCSIID 7 β€” the malicious target asserts only the initiator's own bit) yields cur_target = -1, and every subsequent per-target array index goes out of bounds (amd.c:1833-1835, 1844-1845).

Confirmed offsets (kernel-compiled offsetof dump, x86_64)

sizeof(amd_target_info)=12; untagged_srbs@softc+56 (64 entries Γ— 8 B), tinfo@softc+568 (8 Γ— 12 B), disc_count@softc+664 (8Γ—8 Γ— 2 B):

  • tinfo[-1] spans softc+556..567 = last 4 bytes of untagged_srbs[7][6] + all 8 bytes of untagged_srbs[7][7] (CtrlR1@557, CtrlR3@558, CtrlR4@559, sync_period_reg@560, sync_offset_reg@561).
  • disc_count[-1][lun] @softc+648+2*lun β€” the tail of tinfo[]: lun=0 β†’ tinfo[6].goal{period,offset}; lun=4 β†’ tinfo[7]. {sync_period_reg,sync_offset_reg}; lun=7 β†’ tinfo[7].user. This is exactly the "overlap the tail of tinfo[] (sync_period_reg/offset/disc_tag)" aliasing claimed by the finding.
  • untagged_srbs[-1][lun] @softc+(-8+8lun): lun=0 β†’ 8 bytes BEFORE the softc object* (production: heap header / adjacent chunk preceding the newbus softc allocation); lun=1..7 β†’ the softc's own dev/tag/bsh/buffer_dmat/unit+last_phase/ cur_target+cur_lun/active_srb fields.

What was mechanically observed (run_battery1_clean.log)

Scenario A (byte 0x80, identify lun 0), through the real ISR:

  1. cur_target = -1; the driver itself logs amd0: Unexpected reselection for target -1 (its own printf of the negative ID).
  2. OOB READ (amd.c:1834): active_srb = untagged_srbs[-1][0] = the 8 bytes at softc-8 β€” the harness pre-placed a victim pointer there (production: heap data) and the driver loaded it as the active SRB.
  3. OOB WRITE (amd.c:1845): untagged_srbs[-1][0] = NULL zeroed softc-8..-1 β€” 8 bytes outside the object (canary ring smashed; bytes softc-64..-9 and the post ring untouched).
  4. OOB RMW (amd.c:1844): disc_count[-1][0]-- decremented 16 bits of tinfo[6].goal 0x0000β†’0xFFFF (in-struct tail aliasing).
  5. OOB READ β†’ chip config (amd.c:1848-1853): DESTID=0xFF (cur_target -1 truncated) and SYNCPERIOD=0x77/SYNCOFF=0x66/ C1=0xAA/C3=0x99/C4=0x88 β€” the exact pre-placed bytes of untagged_srbs[7][7]/[7][6] via tinfo[-1] β€” flowed into the chip registers.
  6. Write through the wild pointer: a follow-on MSG IN interrupt (real amd_MsgInPhase0 β†’ amdparsemsg, amd.c:1218) wrote SRB_DISCONNECT (2 bytes at +offsetof(SRBState)) through the wild active_srb into the victim object; amd.c:1219 RMW'd disc_count[-1][0] twice more (net attacker-steered +1 on tinfo[6].goal.period).

Scenario A2 (byte 0x80, lun 6): untagged_srbs[-1][6] aliases &cur_target/cur_lun β€” the OOB read packed (cur_target=-1, lun=6) into a non-canonical wild pointer 0x00000006FFFFFFFF, and the OOB write zeroed both fields. Letting the real MSG IN path then dereference it (battery mode 2):

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x70000016f     (= 0x6FFFFFFFF + 0x170 SRBState)
fault code = supervisor write data, page not present
Stopped at amd_MsgInPhase0+0xae: movw %si,0x170(%rax)

(panic_2427.txt) β€” a supervisor WRITE at wild-pointer+0x170 inside the verbatim interrupt path.

Scenario B (multi-bit byte 0xA8 = initiator|bit5|bit3, device physically at ID 5): ffs() decodes target 3 β€” the reselection nexus (active_srb, disc_count, sync registers) bound to target 3's SRB. Confused-deputy: an attacker at bus ID 5 can redirect its reselection onto ANY other target's queued untagged SRB (data diversion across nexuses; the abort path then also runs with the wrong tinfo).

Scenario C (valid byte 0xA0 β†’ target 5): correct in-bounds binding, both canary rings intact β€” the machinery only corrupts on garbage input.

Threat model and privesc ceiling (honest)

Trigger class: a malicious (or firmware-buggy / signal-garbled) SCSI target device on the bus of an AMD53c974 (Tekram DC-390(T)) HBA. This is a hardware/peripheral attacker (or a flaky cable) β€” there is NO software-only path: the poison byte arrives via the chip FIFO from the wire (no HBA exists in the QEMU guest; the mechanical proof uses the verbatim register path against a fabricated chip).

Escalation ceiling on hosts that do have this HBA:

  • The OOB read at softc-8 converts 8 bytes of heap metadata/adjacent memory into the kernel's active_srb pointer; the MESSAGE IN / STATUS phase handlers then perform fixed-offset 1- and 2-byte stores through it (demonstrated: SRBState write; StatusPhase0 writes TargetStatus/EndMessage from FIFO bytes), plus TAILQ list operations on the forged object (SRBdone). That is a genuine write-what(chosen SCSI bytes)-where(forged pointer) primitive with heap grooming of the softc's preceding chunk β€” plausibly convertible to arbitrary kernel write and host uid=0 by a patient peripheral attacker.
  • Even uncontrolled, the result is a guaranteed kernel fault (demonstrated Fatal trap 12) or silent heap corruption.
  • Scenario B additionally gives a malicious peripheral cross-target SRB redirection (data integrity break) on the same HBA.

Local unprivileged user: no route (needs the HBA + a hostile device on its bus). The realistic rating is kernel memory corruption driven by a malicious peripheral β€” High within the "hardware attacker present" scope, untriggerable otherwise.

A/B fix validation

fix.diff adds the missing validation after the XOR (reject any remainder that is not exactly one bit β€” killing both the -1 decode and the multi-bit wrong-target decode) and resets the bus:

  • Baseline: battery 0 checks failed with full corruption chain; battery=2 β†’ Fatal trap 12 supervisor write.
  • Fixed kernel: every scenario prints amd0: Invalid reselection ID, resetting bus, active_srb stays NULL, canary rings + tinfo tail + victim object untouched, valid reselect (C) still binds correctly, 0 checks failed, no fault, guest stays up (run_fixed_dmesg.log, run_fixed_battery.log).

Rig

Same kernel-compiled verbatim harness as DF-2426 (shared TU; amd.c sha256-gated). Register access runs the real inline bus_space code against a fabricated memory-mapped AMD53c974 register block; only the SCSI FIFO register (SCSIFIFOREG) is shimmed to pop-on-read (the real chip's FIFO semantics) so the verbatim amd_read8(amd, SCSIFIFOREG) calls at amd.c:1829/1832 return the poisoned reselection byte and identify byte. xpt_done interposed (pass-through when unarmed). The softc under test sits inside a canary-ringed wrapper (pre[64] amd post[64]) so any access outside [softc, softc+sizeof) is visible.

Artifacts

  • run_battery1_clean.log β€” contained baseline battery, 0 checks failed (layout dump, A, A2, B, C with all PASS lines).
  • panic_2427.txt β€” serial console: battery=2 with the Fatal trap 12 supervisor write at 0x70000016f, amd_MsgInPhase0+0xae.
  • run_battery1_dmesg.log (and .iter1) β€” raw dmesg captures.
  • run_fixed_dmesg.log / run_fixed_battery.log β€” fixed kernel: all scenarios rejected, 0 checks failed, no fault.
  • layout table: sysctl debug.df2427.layout output is the first block of run_battery1_clean.log.

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

Confirmed kernel references

Detail

Exploit chain

malicious SCSI target drives reselection with ID byte 0x80 (initiator bit only) -> amd_intr RESELECTED -> amd_Reselect: xor 0x80^0x80=0 -> ffs(0)-1 -> cur_target=-1 -> active_srb = *(softc-8) (OOB read of heap bytes preceding the softc; groomable) -> disc_count[-1][0]-- corrupts tinfo[6].goal; untagged_srbs[-1][0]=NULL zeroes softc-8..-1; chip regs loaded from tinfo[-1] -> target then drives MSG IN (MSG_DISCONNECT): amdparsemsg writes SRB_STATE at forged_ptr+0x170 (2-byte store of a chosen constant; StatusPhase0 similarly stores FIFO-chosen bytes at fixed offsets) -> repeated arbitrary-offset corruption or, ungroomed, Fatal trap 12 supervisor write (captured). Multi-bit byte 0xA8 (attacker at ID 5) instead decodes target 3 and steals target 3 SRB nexus (cross-target data diversion).

Evidence (decisive lines)

run_battery1_clean.log: layout table (offsetof-verified alias map) + scenario A/A2/B/C with 0 checks failed - PASS lines for the OOB read at softc-8, the 8-byte NULL write at softc-8, the tinfo[6].goal 0x0000->0xFFFF decrement, chip regs DESTID=ff SYNCPERIOD=77 SYNCOFF=66 C1=aa C3=99 C4=88 loaded from tinfo[-1], and the SRB_DISCONNECT write through the wild active_srb into the victim. panic_2427.txt: Fatal trap 12 page fault, fault virtual address 0x70000016f, supervisor write data, Stopped at amd_MsgInPhase0+0xae: movw %si,0x170(%rax). run_fixed_dmesg.log: fixed kernel prints amd0: Invalid reselection ID, resetting bus for every poison byte, canary rings and tinfo tail untouched, valid reselect still binds, 0 checks failed, no fault.

PoC changes

No runnable sketch existed; built the kernel-compiled verbatim rig for amd.c (shared with DF-2426). Key rig elements: bus_space MEM tag + fabricated register block so the verbatim amd_read8/amd_write8 inline bus_space code runs unmodified; only the SCSI FIFO register is shimmed to pop-on-read (real chip FIFO semantics) so the two consecutive SCSIFIFOREG reads at amd.c:1829/1832 return the poisoned reselect byte then the identify byte; canary-ringed softc wrapper (pre[64]/amd/post[64]) to expose any access outside the object; entry through the real amd_intr dispatcher. Iterations: fixed an off-by-one in my tinfo[-1] field-offset check constants, re-set last_phase before the second follow-on MSG IN, and scoped the scenario-A fixed-branch canary check to pre[0..55] (the harness itself pre-loads the victim pointer at pre[56..63]).

Verified recommended fix

Validate the reselect ID decode in amd_Reselect before any array use: after amd->cur_target ^= amd->HostID_Bit, reject cur_target==0 or multi-bit remainders (reset the bus and return) before ffs()-1 indexes tinfo/untagged_srbs/disc_count.

Verdict

Fully reproduced on the verbatim driver through the real amd_intr() -> amd_Reselect() interrupt path. A reselection byte XORing with HostID_Bit to zero yields cur_target = -1 (signed int, amd.h:204) and every per-target array index goes out of bounds exactly as claimed: tinfo[-1] spans softc+556..567 (tail of untagged_srbs[7][6..7]), disc_count[-1][lun] lands in the tinfo[] tail (tinfo[6].goal .. tinfo[7].user depending on lun, including sync_period_reg/sync_offset_reg at lun=4), and untagged_srbs[-1][0] is 8 bytes BEFORE the softc object. Demonstrated primitives: OOB read of softc-8 loaded as the kernel active_srb pointer (wild pointer under device influence), OOB 8-byte NULL write at softc-8, OOB 16-bit decrement in the tinfo tail, OOB tinfo[-1] bytes programmed into chip config registers, a 2-byte SRB_DISCONNECT write through the wild active_srb into a victim object (write-at-address-taken-from-OOB-memory), field-alias variant zeroing cur_target/cur_lun with a non-canonical 0x00000006FFFFFFFF active_srb, and a final Fatal trap 12 supervisor write at 0x70000016f = wild+offsetof(SRBState) inside amd_MsgInPhase0. The multi-bit variant (byte 0xA8 from a device at ID 5) decodes to target 3 and binds the reselection nexus to target 3 SRB (cross-target confused deputy). Attacker class is a malicious or signal-garbled SCSI peripheral on an AMD53c974 bus - hardware attacker; no software-only or unprivileged-local route exists (guest has no such HBA). On hosts with the HBA the demonstrated chain (groomable 8 bytes before the softc -> forged active_srb -> fixed-offset stores of FIFO-chosen bytes through MSG IN/STATUS handlers, plus TAILQ ops in SRBdone) is a plausible path to arbitrary kernel write and host uid=0, but a local unpriv user cannot trigger it; realistic ceiling: peripheral-driven kernel memory corruption / cross-target data diversion / guaranteed panic.