Should UserSendCBD work on UAS interfaces?

The device I am trying to develop a firmware updater for is an NVMe drive with a USB4 interface. It can connect in USB4 mode (tunneled NVMe), in USB 3 mode or in USB 2 mode.

In USB 2 and USB 3 mode, the device descriptor shows one interface with two alternates. Alternate 0 uses the bulk-only protocol, with one IN and one OUT pipe. Alternate 1 uses the UAS protocol, with two IN and two OUT pipes.

I use identical code in my driver to send custom CDBs. I can see using IORegistryExplorer that in USB 2 mode, macOS chooses alternate 0, the bulk-only protocol. My custom CDBs and their accompanying data pay loads are put on the bus, more or less as expected.

In USB 3 mode, macOS chooses alternate 1, the UAS protocol. My custom CDB is put on the bus, but no payload data is transferred.

Is this expected behavior? If so, is there a way to force the OS to choose alternate 0 even when on USB 3, perhaps with another dext?

I'll file a bug about this when Feedback Assistant lets me.

Answered by DTS Engineer in 860954022

Jumping around a bit, partly from the bug, partly from your post. First off, on your provider:

For USB2, this is IOSCSILogicalUnitNub, for USB 3 it is IOSCSIHierarchicalLogicalUnit.

Basically, you're trying too hard. Both drivers are subclasses of IOSCSIPeripheralDeviceNub, which is what you should use as your provider.

In USB 3 mode, macOS chooses alternate 1, the UAS protocol. My custom CDB is put on the bus, but no payload data is transferred. Is this expected behavior?

No. This should either work or we shouldn't let your driver load. I'm not sure WHY it's not working, but I don't see any obvious failure.

If so, is there a way to force the OS to choose alternate 0 even when on USB 3, perhaps with another dext?

I haven't tried it, but yes, I think this is possible. For the future, the way to figure this out is to start by tracking down its match dictionary in the underlying KEXT. Its bundle id (from the registry snapshot) is "com.apple.iokit.IOUSBMassStorageDriver", which is "IOUSBMassStorageDriver.kext". Here is the UAS entry from that dictionary:

<key>IOUSBMassStorageUASDriver</key>
<dict>
	<key>CFBundleIdentifier</key> = <string>com.apple.iokit.IOUSBMassStorageDriver</string>
	<key>IOClass</key> = <string>IOUSBMassStorageUASDriver</string>
	<key>IOProviderClass</key> = <string>IOUSBMassStorageDriverNub</string>
	<key>Read Time Out Duration</key> = <integer>30000</integer>
	<key>Retry Count</key> = <integer>20</integer>
	<key>Write Time Out Duration</key> = <integer>30000</integer>
	<key>bInterfaceProtocol</key> = <integer>98</integer>
</dict>

If you compare it to the non-UAS case, you'll find that "bInterfaceProtocol" is the key value. I haven't actually tried this, but I believe a codeless DEXT on IOUSBHostInterface that flipped bInterfaceProtocol from 0x62 (UAS) to 0x50 (from your working case) would do what you want.

  1. Every time I re-attach the device, I get a new driver process and a new IOUserServer at the root of the IORegistry. Even if I delete the app, the existing driver process keeps running, unless I kill it from Activity Monitor. Maybe there's something I'm not cleaning up properly, but I don't know what.

I think this is a bug in your code. Similar to KEXT, DEXTs aren't "unloaded" (meaning, the system kills them), they unload "themselves". I'm not sure how "done" your implementation is, but my usual approach with these is to pull out code until it unloads cleanly, then add code back in a piece at a time until I've reconstructed the "full" driver. You can also try logging out "all" of the lifecycle methods, but my experience has been that this will often tell you what the problem is ("stop isn't being called") without telling you WHY the problem is happening ("what you actually need to change"). Finally, you can also try running "lsmp" (command line tool) against one of those orphaned processes. That will give you a list of all its mach ports, which might be helpful if something "odd" is going on.

  1. When I eject the disk, there is a very perceptible delay between ejecting the volume and its associated - Data container. I use Finder/Settings and check the External Disks item in the Sidebar/Locations list. After I click the eject control next to the volume, I unplug the device as soon as it disappears from the sidebar, and very often see a notification about ejecting "volume - data" when it wasn't ready to be ejected.

If I had to guess, this is actually a symptom of #1, though I'm not sure exactly what. Is your DEXT on the I/O path? Does unmount trigger any DEXT activity?

Is this a bug worth filing?

