Thanks Kevin. I went through your answer point by point with experiments — here's what I found.
it's not necessarily true that pwrite DID return EIO (at least for that log message)
I wasn't sure either, so I traced it with dtruss — in my case it really is pwrite(2) returning EIO (fd 3 = the raw device):
pwrite(0x3, ..., 0x1000, 0x2000) = 4096 0
pwrite(0x3, "x\0", 0x1000, 0x0) = -1 Err#5 ← the superblock write
write_nocancel(0x2, "nx_format:308: failed to write superblock to block 0: 5 - Input/output error\n", 0x4D)
lldb shows the failing buffer is a 32-byte-aligned heap pointer, so whether it straddles a page boundary is an allocator lottery — which also explains why diskutil eraseDisk only fails some of the time.
the problem is that you haven't properly configured all of the keys required by UserReportHBAConstraints
All seven keys are defined (and also republished as node properties via SetProperties, per FB21256805):
Key
Value
kIOMaximumSegmentCountRead/WriteKey
1
kIOMaximumSegmentByteCountRead/WriteKey
16384
kIOMinimumSegmentAlignmentByteCountKey
16384
kIOMaximumSegmentAddressableBitCountKey
64
kIOMinimumHBADataAlignmentMaskKey
0xFFFFFFFFFFFFFFFF
maxTransferSize (UserGetDMASpecification)
16384 (alignment = 1, numAddressBits = 64)
That satisfies maxTransferSize >= SegmentCount × SegmentByteCount (16384 = 1 × 16384), and the failing transfer is only 4 KiB.
I would strongly recommend writing your own test tool that generates arbitrarily sized I/O across a broad size range
Did that — byte-verified matrix over buffer alignments × sizes from 512 B to 128 MiB. Everything passes except one shape. Same device, same offset, same 4 KiB size, only the buffer placement varies:
buffer within one 16 KiB page → OK (any alignment)
block-aligned buffer straddling a page (e.g. page+0x3000, 8 KiB) → OK, split at the crossing
sub-block-aligned buffer straddling a page (e.g. page+0x3800, 4 KiB) → EIO, deterministic
From the IOStorageFamily source this looks like IOBreaker::getBreakSize() truncating the split point to a device-block multiple and getting 0 for that shape, so the request fails with kIOReturnDMAError before anything reaches my dext — but I may well be misreading it.
So, two questions:
Am I doing something wrong in my configuration, or misreading how these buffers are supposed to be handled?
If not — what should happen to a sub-block-aligned, page-straddling buffer, and whose job is it to make it deliverable?
The behavior reproduces with a similar configuration:
Condensed controller source
ioreg -w0 -r -c IOUserSCSIParallelInterfaceController
Topic:
App & System Services
SubTopic:
Drivers
Tags: