How to memory-map a PCI BAR using PCIDriverKit?
Asked Answered
G

1

9

How to memory-map a PCI Base Address Register (BAR) from a PCIDriverKit driver (DEXT) to a userspace application?

Memory-mapping from a driver extension to an application can be accomplished by implementing the IOUserClient::CopyClientMemoryForType in the user client subclass (on the driver side) and then calling IOConnectMapMemory64 (from the user-space application side). This has been very nicely and thoroughly explained in this related answer.

The only missing bit is getting an IOMemoryDescriptor corresponding to the desired PCI BAR in order to return it from the CopyClientMemoryForType implementation.

Sample code

Asked another way, given the following simplified code, what would be the implementation of imaginaryFunctionWhichReturnsTheBARBuffer?

kern_return_t IMPL(MyUserClient, CopyClientMemoryForType) //(uint64_t type, uint64_t *options, IOMemoryDescriptor **memory)
{
    IOMemoryDescriptor* buffer = nullptr;
    
    imaginaryFunctionWhichReturnsTheBARBuffer(ivars->pciDevice /* IOPCIDevice */, kPCIMemoryRangeBAR0, &buffer);

    *memory = buffer;

    return kIOReturnSuccess;
}

In the previous code ivars->pciDevice refers to a ready-to-use IOPCIDevice (e.g.: it has been succesfully matched, opened and configured according to latest best practices).

This means it's already possible to use the various configuration and memory read/write methods to access explicit offsets from the desired PCI BAR memory. What's missing (or unclear) is how to use those APIs (or equivalent ones) to map the entire buffer corresponding to a PCI BAR to a user-space application.

Random notes that might or might not be relevant

  • This answer from the related question: How to allocate memory in a DriverKit system extension and map it to another process? contains the following quote:

    [...] The returned memory descriptor need not be an IOBufferMemoryDescriptor as I've used in the example, it can also be a PCI BAR or whatever.

    This is exactly what I want to do, so at least it sounds like it should be possible. The only remaining question is how to implement it.

  • A similar question has been posted on the Apple forums and even though it hasn't received any answers yet (as of the time of this writing), it does contain some useful pointers.

    It looks like there is a private function in PCIDriverKit with the following signature:

    virtual kern_return_t IOPCIDevice::_CopyDeviceMemoryWithIndex(uint64_t memoryIndex, IOMemoryDescriptor** memory, IOService* forClient)
    

    It's hard to tell what it's supposed to do exactly (since it is undocumented), but the signature does seem to match with the function I'm looking for. However, trying to use it always results in an error code (similar to the one reported in the original forum post). The error code seems to be different depending on which argument is passed as IOService *forClient).

    Another good point from that post is that there is a getDeviceMemoryWithIndex available to kernel extensions (KEXT) which could be used to accomplish what we need (if the driver was implemented as a PCI kernel extension which seems to be deprecated now).

    However, this function doesn't seem to be available to driver extensions (DEXT). So another question of framing this question could be: What's the equivalent getDeviceMemoryWithIndex for PCIDriverKit driver extensions?

Girlish answered 12/11, 2021 at 17:29 Comment(0)
G
1

Turns out IOPCIDevice::_CopyDeviceMemoryWithIndex was indeed the function needed to implement this (but the fact that it's private is still an inconvenient).

Sample code

Bellow is some sample code showing how this could be implemented (the code uses MyDriver for the driver class name and MyDriverUserClient for the user client).

Relevant sections from MyDriver.cpp implementation:

struct MyDriver_IVars {
    IOPCIDevice* pciDevice = nullptr;
};

// MyDriver::init/free/Start/Stop/NewUserClient implementation ommited for brevity

IOMemoryDescriptor* MyDriver::copyBarMemory(uint8_t barIndex)
{
    IOMemoryDescriptor* memory;
    uint8_t barMemoryIndex, barMemoryType;
    uint64_t barMemorySize;

    // Warning: error handling is omitted for brevity
    ivars->pciDevice->GetBARInfo(barIndex, &barMemoryIndex, &barMemorySize, &barMemoryType);
    ivars->pciDevice->_CopyDeviceMemoryWithIndex(barMemoryIndex, &memory, this);

    return memory;
}

Relevant sections from MyDriverUserClient.cpp implementation:

struct MyDriverUserClient_IVars {
    MyDriver* myDriver = nullptr;
};

// MyDriverUserClient::init/free/Start/Stop implementation ommited for brevity

kern_return_t
IMPL(MyDriverUserClient, CopyClientMemoryForType) //(uint64_t type, uint64_t *options, IOMemoryDescriptor **memory)
{
    *memory = ivars->myDriver->copyBARMemory(kPCIMemoryRangeBAR0);

    return kIOReturnSuccess;
}

Additional Resources

A complete implementation that uses this pattern can be found in the ivshmem.dext project (which implements a macOS driver for IVSHMEM).

Girlish answered 16/1, 2022 at 17:1 Comment(7)
Are you sure this works on ARM64 based Macs? I was under the impression that memory-mapping BARs wasn't possible there.Icebreaker
That's a very good point and you may be right but unfortunately I don't have an arm64 mac to test this on. For my use-case (the linked IVHSMEM driver), I'm running macOS on an x64 virtual machine and it seems to work fine.Girlish
@Icebreaker Do you happen to know an alternative way to do this memory mapping that would work on arm64 mac too? Or maybe there is some other way of copying large amounts of memory to and from a PCI BAR? I'm all very new to these topics and I've been scrambling to come up with a good solution for this that works everywhere (and ideally without resorting to private APIs). By the way, I was secretly hoping you'd pop by and provide an answer since you seem to be the authority on these issues on Stack Overflow. Thanks for all the valuable information you are providing on these topics!Girlish
Having ported some kexts to arm64e, the only BAR accesses I could find are the MemoryRead* and MemoryWrite* family of methods on IOPCIDevice. To be fair this makes sense in terms of what's happening on the bus as far as I'm aware. If your device is expecting to exchange large amounts of data via BARs, this is unfortunate, because that's really what DMA is for. And DMA works as before on ARM. (IODMACommand etc.)Icebreaker
(I'm not deeply familiar with how the IVSHMEM device works, though glancing at the spec, it appears that DMA is indeed not supported. Strange choice given that virtio devices for example are heavily DMA based.)Icebreaker
That makes sense. In that case I might open an issue with the QEMU project and see if there are any plans to add support for DMA to the IVSHMEM device. Thanks again for your input!Girlish
I suspect it might not even be that hard to write a patch to Qemu implementing this as an optional mode of operation. (I've written the odd patch to Qemu myself.)Icebreaker

© 2022 - 2024 — McMap. All rights reserved.