△◯✕□ RiptOPL DOCS
Downloads GitHub
This is the legacy version of RiptOPL. We have moved over to ps2build's C++ Edition at NathanNeurotic/RiptOPL, where new development now happens.

Notes for ps2sdk

RiptOPL builds or patches several of ps2sdk's IOP modules instead of embedding the SDK's prebuilt copies. This page records the ps2sdk defects behind those changes, and the ones we found and left alone, so that ps2sdk maintainers and anyone else using ps2sdk can act on them. None of it has been filed with ps2sdk. Use any of it freely.

Every "still in master" below was checked on 2026-10-27 against ps2sdk master 329e2bb (2026-10-12), which was still master that day.

Important
Nothing here is tested on a console unless its evidence line says hardware.

Each entry gives its evidence:

Summary

Module ps2sdk source Our copy Still in master Fixed in master since our copy Our own choices
smbman iop/network/smbman modules/network/smbman-ra 4 0 1
usbd (usbd_mini) iop/usb/usbd modules/usb/usbd-ra 2 0 0
ps2ips iop/tcpip/tcpips modules/network/ps2ips 5 0 0
atad (ata_bd) iop/dev9/atad modules/hdd/atad 3 1 1
APA (ps2hdd-osd) iop/hdd/apa, iop/hdd/libapa modules/hdd/apa 7 0 4
BDM iop/fs/bdm, iop/fs/libbdm modules/bdm 9 2 1
bdmfs_fatfs iop/fs/bdmfs_fatfs patched in CI 4 0 1
IEEE1394_bd iop/iLink/IEEE1394_bd modules/ilink/IEEE1394_bd 10 0 1
iLinkman iop/iLink/iLinkman modules/ilink/iLinkman 11 0 0

ps2ips and IEEE1394_bd each had one defect of our own, both fixed on 2026-10-27 and listed with the module. Defects we saw in modules we did not change are under other ps2sdk modules.

The smbman write-count and write ByteCount fixes are ready as ps2sdk patches: they apply in that order to master 329e2bb with git am. For the rest, our copies are the reference. smbman-ra, usbd-ra, ps2ips and iLinkman diff cleanly against ps2sdk. APA, BDM and IEEE1394_bd were reformatted with clang-format in a baseline commit that each ORIGIN.txt describes, so diff them against that commit. atad was added already reformatted and changed (bcac683b), so diff it against its pin with whitespace ignored (diff -w), or follow its /* FORK */ markers.

smbman

Our copy is ps2sdk's iop/network/smbman with four changes. ps2sdk has not changed those sources since d44e0128 (2024-12-12). Every flavour embeds our copy; before 2026-10-27 only the RetroAchievements flavour did. Only the menu loads it; in-game SMB access uses OPL's own driver in modules/iopcore/cdvdman.

A short write is reported as complete

Still in master.

A 65535-byte write request says it carries no data

Still in master.

A file another session still holds cannot be opened, even to read

Still in master.

Our choice: a held file opens read-only and reports -EBUSY

Not a ps2sdk defect. An open that writes, creates or truncates does not retry, and a refused open returns -EBUSY instead of -EIO, so the menu can say that the file is held. A distinct errno for a sharing violation could go to ps2sdk on its own; the read-only limit is OPL's choice.

A password longer than 256 characters overruns a 512-byte buffer

Still in master.

Seen in smbman, not changed here

usbd (usbd_mini)

Our copy is ps2sdk's iop/usb/usbd at 314d87e7 (2024-04-01), built as usbd_mini with byte-identical sources. That is FreeUsbd 0.1.2, the driver before the rewrite b1f7ff96 ("USBD feature update", authored 2024-09-04, merged on 2024-09-24). The rewrite's real parent is 2dc6b32f; our copy lacks three small header changes made in between. Every flavour embeds it, and the game's IOP loads it again after the game's IOP reboot when the game runs from USB or PADEMU is on.

Our copy exports usbd version 1.1, and current SDK clients import 1.2. ps2sdk's loadcore matches on the major version only, so they link. Whether the console ROM's loadcore does the same was not checked; a hardware no-regression run (below) found no problem.

A game run from USB loses its controller and sound at its first IOP reboot

Still in master: master's usbd is still the rewrite.

The endpoint number is masked with 0x1F

Still in master. Latent.

