Basic introduction to DEXT Matching and Loading

Note: This document is specifically focused on what happens after a DEXT has passed its initial code-signing checks. Code-signing issues are dealt with in other posts.

Preliminary Guidance:

Using and understanding DriverKit basically requires understanding IOKit, something which isn't entirely clear in our documentation. The good news here is that IOKit actually does have fairly good "foundational" documentation in the documentation archive. Here are a few of the documents I'd take a look at:

Those documents do not cover the full DEXT loading process, but they are the foundation of how all of this actually works.

Understanding the IOKitPersonalities Dictionary

The first thing to understand here is that the "IOKitPersonalities" is called that because it is in fact a fully valid "IOKitPersonalities" dictionary. That is, what the system actually uses that dictionary "for" is:

  1. Perform a standard IOKit match and load cycle in the kernel.

  2. The final driver in the kernel then uses the DEXT-specific data to launch and run your DEXT process outside the kernel.

So, working through the critical keys in that dictionary:

"IOProviderClass"-> This is the in-kernel class that your in-kernel driver loads "on top" of. The IOKit documentation and naming convention uses the term "Nub", but the naming convention is not consistent enough that it applies to all cases.

"IOClass"-> This is the in-kernel class that your DEXT attaches to and works through. This is where things can become a bit confused, as some families work by:

  1. Routing all activity through the provider reference so that the DEXT-specific class does not matter (PCIDriverKit).

  2. Having the DEXT subclass a specific subclass which corresponds to a specific kernel driver (SCSIPeripheralsDriverKit).

This distinction is described in the documentation, but it's easy to overlook if you don't understand what's going on. However, compare PCIDriverKit:

"When the system loads your custom PCI driver, it passes an IOPCIDevice object as the provider to your driver. Use that object to read and write the configuration and memory of your PCI hardware."

Versus SCSIPeripheralsDriverKit:

Develop your driver by subclassing IOUserSCSIPeripheralDeviceType00 or IOUserSCSIPeripheralDeviceType05, depending on whether your device works with SCSI Block Commands (SBC) or SCSI Multimedia Commands (SMC), respectively. In your subclass, override all methods the framework declares as pure virtual.

The reason these differences exist actually comes from the relationship and interactions between the DEXT families. Case in point, PCIDriverKit doesn't require a specific subclass because it wants SCSIControllerDriverKit DEXTs to be able to directly load "above" it.

Note that the common mistake many developers make is leaving "IOUserService" in place when they should have specified a family-specific subclass (case 2 above). This is an undocumented implementation detail, but if there is a mismatch between your DEXT driver ("IOUserSCSIPeripheralDeviceType00") and your kernel driver ("IOUserService"), you end up trying to call unimplemented kernel methods. When a method is "missing" like that, the codegen system ends up handling that by returning kIOReturnUnsupported.

One special case here is the "IOUserResources" provider. This class is the DEXT equivalent of "IOResources" in the kernel. In both cases, these classes exist as an attachment point for objects which don't otherwise have a provider. It's specifically used by the sample "Communicating between a DriverKit extension and a client app" to allow that sample to load on all hardware but is not something the vast majority of DEXT will use.

Following on from that point, most DEXT should NOT include "IOMatchCategory". Quoting IOKit fundamentals:

"Important: Any driver that declares IOResources as the value of its IOProviderClass key must also include in its personality the IOMatchCategory key and a private match category value. This prevents the driver from matching exclusively on the IOResources nub and thereby preventing other drivers from matching on it. It also prevents the driver from having to compete with all other drivers that need to match on IOResources. The value of the IOMatchCategory property should be identical to the value of the driver's IOClass property, which is the driver’s class name in reverse-DNS notation with underbars instead of dots, such as com_MyCompany_driver_MyDriver." The critical point here is that including IOMatchCategory does this:

"This prevents the driver from matching exclusively on the IOResources nub and thereby preventing other drivers from matching on it."

The problem here is that this is actually the exceptional case. For a typical DEXT, including IOMatchCategory means that a system driver will load "beside" their DEXT, then open the provider blocking DEXT access and breaking the DEXT.

DEXT Launching

