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.
}
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.