Search code examples
c++iokitdriverkit

IOUserClientMethodArguments completion value is always NULL


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?


Solution

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