IOUserSCSIParallelInterfaceController: Single-Segment Requirement Breaks No-DMA / No-IOMMU Controllers

Summary

I have a userspace IOUserSCSIParallelInterfaceController dext presenting a virtual disk. It's a software / no-DMA controller — it moves all data with a kernel CPU copy via UserGetDataBuffer; it never does hardware DMA and never reads fBufferIOVMAddr. Buffered I/O works and the disk mounts. The problem is unbuffered / raw-device I/O.

For a data-carrying task, ProcessParallelTask calls PrepareForDMA then GenerateIOVMSegments on the task's IODMACommand, and if the segment count != 1 it fails the task with EIO before ever calling UserProcessParallelTask / UserProcessBundledParallelTasks — so UserGetDataBuffer never gets a chance. I already use UserGetDataBuffer and it works for single-segment (aligned) tasks, but a client buffer that straddles a page boundary produces 2 segments and is rejected at that gate.

Concrete symptom

newfs_apfs writing the container superblock to the raw device from a page-straddling malloc'd buffer:

nx_format:308: failed to write superblock to block 0: 5 - Input/output error

Minimal repro against my raw device: an aligned 4 KiB pwrite succeeds; a 4 KiB pwrite from a buffer straddling a 16 KiB page returns errno 5.

What I think is going on (not sure)

My guess is that the framework expects a DART/IOMMU to coalesce a scattered buffer into one IOVM segment, and my virtual controller doesn't have one (ioreg shows no mapper and no iommu-parent on the node — attached), so GenerateIOVMSegments emits raw physical segments and a straddling buffer stays 2 segments. But I don't know if that's actually the reason, or whether a no-DMA controller is even supposed to go through GenerateIOVMSegments at all — hence the questions below.

Questions

  1. For a no-DMA controller that services data via UserGetDataBuffer, is there a supported way to make the framework deliver a task whose client buffer maps to more than one IOVM segment (relax/skip the GenerateIOVMSegments single-segment gate), so my upcall can run? UserGetDataBuffer returns a fresh contiguous buffer, so the original buffer's segment count shouldn't matter for a CPU-copy controller.

  2. Is there a characteristic to declare a PIO / no-DMA / software controller so the framework skips segment generation for it?

  3. Can a DriverKit controller get macOS to interpose an IOMapper/DART in front of it (so the buffer is coalesced into one segment) — via a property, a matching personality, or an intermediate provider nub? Or is a hardwareless controller simply not expected to support unbuffered/raw-device I/O?

Environment

  • Build: 26A5368g, arm64e
  • Full ioreg -w0 -r -c IOUserSCSIParallelInterfaceController attached.

Happy to file a Feedback with a sysdiagnose and the minimal repro.

Answered by DTS Engineer in 896993022

So, let me start here:

For a data-carrying task, ProcessParallelTask calls PrepareForDMA then GenerateIOVMSegments on the task's IODMACommand, and if the segment count != 1 it fails the task with EIO before ever calling UserProcessParallelTask / UserProcessBundledParallelTasks — so UserGetDataBuffer never gets a chance. I already use UserGetDataBuffer and it works for single-segment (aligned) tasks, but a client buffer that straddles a page boundary produces 2 segments and is rejected at that gate.

This is not what's going on, as PrepareForDMA will never[1] return more than one segment. There's a bit more detail in this post, but the loose summary is that the DART will happily do mappings FAR in excess of anything you'd ever practically do. Note that the design of IOUserSCSIParallelInterfaceController actually relies on this behavior, as it's why SCSIUserParallelTask only passes in a single address through fBufferIOVMAddr instead of passing in a segment list. It's relying on the DART to map an arbitrarily large buffer to a single range instead of using individual segments.

Next, a quick comment on this point:

Or is a hardwareless controller simply not expected to support unbuffered/raw-device I/O?

As far as the IOKit stack is concerned, there isn't really any difference between buffered and unbuffered I/O. They arrive through slightly different entry points (the VFS layer vs. IOBSDMediaClient), but after that point, the I/O paths are EXACTLY the same. It's critical to understand that because what DOES change here is that it can be easier to generate "arbitrarily" sized I/O through the raw path.

Don't ignore ANY failure here. The key word there is "easier"- that is, any failure in the raw I/O system is a potential buffered I/O failure that's simply harder to generate in real-world use.

Related to that point:

newfs_apfs writing the container superblock to the raw device from a page-straddling malloc'd buffer:

...I would strongly recommend writing your own test tool that generates arbitrarily sized I/O across a broad size range, probably all the way to 100s of MiB and even GiB. The system is designed to be able to divide ANY I/O request into a size you can handle, so "everything" should work.

In addition, at this early stage, every intermediate layer creates additional complication and confusion. Case in point, I wasted a considerable amount of time trying to figure out why pwrite returned EIO before I realized that it's not necessarily true that pwrite DID return EIO (at least for that log message). APFS has its own media client format, which means you're going down a completely different I/O path there.

In any case, shifting to pwrite specifically:

Minimal repro against my raw device: an aligned 4 KiB pwrite succeeds; a 4 KiB pwrite from a buffer straddling a 16 KiB page returns errno 5.

I don't know exactly what's gone wrong, but if I had to guess, the problem is that you haven't properly configured all of the keys required by UserReportHBAConstraints. There's a thread about this issue here, but the short summary is that improper or invalid configuration can produce exactly the failure you're seeing. In particular, I'd highlight this post, which describes how misconfiguration can mean that requests to large for your controller can reach the controller layer, at which point the controller layer will fail the request because your DEXT said it couldn't handle them:

(Referencing maxTransferSize returned through UserGetDMASpecification)

...I'm fairly convinced that maxTransferSize MUST either:

maxTransferSize >= kIOMaximumSegmentCountReadKey * kIOMaximumSegmentByteCountReadKey
OR
maxTransferSize >= kIOMaximumSegmentCountWriteKey * kIOMaximumSegmentByteCountWriteKey

...whichever of the two is larger. maxTransferSize basically defines "the largest possible transfer your controller could EVER handle", which would obviously be your segment count * segment size. Critically, using a smaller maxTransferSize won't cause an immediate failure, only a later failure if/when the kernel actually tried to "give you" a large enough transfer.

Putting that another way, SCSIControllerDriverKit guarantees your DEXT will never receive a transfer larger than maxTransferSize. It enforces that guarantee... by preemptively failing ANY transfer larger than "maxTransferSize". Your job is to then use the configuration keys defined by UserReportHBAConstraints to ensure that the storage system breaks up requests properly before they reach SCSIControllerDriverKit's final "check".

[1] While it is theoretically possible that PrepareForDMA could return multiple segments, I don't know how this would be done, I'm not sure it's actually possible within DriverKit, and most of our DEXTs specifically hard code that they will only EVER have one segment.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

So, let me start here:

For a data-carrying task, ProcessParallelTask calls PrepareForDMA then GenerateIOVMSegments on the task's IODMACommand, and if the segment count != 1 it fails the task with EIO before ever calling UserProcessParallelTask / UserProcessBundledParallelTasks — so UserGetDataBuffer never gets a chance. I already use UserGetDataBuffer and it works for single-segment (aligned) tasks, but a client buffer that straddles a page boundary produces 2 segments and is rejected at that gate.

This is not what's going on, as PrepareForDMA will never[1] return more than one segment. There's a bit more detail in this post, but the loose summary is that the DART will happily do mappings FAR in excess of anything you'd ever practically do. Note that the design of IOUserSCSIParallelInterfaceController actually relies on this behavior, as it's why SCSIUserParallelTask only passes in a single address through fBufferIOVMAddr instead of passing in a segment list. It's relying on the DART to map an arbitrarily large buffer to a single range instead of using individual segments.

Next, a quick comment on this point:

Or is a hardwareless controller simply not expected to support unbuffered/raw-device I/O?

As far as the IOKit stack is concerned, there isn't really any difference between buffered and unbuffered I/O. They arrive through slightly different entry points (the VFS layer vs. IOBSDMediaClient), but after that point, the I/O paths are EXACTLY the same. It's critical to understand that because what DOES change here is that it can be easier to generate "arbitrarily" sized I/O through the raw path.

Don't ignore ANY failure here. The key word there is "easier"- that is, any failure in the raw I/O system is a potential buffered I/O failure that's simply harder to generate in real-world use.

Related to that point:

newfs_apfs writing the container superblock to the raw device from a page-straddling malloc'd buffer:

...I would strongly recommend writing your own test tool that generates arbitrarily sized I/O across a broad size range, probably all the way to 100s of MiB and even GiB. The system is designed to be able to divide ANY I/O request into a size you can handle, so "everything" should work.

In addition, at this early stage, every intermediate layer creates additional complication and confusion. Case in point, I wasted a considerable amount of time trying to figure out why pwrite returned EIO before I realized that it's not necessarily true that pwrite DID return EIO (at least for that log message). APFS has its own media client format, which means you're going down a completely different I/O path there.

In any case, shifting to pwrite specifically:

Minimal repro against my raw device: an aligned 4 KiB pwrite succeeds; a 4 KiB pwrite from a buffer straddling a 16 KiB page returns errno 5.

I don't know exactly what's gone wrong, but if I had to guess, the problem is that you haven't properly configured all of the keys required by UserReportHBAConstraints. There's a thread about this issue here, but the short summary is that improper or invalid configuration can produce exactly the failure you're seeing. In particular, I'd highlight this post, which describes how misconfiguration can mean that requests to large for your controller can reach the controller layer, at which point the controller layer will fail the request because your DEXT said it couldn't handle them:

(Referencing maxTransferSize returned through UserGetDMASpecification)

...I'm fairly convinced that maxTransferSize MUST either:

maxTransferSize >= kIOMaximumSegmentCountReadKey * kIOMaximumSegmentByteCountReadKey
OR
maxTransferSize >= kIOMaximumSegmentCountWriteKey * kIOMaximumSegmentByteCountWriteKey

...whichever of the two is larger. maxTransferSize basically defines "the largest possible transfer your controller could EVER handle", which would obviously be your segment count * segment size. Critically, using a smaller maxTransferSize won't cause an immediate failure, only a later failure if/when the kernel actually tried to "give you" a large enough transfer.

Putting that another way, SCSIControllerDriverKit guarantees your DEXT will never receive a transfer larger than maxTransferSize. It enforces that guarantee... by preemptively failing ANY transfer larger than "maxTransferSize". Your job is to then use the configuration keys defined by UserReportHBAConstraints to ensure that the storage system breaks up requests properly before they reach SCSIControllerDriverKit's final "check".

[1] While it is theoretically possible that PrepareForDMA could return multiple segments, I don't know how this would be done, I'm not sure it's actually possible within DriverKit, and most of our DEXTs specifically hard code that they will only EVER have one segment.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

FYI, you posted your reply to the wrong thread. I'm responding here to try and keep things consolidated, so please reply here as well.

First, let me start by jumping to here:

  1. If not — what should happen to a sub-block-aligned, page-straddling buffer, and whose job is it to make it deliverable?

So, one thing just jumped out at me here. What do you mean by "sub-block-aligned"? The page alignment isn't relevant here, but rdev I/O DOES need to be device block aligned and sized. The transfer size issue below is a serious issue; however, I would also take a look at the "Preferred Block Size" at the IOMedia layer ("myapp Linux Media" in the IOReg you sent). Sending an I/O request smaller than that size to an rdev node will fail (per standard UNIX behavior), and I think it fails with EIO.

Aside from the block size issue, the basic answer is that this should work, and if it doesn't work, then it's an issue with your driver. However, the tricky part here is that "your driver" doesn't just mean "your code“; it also means the configuration you create (or leave out) in the higher-level storage stack. Most importantly, if a request that "should" reach your driver isn't, then the most likely cause is problems in the configuration above you, not the work your DEXT does directly.

Next, I think you need to look at this:

kIOMaximumSegmentByteCountRead/WriteKey	16384

That's FAR too small. Setting the immediate issue aside, you’re asking the I/O system to break every transaction into single pages, which is going to generate an ENORMOUS volume of commands. That’s a huge amount of wasted time tracking and processing commands, but it also means you’re paying the fixed DART cost over and over and over.

The "right" max transfer size probably depends a lot on your actual source, as you don't necessarily want to end up with a bunch of commands "stuck" in your DEXT while your DEXT waits for your source to produce data... maybe.

Stepping back for a moment, the reason the mechanisms exist at all is that the high-level storage system wants to be able to send arbitrarily large commands, but the hardware constraints of the lower-level I/O system limit the size of commands that can actually be processed. That issue is common to "all" storage targets, so IOStorage provides a "convenience" system for subdividing that work. However, the original large command still has to be processed, so all those "pending" commands are basically just sitting there waiting to be processed. In that context, there isn't really any difference between IOStorage breaking up a request into multiple commands and your DEXT receiving one "large" command, which it actually ends up processing as multiple transfers from whatever source you're actually getting data from.

As the most straightforward example, if you were implementing a RAM disk, then I think you'd basically want those limits to be "infinite". From an efficiency perspective, your RAM disk is simply going to call memcpy from your memory data store to the final target, so ANY command subdivision that happens is basically just wasted overhead.

Now, ACTUALLY being "infinite" there probably has a downside, but as some real-world examples (you can just read these from the IORegistry), my boot volume is using:

kIOMaximumSegmentCountRead/WriteKey	0x100-> 256
kIOMaximumSegmentByteCountRead/WriteKey	0x100000-> 1 MiB

And DiskImages are using (segment count undeclared):

kIOMaximumSegmentByteCountRead/WriteKey	16384 0x200000 2 MiB

That's a maxTransferSize of 256 MiB and 2 MiB, both of which are FAR in excess of 16 KiB.

Note: I would not assume that the DiskImage value in an "ideal" choice, particularly in terms of performance. I strongly suspect that value has basically "never" changed, even going back 20+ years when memory was FAR more constrained. I think the real lesson here is that even when performance ISN'T a primary concern, we're still MUCH larger than a single page.

Similarly, even if there was some hardware detail/issue that meant you were ACTUALLY going to end up having to process data in individual 16 KiB chunks, I think you'd still want to be using much larger transfers at this level. One of the major bottlenecks here is that full communication pipeline from the kernel, to your DEXT, and then to user space where you're actually doing any data transfer. You'd want to be pushing commands to user space as quickly as possible so that it can process the next command as soon as it finishes the previous command, and that best way to do that... would be to increase the transfer sizes so that each command can move more data.

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 device — but I may well be misreading it.

Following the math in IOBreaker isn't much fun, but I'm sure there are numeric configurations where the math "falls apart" and I wouldn't be at all surprised if one of those was at or below 1 page. It's one of those cases where we were focused on actual usage and no real hardware has actually used a size that small.

Getting to the bottom line:

  1. Am I doing something wrong in my configuration, or misreading how these buffers are supposed to be handled?

Yes, it's very likely that the single page configuration here just doesn't work. My guess is that a larger two-page (32 KiB) configuration MIGHT work, but my actual answer would be to dramatically increase your maximum byte count, probably to at least 2 MiB. As I talked about above, I think you're going to want to be much larger anyway, so there's no reason to restrict yourself like this.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Expanding on what I said here, as I think it wasn't as clear as it could have been:

What do you mean by "sub-block-aligned"? The page alignment isn't relevant here, but rdev I/O DOES need to be device block aligned and sized. The transfer size issue below is a serious issue; however, I would also take a look at the "Preferred Block Size" at the IOMedia layer ("myapp Linux Media" in the IOReg you sent). Sending an I/O request smaller than that size to an rdev node will fail (per standard UNIX behavior), and I think it fails with EIO.

The issue here is that the rdisk ("raw disk") node is designed to be a "direct link" to the underlying hardware. For example, issuing 5 reads on the rdisk node will result in 5 separate I/O requests going to hardware. The exact same 5 reads on the disk (buffered) node will typically result in a single read (assuming the data isn't already in cache) and then the other 4 requests being serviced from the cache.

However, that also means that you're required to operate within the I/O requirements of the underlying hardware, meaning all requests must be block aligned and sized. That means you can issue I/O commands within these parameters:

512b Device:

Offsets:
0 (block 0), 512 (block 1), 1024 (block 2), 1536 (block 3),....

Sizes:
512 (1 block), 1024 (2 blocks), 1536 (3 blocks), 2048 (4 blocks),...

4k Device:

Offsets:
0 (block 0), 4096 (block 1), 8192 (block 2), 12288 (block 3),....

Sizes:
4096 (1 block), 8192 (2 blocks), 12288 (3 blocks), 16384 (4 blocks),...

Implementing a virtual device somewhat obscures this, but the reality is that actual hardware CANNOT service unaligned I/O requests. That is, at the hardware level, a read like:

read(4,096,8192)

ACTUALLY means either:

512-> read(8, 16)-> read 16 blocks starting at block 4

OR

4k-> read(1, 2) -> read 2 blocks starting at block 1

and a read like this:

read(256,8192)

...is impossible, since the hardware is incapable of referencing a fractional block.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

IOUserSCSIParallelInterfaceController: Single-Segment Requirement Breaks No-DMA / No-IOMMU Controllers
 
 
Q