Search code examples
c++macos-catalinaiokitdriverkitmacos-system-extension

How to create a IOUSBHostPipe::CompleteAsyncIO callback?


I am writing a SystemExtension to communicate with a usb-device. My initial plan is to create a class Transfer that allocates the necessary IOMemoryDescriptor, and then pass the interface that I want the Transfer class to communicate with. I would like to have the callback resulting from AsyncIO completing to be made to the Transfer class. If I need to queue up multiple reads I could then create more instances of this class. In the callback completeCallback I would then unpack the data and then submit another read.

I create the Transfer class with OSTypeAlloc(Transfer).

The problem I am facing is that creating the OSAction fails with this stack trace:

0   libsystem_kernel.dylib          0x000000010df950ce __pthread_kill + 10
1   libsystem_pthread.dylib         0x000000010e07cf6a pthread_kill + 152
2   libsystem_c.dylib               0x000000010df2c3a0 abort + 120
3   com.apple.DriverKit             0x000000010dc9124b __assert_rtn + 102
4   com.apple.DriverKit             0x000000010dc91a34 OSCopyOutObjects(IOUserServer_IVars*, IORPCMessageMach*, IORPCMessage*, bool) (.cold.4) + 35
5   com.apple.DriverKit             0x000000010dc7a2ff OSCopyOutObjects(IOUserServer_IVars*, IORPCMessageMach*, IORPCMessage*, bool) + 328
6   com.apple.DriverKit             0x000000010dc79126 InvokeRemote(IOUserServer_IVars*, unsigned long long, IORPC) + 159
7   com.apple.DriverKit             0x000000010dc796b6 OSMetaClassBase::Invoke(IORPC) + 754
8   com.apple.DriverKit             0x000000010dc90048 OSAction::Create_Call(OSObject*, unsigned long long, unsigned long long, unsigned long, OSAction**) + 212
9   com.apple.DriverKit             0x000000010dc7ca15 OSAction::Create(OSObject*, unsigned long long, unsigned long long, unsigned long, OSAction**) + 37
10  sc.example.MyUserUSBInterfaceDriver 0x000000010dc3bffc Transfer::CreateActioncompleteCallback(unsigned long, OSAction**) + 60 (Transfer.iig.cpp:175)
11  sc.example.MyUserUSBInterfaceDriver 0x000000010dc34e6e Transfer::setup(IOUSBHostInterface*, int, int) + 638 (Transfer.cpp:53)

If I instead move the callback to be defined, implemented and created in the class that is instantiated by the system when the usb-device is attached (this class is specified in the plist with key IOUserClass), then creating the OSAction object works fine.

The call to IOUSBHostInterface::Open is made from the IOUserClass passing a pointer to the IOUserClass as the first argument to Open. Should it be ok to do this? Or is it required that the IOService object is also the same object that receives the callbacks from AsyncIO.

class Transfer : public OSObject {
public:
  bool init() override;
  void free() override;

  bool setup(IOUSBHostInterface* interface, int endpointAddress, int maxPacketSize) LOCALONLY;
  bool submit() LOCALONLY;

protected:
   virtual void completeCallback(OSAction* action,
                        IOReturn status,
                        uint32_t actualByteCount,
                        uint64_t completionTimestamp) TYPE(IOUSBHostPipe::CompleteAsyncIO);
};
struct Transfer_IVars {
  IOUSBHostInterface* interface;
  int maxPacketSize;
  IOUSBHostPipe* pipe;
  IOBufferMemoryDescriptor* buffer;
  OSAction* ioCompleteCallback;
};

bool Transfer::init() {
  LOG_DEBUG();
  if (!super::init()) {
    LOG_ERROR("super::init failed");
  }

  if (ivars = IONewZero(Transfer_IVars, 1); ivars == nullptr) {
    LOG_ERROR("Allocating ivars failed");
    return false;
  }

  return true;
}

void Transfer::free() {
  LOG_DEBUG();
  IOSafeDeleteNULL(ivars, Transfer_IVars, 1);
  super::free();
}

bool Transfer::setup(IOUSBHostInterface* interface, int endpointAddress,
                     int maxPacketSize) {
  ivars->interface = interface;
  ivars->maxPacketSize = maxPacketSize;

  if (auto ret = interface->CopyPipe(endpointAddress, &ivars->pipe);
      ret != kIOReturnSuccess) {
    LOG_ERROR("Could not copy pipe: %{public}s, endpointAddress: %{public}d",
              kern_return_t_toCStr(ret), endpointAddress);
  }

  if (auto ret =
        interface->CreateIOBuffer(kIOMemoryDirectionIn, maxPacketSize, &ivars->buffer);
      ret != kIOReturnSuccess) {
    LOG_ERROR("CreateIOBuffer failed, ret: %{public}d", ret);
    return false;
  }

  if (auto ret = CreateActioncompleteCallback(0, &ivars->ioCompleteCallback);
      ret != kIOReturnSuccess) {
    LOG_ERROR("Failed to set iocomplete callback, ret: %{public}d", ret);
    return false;
  }
  return true;
}

bool Transfer::submit() {
  if (auto ret = ivars->pipe->AsyncIO(ivars->buffer, ivars->maxPacketSize,
                                      ivars->ioCompleteCallback, 0);
      ret != kIOReturnSuccess) {
    LOG_ERROR("Failed to call AsyncIO, ret: %{public}d", ret);
    return false;
  }
  return true;
}

void IMPL(Transfer, completeCallback) {
  LOG_DEBUG(
    "Complete callback bytecount %{public}d, timestamp: %{public}llu, status %{public}s",
    actualByteCount, completionTimestamp, kern_return_t_toCStr(status));
// TODO Unpack and then schedule another read.
}

Solution

  • As you've probably found, this isn't documented anywhere. From my experimentation however, I get the impression that OSAction is basically a wrapper for IPC messages. If true, that would imply that the action's target object must have a representation in both the kernel and dext contexts, so that it makes sense for the kernel to send a message to it.

    This doesn't appear to be the case for objects created with OSTypeAlloc(), as that just creates an object in the dext. It seems that only objects created using IOService::Create get representations in both contexts. This function only works for IOService subclasses though, which of course are rather heavyweight.

    My recommendation would be to send your I/O completions through a completion method in your main driver object or your user client object, and in that completion method, forward the call to whatever actually originated the I/O. You can store arbitrary data in an OSAction by passing sizeof(some_struct) as the first argument to your CreateAction… call that creates it, and then get a pointer to that struct using

    some_struct* my_context = static_cast<some_struct*>(action->GetReference());
    

    You can put a regular function pointer and pointer to the ultimate target in there, or whatever you like really.

    I don't believe the client object passed to the interface's or device's Open() function needs to be the same as the target for the OSAction used for the I/O callback. They both need to be kernel/dext shadowed objects, but they can be different objects.