The key point here is that the entire process above is the standard IOKit loading process used by all KEXT. Once that process finishes, what actually happens next is the DEXT-specific part of this process:

IOUserServerName-> This key is the bundle ID of your DEXT, which the system uses to find your DEXT target.

IOUserClass-> This is the name of the class the system instantiates after launching your DEXT. Note that this directly mimics how IOKit loading works.

Keep in mind that the second, DEXT-specific, half of this process is the first point your actual code becomes relevant. Any issue before that point will ONLY be visible through kernel logging or possibly the IORegistry.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

The IOKit Matching, from Load to DEXT...

The section above briefly outlined the IOKit to DEXT loading process, which this section will now expand and elaborate on:

(1) Installation

The first step in this process is telling the kernel that your driver exists at all. The details are handled automatically as part of the system extension loading process, but the basic process basically means that your kernel has a "copy" of your DEXTs IOKitPersonalities dictionary. Critically, this process is the same way IOKit KEXTs work, which means the dictionary is subject to exactly the same requirements as IOKit KEXTs. That's also why certain details like bundle ID length and CFBundleVersion are subject to limitations that other components aren't. KEXT validation was implemented in a VERY specific way 26+ years ago and, at this point, changing any of these details is guaranteed to create more problems than it would solve.

(2) Passive Matching

Inside the kernel, the matching process starts when a driver calls IOService.registerService() on itself, kicking off the entire matching sequence. Note that the class object which initiates this process is also the object that becomes the provider for whatever driver ends up completing the entire matching process.

That process starts with the passive matching process, which uses the IOKitPersonalities dictionary to identify drivers that "might" properly match a given target.

In general terms, that consists of two phases:

  1. Find all of the IOKitPersonalities dictionary entries with an IOProviderClass that matches the provider. Note that this relies on standard class inheritance, which means that, yes, an IOProviderClass of "IOService" is equivalent to saying "I can match anything" in the entire kernel. Don’t do this.

  2. Use the other keys in the IOKitPersonalities dictionary to prune matches down to JUST those matches that are actually valid.

The details of #2 are determined by the implementation of each individual driver, but for DEXT, the process is to first exclude matches using their personality keys, then check the entitlements of that driver. This is also why it's fine to have an entitlement configuration that's much broader than the hardware you're trying to match, since we only look at the entitlement configuration AFTER we do that standard match. See IOPCIDevice::matchPropertyTable() for an example of what this actually looks like on the kernel side.‎

(3) Active Matching

The passive matching process creates a list of every IOKitPersonalities entry that's configured in a way that COULD match the service. The active matching process then takes that list and starts sorting out which drivers will actually work and then "finding" the best match. That works by:

  1. The kernel loads the driver by using the "CFBundleIdentifier" property of the "IOKitPersonalities" dictionary to find the target KEXT bundle, loading that executable into the kernel, then finding and instantiating the class named in "IOClass".

  2. Calling probe() on each of the newly created objects. Each object is then given a chance to examine the provider and, in theory, adjust its probe score based on that. However, in practice this is more commonly used to reject match entirely, not adjust the probe score.

  3. The scores returned by probe are then used to sort the list of objects based on the scores returned in probe. start() is then called starting with the highest scored object, proceeding down the list until start() succeeds.

(4) Running a driver

DEXT loading/execution actually begins at the point start() is called. IOUserServerName is used to find your DEXTs bundle, after which a new process is created for your DEXT and IOUserClass is used to instantiate a new instance of the class you selected. Once that object is live, IOService.Start() is then called, at which point your DEXT is now "live". All of this happens while your IOClass is still in "start()", so if your DEXT fails "Start()", then the kernel will simply proceed down the match list in #3.

Oddities and Details:

  • Codeless DEXTs work by manipulating what happens in active stages #1 or #2 without ever reaching #3, meaning they never actually reach "start". See this post for a more in-depth description of how this works.

  • Not every object in the IORegistry actually occurs through the formal match process. Many objects are actually created by a driver directly creating and attaching that new driver to "itself". These two creation paths are what differentiate "Drivers" and "Nubs". Drivers are objects that load through the match process above. They then create Nubs (using new) and those Nubs then call "registerService()", starting the process all over again.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Basic introduction to DEXT Matching and Loading
 
 
Q