Skip to content

fix(hdd): fork-vendored atad with a retryable probe -- the all-session APA death (HW-confirmed) - #250

Merged
NathanNeurotic merged 2 commits into
masterfrom
claude/atad-unlatch
Jul 21, 2026
Merged

fix(hdd): fork-vendored atad with a retryable probe -- the all-session APA death (HW-confirmed)#250
NathanNeurotic merged 2 commits into
masterfrom
claude/atad-unlatch

Conversation

@NathanNeurotic

@NathanNeurotic NathanNeurotic commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Your retest confirmed #249's documented residual exactly: the 40x popup is the retry loop running into the SDK atad's permanently latched failed probe — unreachable from EE code. So the driver is now fork-vendored (modules/hdd/atad, ps2sdk pin 426de26cc, built in-tree like xhdd → identical across all three flavours) with four /* FORK */ changes: latch-on-success-only, IDENTIFY-race returns an error instead of latching a dead devinfo, idempotent BDM connect across re-probes, and the .path assignment dropped for PS2MAXSDK header compat (ABI-safe, doctrine-aligned). Full provenance + re-vendor procedure in ORIGIN.txt; bonus: two upstream probe fixes both pinned containers lack.

Expected on your rig: same boot, the retry loop now heals within a few passes once the drive spins up — APA populates. If both HDD pages come alive, the "blocking each other" was this one corpse wearing two hats.

Builds clean (vendored ata_bd.irx 11,933 B). The BDMA-embed PR (your POPSLoader directive) is next.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved HDD detection during startup, including drives that are still spinning up.
    • Added retry handling for temporary ATA probe failures and improved error handling to avoid stale device info.
    • Improved reliability of reads, writes, DMA transfers, cache/idle operations, and device shutdown.
  • New Features

    • Added a dedicated ATAD HDD driver with 28-bit and 48-bit LBA support.
    • Added SMART, ATA security, flush-cache, idle, and block-device operations.
    • Enhanced compatibility across DEV9, AIFDEV9, and BDM configurations.
  • Documentation / Build

    • Documented the vendored ATAD driver source and build provenance.

…n "40x: HardDisk Drive not detected" APA death (HW-confirmed residual)

Maintainer HW retest on 8ac163f: APA still empty both views, with the #249 retry loop toasting
"40x: HardDisk Drive not detected" -- the diagnostics working as designed and pointing at the ONE
place no EE code can reach: the SDK ata_bd driver latches ata_devinfo_init=1 BEFORE probing, and
_start() probes at module load. A drive still spinning up at that instant stays "dead" for the whole
session; every later sceAtaInit (the EE retry, xhdd's re-probe devctl) is a no-op returning the
corpse. This was #249's documented residual; the retest confirmed it, so the driver fix lands.

VENDORED: modules/hdd/atad = ps2dev/ps2sdk iop/dev9/atad at pin 426de26cc (2026-07-20), built with
the ata_bd flag set, in-tree like xhdd so ALL THREE flavours ship the identical fixed driver. Four
local changes, all /* FORK */-marked (full rationale + re-vendor procedure in ORIGIN.txt):
1. sceAtaInit latches init-done only on SUCCESS -> failed probes become retryable.
2. ata_init_devices returns ATA_RES_ERR_NODEV when device 0 answered the probe but failed IDENTIFY
   (the spin-up race) -- stock returned 0 and latched a dead devinfo.
3. g_ata_bd_connected[] guard: bdm_connect_bd at most once per unit across re-probes (no double
   massN: after a heal).
4. The g_ata_bd[i].path assignment dropped: the field is absent from the PS2MAXSDK container's
   bdm.h (appended 2026-02-26, ABI-safe to omit; bdmfs registers "mass" separately and the fork
   forces massN: -- doctrine).

Bonus picked up from the pin: upstream probe fixes ab3c5a883 + 221f6e27a, missing from BOTH pinned
containers. HDPro's hdpro_atad stays the SDK prebuilt (own latch; vendor only if an HDPro rig ever
reports this). Root Makefile now builds + embeds ours (generated name stays ps2atad.c -> symbol
ps2atad_irx -> zero EE-side changes). Known trade-off: a genuinely absent drive pays a real re-probe
per EE retry instead of a latched fast NULL -- the retry loop is bounded by design.

EXPECTED ON HW: the existing 40x retry loop now HEALS within a few passes once the drive is up --
same rig, same boot, APA populates. If BOTH HDD pages come up after this, "APA and ATA blocking each
other" was this same latched corpse wearing two hats.

Builds clean (vendored ata_bd.irx 11933 B; make clean + make opl.elf, exit 0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A vendored ATAD IOP module was added, including build rules, IRX exports/imports, ATA command execution, controller support, device discovery, sector transfers, security operations, shutdown handling, and BDM integration. The top-level build now produces and consumes this local IRX.

Changes

ATAD driver

Layer / File(s) Summary
Vendored module contract and build integration
modules/hdd/atad/ORIGIN.txt, modules/hdd/atad/src/exports.tab, modules/hdd/atad/src/imports.lst, modules/hdd/atad/src/irx_imports.h, modules/hdd/atad/Makefile, Makefile
The vendored driver’s IRX interfaces, conditional imports, build flags, provenance, and top-level build and clean targets were added or updated.
Module startup and command execution
modules/hdd/atad/src/ps2atad.c
Module initialization, controller callbacks, event signaling, ATA command setup, PIO transfers, DMA completion, timeouts, and result handling were implemented.
Device discovery, transfers, and ATA maintenance
modules/hdd/atad/src/ps2atad.c
Device identification, transfer-mode configuration, 28-bit and 48-bit sector I/O, SMART, security, reset, cache, and idle operations were implemented.
Shutdown and BDM integration
modules/hdd/atad/src/ps2atad.c
Shutdown standby handling and BDM read, write, flush, and stop callbacks were added.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant IOPModule
  participant DEV9AIFDEV9
  participant ATADrive
  participant BDM
  IOPModule->>DEV9AIFDEV9: initialize callbacks and event signaling
  IOPModule->>ATADrive: probe and identify device
  IOPModule->>ATADrive: issue sector I/O command
  ATADrive-->>DEV9AIFDEV9: signal transfer completion
  DEV9AIFDEV9-->>IOPModule: deliver completion event
  IOPModule->>BDM: register ATA block device
  BDM->>IOPModule: request read or write
  IOPModule->>ATADrive: perform sector transfer
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: vendoring the ATAD driver with a probe fix for HDD/APA issues.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/atad-unlatch

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request integrates a fork-vendored version of the atad (ATA device driver) module into the project, modifying it to allow retryable initialization on failed probes and to prevent duplicate block device connections. The review feedback highlights a critical bug where passing NULL to ata_get_security_status in sceAtaSecurityEraseUnit causes a NULL pointer dereference and crashes the IOP. Additionally, several functions lack validation for the device parameter, which could result in out-of-bounds array accesses on atad_devinfo; adding defensive bounds checks is highly recommended to improve robustness.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1095 to +1099
static void ata_get_security_status(int device, ata_devinfo_t *devinfo, u16 *param)
{
if (ata_device_identify(device, param) == 0)
devinfo[device].security_status = param[ATA_ID_SECURITY_STATUS];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Passing NULL as the param argument to ata_get_security_status (as done in sceAtaSecurityEraseUnit at line 1167) will cause a critical NULL pointer dereference. This happens because ata_device_identify will pass NULL down to sceAtaExecCmd and ata_pio_transfer, which attempts to write the PIO data directly to the NULL buffer, crashing the IOP. Additionally, param[ATA_ID_SECURITY_STATUS] will also dereference NULL.

To prevent this, fallback to the static ata_param buffer if param is NULL.

Suggested change
static void ata_get_security_status(int device, ata_devinfo_t *devinfo, u16 *param)
{
if (ata_device_identify(device, param) == 0)
devinfo[device].security_status = param[ATA_ID_SECURITY_STATUS];
}
static void ata_get_security_status(int device, ata_devinfo_t *devinfo, u16 *param)
{
u16 *buf = param ? param : ata_param;
if (ata_device_identify(device, buf) == 0)
devinfo[device].security_status = buf[ATA_ID_SECURITY_STATUS];
}

Comment on lines +1361 to +1363
ata_devinfo_t *sceAtaInit(int device)
{
if (!ata_devinfo_init) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The device parameter is not validated. If an invalid device index (e.g., negative or >= 2) is passed to sceAtaInit, it will result in an out-of-bounds array access on atad_devinfo when returning &atad_devinfo[device]. Adding a bounds check ensures defensive programming and robustness against invalid inputs.

ata_devinfo_t *sceAtaInit(int device)
{
    if (device < 0 || device >= 2)
        return NULL;

    if (!ata_devinfo_init) {

Comment on lines +1024 to +1030
int ata_device_sector_io64(int device, void *buf, u64 lba, u32 nsectors, int dir)
{
USE_SPD_REGS;
int res = 0, retries;
u16 sector, lcyl, hcyl, select, command, len;

while (res == 0 && nsectors > 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The device parameter is not validated before being used as an index for atad_devinfo. If an invalid device index is passed, it will cause an out-of-bounds array access. Add a defensive bounds check at the beginning of the function.

int ata_device_sector_io64(int device, void *buf, u64 lba, u32 nsectors, int dir)
{
    USE_SPD_REGS;
    int res = 0, retries;
    u16 sector, lcyl, hcyl, select, command, len;

    if (device < 0 || device >= 2)
        return ATA_RES_ERR_NODEV;

    while (res == 0 && nsectors > 0) {

Comment on lines +884 to +888
int sceAtaFlushCache(int device)
{
int res;

if (!(res = sceAtaExecCmd(NULL, 1, 0, 0, 0, 0, 0, (device << 4) & 0xffff, (atad_devinfo[device].lba48 && !ata_dvrp_workaround) ? ATA_C_FLUSH_CACHE_EXT : ATA_C_FLUSH_CACHE)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The device parameter is used directly as an index for atad_devinfo without validation, which can lead to out-of-bounds memory access if an invalid index is passed. Add a defensive bounds check.

int sceAtaFlushCache(int device)
{
    int res;

    if (device < 0 || device >= 2)
        return ATA_RES_ERR_NODEV;

    if (!(res = sceAtaExecCmd(NULL, 1, 0, 0, 0, 0, 0, (device << 4) & 0xffff, (atad_devinfo[device].lba48 && !ata_dvrp_workaround) ? ATA_C_FLUSH_CACHE_EXT : ATA_C_FLUSH_CACHE)))

Comment on lines +1102 to +1108
int sceAtaSecuritySetPassword(int device, void *password)
{
ata_devinfo_t *devinfo = atad_devinfo;
u16 *param = ata_param;
int res;

if (devinfo[device].security_status & ATA_F_SEC_ENABLED)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The device parameter is used directly as an index for devinfo without validation, which can lead to out-of-bounds memory access if an invalid index is passed. Add a defensive bounds check.

Suggested change
int sceAtaSecuritySetPassword(int device, void *password)
{
ata_devinfo_t *devinfo = atad_devinfo;
u16 *param = ata_param;
int res;
if (devinfo[device].security_status & ATA_F_SEC_ENABLED)
int sceAtaSecuritySetPassword(int device, void *password)
{
ata_devinfo_t *devinfo = atad_devinfo;
u16 *param = ata_param;
int res;
if (device < 0 || device >= 2)
return ATA_RES_ERR_NODEV;
if (devinfo[device].security_status & ATA_F_SEC_ENABLED)

Comment on lines +1123 to +1129
int sceAtaSecurityUnLock(int device, void *password)
{
ata_devinfo_t *devinfo = atad_devinfo;
u16 *param = ata_param;
int res;

if (!(devinfo[device].security_status & ATA_F_SEC_LOCKED))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The device parameter is used directly as an index for devinfo without validation, which can lead to out-of-bounds memory access if an invalid index is passed. Add a defensive bounds check.

Suggested change
int sceAtaSecurityUnLock(int device, void *password)
{
ata_devinfo_t *devinfo = atad_devinfo;
u16 *param = ata_param;
int res;
if (!(devinfo[device].security_status & ATA_F_SEC_LOCKED))
int sceAtaSecurityUnLock(int device, void *password)
{
ata_devinfo_t *devinfo = atad_devinfo;
u16 *param = ata_param;
int res;
if (device < 0 || device >= 2)
return ATA_RES_ERR_NODEV;
if (!(devinfo[device].security_status & ATA_F_SEC_LOCKED))

Comment on lines +1150 to +1155
{
ata_devinfo_t *devinfo = atad_devinfo;
int res;

if (!(devinfo[device].security_status & ATA_F_SEC_ENABLED) || !(devinfo[device].security_status & ATA_F_SEC_LOCKED))
return 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The device parameter is used directly as an index for devinfo without validation, which can lead to out-of-bounds memory access if an invalid index is passed. Add a defensive bounds check.

int sceAtaSecurityEraseUnit(int device)
{
    ata_devinfo_t *devinfo = atad_devinfo;
    int res;

    if (device < 0 || device >= 2)
        return ATA_RES_ERR_NODEV;

    if (!(devinfo[device].security_status & ATA_F_SEC_ENABLED) || !(devinfo[device].security_status & ATA_F_SEC_LOCKED))

…pstream style flattened -- re-vendor procedure in ORIGIN.txt now implies re-formatting too)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/hdd/atad/src/ps2atad.c (1)

1024-1093: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Keep the host-side transfer count wide

sceAtaDmaTransfer() takes a 32-bit sector count, and xhdd forwards buflen / 512 directly, so a 32 MiB LBA48 read can hit this path. With len as u16, 65536 wraps to 0, nsectors never decreases, and the outer loop spins forever.

Proposed fix
-    int res = 0, retries;
-    u16 sector, lcyl, hcyl, select, command, len;
+    int res = 0, retries;
+    u16 sector, lcyl, hcyl, select, command;
+    u32 len;
@@
-            len = (u16)((nsectors > 65536) ? 65536 : nsectors); /* 0 means 65536 in LBA48 */
+            len = (nsectors > 65536) ? 65536 : nsectors; /* 0 means 65536 in LBA48 */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@modules/hdd/atad/src/ps2atad.c` around lines 1024 - 1093, Use a wide
host-side type for the transfer count in ata_device_sector_io64, including len
and the nsectors-to-len assignments, so a 65536-sector LBA48 chunk remains 65536
instead of wrapping to zero. Preserve the ATA register field types and ensure
pointer, LBA, and nsectors advancement use the full chunk count so the outer
loop terminates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@modules/hdd/atad/src/ps2atad.c`:
- Around line 1024-1093: Use a wide host-side type for the transfer count in
ata_device_sector_io64, including len and the nsectors-to-len assignments, so a
65536-sector LBA48 chunk remains 65536 instead of wrapping to zero. Preserve the
ATA register field types and ensure pointer, LBA, and nsectors advancement use
the full chunk count so the outer loop terminates.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: aa223e2f-74cc-4850-b388-8fac4b93c954

📥 Commits

Reviewing files that changed from the base of the PR and between e0d8072 and 7aedee7.

📒 Files selected for processing (1)
  • modules/hdd/atad/src/ps2atad.c
📜 Review details
⏰ Context from checks skipped due to timeout. (11)
  • GitHub Check: build-debug-ps2dev-latest (iopcore_debug)
  • GitHub Check: build-debug (ingame_ppctty_debug, :v20250725-2)
  • GitHub Check: build-debug (DTL_T10000=1, :v20250725-2)
  • GitHub Check: build-debug (ingame_debug, :v20250725-2)
  • GitHub Check: build-lang
  • GitHub Check: build-debug-ps2dev-latest (DTL_T10000=1)
  • GitHub Check: build-debug (iopcore_debug, :v20250725-2)
  • GitHub Check: build-debug (ingame_ppctty_debug, :v20250725-2)
  • GitHub Check: build-debug (eesio_debug, :v20250725-2)
  • GitHub Check: build-debug (DTL_T10000=1, :v20250725-2)
  • GitHub Check: build-debug-ps2dev-latest (eesio_debug)
🔇 Additional comments (7)
modules/hdd/atad/src/ps2atad.c (7)

357-369: LGTM!


520-651: LGTM!

Also applies to: 654-703, 707-757


1102-1169: LGTM!


1181-1211: LGTM!

Also applies to: 1267-1283, 1355-1357


1378-1492: LGTM!


1355-1358: 🩺 Stability & Availability

sceAtaInit() already latches only after successful probing. It returns NULL until ata_bus_reset() and ata_init_devices() both succeed, then sets ata_devinfo_init = 1.

			> Likely an incorrect or invalid review comment.

1347-1353: 🩺 Stability & Availability

No issue: bdm_connect_bd() is void. There’s no success return to gate g_ata_bd_connected[i] on, and this call pattern matches the other BDM users.

			> Likely an incorrect or invalid review comment.

NathanNeurotic added a commit that referenced this pull request Jul 21, 2026
- vcdWriteBufFile deleted: the EXISTING vcdSafeWriteFile (free-space check + partial-write cleanup)
  stages the embedded pair (Gemini -- real, the helper was redundant and weaker).
- Staging failures propagate their REAL class: -2 card-full / -3 IO keep their specific toasts;
  only no-pair/unpack-failure maps to -4 (Gemini -- real, a full card mis-toasted as 'source absent').
- The -4 contract comment in vcdsupport.h updated for the embedded-fallback era (CodeRabbit -- real).

#250's Gemini findings (NULL param + device bounds in the vendored atad) are DECLINED under the
vendoring doctrine: all target upstream-verbatim code, byte-faithful to the pin except the four
documented FORK hunks; no OPL caller passes invalid units, and drive-by hardening of vendored source
makes every re-vendor a conflict festival. Recorded in ORIGIN.txt's spirit -- upstream fixes belong
upstream.

Builds clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NathanNeurotic

Copy link
Copy Markdown
Owner Author

Gemini's findings (NULL param via sceAtaSecurityEraseUnit, unvalidated device indices ×5) — declined under the vendoring doctrine: every one targets upstream-verbatim ps2sdk code, unchanged from the pin. This module stays byte-faithful to 426de26cc except the four documented /* FORK */ hunks (ORIGIN.txt) — no OPL caller invokes the security ops or passes units outside 0/1, and drive-by hardening of vendored source turns every re-vendor into a conflict festival. Anything genuinely worth fixing there belongs upstream in ps2dev/ps2sdk.

@NathanNeurotic
NathanNeurotic merged commit cebffad into master Jul 21, 2026
63 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant