发送具有动态长度的 IOKit 命令

sending IOKit command with dynamic length

我正在使用 IOKit 框架通过 user-space 客户端的 IOConnectCallMethod 和 driver 端的 IOExternalMethodDispatch 与我的 driver 通信。

到目前为止,我能够发送固定长度的命令,现在我希望发送不同大小的字符数组(即完整路径)。

但是,driver 和客户端命令长度似乎是耦合的,这意味着 driver 中的 IOExternalMethodDispatch 中的 checkStructureInputSize 必须等于 inputStructCnt 来自 IOConnectCallMethod 在客户端。

这是两边的结构内容:

DRIVER :

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

客户:

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

这是我使用不同大小命令的失败尝试:

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);

从 driver 命令处理程序端,我用 IOExternalMethodArguments *argumentsIOExternalMethodDispatch *dispatch 调用 IOUserClient::ExternalMethod 但这需要我传递的数据的确切长度来自动态的客户端。

除非我将调度函数设置为它应该期望的确切数据长度,否则这是行不通的。

知道如何解决这个问题,或者在这种情况下我应该使用不同的 API 吗?

再次翻阅相关手册后,我找到了相关段落:

The checkScalarInputCount, checkStructureInputSize, checkScalarOutputCount, and checkStructureOutputSize fields allow for sanity-checking of the argument list before passing it along to the target object. The scalar counts should be set to the number of scalar (64-bit) values the target's method expects to read or write. The structure sizes should be set to the size of any structures the target's method expects to read or write. For either of the struct size fields, if the size of the struct can't be determined at compile time, specify kIOUCVariableStructureSize instead of the actual size.

因此,为了避免大小验证,我所要做的就是将字段 checkStructureInputSize 设置为 IoExternalMethodDispatch 中的值 kIOUCVariableStructureSize 并将命令正确传递给驱动程序.

正如您已经发现的那样,接受可变长度 "struct" 输入和输出的答案是在 IOExternalMethodDispatch 中指定输入或输出结构大小的特殊 kIOUCVariableStructureSize 值.

这将允许方法分派成功并调用您的方法实现。然而,一个令人讨厌的陷阱是结构输入和输出不一定通过 IOExternalMethodArguments 结构中的 structureInputstructureOutput 指针字段提供。在结构定义(IOKit/IOUserClient.h)中,注意:

struct IOExternalMethodArguments
{
    …

    const void *    structureInput;
    uint32_t        structureInputSize;

    IOMemoryDescriptor * structureInputDescriptor;

    …

    void *      structureOutput;
    uint32_t        structureOutputSize;

    IOMemoryDescriptor * structureOutputDescriptor;

    …
};

根据实际大小,内存区域可能被structureInput structureInputDescriptor(和structureOutput or structureOutputDescriptor) - 交叉点通常为 8192 字节,或 2 个内存页。任何较小的都会作为指针进入,任何较大的都会被内存描述符引用。不过不要指望特定的交叉点,那是一个实现细节,原则上可能会发生变化。

你如何处理这种情况取决于你需要对输入或输出数据做什么。不过,通常情况下,您会希望直接在您的 kext 中读取它 - 因此如果它作为内存描述符出现,您需要先将其映射到内核任务的地址 space。像这样:

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 …;
}

可以类似地处理输出描述符,当然,除了没有 kIOMapReadOnly 选项!

注意:轻微的安全风险:

在内核中解释用户数据通常是一项对安全敏感的任务。直到最近,结构输入机制特别脆弱——因为输入结构是从用户 space 到内核 space 的内存映射,另一个用户 space 线程 仍然可以修改内核正在读取它时的内存。您需要非常小心地编写内核代码,以避免将漏洞引入恶意用户客户端。例如,在映射内存中对用户space 提供的值进行边界检查,然后在假设它仍在有效范围内的情况下重新读取它是错误的。

避免这种情况最直接的方法是复制一次内存,然后只使用复制的数据版本。要采用这种方法,您甚至不需要对描述符进行内存映射:您可以使用 readBytes() 成员函数。对于大量数据,您可能不想为了提高效率而这样做。

最近(在 10.12.x 周期中)Apple 更改了 structureInputDescriptor,因此它是使用 kIOMemoryMapCopyOnWrite 选项创建的。 (据我所知,这是专门为此目的创建的。)这样做的结果是,如果 userspace 修改了内存范围,它不会修改内核映射,而是透明地创建它所在页面的副本写给。依赖于此假设您的用户系统已完全修补。 即使在完全打补丁的系统上,structureOutputDescriptor 也会遇到同样的问题,因此从内核的角度来看,将其视为只写。 永远不要读回你写在那里的任何数据。(写时复制映射对输出结构没有意义。)