So, arguably, that fact that this is happening is a bug:

After I click the eject control next to the volume, I unplug the device as soon as it disappears from the sidebar, and very often see a notification about ejecting "volume - data" when it wasn't ready to be ejected.

...since the icon shouldn't disappear until the kernel is "done". However, the problem is that the main thing that causes this is storage driver issues* or bad hardware, which is why you don't see it very much.

*I say this from personal experience (many years ago), having specifically had a storage driver that was "doing this" for a while it was in development.

BTW, if you're curious about what's triggering this dialog, the code is here.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Filed FB20382589

I'm not sure what's going on here.

Can you upload IORegistryExplorer files for the two cases (working and failure) to your bug? Saved from the app please, not the ioreg tool. Working with ioreg text files is always a pain.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I uploaded the requested IORegistryExplorer files.

I also note a couple of other things:

  1. every time I re-attach the device, I get a new driver process and a new IOUserServer at the root of the IORegistry. Even if I delete the app the existing driver process keeps running, unless I kill it from Activity Monitor. Maybe there's something I'm not cleaning up properly, but I don't know what.

  2. when I eject the disk, there is a very perceptible delay between ejecting the volume and its associated - Data container. I use Finder/Settings and check the External Disks item in the Sidebar/Locations list. After I click the eject control next to the volume, I unplug the device as soon as it disappears from the sidebar, and very often see a notification about ejecting "volume - data" when it wasn't ready to be ejected.

If I keep /Volumes open I can see that "Volume"'s icon disappears first, then "Volume - Data"'s icon.

Is this a bug worth filing?

Jumping around a bit, partly from the bug, partly from your post. First off, on your provider:

For USB2, this is IOSCSILogicalUnitNub, for USB 3 it is IOSCSIHierarchicalLogicalUnit.

Basically, you're trying too hard. Both drivers are subclasses of IOSCSIPeripheralDeviceNub, which is what you should use as your provider.

In USB 3 mode, macOS chooses alternate 1, the UAS protocol. My custom CDB is put on the bus, but no payload data is transferred. Is this expected behavior?

No. This should either work or we shouldn't let your driver load. I'm not sure WHY it's not working, but I don't see any obvious failure.

If so, is there a way to force the OS to choose alternate 0 even when on USB 3, perhaps with another dext?

I haven't tried it, but yes, I think this is possible. For the future, the way to figure this out is to start by tracking down its match dictionary in the underlying KEXT. Its bundle id (from the registry snapshot) is "com.apple.iokit.IOUSBMassStorageDriver", which is "IOUSBMassStorageDriver.kext". Here is the UAS entry from that dictionary:

<key>IOUSBMassStorageUASDriver</key>
<dict>
	<key>CFBundleIdentifier</key> = <string>com.apple.iokit.IOUSBMassStorageDriver</string>
	<key>IOClass</key> = <string>IOUSBMassStorageUASDriver</string>
	<key>IOProviderClass</key> = <string>IOUSBMassStorageDriverNub</string>
	<key>Read Time Out Duration</key> = <integer>30000</integer>
	<key>Retry Count</key> = <integer>20</integer>
	<key>Write Time Out Duration</key> = <integer>30000</integer>
	<key>bInterfaceProtocol</key> = <integer>98</integer>
</dict>

If you compare it to the non-UAS case, you'll find that "bInterfaceProtocol" is the key value. I haven't actually tried this, but I believe a codeless DEXT on IOUSBHostInterface that flipped bInterfaceProtocol from 0x62 (UAS) to 0x50 (from your working case) would do what you want.

  1. Every time I re-attach the device, I get a new driver process and a new IOUserServer at the root of the IORegistry. Even if I delete the app, the existing driver process keeps running, unless I kill it from Activity Monitor. Maybe there's something I'm not cleaning up properly, but I don't know what.

I think this is a bug in your code. Similar to KEXT, DEXTs aren't "unloaded" (meaning, the system kills them), they unload "themselves". I'm not sure how "done" your implementation is, but my usual approach with these is to pull out code until it unloads cleanly, then add code back in a piece at a time until I've reconstructed the "full" driver. You can also try logging out "all" of the lifecycle methods, but my experience has been that this will often tell you what the problem is ("stop isn't being called") without telling you WHY the problem is happening ("what you actually need to change"). Finally, you can also try running "lsmp" (command line tool) against one of those orphaned processes. That will give you a list of all its mach ports, which might be helpful if something "odd" is going on.

  1. When I eject the disk, there is a very perceptible delay between ejecting the volume and its associated - Data container. I use Finder/Settings and check the External Disks item in the Sidebar/Locations list. After I click the eject control next to the volume, I unplug the device as soon as it disappears from the sidebar, and very often see a notification about ejecting "volume - data" when it wasn't ready to be ejected.

If I had to guess, this is actually a symptom of #1, though I'm not sure exactly what. Is your DEXT on the I/O path? Does unmount trigger any DEXT activity?

Is this a bug worth filing?

So, arguably, that fact that this is happening is a bug:

After I click the eject control next to the volume, I unplug the device as soon as it disappears from the sidebar, and very often see a notification about ejecting "volume - data" when it wasn't ready to be ejected.

...since the icon shouldn't disappear until the kernel is "done". However, the problem is that the main thing that causes this is storage driver issues* or bad hardware, which is why you don't see it very much.

*I say this from personal experience (many years ago), having specifically had a storage driver that was "doing this" for a while it was in development.

BTW, if you're curious about what's triggering this dialog, the code is here.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I tried making a codeless dext which matches to my IOUSBHostInterface. It has a plist that looks like this:

CFBundleIdentifierKernel = com.apple.kpi.iokit // not sure if this is required
IOClass = AppleUSBHostMergeProperties // uses IOProviderMergeProperties
IOMatchCategory = $(PRODUCT_BUNDLE_IDENTIFIER) // makes its matching unique
IOProviderClass = IOUSBHostInterface // what I want to match against
IOUserClass = force_alt0 // not sure I need this, because we don't need a user-space process at all

// required properties for matching to a IOUSBHostInterface, 
// see https://developer.apple.com/library/archive/qa/qa1076/_index.html
idProduct = x // my product's usb product ID, as a Number
idVendor = y // my product's usb vendor ID, as a Number
bInterfaceNumber = 0
bConfigurationValue = 1
IOProviderMergeProperties = {
   bInterfaceProtocol = 80 // 0x50
  sentinelProperty = "I am just a string"
}

I can see in the IORegistry that my sentinelProperty is applied to the interface, but bInterfaceProtocol is not.

If I look at the IORegistry without my codeless dext, I see this hierarchy

IOUSBInterface bInterfaceProtocol = 0x62
    IOUSBMassStorageInterfaceNub, bInterfaceProtocol = 0x50
        IOUSBMassStorageDriverNub, bInterfaceProtocol = 0x62
            IOUSBMassStorageUASDriverNub, bInterfaceProtocol = 0x62

with my codeless dext, I see the same classes, with the same values of bInterfaceProtocol.

I expect that tweaking the bInterfaceProtocol value on the IOUSBHostInterface at driver match time doesn't make any difference. Something in the USB stack that matches later, perhaps the IOUSBMassStorageDriverNub, is looking at the IOUSBHostInterface and making its own decision about what alternate interface to set, which causes IOUSBHostInterface to set its own bInterfaceProtocol property value. The value I designate in my IOProviderMergeProperties dictionary may well be briefly applied to the IOUSBHostInterface, but is later overwritten.

Is there some other way to convince the OS to not use alternate interface 1?

I can see in the IORegistry that my sentinelProperty is applied to the interface, but bInterfaceProtocol is not.

...

I expect that tweaking the bInterfaceProtocol value on the IOUSBHostInterface at driver match time doesn't make any difference.

That's unfortunate. So, looking at our code, it turns out that AppleUSBHostMergeProperties will actually only work in one of two ways:

  1. If a property is absent, it will add that property.

  2. If a property already exists AND that property is a dictionary, then it will merge/modify "your" dictionary into the KEXT dictionary.

I hadn't looked closely at this today, but, ironically, that's almost certainly why many properties at the "root" get merged into a dictionary by the nub layer, as it then allows those properties to be modified. In this particular case, that modification point would be the "USB Device Info" dictionary of "IOUSBMassStorageDriverNub".

The good news here is that I think a codeless DEXT might still work for this. I haven't had a chance to test this, but my understanding of how the DEXT loading architecture makes me think that you would be allowed to match against IOUSBMassStorageDriverNub. A normal DEXT wouldn't be able to "do" anything (since you couldn't match with a functional support KEXT), but you don't need that for the property match to work.

Give that a try and let me know what happens. If it fails, then I think this will need a bug and much deeper investigation, but I think you may still be able to make this work.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Should UserSendCBD work on UAS interfaces?
 
 
Q