Search code examples
c++xcodemacoskernel-extension

How to use deviceRequest of IOUSBHostDevice / IOUSBHostInterface?


I'm currently experimenting with Apple's I/O Kit to develop a kernel module. I'm trying to create a device driver for a USB Wireless Card (TP-Link WN722N-v1) using IOUSBHostDevice. I managed to get the kext loaded and it probes correctly with the card, but I need to send the firmware to the card, which I am trying to do in probe(). I don't have the perfect knowledge of how everything goes, but it seems that I need to send a request to the configuration endpoint 0, but the ways I have tried I get kernel panic, and the other examples I have are using an obsolete DeviceRequest with no exchangeable arguments with the new deviceRequest. I have a reference for writing the firmware from github project "BrcmPatchRAM" but all the forks use old IOUSBInterface instead of IOUSBHostInterface. I have search this site for similar questions but found none helpful in this matter, I also searched Apple developer website documentation and it references also the old ways of DeviceRequest. Which is the right way of re-writing this for example:

IOReturn USBInterfaceShim::hciCommand(void* command, UInt16 length)
{
    IOUSBDevRequest request =
    {
        .bmRequestType = USBmakebmRequestType(kUSBOut, kUSBClass, kUSBDevice),
        .bRequest = 0,
        .wValue = 0,
        .wIndex = 0,
        .wLength = length,
        .pData = command
    };
    return m_pInterface->DeviceRequest(&request);
}

Using some other implementation of this function gives me an error when loading the driver and tells me that I am sending the request before calling prepare, but I did called prepare before sending the request.

Here is a section of my messy code:

<pre>IOReturn AirPort_Atheros9271::pipeCommand(UInt8 requestType, UInt8 command, UInt16 address, IOMemoryDescriptor *buffer)
{
    DeviceRequest request;
    request.bmRequestType = requestType;
    request.bRequest = command;
    request.wValue = address;
    request.wIndex = 0;
    request.wLength = buffer->getLength();

    uint32_t bytesTransferred;
    return fInterface->deviceRequest(request, buffer, bytesTransferred, kUSBHostStandardRequestCompletionTimeout);
}

IOReturn AirPort_Atheros9271::pipeCommand(UInt8 requestType, UInt8 command, UInt16 address, void *buffer, UInt16 length)
{
    DeviceRequest request;
    request.bmRequestType = requestType;
    request.bRequest = command;
    request.wValue = address;
    request.wIndex = 0;
    request.wLength = length;

    uint32_t bytesTransferred;
    return fInterface->deviceRequest(request, buffer, bytesTransferred, kUSBHostStandardRequestCompletionTimeout);
}

bool TL_WN722N::performUpgrade()
{
    IOLog("TL_WN722N::[%04x:%04x]: Performing firmware upgrade.\n", fVendorId, fProductId);

    OSData *firmwareData = OSData::withBytes(ar9271_fw, ar9271_fw_len);
    IOMemoryDescriptor *buffer = IOMemoryDescriptor::withAddress((void*)firmwareData->getBytesNoCopy(), firmwareData->getLength(), kIODirectionIn);
    bool success = true;
    IOLog("TL_WN722N::[%04x:%04x]: I have firmwareData and created the buffer\n", fVendorId, fProductId);
    IOLockLock(fCompletionLock);

    IOReturn result;
    if (buffer!=NULL)
    {
        IOLog("TL_WN722N::[%04x:%04x]: Buffer is not null, now calling prepare()\n", fVendorId, fProductId);
        if ((result = buffer->prepare(kIODirectionNone)) == kIOReturnSuccess)
        {
            IOLog("TL_WN722N::[%04x:%04x]: prepare() called and pass! Piping I - writing firmware\n", fVendorId, fProductId);
            IOLog("TL_WN722N::[%04x:%04x]: Resultado de prepare: Result: %d\n", fVendorId, fProductId,result);

            IOLog("TL_WN722N::[%04x:%04x]: About to excecute pipeCommand\n", fVendorId, fProductId);
            result = pipeCommand(0x40, FIRMWARE_DOWNLOAD, AR9271_FIRMWARE >> 8, buffer); //TODO: EXPLOTION LINE
            if (result>0) {
                IOLog("TL_WN722N::[%04x:%04x]: Resultado de pipeCommand: %d\n", fVendorId, fProductId,result);
            }

            if (result != kIOReturnSuccess)
                IOLog("TL_WN722N::[%04x:%04x]: Unable to write the firmware (0x%08x).\n", fVendorId, fProductId, result);
            else
            {
                if ((result = pipeCommand(0x40, FIRMWARE_DOWNLOAD_COMP, AR9271_FIRMWARE_TEXT >> 8, NULL, 0)) != kIOReturnSuccess)
                    IOLog("TL_WN722N::[%04x:%04x]: Unable to write the firmware complete sequence (0x%08x).\n", fVendorId, fProductId, result);
                else
                {
                    IOLog("TL_WN722N::[%04x:%04x]: Success in writing the firmware sequence.\n", fVendorId, fProductId);
                    success = true;
                }
            }
        }
        else
            IOLog("TL_WN722N::[%04x:%04x]: Failed to prepare write memory buffer (0x%08x).\n", fVendorId, fProductId, result);

        if ((result = buffer->complete()) != kIOReturnSuccess)
            IOLog("TL_WN722N::[%04x:%04x]: Failed to complete write memory buffer (0x%08x).\n", fVendorId, fProductId, result);


    }
    else
        IOLog("TL_WN722N::[%04x:%04x]: Unable to allocate write memory buffer.\n", fVendorId, fProductId);

    IOLockUnlock(fCompletionLock);
    OSSafeReleaseNULL(buffer);
    OSSafeReleaseNULL(firmwareData);
    return success;
}</pre>

Solution

  • Putting together a few pieces here:

    Using some other implementation of this function gives me an error when loading the driver and tells me that I am sending the request before calling prepare, but I did called prepare before sending the request.

    This gives us a pretty big clue that your issue is with the memory buffer. So let's see where the creation and prepare() are happening:

        IOMemoryDescriptor *buffer = IOMemoryDescriptor::withAddress(
            (void*)firmwareData->getBytesNoCopy(), firmwareData->getLength(),
            kIODirectionIn);
        //  ^^^^^^^^^^^^^^^^
    

    So you're creating an input (read) memory descriptor.

            if ((result = buffer->prepare(kIODirectionNone)) == kIOReturnSuccess)
    

    kIODirectionNone just uses the creation direction, so you're preparing for reading data from the device.

    And then, the I/O:

                result = pipeCommand(0x40, FIRMWARE_DOWNLOAD, AR9271_FIRMWARE >> 8, buffer); //TODO: EXPLOTION LINE
        // --------------------------^^^^
    

    This means your bmRequestType is 0x40, and therefore does not have the 0x80 bit set. This means it's host-to-device, i.e. an output/write.

    So you've got a direction mismatch: you're preparing some memory for reading from the USB device and then try to use it to write to the device. This won't work.

    A few more comments:

    • An OSData isn't really an ideal candidate for I/O buffer allocation. If you want to allocate system memory for I/O, use an IOBufferMemoryDescriptor. If ar9271_fw is a statically allocated array, you can also simply wrap it using IOMemoryDescriptor::withAddress() - this avoids the copy that OSData::withBytes() performs.
    • You're performing I/O while holding the IOLockLock(fCompletionLock); lock. This isn't a great idea even when launching async I/O, but it looks like you're using the blocking version of deviceRequest? That's almost certainly not what you want: the function call can literally take seconds to complete or fail. You should not be holding locks for that long.