2

I'm trying to use IOConnectCallAsyncStructMethod in order set a callback between a client and a driver in DriverKit for iPadOS.

This is how I call IOConnectCallAsyncStructMethod

    ret = IOConnectCallAsyncStructMethod(connection, MessageType_RegisterAsyncCallback, masterPort, asyncRef, kIOAsyncCalloutCount, nullptr, 0, &outputAssignCallback, &outputSize);

Where asyncRef is:

    asyncRef[kIOAsyncCalloutFuncIndex] = (io_user_reference_t)AsyncCallback;
    asyncRef[kIOAsyncCalloutRefconIndex] = (io_user_reference_t)nullptr;

and AsyncCallback is:

static void AsyncCallback(void* refcon, IOReturn result, void** args, uint32_t numArgs)
{
    const char* funcName = nullptr;
    uint64_t* arrArgs = (uint64_t*)args;
    ReadDataStruct* output = (ReadDataStruct*)(arrArgs + 1);

    switch (arrArgs[0])
    {
        case 1:
        {
            funcName = "'Register Async Callback'";
        } break;

        case 2:
        {
            funcName = "'Async Request'";
        } break;

        default:
        {
            funcName = "UNKNOWN";
        } break;
    }

    printf("Got callback of %s from dext with returned data ", funcName);
    
    printf("with return code: 0x%08x.\n", result);

    // Stop the run loop so our program can return to normal processing.
    CFRunLoopStop(globalRunLoop);
}

But IOConnectCallAsyncStructMethod is always returning kIOReturnBadArgument and I can see that when the method:

kern_return_t MyDriverClient::ExternalMethod(uint64_t selector, IOUserClientMethodArguments* arguments, const IOUserClientMethodDispatch* dispatch, OSObject* target, void* reference) {
    
    kern_return_t ret = kIOReturnSuccess;

    if (selector < NumberOfExternalMethods)
    {
        dispatch = &externalMethodChecks[selector];
        if (!target)
        {
            target = this;
        }
    }

    return super::ExternalMethod(selector, arguments, dispatch, target, reference);

is called, in IOUserClientMethodArguments* arguments, completion is completion =(OSAction •) NULL

This is the IOUserClientMethodDispatch I use to check the values:

    [ExternalMethodType_RegisterAsyncCallback] =
    {
        .function = (IOUserClientMethodFunction) &Mk1dDriverClient::StaticRegisterAsyncCallback,
        .checkCompletionExists = true,
        .checkScalarInputCount = 0,
        .checkStructureInputSize = 0,
        .checkScalarOutputCount = 0,
        .checkStructureOutputSize = sizeof(ReadDataStruct),
    },

Any idea what I'm doing wrong? Or any other ideas?

xarly
  • 2,054
  • 4
  • 24
  • 40

1 Answers1

3

The likely cause for kIOReturnBadArgument:

The port argument in your method call looks suspicious:

IOConnectCallAsyncStructMethod(connection, MessageType_RegisterAsyncCallback, masterPort, …
------------------------------------------------------------------------------^^^^^^^^^^

If you're passing the IOKit main/master port (kIOMasterPortDefault) into here, that's wrong. The purpose of this argument is to provide a notification Mach port which will receive the async completion message. You'll want to create a port and schedule it on an appropriate dispatch queue or runloop. I typically use something like this:

    // Save this somewhere for the entire time you might receive notification callbacks:
    IONotificationPortRef notify_port = IONotificationPortCreate(kIOMasterPortDefault);
    // Set the GCD dispatch queue on which we want callbacks called (can be main queue):
    IONotificationPortSetDispatchQueue(notify_port, callback_dispatch_queue);
    // This is what you pass to each async method call:
    mach_port_t callback_port = IONotificationPortGetMachPort(notify_port);

And once you're done with the notification port, make sure to destroy it using IONotificationPortDestroy().

It looks like you might be using runloops. In that case, instead of calling IONotificationPortSetDispatchQueue, you can use the IONotificationPortGetRunLoopSource function to get the notification port's runloop source, which you can then schedule on the CFRunloop object you're using.

Some notes about async completion arguments:

You haven't posted your DriverKit side AsyncCompletion() call, and at any rate this isn't causing your immediate problem, but will probably blow up once you fix the async call itself:

If your async completion passes only 2 user arguments, you're using the wrong callback function signature on the app side. Instead of IOAsyncCallback you must use the IOAsyncCallback2 form.

Also, even if you are passing 3 or more arguments where the IOAsyncCallback form is correct, I believe this code technically triggers undefined behaviour due to aliasing rules:

    uint64_t* arrArgs = (uint64_t*)args;
    ReadDataStruct* output = (ReadDataStruct*)(arrArgs + 1);

    switch (arrArgs[0])

The following would I think be correct:

    ReadDataStruct* output = (ReadDataStruct*)(args + 1);

    switch ((uintptr_t)args[0])

(Don't cast the array pointer itself, cast each void* element.)

Notes about async output struct arguments

I notice you have a struct output argument in your async method call, with a buffer that looks fairly small. If you're planning to update that with data on the DriverKit side after the initial ExternalMethod returns, you may be in for a surprise: an output struct arguments that is not passed as IOMemoryDescriptor will be copied to the app side immediately on method return, not when the async completion is triggered.

So how do you fix this? For very small data, pass it in the async completion arguments themselves. For arbitrarily sized byte buffers, the only way I know of is to ensure the output struct argument is passed via IOMemoryDescriptor, which can be persistently memory-mapped in a shared mapping between the driver and the app process. OK, how do you pass it as a memory descriptor? Basically, the output struct must be larger than 4096 bytes. Yes, this essentially means that if you have to make your buffer unnaturally large.

pmdj
  • 22,018
  • 3
  • 52
  • 103
  • 1
    Thanks a lot. This totally solve the problem. You made my day! Just a clarification about the other two points. Some of my code is from the Apple example "DriverKitUserClientSample". In this, my understanding is that in order to have a communication from Driver to client, it's first to set a callback and then inform to the driver, the client is ready to get data. So I though in this first step to set the call back, no output is needed, so I've changed the last four parameters of IOConnectCallAsyncStructMethod to: ... ,nullptr, 0, nullptr, 0); Is this assumption right? – xarly Nov 21 '22 at 11:29
  • @xarly Yes, if you don't need to send a struct or buffer of data back from driver to user space, you can specify struct size 0 in the `IOUserClientMethodDispatch` and pass `nullptr`, or use `IOConnectCallAsyncScalarMethod` instead if you want to pass some simple arguments to the call. – pmdj Nov 21 '22 at 11:40
  • So for example in order to read from the device, I would need two calls to the driver. One first with `IOUserClientMethodDispatch` and pass `nullptr` to set up the callback method. And then another call to `IOUserClientMethodDispatch` and pass the output size and output object. So when the driver calls `AsyncCompletion()` it will return same time of object and size. Not sure why it can be done everything in the first call... – xarly Nov 21 '22 at 14:17
  • @xarly You *can* use an "output struct" in the original `IOConnectCallAsyncStructMethod` call for returning data asynchronously. However, the buffer you pass *must* be larger than 4096 bytes because the external method dispatch will otherwise expect your code to instantly return an `OSData` object. So if you only need to return 128 bytes, you'll have to use at least a buffer of 4097 bytes and just not use most of it. This is an odd quirk of the implementation which has been around since the very beginning of Mac OS X. It was less of an issue with kexts as there were other options available. – pmdj Nov 22 '22 at 14:34
  • Sorry @pmdj I'm revisiting because for external reasons I didn't get the chance to fix it. You mentioned for not very small data (let say 2048 bytes ) I should use `IOMemoryDescriptor`. Where should I create the `IOMemoryDescriptor` in the App side when I call `IOConnectCallAsyncStructMethod` and pass it as parameter so the dext can write on it? Or should I create it in the Dext and pass it as parameter in `AsyncCompletion`? Thanks – xarly Aug 08 '23 at 10:56