BlockStorageDeviceDriverKit grant confirmed by support but shows "No Requests" in the portal. How to resolve?

Hello!

I am hoping a DTS engineer or someone who knows the Capability Requests portal can help, because I am stuck between a written support confirmation and what the portal actually shows.

Background. We are building a native macOS iSCSI initiator for SOHO and home NAS use, developed over close to two years. A userspace daemon runs the iSCSI protocol and a DriverKit system extension presents the remote LUN as a block device. The code is essentially complete. Only the DriverKit extension cannot be signed, loaded and validated without the entitlement.

We submitted request 32PC8MGU57 for two entitlements: com.apple.developer.driverkit.family.block-storage-device for the extension com.aviontex.iscsi.AviontexISCSI.AviontexInitiator com.apple.developer.driverkit.userclient-access for the app com.aviontex.iscsi.AviontexISCSI, scoped to the extension bundle id

The problem. On June 25 Developer Support confirmed in writing that both entitlements were granted. The portal does not match that: Block Storage Device: No Requests: on both App IDs UserClient Access: Assigned: on the app SCSI Controller: Submitted: on the app

So the one entitlement we actually need, Block Storage Device, shows as never requested, even though request 32PC8MGU57 covered it and support confirmed the grant. The case was escalated to the senior team on July 2 (case 102922935570). Follow-up emails since then have not received a response.

Why Block Storage Device specifically

Our initiator has no PCI or Thunderbolt bus and no DMA path, so SCSIControllerDriverKit does not fit. This is confirmed by DTS in thread 776020, where Kevin Elliott explains that SCSIControllerDriverKit passes data through fBufferIOVMAddr as a physical address with no mechanism to convert it into a VM address the dext can access. He also notes it cannot be used with any bus other than PCI or Thunderbolt. Block Storage Device is therefore the family we need.

My questions: Am I reading the portal correctly: Block Storage Device not requested, UserClient Access assigned, SCSI Controller submitted? From here, what is the correct way to get Block Storage Device onto these two App IDs, with both the Development and the Distribution grant, since our public beta depends on Distribution? Should I submit a new request through the Capability Requests tab or does the escalated case handle it? Is there any way to get visibility on the escalated case, since email follow-ups are not being answered?

A full technical justification is prepared and we are happy to share the source code. Any guidance would be appreciated.

Thank you.

I am hoping a DTS engineer or someone who knows the Capability Requests portal can help, because I am stuck between a written support confirmation and what the portal actually shows.

Looking into this, it looks like this was sorted out earlier today.

However, I have another question:

Our initiator has no PCI or Thunderbolt bus and no DMA path, so SCSIControllerDriverKit does not fit. This is confirmed by DTS in thread 776020, where Kevin Elliott explains that SCSIControllerDriverKit passes data through fBufferIOVMAddr as a physical address with no mechanism to convert it into a VM address the dext can access. He also notes it cannot be used with any bus other than PCI or Thunderbolt. Block Storage Device is therefore the family we need.

Unfortunately, I don't see how BlockStorageDeviceDriverKit will work any better. The I/O path here is DoAsyncReadWrite, which passes in "dmaAddr", which creates exactly the same problem SCSIControllerDriverKit has. I'd love to be wrong about this, but I don't see how this is going to work.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks Kevin, that was fast and clear, and I appreciate you taking the time to look at it.

I really hope you are wrong, and not just for selfish reasons. If neither storage family can carry a network backed block device, then there is no DEXT path to iSCSI on macOS at all. Not for us, not for anyone. It stays a kext, and on Apple Silicon that means telling users to drop to Reduced Security to reach their own storage. Nobody should have to weaken their machine to mount a disk. As a product that is simply not shippable, so for us that road ends there.

The selfish reason is easier to explain: three years of my life are in this thing. So you can imagine I read your reply twice, went for a walk, and read it a third time. Right now the outcome is either "we built something that cannot exist" or "the DTS engineer is wrong about one detail". I know which one I am betting on, though I admit the odds are not in my favour.

No argument from me either way, you know this stack better than I ever will. But I would rather find out than debate it. We are going to test this thoroughly now and see what actually happens, and as soon as I know more I will come back with the details.

Thanks again.

I really hope you are wrong, and not just for selfish reasons. If neither storage family can carry a network-backed block device, then there is no DEXT path to iSCSI on macOS at all. Not for us, not for anyone. It stays a kext, and on Apple Silicon that means telling users to drop to Reduced Security to reach their own storage. Nobody should have to weaken their machine to mount a disk.

I completely agree and, in fact, a lot of my attention this week has been on trying to sort out what a solution for this should actually look like. The summary is that I think we should have a better solution for this, but also that this probably is possible today. So, summarizing the situation:

  1. BlockStorageDeviceDriverKit -> I don't think this is currently workable. I'd strongly encourage you to file a bug asking for or it to add a memory descriptor only I/O path, then post that bug number back here.

  2. SCSIControllerDriverKit -> It appears I was actually wrong about this, as UserMapHBAData UserGetDataBuffer does give access to a semi-working MemoryDescriptor and, I'm told, it does work. Unfortunately, it appears there's also an issue on the DMA side which means you have to use a configuration that forces every I/O request to a single page. Again, I'd strongly encourage you to file a bug asking for or it to add a memory descriptor only I/O path, then post that bug number back here.

  3. FSKit Dark Horse -> This is an odd idea I just came up with this week. The details are below, but implementation should be straightforward and performance might actually be quite good.

As noted above, please get both of those bugs filed and the numbers back to me. I can't promise if/when we'll address these issues, but I think this is something we should provide better support for and I'm trying to collect bugs to "encourage".

Shifting back to the "FSKit Dark Horse", the idea here is to combine FSKit and our DiskImage infrastructure so that the system ends up handling all of the "drive" infrastructure while your FSKit extension does all of the I/O.

Here's how that would work:

(1)
Create a very simple FSKit extension. This is mounted as normal (probably at a "private" location, as the user never needs to see) but all it exposes is a single file, which corresponds to the device you'll ultimately be presenting to the user.

Note that the file handling here could actually be handled by either having one file per mount and one mount per device, or by having a single mount point which adds/removes device files dynamically (as you add or remove new devices). I think either approach could work fine, so this really depends on how you want the overall experience to work.

(2)
Create a dev node for that file using this hdiutil command:

hdiutil attach -imagekey diskimage-class=CRawDiskImage -nomount <file target>

...or the "raw" format of DiskImageKit. This target is what's then mounted and what the user interacts with.

(3)
Handle all I/O requests through the normal FSKit read/write process.

...and that's it. It may seem like a strange architecture, but at a technical level, this is exactly how disk images already work on any other file system. On the performance side, my guess is that it's probably slower than the "ideal" version of #1 & #2 but probably faster than #2 (in its current state). However, no matter what, I think it will be FAR easier to build/debug than either of the other options.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware


Hi Kevin,

I'm afraid I have to disagree with one detail. Please give me two seconds to enjoy the moment.

You know, the Big Five for Life: getting married, having children, planting a tree, building a house and correcting an Apple DTS engineer. :-)

All right, moment over. Seriously, though: thank you for revisiting this so openly, investigating the alternatives and trying to move the underlying issues forward internally.

Your revised overall conclusion appears to be correct: SCSIControllerDriverKit is viable for this use case, at least on the system we have now tested.

One detail may need clarification, though: the descriptor API path.

In the public DriverKit 25.5 headers, UserMapHBAData does not return a memory descriptor. Its only output is the unique controller task identifier:

UserMapHBAData(
    uint32_t *uniqueTaskID
);

The descriptor itself is retrieved later through:

UserGetDataBuffer(
    targetID,
    controllerTaskID,
    &buffer
);

when called from UserProcessParallelTask, using the request's fControllerTaskIdentifier.

Perhaps this is the mechanism you meant. UserMapHBAData establishes the task identity, and UserGetDataBuffer subsequently retrieves the task's IOBufferMemoryDescriptor.

As we are still testing, we would like to keep the conclusions narrow and clearly distinguish between what we have measured and what remains open.

Current test configuration

Hardware:       Intel Mac
macOS:          26.5.2
Xcode:          26.5
DriverKit SDK:  25.5

IOClass:
IOUserSCSIParallelInterfaceController

IOProviderClass:
IOUserResources

There is no PCI or Thunderbolt provider. The signed DEXT does not request the PCI transport entitlement.

The extension was installed and activated successfully. The controller became registered, matched and active.

The following lifecycle and reporting steps completed:

UserInitializeController
UserReportHBAConstraints
UserStartController
UserInitializeTargetForID

The framework then submitted real SCSI commands through UserProcessParallelTask.

Inside those callbacks:

UserGetDataBuffer
    -> kIOReturnSuccess
    -> non-null IOBufferMemoryDescriptor

GetAddressRange
    -> kIOReturnSuccess
    -> nonzero DEXT-local address
    -> expected transfer length

We initially performed a descriptor-only probe without touching the buffer. That succeeded.

We then performed a strictly bounded standard INQUIRY write using the address returned by GetAddressRange.

The observed sequence was:

INQUIRY allocation length 6
    -> wrote 6 bytes
    -> completed GOOD

INQUIRY allocation length 36
    -> wrote 36 bytes
    -> completed GOOD

macOS consumed the initial six bytes and subsequently requested the complete 36-byte INQUIRY response.

The resulting IORegistry properties contained the exact data written by the DEXT:

Vendor:    AVIONTEX
Product:   iSCSI4NAS VIRT
Revision:  0001

The native storage stack then advanced through:

IOSCSIParallelInterfaceDevice
IOSCSITargetDevice
IOSCSIHierarchicalLogicalUnit
IOSCSIPeripheralDeviceType00
IOBlockStorageServices
IOBlockStorageDriver

It is now issuing subsequent discovery commands, including READ CAPACITY(10).

There were no panics and no DEXT crashes. The DriverKit crash counter remained at zero.

For this specific configuration, we have therefore demonstrated:

IOUserResources
-> IOUserSCSIParallelInterfaceController
-> UserProcessParallelTask
-> UserGetDataBuffer
-> IOBufferMemoryDescriptor
-> GetAddressRange
-> bounded byte write
-> successful SCSI completion
-> data consumed and interpreted by the macOS storage stack

This confirms your revised conclusion that the SCSI path is possible today.

It also shows that the original fBufferIOVMAddr limitation does not prevent the DEXT from accessing the request data, because UserGetDataBuffer provides a separate descriptor-based path. We still do not use or dereference fBufferIOVMAddr.

Single-page limitation

Your warning about the single-page DMA limitation remains open.

Our successful transfers were only:

6 bytes
36 bytes

These results therefore neither confirm nor disprove a single-page limitation. We have not yet tested a cross-page request, Scatter/Gather I/O or real READ/WRITE commands.

Could you clarify the exact configuration Apple currently recommends to enforce the single-page restriction?

In particular, should a virtual HBA use:

maximum segment count read  = 1
maximum segment count write = 1

maximum segment byte count read  = runtime system page size
maximum segment byte count write = runtime system page size

maxTransferSize = runtime system page size

Should any additional alignment constraint be applied?

And should "one page" always use the runtime system page size rather than a fixed 4096-byte value, particularly when validating the same implementation across Intel and Apple Silicon?

BlockStorageDeviceDriverKit and FSKit

We also agree with your assessment of BlockStorageDeviceDriverKit.

Its current DoAsyncReadWrite interface exposes only the DMA address and does not provide an equivalent to UserGetDataBuffer. We will file the two requested Feedback Assistant reports:

  1. A descriptor-only read/write path for BlockStorageDeviceDriverKit
  2. A documented descriptor-only, multi-page I/O path for SCSIControllerDriverKit

We will post both feedback numbers here once they have been submitted.

We will also retain the FSKit/raw-DiskImage design as a fallback and potential performance comparison. For now, however, the SCSI path has progressed far enough that we would like to finish validating it before changing architectures.

Scope

For completeness, the results above currently apply only to:

Intel
macOS 26.5.2
DriverKit 25.5
development signing
Developer Mode
SIP disabled

The following remain separate validation steps:

Apple Silicon
SIP-enabled standard security
distribution provisioning
multi-page I/O
real READ/WRITE traffic
long-running stability

This is an early status report, not a final result. We will keep testing and keep you posted as we go, including the feedback numbers once both reports are filed.

One more thing, and I mean it. When we started, we did not see the SCSI option at all. In hindsight we were probably too fixated on BlockStorageDeviceDriverKit, because from where we sit that is the more logical family for this product. It took both the entitlement process and your pointers to send us back to the SCSI DEXT and look at it properly.

So thank you again. Your correction and the pointer back toward the SCSI path appear to have saved the architecture.

Perhaps this is the mechanism you meant. UserMapHBAData establishes the task identity, and UserGetDataBuffer subsequently retrieves the task's IOBufferMemoryDescriptor.

UserMapHBAData was a typo on my part, and UserGetDataBuffer is what I meant. I've corrected that above.

Could you clarify the exact configuration Apple currently recommends to enforce the single-page restriction?

In particular, should a virtual HBA use:

Yes, that looks right.

Your warning about the single-page DMA limitation remains open.

One more quick comment here. The actual failure here is specifically caused by IODMACommand forcing multiple segments (due to the lack of DART), which means the actual failure case is more complicated than "anything greater than 1 page always fails".

And should "one page" always use the runtime system page size rather than a fixed 4096-byte value, particularly when validating the same implementation across Intel and Apple Silicon?

Yes, you should use the system page size, and that's 16k (not 4k) on Apple Silicon.

We will post both feedback numbers here once they have been submitted.

Thank you...

We will also retain the FSKit/raw-DiskImage design as a fallback and potential performance comparison. For now, however, the SCSI path has progressed far enough that we would like to finish validating it before changing architectures.

So, just so you're aware, my intuition is that the FSKit approach will be faster, potentially MUCH faster, right now. I don't think that would be true without the single-page I/O limitation, but the overhead cost here is quite high.

One final note— given all the entitlement shuffling that happened here, I went ahead and requested SCSIControllerDriverKit on your behalf, which I expect should be approved shortly.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

Both bugs are filed:

FB23814013
FB23814092

If you need more detail on either, or want anything in a different form, just say the word and I will add it.

Filing them was the easy part. We spent most of yesterday testing, so the data was already sitting there.

Thanks again for requesting SCSIControllerDriverKit on our behalf. That was not something I expected and it is appreciated.

We will keep testing and give you an update once we know more, probably sometime next week.

Best regards & enjoy your weekend!!!

Torsten

Both bugs are filed:

Perfect, thank you.

Thanks again for requesting SCSIControllerDriverKit on our behalf. That was not something I expected and it is appreciated.

You're very welcome.

We will keep testing and give you an update once we know more, probably sometime next week.

Sounds good and good luck!

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

We ran the whole thing through on paper first. Everything below is calculation and assumption, not measurement. The real numbers come once we measure, but the calculations already let us make a call.

Short version: we build on SCSI and validate it. FSKit stays as a last resort. Extending BlockStorage with a descriptor path looks like the clean fix.

1. SCSI DEXT

On the single-page limit we did the math instead of guessing. Assuming 16 KB per request on Apple Silicon, a 20 GB transfer comes to 1,310,720 requests (on Intel at 4 KB it would be 5,242,880). Whether that becomes a problem depends on per-request latency. Working it through, in theory it looks like this:

Latency per request   20 GB @ 16 KB     Effective throughput
   10 us                     13 s             1.5 GB/s
   50 us                     66 s             305 MB/s
  100 us                    131 s             153 MB/s
  140 us                    183 s             110 MB/s   <- 1 GbE line
  200 us                    262 s              76 MB/s
  500 us                     11 min            31 MB/s

If those numbers hold, the single-page limit stays acceptable up to roughly 140 us per request, which keeps us at or above the 1 GbE ceiling of about 110 MB/s. Our target market of SOHO and home NAS runs mostly on 1 GbE and Wi-Fi anyway, where real-world throughput sits at or below that line. And if there is a way to lift the single-page constraint, that only improves the picture.

2. FSKit

This is the option you raised, and on raw speed you are probably right: it avoids the DMA path completely. We took the suggestion seriously and ran the numbers. Since FSKit has no fixed transfer size like the single-page SCSI path, the only thing we can really compare is request count. Assuming cluster-sized I/O, a 20 GB transfer looks like this:

20 GB transfer, requests by I/O size (assumption)

  SCSI single-page   16 KB   1,310,720 requests
  FSKit              64 KB     327,680 requests    (4x fewer)
  FSKit             128 KB     163,840 requests    (8x fewer)
  FSKit               1 MB      20,480 requests   (64x fewer)

On request count FSKit in theory clearly wins. The trade-off is higher per-request overhead through the VFS layer, so which approach is faster in the end we cannot say without measuring both.

What makes FSKit unattractive for us is not throughput, but what it costs at the layer above it. It hands us file offsets instead of SCSI semantics. Reservation handling, sense data, command ordering and error semantics would all have to be rebuilt on top of a layer that no longer speaks SCSI.

And more important, our own in-house iSCSI protocol extensions (iSCSI-over-TLS or iSCSI-via-Remote) are built on native iSCSI. A SCSI or BlockStorage DEXT keeps that native layer underneath them. FSKit replaces it, and we cannot yet say what that does to those extensions, but building on a file abstraction instead of the native wire is a risk we would rather not take. If nothing else works we would have to, but it would set our development back considerably.

3. Extending BlockStorage with a descriptor path

This would be additive and entitlement-gated. Leaving architecture, design, conception, testing and review aside completely and looking purely at the implementation effort, our estimate is less than a week of native code with no impact, since it is an extension and not a change.

The reasoning:

DoAsyncUnmap already carries an IOMemoryDescriptor across the DriverKit boundary in the same class, while DoAsyncReadWrite carries only a bare address one method away. So the plumbing to pass a descriptor already exists in the family. The descriptor also has to stay alive in the request until CompleteIO regardless, so it looks reachable via request ID.

More broadly, this looks like a gap and not a missing capability. Other DriverKit families hand the dext a real IOMemoryDescriptor for their data path. Even SCSIController does, through UserGetDataBuffer, which is exactly what we are relying on. BlockStorage is the one that does not: its DoAsyncReadWrite passes only a bare address, with no descriptor equivalent anywhere in the class. And storage is exactly the area where getting the bytes wrong costs data.

Our conclusion

Of the three paths, only the descriptor extension actually solves this rather than working around it. SCSI seems to work but stays capped. FSKit works but breaks the native semantics. The descriptor path is the only one that removes the underlying reason this device class still needs a kext.

Laid out plainly, this is why it looks like the right call to us, and why we think it is the sensible one for the platform too:

  • Apple users finally get a native iSCSI solution. The missing piece is only the descriptor path in the family.
  • It makes the DMA problem moot instead of fixing it. No IODMACommand, no segments, no DART, no single-page limit, and no impact on the general DMA path that every driver depends on.
  • It moves a whole device class off kexts. iSCSI on macOS means a kext today, and on Apple Silicon that means Reduced Security. The descriptor path removes the technical reason for that, for everyone, not just us.
  • It strengthens platform security. Nobody has to weaken their machine to reach their own storage.
  • It gives people a reason to move to Apple Silicon. A fix lands from macOS 27 onward, which is Apple Silicon only, so native high-throughput iSCSI becomes a capability of the newer machines rather than a reason to stay on a kext.

Realistically a change like this would take maybe six to nine months through the normal release cycle. That would put it in macOS 27 or later. Older Intel machines would keep running on the single-page SCSI path on Tahoe, without regression. Nobody would lose a working setup. That is a bigger outcome than one product, and that is why we keep coming back to it.

We are building on SCSI now and will test whether it holds up. If it is good enough for SOHO and home NAS traffic, we can live with it, and none of this blocks us.

We would of course like to know how long something like this might take, but we know that is not a question you can answer.

The one thing we are asking for is your technical read. Does this match how you see the constraints today? And is there a fundamental blocker in this approach that we are missing?

Best regards,

Torsten

Hi Kevin,

We now have the measurements we promised. My previous post was still based on calculations and on the assumption that the single-page SCSI path might remain viable if per-request overhead stayed low enough. On our Intel test system, that assumption does not hold.

Functional result

SCSIControllerDriverKit works functionally for a native software-backed iSCSI device. We validated an RMB=0 fixed disk backed by a real 4.29 TB Synology LUN with 512-byte blocks, native IOMedia and APFS mounting, verified READ and WRITE traffic, clean target creation and removal, QD64, bundled task intake, multiple iSCSI ITTs, 64 KB Data-In PDUs and ImmediateData. This is a mounted native macOS disk carrying real file I/O, not an INQUIRY-only probe.

Measured performance

The same approximately 152 MiB file was used throughout.

Path / directionElapsed timeEffective rate
Initial request-by-request path, WRITE7-8 minutes0.32-0.36 MB/s
Shared-memory ring, WRITE52.5-62 seconds2.5-2.9 MB/s
Shared-memory ring, READ19.8-26.3 seconds5.8-8.0 MB/s
1 GbE payload ceiling, context only~1.4 seconds~110 MB/s

The ring improved representative WRITE performance by roughly 7-9x. That is a real gain, but a result of only a few MB/s is still not viable for a LAN-connected NAS block device.

Why we built the ring

The initial request-by-request path took seven to eight minutes for 152 MiB, so we built a substantial workaround to determine whether the App/DEXT handoff was the primary bottleneck.

The DEXT now creates and shares a roughly 16 MB IOBufferMemoryDescriptor containing a request queue, a completion queue, 64 request slots, 64 completion slots and 64 payload slots of 256 KB each. The path supports QD64, bundled DriverKit intake, multiple ITTs, out-of-order completion, doorbells, completion kicks and ImmediateData.

The data path is effectively:

SCSI task -> UserGetDataBuffer -> request mapping -> shared staging slot -> userspace iSCSI -> completion ring -> READ copy-back -> individual framework completion

This removed the old payload-sized UserClient transport and the QD1 bottleneck. It did not remove the framework lifecycle of each original SCSI task.

What remained

Despite advertising 256 KB through Block Limits VPD, representative epochs were still almost entirely 4 KB tasks:

Task sizeWRITEREAD
exact 4 KB37,23838,315
exact 16 KB182
exact 64 KB022
exact 128/256 KB00
other small1025

An application-side coalescer was byte-correct, but 92,026 original requests became 91,876 wire commands, a reduction of only 0.16%. The requests were already individually active rather than accumulating as a mergeable batch. The limiting granularity therefore originates above the ring.

The single-page restriction is the blocker

Every workaround still pays one complete framework lifecycle per page-sized task: callback, UserGetDataBuffer, descriptor and mapping ownership, data movement and individual completion. QD64 can overlap these lifecycles; it cannot remove them.

For a 152 MiB transfer, the arithmetic is:

Request sizeRequest count
4 KB38,912
16 KB9,728
64 KB2,432
256 KB608

We have not yet measured Apple Silicon, but its 16 KB page does not change the verdict. Even assuming ideal 16 KB tasks, the same file still requires almost ten thousand complete framework lifecycles. A larger system page reduces the count but does not remove the page-bound architecture. Measuring Silicon would refine the number, not the conclusion, because the constraint we need removed is single-page, not 4 KB specifically.

What we actually need

A self-created IOBufferMemoryDescriptor only describes DEXT-owned staging memory. It is not the descriptor of the current DoAsyncReadWrite request, and it does not make dmaAddr a documented CPU-accessible pointer.

For a software-backed block device, we need the request-scoped memory object: a documented, CPU-accessible, multi-page descriptor with defined length, direction, mapping, synchronization, ownership and asynchronous lifetime. DoAsyncUnmap already carries an IOMemoryDescriptor in the same IOUserBlockStorageDevice class, while DoAsyncReadWrite exposes only dmaAddr.

Such an API would not guarantee line-rate performance. It would remove the artificial requirement that a software network block device be forced through an IODMACommand/DART-dependent single-page workaround.

FSKit

We considered the FSKit/raw-DiskImage approach seriously, but for a native iSCSI initiator it is not an equivalent solution.

The device it produces is a raw disk image, not a SCSI device. It exposes disk-image block semantics, so the behavior our initiator implements at the SCSI transport level has nowhere to live: Persistent Reservations, sense data, Unit Attention, proper SCSI error reporting, task management and task ordering. FSKit models a filesystem, and the disk-image indirection repurposes it as a byte-backing store, so the result is a block device layered over a file rather than a native one.

It is also fragile as a product foundation. The block device exists only while the FSKit mount and the hdiutil attachment stay alive, so an extension crash, an app update or an unclean teardown takes the device, and anything mounted on it, with it.

For byte movement it may well be faster than the current single-page SCSI path, and we are not disputing that. It is a different, non-native architecture that discards the SCSI device model our product is built on.

SCSIControllerDriverKit is therefore functionally viable for this use case, but the current page-bound request path is not product-viable for us. The shared ring makes the workaround substantially better; it cannot remove the reason the workaround exists.

Without the single-page restriction, performance is an engineering problem. With it, performance is an API-architecture problem.

Does this measured result match your technical view of the remaining constraint?

If it does, we are left waiting for a kernel fix: a request-scoped, multi-page descriptor. For a software device the clean home for it is BlockStorageDeviceDriverKit, which sidesteps IODMACommand entirely; without a multi-page path there or in SCSIControllerDriverKit, every path we have stays pinned to one page per request, 4 KB on Intel and 16 KB on Apple Silicon. Such a change would ship only in a future macOS, which puts Intel-based Macs permanently outside this feature. That is a trade-off we can live with.

Best regards,

Torsten

Hello @Aviontex and @DTS Engineer, I am a user who has been following this thread from the sidelines. I will not rehash the technical detail. You and the developer have that covered in far more depth than I could add. My angle is simpler. For a long time the ways to reach a NAS over iSCSI on a Mac have felt either dated or expensive to me, so a modern native option is something I would really want for home or business use. If it can reach normal LAN speed, I am in.

So here is my only question. From where things stand today, do you see a realistic chance that write speed on this native path reaches usable levels? Or is that a hard limit for the foreseeable future? I am not asking for a date, only your honest read, because it tells someone in my position whether to wait for the native route or stay on what exists now.

Thank you for the time you have already put in here.

Best regards!

Hi @lazarro!

That’s exactly our goal. We want the native iSCSI path to be really fast.

From my side it no longer feels like a question of if Apple will provide the missing descriptor support, but rather when. I’m hoping they play along because without that change this path simply won’t become what it needs to be. I’m keeping my fingers crossed for all of us ;)

I don’t expect Kevin to be able to answer the timing question here in the forum anytime soon. We will probably both have to wait a bit and it might unfortunately take some more time.

Best regards,

Torsten

First, I wanted to make a quick comment here:

The DEXT now creates and shares a roughly 16 MB IOBufferMemoryDescriptor containing a request queue, a completion queue, 64 request slots, 64 completion slots, and 64 payload slots of 256 KB each.

Basically, I think you're being WAY too conservative here. The memory shared between your DEXT and daemon isn't wired/kernel memory and doesn't really need to be treated as a "scarce" resource, certainly not to the degree KEXT memory would be. At a minimum, I think you could make that ring buffer much larger— definitely 100+ MB, possibly even GBs+. That's without considering other options, like file backing that memory so you're not specifically bound to the "standard" VM system.

I don't think any of that will RADICALLY improve your current performance, but it could make a big difference if/when things get fixed on our side.

One other question— my understanding is that your implementation is working for normal volume I/O if/when the volume is actually mounted. What kind of performance are you getting in that case? I know the numbers aren't directly comparable, but I'd love some concrete data about the "scale" of performance loss single-page I/O is causing.

I don’t expect Kevin to be able to answer the timing question here in the forum anytime soon.

You're right that I can't answer that. What I will say is that SCSIControllerDriverKit is going to be the point we're most likely to fix "first" and that the fix will likely involve minimal change (likely "none") to the SCSIControllerDriverKit API itself. On your DEXT side, I expect it will only require increasing your I/O size when you "know" the KEXT will handle larger I/O sizes. I'll provide full details if/when a fix becomes available, but I wanted to make you aware of where your focus should be "now".

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

thanks, that helps. Let me answer your last post directly first, because we now have concrete data for both the SCSI and BlockStorage paths.

Regarding the ring size: agreed. We were deliberately conservative because we initially treated the shared region more like scarce driver memory. Your clarification changes that assumption. A substantially larger ring is clearly possible and may improve batching and headroom.

However, our measurements also support your other point: increasing the ring would not fundamentally remove the bottleneck while the original SCSI requests remain page-sized.

Using normal mounted-volume I/O on the same ~152 MiB file on our Intel test system, we measured:

Initial request-by-request WRITE:  0.32–0.36 MB/s
Shared-memory ring WRITE:          2.5–2.9 MB/s
Shared-memory ring READ:           5.8–8.0 MB/s
1 GbE payload ceiling:             ~110 MB/s

The shared ring improved WRITE by roughly 7–9x. But almost all framework requests remained 4 KB:

WRITE:
4 KB      37,238
16 KB         18
64 KB          0

READ:
4 KB      38,315
16 KB          2
64 KB         22

Our application-side coalescer reduced 92,026 original requests to 91,876 wire commands, only about 0.16%. By the time requests reach us they are already separate active SCSI tasks, so a larger ring can keep more requests in flight but cannot remove the per-task framework lifecycle.

That gives the scale you asked for: on Intel, the current single-page behavior costs us roughly one to two orders of magnitude compared with normal 1 GbE NAS throughput. QD64, multiple ITTs, ImmediateData, larger Data-In PDUs and the shared ring all work, but they cannot compensate for paying the full SCSI/DriverKit lifecycle for almost every 4 KB request.

On Apple Silicon the 16 KB page size reduces the request count by 4x, but the same limitation remains. For a NAS initiator, that is still not product-viable.

So I agree that SCSIControllerDriverKit is probably the most useful path to fix first. If the kernel starts issuing larger requests while UserGetDataBuffer remains unchanged, our SCSI DEXT should require little architectural change.

There is, however, one new result from the BlockStorage path that may be relevant.

Our IOUserBlockStorageDevice implementation is essentially complete apart from one very small but critical missing bridge.

We already have dynamic geometry from the real iSCSI session, RMB=0 / native fixed-disk presentation, /dev/diskN, publish/unpublish, READ/WRITE/FLUSH orchestration, queue depth, shared-memory App/DEXT transport, real iSCSI READ/WRITE, completion handling and PR / single-writer arbitration.

The remaining problem is specifically this callback:

DoAsyncReadWrite(
    bool isRead,
    uint32_t requestID,
    uint64_t dmaAddr,
    uint64_t size,
    uint64_t lba,
    uint64_t numOfBlocks,
    IOUserStorageOptions options)

DriverKit 25.5 documents dmaAddr only as:

DMA address of the data buffer

Unfortunately, the documentation does not explain how a software-backed BlockStorage driver is supposed to access the data behind that DMA address from CPU context.

We initially tested whether dmaAddr might be CPU-dereferenceable inside the DEXT. Runtime proved that assumption wrong.

READ completion repeatedly crashed the DEXT at:

memcpy(reinterpret_cast<void *>(dmaAddr), ...)

with:

EXC_BAD_ACCESS
KERN_INVALID_ADDRESS

After repeated IOUserServer crashes, macOS eventually panicked with:

Driver IOUserServer(com.aviontex.iscsi...) has crashed too many times
(reason 2:11)

We stopped testing and inspected the actual Xcode 26.5 / DriverKit 25.5 SDK instead of making further assumptions.

We checked IOUserBlockStorageDevice.iig, the generated header, the private StartDev interface, exported BlockStorageDeviceDriverKit symbols, the generic IODMACommand API and the wider DriverKit headers.

We could not find any public equivalent of:

GetDataBuffer(...)
GetDMACommand(...)
dmaAddr -> IOMemoryDescriptor
dmaAddr -> IODMACommand
Map/Resolve/LookupDMAAddress(...)

This is particularly noticeable because other DriverKit families expose descriptors explicitly when CPU-side access is intended. DoAsyncUnmap() in the same BlockStorage class receives an IOMemoryDescriptor *, and SCSIControllerDriverKit explicitly provides UserGetDataBuffer().

IODMACommand::PerformOperation() initially looked promising. DriverKit documents it as a way to perform CPU access to a prepared DMA mapping, for example to/from a driver-allocated bounce buffer.

But that method operates on a specific prepared IODMACommand instance. IOUserBlockStorageDevice::DoAsyncReadWrite() gives us only the resulting DMA address, not the IODMACommand or IOMemoryDescriptor that owns that mapping.

Interestingly, our older experimental BlockStorage implementation had already identified exactly this gap. It contained a proposed:

dmaAddr -> avx_descriptorForDMA() -> IOMemoryDescriptor

path, but avx_descriptorForDMA() was deliberately left as a stub returning nullptr until a real BlockStorage-family API could be identified.

So the BlockStorage path is now in a rather frustrating position: from our side it is almost finished, and the only missing connection is:

framework-created DMA mapping
        |
      dmaAddr
        |
       ???
        |
CPU-accessible request bytes
        |
shared ring
        |
iSCSI transport

We searched the public DriverKit 25.5 SDK for that bridge and cannot find one.

So the remaining question is now very narrow:

Is this absence intentional?

Is IOUserBlockStorageDevice designed on the assumption that dmaAddr is handed to DMA-capable hardware, with no supported CPU-access path for a software-backed device?

Or is there an intended BlockStorage-specific mechanism to access the already-prepared mapping that is not exposed or documented in the public SDK?

If such a mechanism exists, the BlockStorage path may genuinely be only one small missing API connection away from working and would avoid the single-page SCSI limitation entirely.

If it does not exist, then the picture is finally clear:

SCSIControllerDriverKit:
CPU-accessible request descriptor available,
but I/O is currently page-bound.

BlockStorageDeviceDriverKit:
the desired block-I/O model is available,
but READ/WRITE exposes only a DMA address
with no documented CPU-accessible descriptor path.

In that case, your comment that SCSIControllerDriverKit is the path most likely to be fixed first makes complete sense, and we would freeze the BlockStorage work rather than build another unsupported workaround around the DMA contract.

Thanks again for helping us narrow this down. At this point the remaining BlockStorage issue is no longer a large architectural problem on our side, but literally this one missing DMA-to-CPU access bridge.

Best regards,

Torsten

Unfortunately, the documentation does not explain how a software-backed BlockStorage driver is supposed to access the data behind that DMA address from CPU context.

It doesn't. Functionally, "dmaAddr" is exactly the same value as "fBufferIOVMAddr" in SCSIControllerDriverKit. That is, they're both DMA addresses that are only valid for the specific DART target at the bottom of your provider chain. If you're curious, this code is actually open source, so dmaAddr is actually prepared here, while the actual genIOVMSegments call happens here. This code is essentially identical to what happens in SCSIControllerDriverKit and I'd expect it to fail in exactly the same way.

We could not find any public equivalent of: GetDataBuffer(...) GetDMACommand(...) ...

Yes, that's because they don't exist. One special comment here:

Map/Resolve/LookupDMAAddress(...)

I don't think any such function actually exists. That is, I don't think the kernel actually provides any method that would convert a DMA address "back" into a functional virtual address. The issue here is that these DMA addresses aren't actually unique— each PCI target is effectively managing its own independent DART, so it's entirely possible (probably even likely) that two different devices will generate EXACTLY the same "address" for totally unrelated commands. Converting a DMA address to a virtual address requires knowing not just the address but also the specific DART that's managing that particular I/O.

This dynamic is actually why the kernel actually uses IOMemoryDescriptor as its "base" address representation, not DMA addresses. The design is that the higher levels of the stack don't/won't know exactly what their final PCI target is, so the "final" conversion to a DMA address shouldn't happen until the VERY lowest level driver that’s actually targeting a specific PCI device. That's also why there isn't "LookupDMAAddress()" method— the only driver who could "use" that method doesn't need it, as it already has the underlying IOMemoryDescriptor.

That leads to here:

Is IOUserBlockStorageDevice designed on the assumption that dmaAddr is handed to DMA-capable hardware, with no supported CPU-access path for a software-backed device?

Yes, that's correct and that equally applies to SCSIControllerDriverKit. Neither of these classes were actually designed with software-backed I/O in mind— if they had been, then the obvious approach would have been to simply pass your DEXT an IOMemoryDescriptor, avoiding this entire issue.

As an aside, I wasn't involved with the original architecture creation but I suspect the decision to pass in DMA addresses to both drivers was driven by performance concerns— the DART mapping has to happen in the kernel, so providing an IOMemoryDescriptor to a PCI target would have added an additional IPC cycle in order to do exactly the same mapping the kernel is doing today.

The oddity here is actually why this exists:

SCSIControllerDriverKit explicitly provides UserGetDataBuffer().

I don't actually know for certain, but I suspect it was created primarily as a debugging aid, NOT to serve as an actual I/O path. Implementing a real-world SCSI controller is sufficiently complex that being able to determine EXACTLY what you sent/received is EXTREMELY helpful, particularly in early development. It's far too easy to set up an I/O pipeline that "works" (meaning, returns data and doesn't panic the kernel) aside from that small "detail" that it doesn't actually send/return what you thought it did. The simplest way to validate and debug these issues is to be able to see exactly what you sent/received.

In an actual PCI controller, it's likely that you'd ONLY ever call UserGetDataBuffer when during very early bring up (when you wanted to be sure it was doing exactly what you wanted) or perhaps when debugging a specific issue (when you wanted to be sure of exactly what was being sent/received). The rest of the time you'd just use fBufferIOVMAddr.

Or is there an intended BlockStorage-specific mechanism to access the already-prepared mapping that is not exposed or documented in the public SDK?

There is not (public or private). The simpler I/O model of BlockStorageDriverKit is sufficiently straightforward that I don't think it was ever really considered.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

thanks again for the detailed explanation and for requesting the SCSIControllerDriverKit entitlement.

Your last reply makes the architectural situation much clearer. In particular, the point that neither BlockStorageDeviceDriverKit nor SCSIControllerDriverKit was originally designed with software-backed I/O in mind explains very well why we keep ending up at the same boundary from two different directions.

I’m now trying to make a practical product decision and would appreciate your technical read.

For a pure software iSCSI initiator, BlockStorageDeviceDriverKit still seems like the conceptually natural fit: there is no PCI/Thunderbolt device and no hardware DMA target. The block-device model itself already gives us what we need. The missing piece is a request-scoped, CPU-accessible memory descriptor for DoAsyncReadWrite instead of only dmaAddr.

From the outside, this looks like a relatively contained addition to the existing BlockStorage I/O path, particularly since the underlying I/O path already has the memory descriptor needed to establish the DMA mapping. I fully appreciate that the implementation, ABI, security and validation work on your side may make it considerably less trivial than it appears from the API boundary.

SCSIControllerDriverKit works functionally, which is an important result, but the current page-bound path is not viable for our product. Even if that limitation is fixed first, SCSI remains an adaptation of a hardware-controller interface to a software-backed device rather than the natural abstraction for it.

So we are effectively choosing between three paths:

• continue investing in SCSI and wait for the larger-I/O fix you mentioned; • wait for a proper CPU-accessible BlockStorage I/O path; • ship a classic KEXT, which on Apple Silicon means requiring Reduced Security.

The third option is the one I would most like to avoid. Apart from the security implications, asking users to lower their Mac’s security policy simply to access an iSCSI LUN is a poor installation experience.

At the same time, we need to make a realistic engineering decision. If there is a realistic prospect of BlockStorage gaining such a path in a future macOS release, investing significant effort into a new KEXT may make very little sense.

I understand that you cannot provide a roadmap or promise a timeframe; even a qualitative indication of whether this is something worth designing around would be extremely helpful.

What I’m really asking is for your technical intuition: given what you now know about this use case and the current implementation, is BlockStorage support for software-backed I/O something you could realistically see being addressed in a future macOS release, or should we plan on the assumption that SCSI is the only DriverKit path for the foreseeable future, with a KEXT as the fallback if its performance limitations cannot be resolved?

If additional measurements, a minimal reproducer or anything else attached to FB23814013 would help make the case internally, I’m very happy to provide it.

Thanks again,

Torsten

Kevin, regarding your statement that SCSIControllerDriverKit is not intended for virtual or software-based controllers, I believe there might be a small additive path that makes it work without opening the DMA security wall.

After reviewing Apple's public kernel source repository, the public SCSIControllerDriverKit interfaces and the behavior of our software-backed SCSI prototype, we believe there may be a relatively small architectural change that would support software storage controllers without opening or weakening Apple's DMA security boundary.

We are aware that granting developers direct access to DMA is not a direction Apple wants to take. That's the reason for this "workaround idea".

Core Idea

Apple does not need to open the DMA security wall. It can keep that wall fully closed for software-backed controllers and still provide the missing native storage path.

We currently call this the No-DMA Solution.

By "No-DMA" we mean the data-path contract presented to the DriverKit developer. A software-backed controller does not program a physical DMA engine. It therefore does not require a DMA address, IOVA, IOMapper or access to a device-specific IOMMU domain.

The framework would continue to own and enforce all memory authorization.

Current Task Path

Our understanding of the current task path is approximately:

ProcessParallelTask
→ PrepareForDMA
→ GenerateIOVMSegments
→ validate segment geometry
→ UserProcessParallelTask
→ UserGetDataBuffer

Once UserProcessParallelTask() is reached, the existing UserGetDataBuffer() path provides the IOBufferMemoryDescriptor that a CPU-based software controller needs. The problem is that a request whose original buffer produces more than one DMA/IOVM segment can fail before the DEXT callback is delivered. The driver therefore never gets the opportunity to use the CPU-accessible buffer. Apple's public interface already defines UserGetDataBuffer() as returning an IOBufferMemoryDescriptor for the task.

Proposed Change

The proposed change is an explicit, opt-in software-backend capability.

The following code is only illustrative pseudocode. We do not know the internal class names or the most appropriate public API shape.

1. Add a Controller Capability

// Illustrative API only.

constexpr uint64_t
kSCSIControllerOptionSoftwareBackend = 0x00000001ULL;

The existing default would remain the current hardware-DMA behavior:

virtual uint64_t GetControllerOptions()
{
    return 0;
}

A software-backed controller would opt in explicitly:

uint64_t GetControllerOptions() override
{
    return kSCSIControllerOptionSoftwareBackend;
}

The same capability could alternatively be reported as an optional key through the existing UserReportHBAConstraints() contract if that fits the framework ABI better.

2. Add One Task-Admission Branch

Conceptually, the framework-side change could be:

if (controller->GetControllerOptions() &
        kSCSIControllerOptionSoftwareBackend)
{
    /*
     * Keep the original task buffer under framework ownership.
     *
     * Establish the request-scoped CPU-accessible buffer used by
     * UserGetDataBuffer().
     *
     * Do not prepare the task for a physical DMA engine.
     * Do not generate an IOVA for the software controller.
     * Do not reject the task based on DMA segment count.
     */

    PrepareTaskBufferForCPUAccess(task);

    DispatchUserProcessParallelTaskOrBundle(task);
}
else
{
    /*
     * Existing hardware-controller path, unchanged.
     */

    PrepareForDMA(task);
    GenerateIOVMSegments(task);
    ValidateControllerDMALimits(task);

    DispatchUserProcessParallelTaskOrBundle(task);
}

3. Reuse the Existing CPU-Buffer Path

The software controller would continue using the public interfaces that already exist:

IOBufferMemoryDescriptor *buffer = nullptr;
IOAddressSegment range = {};

UserGetDataBuffer(
    targetID,
    controllerTaskIdentifier,
    &buffer
);

buffer->GetAddressRange(&range);

/*
 * CPU-based software processing:
 * iSCSI, NBD, encryption, compression, cloud storage, etc.
 */

ParallelTaskCompletion(...);

Essential Contract

The exact implementation may differ internally. The essential contract is only:

Software-backed controller selected
→ no hardware-DMA preparation
→ no DMA segment-count admission gate
→ normal task delivery
→ complete request range available through UserGetDataBuffer
→ existing completion and cancellation semantics retained

Existing hardware controllers would remain on the current path because they would not set the capability.

For software-backed controllers, multi-page and page-straddling requests could then be delivered up to the controller's reported maximum transfer size without making physical DMA segment count part of the controller contract.

Security Model

The security model would remain narrowly request-scoped:

  • no raw DMA address exposed
  • no IOVA reverse mapping
  • no public IOMapper access
  • no physical-address access
  • no access to another device's IOMMU domain

The DEXT would receive only the buffer already authorized for the current task, with its range, direction and lifetime bound to that task. Access would end with completion, cancellation or teardown.

Architectural Distinction

Architecturally, this would create a clean distinction:

Hardware-backed controller:

request buffer
→ DMA preparation
→ IOMapper / IOVA
→ physical device

Software-backed controller:

request buffer
→ request-scoped CPU access
→ software transport or processing

Immediate Use Case: iSCSI Initiator

For our iSCSI initiator the resulting path would be:

macOS SCSI task
→ UserProcessParallelTask
→ UserGetDataBuffer
→ software iSCSI transport
→ ParallelTaskCompletion

This would remove the current dependency on one DMA/IOVM segment while leaving Apple's DMA isolation fully intact. (Tadaaaa - sounds like a great Jackpot)

Although iSCSI is our immediate use case, the same capability would also support software-defined block storage, NBD, cloud-backed disks, encryption, compression, deduplication and virtual test controllers.

Scope Assessment

We obviously do not know whether the internal framework is structured exactly as the pseudocode suggests, or whether the existing UserGetDataBuffer() backing buffer currently depends on part of the DMA-preparation path. We also do not expect the API names above to be adopted literally.

The architectural change nevertheless appears local and additive:

  • one opt-in controller capability
  • one task-admission distinction
  • one request-buffer lifetime guarantee

If this approach proves successful for SCSIControllerDriverKit, a similar concept could likely be applied to BlockStorageDeviceDriverKit as well though from our perspective, that would require a significantly larger effort.

With DriverKit 27 currently in beta, we thought this might be a useful time to raise the idea & it's the easiest solution. ;)

Best regards,

Torsten

SCSIControllerDriverKit works functionally, which is an important result, but the current page-bound path is not viable for our product. Even if that limitation is fixed first, SCSI remains an adaptation of a hardware-controller interface to a software-backed device rather than the natural abstraction for it.

As a small side comment, one advantage SCSIControllerDriverKit does have is that it would allow you to support a broader range of hardware (for example, disc burners), which could be useful.

So we are effectively choosing between three paths: • continue investing in SCSI and wait for the larger-I/O fix you mentioned; • wait for a proper CPU-accessible BlockStorage I/O path; • ship a classic KEXT, which on Apple Silicon means requiring Reduced Security.

The third option is the one I would most like to avoid.

At this point, I wouldn't consider shipping a KEXT to be a viable solution, as our KEXT certificate program is largely closed.

In terms of choosing between SCSIControllerDriverKit and BlockStorageDriverKit, I think the critical issue here is scheduling priority and your willingness to wait. The critical point here is this:

SCSIControllerDriverKit works functionally, which is an important result, but the current page-bound path is not viable for our product.

The issue in SCSIControllerDriverKit is in fact "a bug"- that is, while neither family was designed to support software I/O, SCSIControllerDriverKit's public API DOES support it and shouldn't require single-page I/O. Most critically, there are options for addressing this that are confined to the IOUserSCSIParallelInterfaceController kernel support driver (not DriverKit itself) and narrow enough that it may be possible to ship a fix in a software update, rather than waiting for a major release.

By comparison, addressing BlockStorageDriverKit would require new API. Along with the additional risk, the other issue here is that the easiest solution to implement (basically, copying UserGetDataBuffer) is simply duplicating the poor solution SCSIControllerDriverKit happened to have already implemented. Ideally, both families should be updated with a "software I/O" path, likely using the same approach in both cases (same problem, same solution). However, that's new API work which we generally avoid shipping in a software update.

At the same time, we need to make a realistic engineering decision. If there is a realistic prospect of BlockStorage gaining such a path in a future macOS release, investing significant effort into a new KEXT may make very little sense.

Hopefully, what I've described above has helped clarify things.

The proposed change is an explicit, opt-in software-backend capability.

I won't go too far into what you've described, but the basic summary is that, yes, that approach is what I've been discussing with the team. However, the main difference is that I'm intentionally trying to avoid ANY change to DriverKit itself, as those changes are what create the ABI risk I've mentioned before. My current suggestion has been that IOUserSCSIParallelInterfaceController public a new property indicating that it supports "software I/O", which the DEXT driver can then modify to bypass the existing DMA process.

Finally, I’ll clarify here:

... I believe there might be a small additive path that makes it work without opening the DMA security wall.

As a technical clarification, the issue with fBufferIOVMAddr isn’t really about security. Theoretically, there's a small benefit in "keeping" the data in the kernel, however:

  • Any PCI storage driver can easily read whatever it wants off the disk by... just reading the disk itself.

  • If this was really about security, UserGetDataBuffer wouldn't exist.

Instead, the primary reason fBufferIOVMAddr was used (instead of an IOMD) was partly to avoid the cost of mapping the kernel IOMD into the DEXT address space and mostly to avoid the DEXT having to do another call into the kernel to perform DMA prep prior to sending the address to the PCI target. Note that the first mapping cost is high enough that it's part[1] of what the bundled parallel task architecture was designed to avoid.

Conversely, the reason fBufferIOVMAddr can't easily be mapped into the DEXT address space is that doing so would require API support that doesn't really exist. Each DART is effectively managing a private address space that’s specific to the PCI bus it’s managing. Theoretically, you COULD ask a given DART to convert a bus address into a VM address in a particular process, but that would start by using the DART address to find its kernel VM address... then generating an IOMD from that kernel VM address. However, that flow never happens in the kernel because every driver that interacts with bus addresses either already HAS the original memory descriptor or is so low level that concepts like "vm" and "processes" aren't part of its world. Basically, we've never created a mechanism for converting bus addresses into IOMDs because there's never been (and still isn't) anyone who'd use it.

This particular DEXT situation makes it "look" like it would be useful, but that's just an accident of how the API happened to be designed, not because it's a common situation.

[1] To be clear, the biggest bottleneck is the task "pile up“ [2] that occurs if/when IOUserSCSIParallelInterfaceController is chopping up a command into many smaller commands. The serialized model UserProcessParallelTask used forces a context switch for every command while the bundled architecture allows the kernel to continue generating tasks while another thread delivers them in batches.

[2] I didn't really think about it till now, but this "pile up" is one of the primary performance bottlenecks that makes single-page I/O so slow. I don't know if it will be "enough", but it’s possible that using the bundled architecture with a VERY high max task count MIGHT improve performance enough to be worth shipping.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Kevin, thank you very much for the detailed response and for taking this back to the SCSI team.

The confirmation that the single-page limitation is a bug rather than an inherent limitation of SCSIControllerDriverKit is probably the most important result of this investigation for us. Even though the current limitation means that we cannot move forward with a production implementation at the moment, having the issue clearly identified and having a potential path to address it is a huge step forward.

The software I/O approach you described makes a lot of sense for our use case. In particular, the possibility of handling this within IOUserSCSIParallelInterfaceController, without requiring a DriverKit API or ABI change, sounds like a very clean solution for software-backed SCSI controllers.

Your suggestion regarding the bundled architecture and a very high maximum task count is also very helpful. As I understand it, this would allow us to reduce the delivery overhead and task pile-up caused by the page-sized requests by keeping substantially more work in flight and delivering tasks in bundles. We already have a highly parallel data path behind the DEXT, so this is something we can test without fundamentally changing the architecture.

We see this primarily as a useful proof of technology. It may demonstrate how far the existing path can be pushed despite the page-sized task limitation and we will certainly test it.

For a production iSCSI initiator, however, we would not consider this workaround a viable foundation. Bundling can reduce delivery and scheduling overhead, but it does not remove the underlying fragmentation. A 1 MiB I/O still becomes 256 separate 4 KiB SCSI tasks. At sustained storage throughput, that means processing very large numbers of tasks, completions and associated bookkeeping for I/O that would naturally be represented by far fewer larger operations. Even if sufficient parallelism makes the throughput look reasonable, the CPU cost, latency characteristics and scaling behavior would remain concerns for a commercial storage product.

That is why the bug fix makes such a fundamental difference for us. It is not simply another performance optimization. It determines whether SCSIControllerDriverKit can provide a clean production data path for this type of software-backed controller rather than requiring us to optimize around artificial I/O fragmentation.

Given that the potential software I/O solution may be contained within IOUserSCSIParallelInterfaceController, without requiring a DriverKit API or ABI change, may I ask two final planning questions?

I completely understand that you cannot provide a roadmap or commit to a release date. From an engineering perspective, is this something you would reasonably expect could be addressed on a near-term macOS software update timescale, or should we plan for the current single-page limitation to remain for the foreseeable future?

Also, since macOS Tahoe 26 is the final major macOS release supporting Intel Macs, could a fix of this kind potentially still reach macOS 26 and therefore Intel Macs, or is it more realistic to expect that the corrected software I/O path would only become available on Apple silicon?

The timing distinction is particularly useful for us. If the underlying issue has a realistic prospect of being addressed, we would much rather avoid investing significant engineering effort into optimizing a proof-of-technology path around a limitation that may disappear.

Regardless of the timing, thank you again to you and the entire SCSI team for taking the time to investigate this. We really appreciate the depth of the technical feedback and the effort that has gone into understanding this use case.

Although we cannot take the production implementation much further with the current limitation, having the limitation confirmed as a bug and having a potential path forward is a fantastic outcome for us. We hope that the underlying issue can be addressed in the near future and we are very much looking forward to testing the proper software I/O path if and when it becomes available.

Best regards,

Torsten

BlockStorageDeviceDriverKit grant confirmed by support but shows "No Requests" in the portal. How to resolve?
 
 
Q