Skip to content

armadsen/CommunicatingBetweenADriverKitExtensionAndAClientApp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

This is a modified copy of Apple's sample code. It's not intended as a reference for others (it doesn't work right) rather as reference for a Stack Overflow question I asked trying to solve a problem with installing a DriverKit driver on iPadOS. Original README below:

Communicating Between a DriverKit Extension and a Client App

Send and receive different kinds of data securely by validating inputs and asynchronously by storing and using a callback.

Overview

This sample code project shows how a DriverKit extension (dext) receives data from a C++ client process. The sample handles both scalar data and structures, and has two code paths for each type: an insecure version, and a "checked" version that validates traits like data size and input count.

The sample also demonstrates registering and executing a callback function, so the driver can call the client asynchronously.

The sample project contains three targets:

  • DriverKitSampleApp - A macOS app, written with SwiftUI, to install or update the driver.
  • NullDriver - The dext itself, which responds to client calls and optionally checks that each call sends the expected data.
  • CppUserClient - A terminal-based client application that calls the driver.

Configure the Sample Code Project

To run the sample code project, you first need to build and run DriverKitSampleApp, which installs the driver. After that, you can build and run CppUserClient, which calls the newly-installed driver.

You can set up the project to build with or without entitlements. To build without entitlements, do the following:

  1. Temporarily turn off SIP, as described in the article Disabling and Enabling System Integrity Protection. After you've done this, confirm that SIP is disabled with the Terminal command csrutil status, and enter dext development mode with systemextensionsctl developer on, as described in the article Debugging and Testing System Extensions.
  2. Select the DriverKitUserClientSample project and use the "Signing & Capabilities" tab to set the DriverKitSampleApp and CppUserClient targets to automatically managed code signing.
  3. While still in the "Signing & Capabilities" tab, set the NullDriver target to manual code signing.
  4. In the "Build Settings" tab, change the "Code Signing Identity" value to "Sign to Run Locally" for all three targets.

If, instead, you want to build manually with entitlements, do the following:

  1. Choose new bundle identifiers for the app, driver, and client. The bundle identifiers included with the project already have App IDs associated with them, so you need unique identifiers to create your own App IDs. Use a reverse-DNS format for your identifier, as described in Preparing Your App For Distribution.
  2. In DriverLoadingViewModel.swift, edit the definition of dextIdentifier to use the string you chose for your driver's bundle identifier.
  3. In Xcode's Project navigator, choose the project and use the Signing & Capabilities tab to replace the existing bundle identfier for each of the targets with your chosen identifier.
  4. Request entitlements, as described in Requesting Entitlements for DriverKit Development. For DriverKitSampleApp, you need the com.apple.developer.system-extension.install entitlement. For NullDriver, you need the com.apple.developer.driverkit entitlement. For CppUserClient you need com.apple.developer.driverkit.userclient-access entitlement, for which you need to need to provide your driver's chosen bundle identifier when you make the request.
  5. On developer.apple.com, select Account and visit the "Certificates, Identifiers, and Profiles" section. Select "Identifiers" and create new App IDs for DriverKitSampleApp, NullDriver, and CppUserClient. For the Bundle ID, choose "explicit", and use the names you chose in the first step. When you reach the "Capabilities" step, DriverKitSampleApp needs the "System Extension" capability, and both NullDriver and CppUserClient need the "DriverKit" capability.
  6. For each of the App IDs you created in the previous step, select "Profiles" to create a new provisioning profile. Start by choosing "macOS App Development," and then "Mac" for the profile type. Next, add any certificates and devices you want to include in the profile. Finally, add the DriverKit entitlement to the profile.
  7. Download each profile and add it to Xcode.
  8. In the "Signing & Capabilities" tab, set each target to manual code signing and select the newly-created profile.
  9. In the CppUserClient.entitlements file, edit the key com.apple.developer.driverkit.userclient-access. For the key's value, enter the bundle identifier you chose for your driver, either as a string value or a one-item array of strings.
  10. If you want to run DriverKitSampleApp directly from Xcode, enter dext development mode with systemextensionsctl developer on, as described in the article Debugging and Testing System Extensions. Alternately, you can drag the built DriverKitSample.app from the build directory into the /Applications directory and run it from there.

Use the System Extensions Framework to Install the Driver Extension

The DriverKitSampleApp target declares the NullDriver as a dependency, so building the app target builds the dext and its installer together. When run, the DriverKitSampleApp shows a single window with an "Install Dext" button.

To install the dext, the app uses the System Extensions framework to install and activate the dext, as described in Installing System Extensions and Drivers.

let request = OSSystemExtensionRequest
    .activationRequest(forExtensionWithIdentifier: dextIdentifier,
                       queue: .main)
request.delegate = self
OSSystemExtensionManager.shared.submitRequest(request)

View in Source

  • Note: This call may prompt a "System Extension Blocked" dialog, which explains that DriverKitSampleApp tried to install a new system extension. To complete the installation, open System Preferences and go to the Security & Privacy pane. Unlock the pane if necessary, and click "Allow" to complete the installation. To confirm installation of the NullDriver extension, run systemextensionsctl list in Terminal.

Call the Driver from the Client

The CppUserClient target is a command-line app that connects to and communicates with the dext. When run from Xcode, it accepts input from inside the Xcode Console. The client app, in main.cpp, consists of a main() function that creates a connection to the driver, receives keyboard input with scanf, and makes calls to the driver.

To connect to the driver, the client starts by declaring the name of the driver to search for, as well as variables for discovering over services, iterating over them, and establishing the connection to the driver.

static const char* dextIdentifier = "NullDriver";

kern_return_t ret = kIOReturnSuccess;
io_iterator_t iterator = IO_OBJECT_NULL;
io_service_t service = IO_OBJECT_NULL;
io_connect_t connection = IO_OBJECT_NULL;

View in Source

The app then uses IOServiceGetMatchingServices to get an iterator of services matching the dext identifier. It iterates over matching services until it finds one that it can connect to with IOServiceOpen.

ret = IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceNameMatching(dextIdentifier), &iterator);
if (ret != kIOReturnSuccess)
{
    printf("Unable to find service for identifier with error: 0x%08x.\n", ret);
    PrintErrorDetails(ret);
}