iop/usb/usbd/src/endpoint.c, openDeviceEndpoint(), builds the OHCI endpoint descriptor with (bEndpointAddress & 0x1F) << 7. The endpoint-number field is 4 bits wide, so bit 4 spills into the direction field; FreeUsbd masks with 0xF. USB reserves that bit, so a compliant device never sets it. This is horizonends's finding, posted on #908 on 2026-08-01. (Source.)

ps2ips

Our copy is ps2sdk's iop/tcpip/tcpips/src/ps2ips.c with five fixes; master has not changed the file since 8451bd58 (2026-07-09). Four fixes are marked "RA fix" in the source; the DNS fix is unmarked. Every flavour embeds it, but only the RetroAchievements flavour's own code calls the receive and DNS paths. The EE side stays ps2sdk's stock libps2ips, which a loader cannot change.

Every recv() that returns data writes 16 bytes past an EE buffer

Still in master.

UDP recvfrom() never fills the caller's buffer

Still in master.

recvfrom() can return a zeroed or stale sender address

Still in master.

do_recvfrom() passes fromlen to lwIP uninitialised. lwIP 2.2.1 copies only *fromlen bytes of the address when that is under 16, and none when it is 0. Our fix: int fromlen = sizeof(struct sockaddr);. (Source.)

Sends and receives near 1 KB overwrite the RPC request being handled

Still in master.

gethostbyname() returns an IOP pointer instead of the address

Still in master.

do_gethostbyname() copies &ret->h_addr_list[0], the pointer's own bytes, instead of ret->h_addr_list[0], the address it points to. Every name resolves to an IOP memory address. This has been so since e9c45741 (2015). Host test test_ps2ips_dns.py fails on master's code and passes on ours.

Our defect, fixed: a receive with a long unaligned tail lost data

This was a defect in our copy, not in ps2sdk. Because of the 128-byte workaround above, when the unaligned tail of a receive was longer than 48 bytes, the IRX lowered the returned length after lwIP had already consumed the bytes, so a TCP stream silently lost up to 15 bytes. It affected the RetroAchievements flavour's TCP readers in src/ranet.c and src/ratls_ps2.c. Since 2026-10-27 our copy peeks with MSG_PEEK and then receives only the bytes whose tail fits (rests_fit()). lwIP 2.2's TCP peek copies from the first queued buffer and leaves it queued, so the second receive returns exactly what it asks for and the rest waits for the next call. A receive therefore returns at most that first buffer, usually one TCP segment, which recv() allows. A UDP datagram with such a tail still arrives up to 15 bytes short, as if the caller's buffer were smaller, because a datagram cannot be continued. A peek that returns 0 is followed by a non-blocking 0-byte receive, so an empty UDP datagram is consumed instead of being returned forever (found by review the same day, before release). Host test (test_ps2ips_recv_tail.py: 8 failures on the old code); lwIP's peek is modelled from its 2.2.1 source, not run.

Seen in ps2ips, not changed here

(Both source.)

atad (ata_bd)

Our copy is ps2sdk's iop/dev9/atad at 426de26c (2026-07-20), built as ata_bd. Our changes are marked /* FORK */ and listed in its ORIGIN.txt. The menu loads it in every flavour; in-game HDD access uses OPL's own ATA code. Since our copy, master has also gained DVRP workaround changes and vendor-specific ATA commands (79bb3112, 77abbe2e, 873208c9, c9542e61); those are not defects in our copy.

A drive that is not ready at the first probe stays undetected

Still in master.

On a drive without LBA48, a request past 2^28 sectors goes to the start of the disk

Still in master.

A 4Kn drive is driven as if it had 512-byte sectors

Still in master.

atad never reads the drive's logical sector size (IDENTIFY words 106 and 117–118). It reports 512 to BDM and sizes every DMA at 512 bytes per sector, so on a 4Kn drive every LBA lands at eight times the intended offset. What a real 4Kn drive then does was not measured. Our copy reads the size and refuses any drive whose sectors are not 512 bytes; it does not add 4Kn support. test_apa_table_safety.py checks the parser. No 4Kn drive has been tested.

Fixed in master: an LBA48 transfer of 65536 sectors loops forever

