IOServiceOpen fails with -308 Error (smUnExBusError)

I have a driver project where I'm opening and closing a connection to a custom driver.

If I do what I think I need to be doing to unmap the memory, when I try to open the service again, it fails. If I skip the step where I do that unmapping, the service opens successfully.

If I call unmap() before trying to call openConnection() again, it will fail with a -308 error return code. If I skip that call to unmap(), it works and I'm able to communicate with my device.

Here's the code where I open the service:

public func openConnection() throws {
        guard !isOpen else { return }
        
        // Open device
        var connection: io_connect_t = IO_OBJECT_NULL
        var result = IOServiceOpen(device, mach_task_self_, 0, &connection)
            if result != kIOReturnSuccess {
                NSLog("Failed opening device with error: 0x%08x.\n", result);
                throw NSError.cdc_kernelReturnErrorWithError(result)
            }

        defer { IOConnectRelease(connection) }

        if device == IO_OBJECT_NULL || connection == IO_OBJECT_NULL {
            throw NSError.cdc_kernelReturnErrorWithError(result)
        }
                
        let receiveDataMappedMemory = ClientDriverMappedMemory(connection: connection, memoryType: MappedMemoryType_ReceiveDataBuffer)
        try receiveDataMappedMemory.map()
        
        let transmitDataMappedMemory = ClientDriverMappedMemory(connection: connection, memoryType: MappedMemoryType_TransmitDataBuffer)
        try transmitDataMappedMemory.map()
        
        // Setup async notification
        IONotificationPortSetDispatchQueue(dataReceivedPort, dataReceivedQueue)
        let callbackPort = IONotificationPortGetMachPort(dataReceivedPort)
        let input = DataStruct(foo: 0, bar: 0)
        var output = DataStruct(foo: 0, bar: 0)
        var outputSize = MemoryLayout<DataStruct>.size
        // Trampoline to C function because I don't quite know how to make this work in Swift
        result = setupCallback(self, connection, callbackPort, input, &output, &outputSize)
        if result != kIOReturnSuccess {
            NSLog("Error registering async callback with driver: \(result)");
            throw NSError.cdc_kernelReturnErrorWithError(result)
        }
        
        self.connection = connection
        self.receivedDataMappedMemory = receiveDataMappedMemory
        self.transmitDataMappedMemory = transmitDataMappedMemory
    }

map() and unmap() functions:

- (BOOL)mapWithError:(NSError **)error
{
    error = error ?: &(NSError * __autoreleasing){ nil };
    kern_return_t result = IOConnectMapMemory64(self.connection,
                                                self.memoryType,
                                                mach_task_self(),
                                                &_address,
                                                &_size,
                                                kIOMapAnywhere);
    if (result != kIOReturnSuccess) {
        *error = [NSError cdc_kernelReturnErrorWithError:result];
        return NO;
    }
    self.mapped = YES;
    return YES;
}

- (BOOL)unmapWithError:(NSError **)error
{
    error = error ?: &(NSError * __autoreleasing){ nil };
    kern_return_t result = IOConnectUnmapMemory64(self.connection,
                                 self.memoryType,
                                 mach_task_self(),
                                 _address);
    if (result != kIOReturnSuccess) {
        *error = [NSError cdc_kernelReturnErrorWithError:result];
        return NO;
    }
    self.mapped = NO;
    return YES;
}

Any insights? What all should I be doing to close the service? Why would the unmapping create this issue or what else could the -308 error be indicated has gone wrong?

If I do what I think I need to be doing to unmap the memory, when I try to open the service again, it fails. If I skip the step where I do that unmapping, the service opens successfully.

Are you saying everything works if you don't unmap the memory? That is, when you open the device again without attempting to un-map memory, can you communication successfully to the device and proceed as normal? The way this is worded, it is unclear to me.

Yes, after successfully opening the service I am able to communicate with the device without any issues.

If I do what I think I need to be doing to unmap the memory, when I try to open the service again, it fails. If I skip the step where I do that unmapping, the service opens successfully.

What is your driver doing with all this? Un-mapping is a bi-directional process and I'd generally expect the driver to be doing it's on unmapping regardless of what the client does. Keep in mind that, on the driver/kernel side, failing to unmap could potentially leak kernel memory and the driver cannot ensure that the client will be able to unmap (the app could crash). IOConnectUnmapMemory64 primarily exists for case where user clients are specifically shifting large buffers in and out of the kernel, so the buffer needs to be unmapped on return. For fixed buffers, it's not unreasonable for the client to simply close it's buffer and the the KEXT/DEXT deal with it. Keep in mind that KEXT/DEXT needs to deal with this case anyway, since the same situation would happen if the user space process crashed:

Also, what error does IOServiceOpen return when it fails?

Related error codes:

If I call unmap() before trying to call openConnection() again, it will fail with a -308 error return code. If I skip that call to unmap(), it works and I'm able to communicate with my device.

If the error code was specifically "-308", then I don't think that error came from the system/kernel. The kernel, particularly, IOKit uses a "structured" error format built around hexadecimal, which why is values tend to look a little "crazy" in decimal.

So, for example, the defintion of kIOReturnBusy:

#define kIOReturnBusy            iokit_common_err(0x2d5) // Device Busy

decimal -> -536870187
hex     -> 0xE00002D5

There's even a system that mapps "errno" codes into the kernel system:

#define unix_err(errno)         (err_kern|err_sub(3)|errno)

unix_err (EPERM);

decimal -> 49153
hex     -> 0xC001

Now, as it happens, IOConnectMapMemory64 & IOConnectUnmapMemory64 are actually relatively "thin" wrappers around underlying functions inside the specific IOUserClient implementation. Note the description in the documentation:

"This is a generic method to create a mapping in the callers task. The family will interpret the type parameter to determine what sort of mapping is being requested. Cache modes and placed mappings may be requested by the caller."

It's vague because the underlying UserClient doesn't really have to "do" anything. What actually happens in both case is that the IOUserClient base implementation calls:

IOUserClient (Kernel) -> clientMemoryForType

IOUserClient (DEXT) -> CopyClientMemoryForType

...and the concrete UserClient implementation then returns the relevant IOMemoryDescriptor (or an error...). The base class implementation then does the work of mapping/unmapping that descriptor in/out of the target process.

However, the error code is the critical detail here. The only way I can see -308 being returned here is because that's what the concrete implementation of clientMemoryForType/CopyClientMemoryForType returned, since that error will then be returned directly to user space.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

IOServiceOpen fails with -308 Error (smUnExBusError)
 
 
Q