Search code examples
c++ciokitkernel-extensionxnu

sending IOKit command with dynamic length


I'm using IOKit framework to communicate with my driver using IOConnectCallMethod from the user-space client and IOExternalMethodDispatch on the driver side.

So far I was able to send fixed length commands, and now I wish to send a varied size array of chars (i.e. fullpath).

However, it seems that the driver and the client sides command lengths are coupled, which means that checkStructureInputSize from IOExternalMethodDispatch in driver must be equal to inputStructCnt from IOConnectCallMethod in client side.

Here are the struct contents on both sides :

DRIVER :

struct IOExternalMethodDispatch
{
    IOExternalMethodAction function;
    uint32_t           checkScalarInputCount;
    uint32_t           checkStructureInputSize;
    uint32_t           checkScalarOutputCount;
    uint32_t           checkStructureOutputSize;
};

CLIENT:

kern_return_t IOConnectCallMethod(
    mach_port_t  connection,        // In
    uint32_t     selector,      // In
    const uint64_t  *input,         // In
    uint32_t     inputCnt,      // In
    const void      *inputStruct,       // In
    size_t       inputStructCnt,    // In
    uint64_t    *output,        // Out
    uint32_t    *outputCnt,     // In/Out
    void        *outputStruct,      // Out
    size_t      *outputStructCnt)   // In/Out

Here's my failed attempt to use a varied size command :

std::vector<char> rawData; //vector of chars

// filling the vector with filePath ...

kr = IOConnectCallMethod(_connection, kCommandIndex , 0, 0, rawData.data(), rawData.size(), 0, 0, 0, 0);

And from the driver command handler side, I'm calling IOUserClient::ExternalMethod with IOExternalMethodArguments *arguments and IOExternalMethodDispatch *dispatch but this requires the exact length of data I'm passing from the client which is dynamic.

this doesn't work unless I set the dispatch function with the exact length of data it should expect.

Any idea how to resolve this or perhaps there's a different API I should use in this case ?


Solution

  • As you have already discovered, the answer for accepting variable-length "struct" inputs and outputs is to specify the special kIOUCVariableStructureSize value for input or output struct size in the IOExternalMethodDispatch.

    This will allow the method dispatch to succeed and call out to your method implementation. A nasty pitfall however is that structure inputs and outputs are not necessarily provided via the structureInput and structureOutput pointer fields in the IOExternalMethodArguments structure. In the struct definition (IOKit/IOUserClient.h), notice:

    struct IOExternalMethodArguments
    {
        …
    
        const void *    structureInput;
        uint32_t        structureInputSize;
    
        IOMemoryDescriptor * structureInputDescriptor;
    
        …
    
        void *      structureOutput;
        uint32_t        structureOutputSize;
    
        IOMemoryDescriptor * structureOutputDescriptor;
    
        …
    };
    

    Depending on the actual size, the memory region might be referenced by structureInput or structureInputDescriptor (and structureOutput or structureOutputDescriptor) - the crossover point has typically been 8192 bytes, or 2 memory pages. Anything smaller will come in as a pointer, anything larger will be referenced by a memory descriptor. Don't count on a specific crossover point though, that's an implementation detail and could in principle change.

    How you handle this situation depends on what you need to do with the input or output data. Usually though, you'll want to read it directly in your kext - so if it comes in as a memory descriptor, you need to map it into the kernel task's address space first. Something like this:

    static IOReturn my_external_method_impl(OSObject* target, void* reference, IOExternalMethodArguments* arguments)
    {
        IOMemoryMap* map = nullptr;
        const void* input;
        size_t input_size;
        if (arguments->structureInputDescriptor != nullptr)
        {
            map = arguments->structureInputDescriptor->createMappingInTask(kernel_task, 0, kIOMapAnywhere | kIOMapReadOnly);
            if (map == nullptr)
            {
                // insert error handling here
                return …;
            }
            input = reinterpret_cast<const void*>(map->getAddress());
            input_size = map->getLength();
        }
        else
        {
            input = arguments->structureInput;
            input_size = arguments->structureInputSize;
        }
    
        // …
        // do stuff with input here
        // …
    
        OSSafeReleaseNULL(map); // make sure we unmap on all function return paths!
        return …;
    }
    

    The output descriptor can be treated similarly, except without the kIOMapReadOnly option of course!

    CAUTION: SUBTLE SECURITY RISK:

    Interpreting user data in the kernel is generally a security-sensitive task. Until recently, the structure input mechanism was particularly vulnerable - because the input struct is memory-mapped from user space to kernel space, another userspace thread can still modify that memory while the kernel is reading it. You need to craft your kernel code very carefully to avoid introducing a vulnerability to malicious user clients. For example, bounds-checking a userspace-supplied value in mapped memory and then re-reading it under the assumption that it's still within the valid range is wrong.

    The most straightforward way to avoid this is to make a copy of the memory once and then only use the copied version of the data. To take this approach, you don't even need to memory-map the descriptor: you can use the readBytes() member function. For large amounts of data, you might not want to do this for efficiency though.

    Recently (during the 10.12.x cycle) Apple changed the structureInputDescriptor so it's created with the kIOMemoryMapCopyOnWrite option. (Which as far as I can tell was created specifically for this purpose.) The upshot of this is that if userspace modifies the memory range, it doesn't modify the kernel mapping but transparently creates copies of the pages it writes to. Relying on this assumes your user's system is fully patched up though. Even on a fully patched system, the structureOutputDescriptor suffers from the same issue, so treat it as write-only from the kernel's point of view. Never read back any data you wrote there. (Copy-on-write mapping makes no sense for the output struct.)