printf("Searching for dext service...\n");
while ((service = IOIteratorNext(iterator)) != IO_OBJECT_NULL)
{
    // Open a connection to this user client as a server to that client, and store the instance in "service"
    ret = IOServiceOpen(service, mach_task_self_, kIOHIDServerConnectType, &connection);

    if (ret == kIOReturnSuccess)
    {
        printf("\tOpened service.\n");
        break;
    }
    else
    {
        printf("\tFailed opening service with error: 0x%08x.\n", ret);
    }

    IOObjectRelease(service);
}
IOObjectRelease(iterator);

if (service == IO_OBJECT_NULL)
{
    printf("Failed to match to device.\n");
    return EXIT_FAILURE;
}

View in Source

As soon as the client populates its connection variable, it can accept user commands from the keyboard input. The command menu looks like the following:

1. Scalar
2. Struct
3. Large Struct (structureInputDescriptor flow)
4. Checked Scalar
5. Checked Struct
6. Assign Callback to Dext
7. Async Action
0. Exit
Select a message type to send: 

Options 1 through 5 exercise different code paths that send scalar values and structures to the dext. Note that these are synchronous calls that block until the driver returns a result. Options 6 and 7 perform asynchronous operations that allow the driver to call back to the client after a delay.

Each of these options uses the connection in calls to IOConnectCallScalarMethod, IOConnectCallStructMethod, and IOConnectCallAsyncStructMethod (or IOConnectCallMethod and IOConnectCallAsyncMethod, which this sample doesn't use). For example, the following listing shows the scalar call, option 1 in the menu, which sends an array of 16 uint64_t values, and receives a different array back.

kern_return_t ret = kIOReturnSuccess;

// IOConnectCallScalarMethod will fail intentionally for any inputCount or outputCount greater than 16
const uint32_t arraySize = 16;
const uint64_t input[arraySize] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };

uint32_t outputArraySize = arraySize;
uint64_t output[arraySize] = {};

ret = IOConnectCallScalarMethod(connection, MessageType_Scalar, input, arraySize, output, &outputArraySize);
if (ret != kIOReturnSuccess)
{
    printf("IOConnectCallScalarMethod failed with error: 0x%08x.\n", ret);
    PrintErrorDetails(ret);
}

View in Source

The other options are all similar, differing only in which IOConnect... function they call and the type of data they send.

Validate Arguments to Driver Function Calls

The NullDriver receives calls from the client in its overridden ExternalMethod method. Options 1 through 3 in the client app perform calls that the driver passes unchecked to its ExternalMethod implementation. In practice, it's important that a driver validates its inputs before passing them along, to make sure the data is the expected size and contains reasonable values. NullDriver has functions that check scalar and struct calls, which are exercised by options 4 and 5 in the client app.

The "checked" methods in NullDriver use an IOUserClientMethodDispatch instance to describe the expected fields of the IOUserClientMethodArguments. The sample code stores these dispatch instances in an array called externalMethodChecks. For example, the dispatch instance for the checked scalar call (option 4 in the client) expects to receive and return 16 scalar values, as seen below:

[ExternalMethodType_CheckedScalar] =
{
    .function = (IOUserClientMethodFunction) &NullDriver::StaticHandleExternalCheckedScalar,
    .checkCompletionExists = false, // Since this call doesn't use a callback, this value is false and IOUserClientMethodArguments.completion must be 0.
    .checkScalarInputCount = 16,
    .checkStructureInputSize = 0,
    .checkScalarOutputCount = 16,
    .checkStructureOutputSize = 0,
},

View in Source

After fetching the appropriate IOUserClientMethodDispatch instance from the array, the driver passes it in its call to the superclass's ExternalMethod, along with the method selector and its arguments. If the number of arguments or return values don't match what's in the dispatch instance, the call fails and returns kIOReturnBadArgument. Checking client calls like this prevents a malicious call to the driver from using attack vectors like buffer overruns.

Prepare the Driver's Instance Variables to Perform Driver-to-Client Callbacks

CppUserClient also shows how to communicate from the driver to the client by using a callback function. Option 6 sets up a callback to make an asynchronous call to the client, and then invokes the callback after a short delay to simulate the driver acting on its own. After registering a callback with option 6, calls to option 7 re-invoke the callback.

The NullDriver class defines NullDriver_IVars, the DriverKit structure that holds the driver's instance variables. NullDriver_IVars stores the callback action, as well as a dispatch queue and a timer dispatch source to use when calling back to the client.

struct NullDriver_IVars {
    OSAction* callbackAction = nullptr;
    IODispatchQueue* dispatchQueue = nullptr;
    IOTimerDispatchSource* dispatchSource = nullptr;
    OSAction* simulatedAsyncDeviceResponseAction = nullptr;
};

View in Source

NullDriver initializes the dispatchQueue and dispatchSource in its Start implementation.

The driver's implementation of Start also sets up the ivars member simulatedAsyncDeviceResponsAction, which the example uses to simulate asynchronous processing that happens on real hardware. This OSAction refers to an asynchronous timer callback to the SimulatedAsyncEvent function defined in the .iig file:

virtual void SimulatedAsyncEvent(OSAction* action, uint64_t time) TYPE(IOTimerDispatchSource::TimerOccurred);

View in Source

This declaration takes the same arguments as the IOTimerDispatchSource::TimerOccurred method that that the TYPE macro wraps. By declaring the callback's name as SimulatedAsyncEvent, the TYPE macro synthesizes CreateActionSimulatedAsyncEvent, the function that creates the OSAction. The driver's Start implementation can then call this synthesized method to initialize the simulatedAsyncDeviceResponseAction member of the ivars structure:

ret = CreateActionSimulatedAsyncEvent(sizeof(DataStruct), &ivars->simulatedAsyncDeviceResponseAction);
if (ret != kIOReturnSuccess)
{
    Log("Start() - Failed to create action for simulated async event with error: 0x%08x.", ret);
    goto Exit;
}

View in Source

Retain and Use the Callback to Notify the Client

When the driver is running and it receives a request from the client to register a callback, it calls NullDriver::RegisterAsyncCallback. This method stores the completion, if it exists, in the ivars structure, like this:

if (arguments->completion == nullptr)
{
    Log("Got a null completion.");
    return kIOReturnBadArgument;
}

// Save the completion for later.
// If not saved, then it might be freed before the asychronous return.
ivars->callbackAction = arguments->completion;
ivars->callbackAction->retain();

View in Source

Next, the NullDriver::RegisterAsyncCallback method sets up a delayed callback to the client to simulate a hardware delay, allowing it to return quickly, by using the simulatedAsyncDeviceResponseAction:

input = (DataStruct*)arguments->structureInput->getBytesNoCopy();

// Retain action memory for later work.
void* osActionRetainedMemory = ivars->simulatedAsyncDeviceResponseAction->GetReference();
memcpy(osActionRetainedMemory, input, sizeof(DataStruct));

output.foo = input->foo + 1;
output.bar = input->bar + 10;

arguments->structureOutput = OSData::withBytes(&output, sizeof(DataStruct));

// Dispatch action that waits five to seven seconds and then calls the callback.
const uint64_t fiveSecondsInNanoSeconds = 5000000000;
const uint64_t twoSecondsInNanoSeconds = 2000000000;
uint64_t currentTime = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW);

Log("Sleeping async...");
ivars->dispatchSource->WakeAtTime(kIOTimerClockMonotonicRaw, currentTime + fiveSecondsInNanoSeconds, twoSecondsInNanoSeconds);

View in Source

After the driver stores the callback, the client app can perform multiple simulated callbacks with option 7. This calls NullDriver::HandleAsyncRequest, which is largely similar to the delayed call perfomed in the previous listing.

  • Important: The driver must register the callback function before the client makes an asynchronous request, or the kernel may panic.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published