f30f05eb (LAP87, PR #913, merged 2026-08-27) fixed it. Our copy carries the same change as a backport.

Our choice: BDM never writes an APA disk's first 128 MiB

ata_bd_write() refuses a write below LBA 0x40000 when LBA 0 carries the APA magic or cannot be read. A loader that mounts FAT/exFAT and APA on the same disk wants this guard; it is not a ps2sdk defect.

APA (ps2hdd-osd)

Our copy is ps2sdk's iop/hdd/apa and iop/hdd/libapa at 3e46a44a (2026-09-03); master has not changed those paths since. It is built as ps2hdd-osd and loaded by the menu. APA safety explains why the fork limits writes. PFS stays ps2sdk's prebuilt.

apaSetPartErrorSector writes through NULL when the cache is exhausted

Still in master.

A failed error-record read leaks a cache buffer

Still in master.

apaGetPartErrorSector() returns -EIO without apaCacheFree(clink) when its read fails. After three failures, with the default cache, every allocation fails until the module is reloaded. Our copy frees the buffer. (Source; no test.)

O_CREAT with no size divides by zero

Still in master.

Opening hdd0:name with O_CREAT and no size field leaves the size at 0. When no free block exists, hddAddPartitionHere() divides by it (part_end % params->size). When one exists, apaInsertPartition() halves that block down to zero length, writing each split (source reading, not run). Our copy refuses every create, so it cannot reach this and has no direct fix. ps2sdk could reject a size of 0 for O_CREAT with -EINVAL. (Source.)

HIOCTRANSFER's bounds check wraps

Still in master.

hdd_fio.c, ioctl2Transfer(), checks length < sector + size, which wraps at 2^32. A huge sector passes, and the transfer, writes included, can land anywhere on the disk. Our check: sector > length || size > length - sector. (Source; test_apa_table_safety.py checks the text.)

A header claiming more than 64 sub-partitions indexes past parts[]

Still in master.

apaOpen() copies the on-disk nsub without checking it against APA_MAXSUB (64). ioctl2Transfer() then accepts sub numbers up to that count and reads a start and length from beyond the 65-entry parts[] array, so the I/O can go to an arbitrary LBA. Our copy refuses nsub > 64 in apaOpen(). Still unbounded in both: apaRemove()'s sub loop (our copy cannot reach it) and the caller's index in HIOCGETSIZE and HIOCGETPARTSTART. (Source; checked as text.)

Listing a partition with a huge sub-partition count can hang

Still in master, since 15efee2a (2025-07-09).

fioGetStatFiller() sums subs[i].length for every i below the on-disk 32-bit nsub. It reads past the header, and a large value loops up to about 2^32 times during a directory listing. Nothing is written. Our copy also stops at APA_MAXSUB. (Source; checked as text.)

A signed 1 << 31

Still in master. Latent.

hdd.c, hddAddPartitionHere(), computes (u32)(1 << i) with i up to 31, which is undefined behaviour in C; GCC produces the expected value. Our copy uses 1U << i. (Source.)

Our choices in the APA driver

None of these is a ps2sdk defect; a game loader simply does not need these writes.

test_apa_table_safety.py covers them on the host. None has been tested on hardware, including power-cut tests.

Seen in APA, not changed here

Since f978fb75 (2025-01-26), hdd.c runs apaJournalRestore() only when a drive's status is not 1. At that point the status is 1 for every present, unlocked drive, because the format check comes later. So ps2hdd built for ATA never replays the APA journal at boot. Our copy has the same line. (Source reading, not run.)

BDM

Our copy is ps2sdk's iop/fs/bdm and iop/fs/libbdm at 3e46a44a (2026-09-03). The menu loads it in every flavour. In-game, OPL's cdvdman still links ps2sdk's libbdm, but uses only its unchanged defragmentation code. Since our copy, master has fixed two of these defects independently.

Fixed in master: a failed cache refill returns another block's data

bd_cache.c, _read(), ignored the result of its 8-sector refill and returned the evicted block's bytes as the requested sectors, then kept them cached. A small read that misses the cache in a device's last 7 sectors always took that path. Master fixed it in e18d7cc1 (PR #935, 2026-10-06); ours dates from 2026-09-14. Master caches a partial block and turns a short read into -1; ours reads straight into the caller's buffer and returns the driver's result. test_bdm_cache.py fails on our pin's bd_cache.c and passes on master's.

Fixed in master: GPT partitions after an unused entry are invisible

The GPT scan stopped at the first entry with an all-zero type GUID, which only marks that entry unused. Master fixed it in 65f988c4 (PR #933, 2026-10-05); ours dates from 2026-09-15. Master's fix leaves the scan unbounded (next entry).

Master's GPT scan has no bound

Still in master, introduced by 65f988c4.

GPT partitions are one sector short

Still in master.

part_driver_gpt.c sets sectorCount = last_lba - first_lba, but last_lba is inclusive, as ps2sdk's own gpt_types.h says. Our fix adds 1. It matters once partition I/O is bounded (next entry). (test_bdm_partitions.py.)

Partition reads and writes are not bounded by the partition

Still in master.

part_driver.c, part_read() and part_write(), add the partition offset and pass the I/O on without checking it against the partition's size. FatFs trusts the boot sector's size, so a filesystem that claims more sectors than its partition holds writes into the next partition. Our copy refuses I/O past the end, written to be overflow-safe; it needs the previous fix. (test_bdm_partitions.py.)

A device whose sectors are not 512 bytes overflows the block cache

Still in master.

bd_cache.c slots hold 8 × 512 bytes and every offset assumes 512, but bdm_connect_bd() creates a cache for every whole device (parNr 0), whatever its sector size. A device with 4096-byte sectors writes 32 KiB into each 4 KiB slot, past the end of the allocation once a high slot is used, and cached reads return wrong data. Our bd_cache_create() returns NULL when the sector size is not 512. (The overflow is source reading; test_bdm_partitions.py checks the refusal.)

A 4K-sector device is offered to drivers that use 512-byte buffers

Still in master.

bdm_try_mount() offers every device to every filesystem driver. The MBR probe reads one sector into a 512-byte buffer, the GPT probe reads into 512-byte halves, and ps2sdk builds FatFs with FF_MAX_SS 512. An uncached probe of a 4096-byte device overruns a 512-byte heap buffer. Our copy never offers a device whose sectors are not 512 bytes; ps2sdk might prefer real 4K support. (Source; the refusal is checked as text.)

bd_cache_create writes through NULL when IOP memory runs out

Still in master.

Neither AllocSysMemory() result in bd_cache_create() is checked, and the second asks for about 131 KB. Our copy frees what it got and returns NULL; BDM then uses the device without a cache. (test_bdm_partitions.py.)

The connect log line divides by zero

Still in master.

bdm_connect_bd() prints the size as sectorCount / ((1000 * 1000) / sectorSize), which divides by zero when the sector size is 0 or above 1,000,000. ps2sdk's usbmass_bd connects a device even when its warm-up failed, from a slot whose sector size is still 0 if it was never used, so this needs no unusual hardware. A prebuilt bdm.irx traps (break 7) on that path. Our copy prints sectorCount * sectorSize / (1000 * 1000). (Source and disassembly; neither the trap's effect nor the size-0 device has been seen on hardware.)

The GPT parser divides by zero for sectors under 128 bytes

Still in master. Latent.

entriesPerSector becomes 0, and the parser then computes i % entriesPerSector. Our copy refuses such a device. (Source.)

The MBR probe leaks its buffer when sector 0 cannot be read

Still in master.

part_connect_mbr() returns without FreeSysMemory(pMbrBlock) when the read fails. BDM retries the mount on every mount event, so the 512-byte leak repeats. Our copy frees it. (Source.)

Our choice: report refused devices

Our copy marks a refused device and fires the mount callback for it, so OPL can show code 500. That changes what the callback means, so it is not meant for ps2sdk.

Seen in BDM, not changed here

(Both source; the effects were not checked.)

bdmfs_fatfs

We do not vendor this one. CI rebuilds the SDK's own driver with install_safe_bdmfs.py before make. It replaces get_frag_list() (bdmfs-vmc-fragment-walk.c), fixes O_EXCL, and adds our VMC reservation calls. Every CI release flavour (except two Korium comparison builds) embeds the rebuilt driver. A local make without the installer embeds the stock one, and so does the Neutrino that RiptOPL bundles. A ps2sdk-style version of the walker fix, with tests, is on NathanNeurotic/ps2sdk PR #1; it has not been offered to ps2sdk.

A FAT error or a short chain is reported as one contiguous extent

Still in master, since 8b6c3100 (2022).

The fragment query hangs the IOP on an empty file, a loop or a free entry

Still in master.

The walk ends only at a cluster of at least n_fatent, and get_fat() returns 1 for any cluster below 2. So it spins forever on a 0-byte file (start cluster 0), on a free or value-1 entry inside a chain, or on a volume unmounted under an open file; a chain that loops back never ends either. It runs under the filesystem lock, so every other bdmfs_fatfs call, and disconnecting the device, block too. Our walker returns 0 for an empty file, stops after the expected number of clusters, and rejects clusters below 2. (test_bdmfs_frag_walk.py covers loops, a free entry, a value-1 entry and a 0-byte file on our walker. Its 0-byte file has start cluster 2, so it does not reproduce the stock walker's hang on start cluster 0; only a review harness does.)

The fragment list after the device is gone reads through NULL

Still in master.

After a disconnect or a STOP devctl, an open file's block device is NULL, and get_frag_list() reads bd->sectorOffset through it. It also writes through a NULL rdata when rdatalen is not 0, and truncates an extent of more than 2^32 sectors. Our walker refuses all three. After a disconnect or STOP, f_unmount() has already cleared fs_type, so the walker returns -EINVAL (0 for an empty file), and a -ENODEV check on the block device is a backstop. A NULL rdata gives -EINVAL, and an extent of more than 2^32 sectors gives -EOVERFLOW. (Source. The host test models the lost device as a NULL block device with fs_type intact and checks only for a negative result. The -EOVERFLOW path is not exercised.)

O_CREAT | O_EXCL opens or truncates an existing file

Still in master.

fs_open() maps O_CREAT to FA_OPEN_ALWAYS and never looks at O_EXCL (0x0800), which libcglue does pass down. An exclusive create of an existing file succeeds, and truncates it if O_TRUNC is also set. Our patch maps O_CREAT | O_EXCL to FA_CREATE_NEW. A refused open still returns -FR_EXIST (-8), not -EEXIST. (Source; no test runs the patched fs_open().)

Our addition: reserve a contiguous file

We build FatFs with FF_USE_EXPAND 1 and add private calls that reserve and verify a single-extent file for native VMCs. It is not a ps2sdk patch as it stands. One reason for its odd shape: fs_ioctl2() returns 0 for an unknown command, so 0 cannot tell "unsupported" from "done", and our calls return positive magic values instead.

Seen in bdmfs_fatfs, not changed here

(All source.)

Our copies of IEEE1394_bd and iLinkman come from ps2sdk master 329e2bb, which has not changed them since, so nothing below is fixed upstream. Every flavour embeds both, and we also replace the copies in the bundled Neutrino. POPStarter's i.LINK driver comes from an older copy (BDMAssault) with the same changes, plus ps2sdk's own SBP-2 direction fix (5eb6bbc6, 0123ebbc).

None of these changes is confirmed on hardware. Console reports so far are mixed. Before the changes, games listed and Ember booted, but native OPL, Neutrino and POPStarter did not. The latest report, after the first of the changes, has Ember working, native OPL intermittent, and Neutrino and POPStarter failing (i.LINK audit). Our host findings do not establish what caused the failures. Several of the scsi.c defects are also in iop/usb/usbmass_bd/src/scsi.c, and some sbp2_driver.c ones in iop/iLink/IEEE1394_disk.

"Host test" here means a test in this repository that fails on ps2sdk's code: for some entries, an earlier version of the test, because the current one needs functions only our copy has. "Source" on an entry means the ps2sdk side was only read; our side is host-tested unless stated.

IEEE1394_bd

All ten ps2sdk defects below are still in master. The entries headed "Our defect" and "Our choice" are about our copy, not ps2sdk.

After an IOP reset the drive may never attach

The driver never logs out, so a new instance after an IOP reset can find the drive still holding the old exclusive login; SBP-2 keeps it for about 1 s after the bus reset, longer on some bridges. initConfigureSBP2Device() tries LOGIN 5 times, 200 ms apart, then gives up until the next bus reset. A unit whose management timeout is 0 also times out after one 50 µs poll. Our copy retries for up to 5 s and waits for management ORBs per the unit's timeout (500 ms when 0, at most 5 s). Host test (test_ilink_bd_reset.py, first version). No bridge's login hold time was measured.

TEST UNIT READY has no limit and no delay

scsi_warmup() repeats TEST UNIT READY and REQUEST SENSE forever, with no pause. A drive that never becomes ready holds the bus-reset thread, so no device is published and later bus resets are never handled. Our copy polls at most 300 times, 100 ms apart, within 30 s. Host test.

A drive that failed setup is still offered to BDM

scsi_connect() ignores scsi_warmup()'s result and connects the device anyway, with whatever sector size the slot had (0 for a fresh slot), and a READ CAPACITY block length of 0 is accepted. The slot stays taken by the half-set-up device until the next bus reset disconnects it. Our copy connects only after a successful warm-up and frees the slot otherwise. Host test (test_ilink_bd_reset.py, test_ilink_scsi_geometry.py).

Fetch-agent setup reads the previous status and frees its ORB too early

After LOGIN, the shared status FIFO still holds LOGIN's status. ieee1394_InitializeFetchAgent() waits without clearing it, returns at once, and frees the dummy ORB while the target may still fetch it. It ignores the wait's result and reports success whenever the ORB_POINTER write went through. Our copy clears the status, keeps the ORB until its own completion, and passes failures on. Host test (test_ilink_sbp2_completion.py, an earlier version: 12 failures on ps2sdk's code). IEEE1394_disk frees its dummy ORB the same way.

A command can complete on the wrong status, or wait forever

All devices share one status FIFO, and ieee1394_Sync() accepts any status without matching its ORB, so unsolicited status or another device's status completes the current command while the target may still DMA into the caller's buffer. The wait has no timeout and no bus-reset exit, and a failed ORB_POINTER write is ignored. LOGIN's ORB lives on the stack although that wait can time out. Our copy gives each device its own FIFO, matches the ORB, stops after 30 s or a bus reset, and keeps ORBs in resident memory. (Source.)

After a DEAD status the next command hangs

When a status sets the dead bit, nothing resets the fetch agent, and under SBP-2 a dead agent fetches no new ORBs. The retries then publish ORBs that are never fetched. Our copy resets the agent before the next command. (Source.)

Every failure reports sense key 1, and an extended GOOD status fails

ProcessStatus() computes (data[0] >> 16) & 0xF on a u8, which is always 0 and is then forced to 1, so the real sense key is always lost. It also requires a status length of 1, so a bridge that returns a longer status block for a successful command fails it, and after 16 retries the read or write returns -EIO. Both date from the 2018 import, and IEEE1394_disk has the same code. Our copy decodes the sense key and accepts a longer GOOD status. Our first attempt read the wrong bytes; see the next entry. (Source; our side is host-tested.)

Our defect, fixed: the status decode read the wrong bytes

Like everything the target writes, the status quadlets are stored in host order. The driver reads the status header, ORB_low and the login response without swapping and byte-swaps only data-phase payload, and ps2sdk's own wait loop, which listed games on hardware, could not end if the header were in wire order. Our first decode indexed the third status quadlet as bytes in wire order, so data[1] & 0xf was the low nibble of the ASC and the GOOD check read the ASCQ: a CHECK CONDITION whose ASCQ is 0, such as an unrecovered read error (ASC 0x11, ASCQ 0x00), passed as success. sbp2StatusReady() tested the same byte for 0xff, so a real ASCQ of 0xff looked like a status that had not arrived. Since 2026-10-27 both our copies read that quadlet as a number: format q >> 30, status (q >> 24) & 0x3F, sense key (q >> 16) & 0xF. An all-ones quadlet marks a status not yet written; a target would have to send the vendor-specific format 3 with every bit set to match it. Host test (test_ilink_sbp2_completion.py: 6 failures on each copy's old decode). No hardware dump of a CHECK CONDITION status exists, so the host-order premise rests on the source.

Disk capacity is one sector short

scsi_warmup() stores READ CAPACITY's last LBA as the sector count, so the last sector is unreachable. The READ CAPACITY(16) path has the same off-by-one, and does not check its block length or an all-ones LBA. usbmass_bd uses the same formula. Host test (test_ilink_scsi_geometry.py).

Sectors larger than 512 bytes overflow the transfer buffer

The driver always allows 127 sectors per request, whatever block size READ CAPACITY reported. With 2048- or 4096-byte sectors, one request copies up to 520192 bytes into the 65024-byte buffer, and the ORB length field is cut to 16 bits. Our copy sizes requests from the sector size and refuses oversized commands before publishing them. Host test (test_ilink_scsi_geometry.py, test_ilink_command_orb.py).

Start-up and setup failures are ignored and leak

init_ieee1394DiskDriver() ignores failed allocations, event flags and threads, and the module stays resident anyway. The fetch-agent setup does not check ORB_size before writing a management ORB of that size. A unit that fails setup keeps its transaction context, which leaks. Our copy unwinds and refuses. (Source.)

Our choice: keep memory the target can still reach

Once waits are bounded, a timed-out command may still be fetched or written by the target. Our copy keeps every ORB, pointer and status block resident, reads through a staging buffer instead of the caller's memory, and refuses new commands to that storage until the late status arrives. ps2sdk's unbounded waits do not need this.

iLinkman

All eleven are still in master.

iLinkSendData() takes the transmit semaphore and, when the request's bus generation is stale, returns -1021 without releasing it. Every later transmission, including the receive thread's responses, blocks forever. Any 4-byte request that crosses a bus reset triggers it. Our copy releases the semaphore. Host test (test_ilink_manager_recovery.py, earlier versions).

A leftover timeout alarm breaks a later transaction

iLinkSync() cancels its 0.25 s alarm only when the wait ends without an error. After an interrupt error, the alarm later marks an unrelated transaction as failed, or wakes a block transfer early. Our copy always cancels it. Host test (test_ilink_manager_recovery.py, earlier versions).

Default builds misread packets after an unhandled packet type

NoResponseHandler()'s FIFO read sits inside #ifdef DEBUG_TTY_FEEDBACK, so in default builds the drain loop does nothing, and the leftover quadlets are parsed as the next packet's header. Lock transactions and other unhandled packet types trigger it. Our copy always drains the FIFO. Host test (test_ilink_manager_recovery.py, earlier versions).

Block writes report success on errors, or hang

The interrupt handler's cached register is always 0 by the time a waiter reads it, so iLinkPHTSendData(), used for every request that is not 4 bytes, reports errors as success. Split timeout, missing ACK and bus reset raise nothing it waits for, so it hangs unless the transfer completes, and its DMA is not stopped on an error. A response sent by the receive thread also clears the event bits a client request may be waiting on. Our copy latches the errors, stops the DMA and keeps request events separate. Host test (test_ilink_manager_irq.py). The controller behaviour it simulates has not been observed.

Block reads can return before the data arrives

For a block read, iLinkPHTSendData() waits only for the transfer to finish, but the response data is copied into the caller's buffer later by the receive thread. Neither ps2sdk SBP-2 driver issues block reads. Our copy waits for the response. Host test (test_ilink_manager_irq.py).

Late, foreign or error responses write caller memory

ResponseHandler() does not check the source node, transaction label, bus generation, response code or length; every quadlet request uses label 1 and every block request label 0x3F, and the target buffer pointer is never cleared. A response that arrives after its request timed out writes through a stale pointer, often a stack variable, and an error response counts as success. Our copy matches each response to its request and rotates labels. (Source.)

Two threads can take each other's replies

The public read and write functions take no lock, and the response buffer and events are global. They do not check the transaction context index either, and a racing IEEE1394_bd thread can pass -1 after a bus reset. Our copy adds a lock and refuses bad contexts and sizes. (Source.)

Incoming requests are checked only at their start

WriteRequestHandler() checks only that the start address is under 2 MB. A rejected write leaves its payload in the FIFO, to be parsed as headers, and the callback runs anyway. ReadRequestHandler() can return memory past the configuration ROM or past 2 MB. Neither checks the packet length. Our copy checks the whole span, drains what it rejects, and calls back only on success. (Source.)

A refused load leaves its interrupt handler running

_start ignores failures to create its kernel objects, and when RegisterLibraryEntries() fails it returns without undoing the interrupt handler, thread and allocations it already set up, so the unloaded module's code keeps running. A second load without an IOP reset triggers this. Our copy unwinds. (Source.)

Importing iLinkGetLocalNodeEUI64 calls iLinkTrAlloc

iLinkman.h declares I_iLinkGetLocalNodeEUI64 as import 12, which is iLinkTrAlloc; the export table has it at 25. Our copy uses 25. No ps2sdk module we searched imports it. (Source.)

The configuration ROM has wrong offsets, and deleting a unit corrupts it

iLinkDeleteCROMUnit() checks neither the index nor whether the slot is used, so a double delete frees NULL and decrements the count twice. BuildConfigurationROM() publishes the new size before its allocation can fail, and the vendor-ID and textual-descriptor offsets are miscounted. Our copy fixes all of these. (Source.)

Other ps2sdk modules

Defects we found in modules we embed unchanged. We worked around them, or left them.