DriverKit - single process with multiple services

I'm working on custom solution that uses USB device/interface drivers.
For correct setup I need ability to communicate between my own Services, declared in IOKit Personalities of my DEXT.

At the moment I'm seeing the one way to do it via some shared state. But DriverKit by default launches each USB service in separate process when device is connected.

Documentation says that there is "IOUserServerOneProcess" key could be declared in Info.plist. But seems it does not work: all my USB services run in different processes.

Could anybody suggest a way how to interact between own DriverKit services or run them in context of the single process?
IOUserServerOneProcess only coalesces dexts with the same provider, so you'll only see it if all of these dexts have the same provider.

If all of these dexts are under a singular USB device, then you can match to the device and manually match to all of the interfaces in your Start implementation. By creating your interface instances manually instead of matching via plist, then it will all be running in a single process and it should be easy to transfer data around.

Alternatively, or if they aren't all under the same device, you should be able to use an IOUserClient to communicate between the different dexts. See https://developer.apple.com/documentation/driverkit/iouserclient.

I'm working of functional like USB device filtering. So it adds some restrictions

  • I have to make a decision about plugged device before Start succeeds or fails
  • IOUserClient, as I understood, used for App<->DEXT communication, but not DEXT<->DEXT. Am I right?

Conceptually, I need some way to

  • prepare IOService kind of IOResources that starts on DEXT bootstrap and can be reached within the App
  • each time USB device is connected, I need to obtain set of rules to filter particular USB device/interface.

This can be achieved by communicating inside DEXT if I could have such mechanism

I'll refrain from giving specific advice on how you should meet your goals, since I'm not sure I fully understand, but inter-dext communication is possible in a few different ways, depending on what you want to achieve.

First, you'll need to make sure that you have an IODispatchQueue in your dext. You'll be using this to get notifications about other dexts that are available. Then create a matching dictionary for your other dexts using something like CreateUserClassMatchingDictionary. Then create an IOServiceNotificationDispatchSource with your notification queue and matching dictionary. Then you can create a new OSAction with CreateActionServiceNotificationReady to get a notification when a new service is ready. You'll then need to use your IOServiceNotificationDispatchSource to handle that action. This process should look relatively similar to how you would do something similar in IOKit. Here's a highly simplified code example:

struct ParentDext_IVars
{
    IOServiceNotificationDispatchSource* fServiceNotifier;
    OSAction*           fServiceNotificationAction;
    IODispatchQueue*    fServiceNotificationQueue;

    // ...
};

// ...

kern_return_t ret = kIOReturnSuccess;
OSDictionaryPtr dict = NULL;

ret = IODispatchQueue::Create("Notify", 0, 0, &ivars->fServiceNotificationQueue);
// Error check

ret = SetDispatchQueue("Notify", ivars->fServiceNotificationQueue);
// Error check

dict = CreateUserClassMatchingDictionary("ChildDext", NULL);
// Check that dict is non-null

ret = IOServiceNotificationDispatchSource::Create(dict, 0, ivars->fServiceNotificationQueue, &ivars->fServiceNotifier);
// Error check

ret = CreateActionServiceNotificationReady(0, &ivars->fServiceNotificationAction);
// Error check

ret = ivars->fServiceNotifier->SetHandler(ivars->fServiceNotificationAction);
// Error check

From here, you'll need a function to handle your IOServiceNotificationDispatchSource callbacks. Define a function like so in your ParentDext.iig file:

virtual void ServiceNotificationReady(OSAction* action) TYPE(IOServiceNotificationDispatchSource::ServiceNotificationReady) QUEUENAME(Notify);

Note the QUEUENAME and how it matches to the queue name we made in the code. The TYPE macro is also important for denoting that this function is a callback for a IOServiceNotificationDispatchSource::ServiceNotificationReady action. The CreateActionServiceNotificationReady function was created because of the name of this function and its macros. Note that if you change the name of the function, that call will also change to CreateActionMyFuncName.

Finally, implement this function, and you will have access to communicate with other dexts:

void ParentDext::ServiceNotificationReady_Impl(OSAction* action)
{
    kern_return_t ret = ivars->fServiceNotifier->DeliverNotifications(^(uint64_t type, IOService* service, uint64_t options)
    {
        // ...

        service->retain();

        // ...

        ChildDext* childDextInstance = OSDynamicCast(ChildDext, service);

        // ...
    });
}

That should cover your need for Dext to Dext communication. You can extrapolate this method for peer-to-peer if you prefer that, instead of a parent aggregator.

@Drewbadour , followed your advice. Created for test purposes DEXT with 2 IOServices of IOUserResources kind.

Now facing two problems:

  1. Somewhy they are launched in separate processes. I.e. after DEXT is loaded, there are two instances of DEXT process, one per IOService. Doy you know what IOPersonalities keys should match to force DEXTs to be loaded as single process?

  2. ServiceNotificationReady callback is not called. Example above is replicated accurately. Can you suggest any ideas why?

Accepted Answer

I've revised my previous answer to try and collate all of the knowledge in the comments into a single post, for clarity.

Cross-Dext Communication

Parent Dext Setup

Both the parent and the child dexts require special setup, so we'll cover the parent first.

Initializing the Matching Mechanism

First, you'll need to make sure that you have an IODispatchQueue in your dext. You'll be using this to get notifications about other dexts that are available. Then create a matching dictionary for your other dexts using something like CreateUserClassMatchingDictionary. Then create an IOServiceNotificationDispatchSource with your notification queue and matching dictionary. Then you can create a new OSAction with CreateActionServiceNotificationReady to get a notification when a new service is ready. You'll then need to use your IOServiceNotificationDispatchSource to handle that action. This process should look relatively similar to how you would do something similar in IOKit. Here's a highly simplified code example:

struct ParentDext_IVars
{
    IOServiceNotificationDispatchSource* fServiceNotifier;
    OSAction*           fServiceNotificationAction;
    IODispatchQueue*    fServiceNotificationQueue;

    // ...
};

// ...

ParentDext::Start_Impl(IOService* provider) // Can also use IMPL macro
{
    kern_return_t ret = kIOReturnSuccess;
    OSDictionaryPtr dict = NULL;

    ret = IODispatchQueue::Create("Notify", 0, 0, &ivars->fServiceNotificationQueue);
    // Error check

    ret = SetDispatchQueue("Notify", ivars->fServiceNotificationQueue);
    // Error check

    dict = CreateUserClassMatchingDictionary("ChildDext", NULL);
    // Check that dict is non-null

    ret = IOServiceNotificationDispatchSource::Create(dict, 0, ivars->fServiceNotificationQueue, &ivars->fServiceNotifier);
    // Error check

    ret = CreateActionServiceNotificationReady(0, &ivars->fServiceNotificationAction);
    // Error check

    ret = ivars->fServiceNotifier->SetHandler(ivars->fServiceNotificationAction);
    // Error check

    // The rest of your Start method

    // You *must* register this service and all child services, otherwise you will get strange behavior.
    RegisterService();
}

Handling New Dext Callbacks

From here, you'll need a function to handle your IOServiceNotificationDispatchSource callbacks. Define a function like so in your ParentDext.iig file:

virtual void ServiceNotificationReady(OSAction* action) TYPE(IOServiceNotificationDispatchSource::ServiceNotificationReady) QUEUENAME(Notify);

Note the QUEUENAME and how it matches to the queue name we made in the code. The TYPE macro is also important for denoting that this function is a callback for a IOServiceNotificationDispatchSource::ServiceNotificationReady action. The CreateActionServiceNotificationReady function was created because of the name of this function and its macros. Note that if you change the name of the function, that call will also change to CreateActionMyFuncName.

Implement this function, and you will have access to communicate with other dexts:

void ParentDext::ServiceNotificationReady_Impl(OSAction* action)
{
    kern_return_t ret = ivars->fServiceNotifier->DeliverNotifications(^(uint64_t type, IOService* service, uint64_t options)
    {
        // ...

        service->retain();

        // ...

        ChildDext* childDextInstance = OSDynamicCast(ChildDext, service);

        // ...
    });
}

Child Dext Setup

There's a couple things you'll need to do in your child dext to make sure that everything will work appropriately, but thankfully it's relatively simple.

Make sure that both dexts have the same IOUserServerName property so they can see each other.

Special IIG Setup

Add the REMOTE keyword to your definition of your class in the .iig like so:

class REMOTE ChildDext: public IOService {

This allows a dext to be called remotely. Otherwise you will see issues with references not casting correctly.

Creating a Link Between Dexts

Declare a function in your child dext to receive a call from the parent dext and set a reference, so the child dext can communicate to the parent dext. For this example, it will be called Connect. The definition in the .iig:

kern_return_t Connect(IOService* peerService) LOCALONLY;

The implementation is relatively straightforward:

kern_return_t PeerDextB::Connect(IOService* peerService)
{
    Log("Connect()");

    ivars->peerDext = OSDynamicCast(PeerDextA, peerService);

    if (ivars->peerDext == nullptr)
    {
        Log("Connect() - Peer dext is null.");
        return kIOReturnBadArgument;
    }

    // Retain an instance of the peer dext so it can be accessed later.
    ivars->peerDext->retain();

    return kIOReturnSuccess;
}

This should complete the requirements to have two dext communicate with each other.

One Process

Add this key to both IOKitPersonalities entries:

<key>IOUserServerOneProcess</key>
<true/>

This is equivalent to IOUserServerOneProcess - Boolean - 1, if you are looking at it in the visual editor.

Note that this key was added as part of macOS 12 Monterey, so you will need to use Monterey to combine dexts in this manner.

Eratta

  • Previously in comments I had mentioned that C-style casting was acceptable. You are likely to have issues if you choose to do this long term. Please stick to OSDynamicCast as much as possible.
  • Calls from parent to child and child to parent follow simple C-style function call parameters. No further special actions are required.
  • Make sure that both dexts call RegisterService() in their Start implementations, otherwise they will not be able to find each other.
DriverKit - single process with multiple services
 
 
Q