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)
PoC verification
Evidence pack
findings/poc/DF-2427 Β· 25 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 asffs(raw ^ HostID_Bit) - 1with no check thatffs()was non-zero (amd.c:1831). A reselection byte that XORs with HostID_Bit to zero yieldscur_target = -1(cur_target is a signed int, amd.h:204); the subsequenttinfo[-1],untagged_srbs[-1][lun],disc_count[-1][lun]accesses (amd.c:1833-1835, 1844-1845) read and WRITE outside the per-target arrays instruct 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 wildactive_srbfrom 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 fromtinfo[-1](OOB bytes of untagged_srbs[7][6..7]); a follow-on MSG IN writesSRB_DISCONNECTthrough the wildactive_srbinto 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).
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 ofuntagged_srbs[7][6]+ all 8 bytes ofuntagged_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:
cur_target = -1; the driver itself logsamd0: Unexpected reselection for target -1(its own printf of the negative ID).- 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. - OOB WRITE (amd.c:1845):
untagged_srbs[-1][0] = NULLzeroed softc-8..-1 β 8 bytes outside the object (canary ring smashed; bytes softc-64..-9 and the post ring untouched). - OOB RMW (amd.c:1844):
disc_count[-1][0]--decremented 16 bits of tinfo[6].goal 0x0000β0xFFFF (in-struct tail aliasing). - 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.
- 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_srbpointer; 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.layoutoutput 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_testableConfirmed 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.
No comments yet.