Windows System Software -- Consulting, Training, Development -- Engineering Excellent, Every Time.

Making Device Objects Accessible…and Safe

Over the years, much has been written – both in the MSDN documentation and here in The NT Insider – about how the Device Objects a driver creates can be accessed.  And since Windows 2000 changed the world by introducing the concept of PDOs and FDOs, much has also been written about Device Object security.  Alas, much of what’s been written has been wrong, misleading, outdated, confusing, confused, or some combination of all of these.

When we started writing this article, we figured it would be straight-forward.  You know, crank something out for The NT Insider about a topic that is helpful and about which we have a lot of experience. Making Device Objects secure and accessible: How hard can that be?  Well, surprise!  Once we started a careful analysis of each possible option, and the interaction of those options, we discovered the proverbial “can of worms.” It turns out to be shockingly easy to screw things up.  And it’s scary simple to end up with a different protection on the Device Objects in your device stack than you expected.

In this article, we’ll try to address some of the most basic questions about how a WDF function driver can securely make its devices accessible. Our goal is to define and describe what we believe to be best practices for WDF (specifically, KMDF) drivers. We’re going to ignore WDM drivers because, well, you probably shouldn’t be writing WDM drivers these days.  And if you are, you should already know how to deal with security.

Quick Review: PDOs and FDOs

Let’s start at the beginning, with PDOs and FDOs, because this is actually where most of the trouble starts.

As a rule, every device in Windows is discovered through the Plug-and-Play (PnP) process. The only exceptions to this rule are (a) software-only drivers that we refer to as “kernel services”, and (b) super-ancient hardware drivers that use the original Windows NT model.   “Kernel services” are sometimes also referred to as “legacy style software drivers” or “NT V4 style software drivers.”  The drivers in these exception categories create their Device Objects within their DriverEntry entry point.  Lots of folks (including us) write kernel services to do things like monitor process creation, watch for registry changes, or provide other sorts of services from kernel mode.  For the purposes of this article, we’re going to ignore all drivers that aren’t started by PnP.

So… as we said… as a rule, every device in Windows is discovered through the PnP process.  This is true regardless of whether the device lives on a dynamically enumerable bus (such as PCIe or USB) or on a bus where the attached devices can’t be discovered at run time (such as I2C or SPI).  When a bus driver enumerates a device on its bus, it creates a Device Object that represents the physical instance of the discovered device on its bus.  In WDF, the bus driver creates this Device Object using the function WdfDeviceCreate.  This Device Object, created by the bus driver, is referred to as a Physical Device Object, or PDO for short.

As part of the overall PnP process, the PnP Manager queries the bus driver for a list of its “child devices.”  If the bus driver has discovered any devices on its bus, it replies with a list of pointers to the PDOs that is has previously created to represent those devices.

For each PDO returned from a bus driver (with some limited, special-case, exceptions), Windows attempts to find and start a driver that will be responsible for the functional aspects of the device.  This is the function driver. It’s at this point that WDF function drivers are called at their EvtDriverDeviceAdd Event Processing Callback.  Within this Callback, a WDF function driver creates the Device Object (using WdfDeviceCreate) that represents the functional aspect of the device.  This Device Object, created by the function driver, is referred to as the Functional Device Object or FDO for short.

After the FDO has been created, it is attached to the underlying PDO that was previously created by the bus driver.  The result is a pair of Device Objects that together represent (a) the physical presence of a device on a given bus (PDO), and (b) the functional aspect of that device (the FDO).  This pair forms the basis of the Device Stack, and is shown in Figure 1.  For the sake of simplicity, we’re ignoring filter drivers and their Device Objects in this discussion.

Figure 1—Basic Device Stack

It’s important to note that, regardless of how a device is accessed, any I/O operations that are sent to a device will always enter at the top of the Device Stack in which the device appears.  In other words, I/O requests will always go to the FDO first.  This makes sense, because it’s the function driver (the one that created the FDO) that is responsible for the functional aspects of the device. And it’s most typically only the function driver (and not the bus driver) that knows how to process I/O requests for the device.  In fact, the bus driver is rarely involved in processing typical I/O operations.

The fact that in Windows we have two Device Objects that together represent one single device is the root cause of many of the problems and much of the confusion about how devices are accessed and protected.  When you add to this the fact that there are multiple ways that you can access the device associated with these Device Objects, things can get tricky, fast.

Device Object Attributes

Whenever a Device Object is created, the creator can optionally specify a wide variety of characteristics and attributes. For the purposes of this article, the most interesting among these are device name, Device Setup Class, and default Device Object security.

Device Naming

Specifying a device name provides an “internal” name, otherwise known as a “native” name, for the Device Object.  This name is not easily accessible from user mode, but rather is typically used by other kernel mode entities to find or identify the device by name.

Naming an FDO is strictly optional, and best practice dictates that you should not name your FDO unless you absolutely need to. We’ll have more to say about this later in this article, when we examine the implications of naming your Device Objects in more detail.

Due to some dubious architectural choices in Windows 2000, PDOs must always be named.  However, because the bus driver rarely intends this name to be used to access the device, the name can be, and almost always is, arbitrary and meaningless (such as “\Device\NTPNP_0005”). There’s even a commonly used option to have the I/O Manager automatically generate the device name for a PDO.

In KMDF, if you want to provide a name for the Device Object you’re creating (regardless of whether that Device Object is a PDO or an FDO), you use a method on the WDFDEVICE_INIT structure.  This method is WdfDeviceInitAssignName, as shown in the following example:

DECLARE_CONST_UNICODE_STRING(InternalName, L"\\Device\\MissileLauncher1") ;
status = WdfDeviceInitAssignName(DeviceInit,
                                 &InternalName);

The code above assigns the internal (native) name “MissileLauncher1” to the Device Object that will be created with the WDFDEVICE_INIT pointed to by DeviceInit.  Note that, by convention, the name is placed in the Object Manager’s \Device\ directory.

Device Setup Class

In addition to device name, a driver can also specify the Device Setup Class that is associated with the Device Object that is being created.  The Device Setup Class is, among other things, the category under which the device is shown in Windows Device Manager.  Note that the Device Setup Class is also specified in the INF file that is used to install the driver.  Specifying a Device Setup Class from your driver allows Windows to locate class-specific default settings for the Device Object that is being created.  Such settings can include device type, device characteristics, and most importantly the security descriptor to be applied to the newly created Device Object within the class.

Specifying the Device Setup Class when you create an FDO is both simple and a best practice.  As in specifying an internal name, the method uses the WDFDEVICE_INIT structure, and can be invoked as follows:

WdfDeviceInitSetDeviceClass(DeviceInit,
                            &GUID_DEVCLASS_OSR_MISSILE);

In this example, the caller calls WdfDeviceInitSetDeviceClass to set the Device Setup Class to GUID_DEVCLASS_OSR_MISSILE.  Note that because this is a properties method (the verb used is “set”) it cannot fail.  Thus, there is no status returned from the call.

Specifying the Device Setup Class in your driver gives you the best chance of Windows taking the device type, device characteristics, exclusive access, and – most importantly – the security descriptor that is applied to your FDO and “harmonizing” them throughout your device stack.  By “harmonize” we mean that it takes the settings from your FDO and copies them to the PDO (and, presumably, any other Device Objects in your device stack).  We’ll explain more about this later.

Device Object Security

The final characteristic of interest that can be specified when creating a Device Object is the security descriptor.  This allows you to define the access rights that specific users and groups will have to the Device Object you’re creating.  Again, this operation is performed using a method on the WDFDEVICE_INIT structure, like the following:

status = WdfDeviceInitAssignSDDLString(DeviceInit,
                                       &SDDL_DEVOBJ_SYS_ALL_ADM_ALL);

The method is WdfDeviceInitAssignSDDLString.  Note that the verb used in this method is “assign” so the call can fail, a status value is returned, and that status must be checked.  The security descriptor is specified using a limited form of Security Descriptor Definition Language (SDDL).  SDDL is a short-hand method of specifying standard Windows access control specifications.  The sub-set of SDDL that is used for Device Objects allows most of the common options, but also provides a few pre-defined values for commonly used security profiles.  For example, the option shown in the example is SDDL_DEVOBJ_SYS_ALL_ADM_ALL.  This provides System and Administrators all access to the device, but no access to any other class of user. There are numerous other shorthand specifications that allow a wide variety of access combinations.

While specifying an SDDL string to apply a specific security descriptor to your Device Object probably sounds like a good way to ensure security, it turns out that specifying this option is almost never a good idea.  In fact, best practices call for not specifying an SDDL string using WdfDeviceInitAssignSDDLString (we’ll explain why a bit later). If you want to change the default protection applied to the Device Object’s in your device stack, the best way to do that is to specify a security descriptor during device/driver installation.  You do this in your INF either as part of the DDInstall.HW section (where the security descriptor is applied to your specific device) or in the ClassInstall32 section (where the security descriptor is applied to all the devices in your Device Setup Class).  In either case, when you specify a security descriptor in your INF file, it is stored in the Registry and later used for your device, as appropriate.  See the sidebar Specifying Security in the INF File.

There are two important, and probably unexpected, things to keep in mind about using WdfDeviceInitAssignSDDLString:

  • To create a Device Object successfully after having called WdfDeviceInitAssignSDDLString, you must also specify an internal name for your device. So, if you call WdfDeviceInitAssignSDDLString you must also call WdfDeviceInitAssignName (or request that Windows autogenerate a device name for the Device Object you’re creating).
  • The protection you specify when you call WdfDeviceInitAssignSDDLString is the last-chance default protection. It will only be used if a security descriptor for your device or Device Setup Class has not been previously stored in the Registry.  In the case that there is no security descriptor already stored in the Registry, the security descriptor you specify will be stored in the registry and become the new default security descriptor for Device Objects subsequently created in the Device Setup Class (assuming you have specified a Device Setup Class using WdfDeviceInitSetDeviceClass).

The reason it is best practice to not call WdfDeviceInitAssignSDDLString is because the specified security descriptor is only used when there is no default security descriptor setting already known for the device or the Device Setup Class. This is true, even if the security descriptor specified by the driver is less permissive than the one that has been previously stored in the Registry. So, even when you specify a security descriptor in your driver, there’s no guarantee that it will define the security that is actually used on your Device Object.  And if that is the case, why specify it at all?

If you do not specify a security descriptor for your Device Object by calling WdfDeviceInitAssignSDDLString, and if there’s no default stored in the Registry for your device or your Device Setup Class, the system will assign a security descriptor.  For FDOs, the Framework assigns SDDL_DEVOBJ_SYS_ALL_ADM_ALL, which provides access only to system and administrators as described above.  For PDOs, Windows provides the default based on Device Type.  In general, this protection allows all users read and write access to the device.

Making Devices Accessible

After characteristics and properties are established and a Device Object is created, how do user-mode applications access the device?

A user-mode application opens and sends I/O operations to a Device Stack by opening a Device Object in the stack that is been explicitly made accessible to user-mode by a kernel-mode module.  Drivers can make either the FDO or the PDO accessible, or both. There are three different ways that drivers can make Device Objects accessible to user-mode applications:

  • Create a symbolic link to the PDO in the Device Stack
  • Create a symbolic link to the FDO in the Device Stack
  • Create a Device Interface GUID that points to the PDO in the Device Stack

All of these mechanisms result in the user-mode application eventually calling the Win32 function CreateFile to access a Device Object.

To make either the PDO or FDO easily accessible by name to user-mode applications, the driver explicitly creates a symbolic link for the device by calling WdfDeviceCreateSymbolicLink:

DECLARE_CONST_UNICODE_STRING(userDeviceName, L"\\DosDevices\\ML1");

	status = WdfDeviceCreateSymbolicLink(device, &userDeviceName);

The method WdfDeviceCreateSymbolicLink is, obviously, a method on a WDFDEVICE, and thus must be called after the Device Object has been created.  In the example above, the driver creates a symbolic link in the Object Manager’s \DosDevices\ namespace, which will allow a user-mode application to access the device using code similar to the following:

hFile = CreateFile("\\\\.\\ML1", 
                   GENERIC_READ|GENERIC_WRITE,    // requested access
                   0,                             // share mode
                   NULL,                          // security attributes
                   OPEN_EXISTING,                 // create disposition
                   0,                             // flags
                   NULL);                         // template file

Note all the ugly doubled backslash characters, as required by C syntax.  The “\\.\” syntax represents a UNC path and is required.  Note that it is possible for user-mode applications to access Device Objects even without such a symbolic link being created.  But it’s ugly and/or arbitrary.  In any case, it’s certainly not convenient for the app developer, and convenience is the primary reason for creating the symbolic link in the first place.

The tricky thing for driver devs about WdfDeviceCreateSymbolicLink is that what it does varies depending on whether you’ve named your FDO or not.  Yes, seriously.

If you have not named your FDO, that is you did not call WdfDeviceInitAssignName prior to creating your Device Object, the symbolic link that is created will point to the PDO.

If your driver chose to provide an internal name for your FDO by calling WdfDeviceInitAssignName prior to creating your Device Object, the symbolic link that is created will point to the FDO.

The third way a driver can make a device accessible is through the use of a Device Interface GUID.  After the Device Object has been created, the function driver tells the Framework that the Device Object that was created supports a given Device Interface GUID.  It does this via the function WdfDeviceCreateDeviceInterface.  Supporting a given Device Interface implies the services provided by the driver on behalf of the device and even the I/O function codes (including IOCTLs) the device supports. There are numerous system-defined Device Interface Classes (search C:\Program Files (x86)\Windows Kits\10\include\ to get an idea of just how many there are!).  So, for example, if a device supports GUID_DEVINTERFACE_COMPORT,  you know that it is associated with a traditional PC serial port, and will support all the IOCTL_SERIAL_Xxxx requests.

For custom drivers, the GUID that represents the Device Interface will be one that is been uniquely assigned to a given class of devices.  So, in our example of the OSR Missile Launcher device, I would probably define a new Device Interface that exclusively represents that type of device.  I’d do this by defining something like GUID_DEVINTERFACE_OSR_MISSILE in a header file that can be shared between my driver and any applications that need to use my device.

Device Interfaces are cool, because they make a number of interesting things possible, including:

  • Providing a mechanism for a driver to categorize a device as supporting a particular interface and thus providing a particular type of service. User programs can enumerate which devices in the system support a particular interface without regard to device name.
  • Allowing both kernel-mode and user-mode entities to register callbacks that are invoked whenever a Device Interface changes state — Such as when a new instance of an interface is enabled or an existing interface is disabled.

Another nice thing about Device Interfaces is that when they are used in place of device names, any potential for naming conflicts is avoided.  Not that I’ve ever really had a problem with Device Object naming conflicts.  But, whatever.

One of the primary things that driver devs need to keep in mind about Device Interfaces is that Device Interfaces always point to a PDO.

When a driver creates a Device Interface, a user-mode application accesses the device by first finding the set of devices that support a given Device Interface GUID.  This is done using interfaces provided by the SetupDiXxxx family of functions.  Once a particular device supporting a given Device Interface is chosen, the user-mode app opens that device using CreateFile with an opaque name that the application retrieves from the details of Device Interface instance.  Basic code to open the first device found that is associated with a given Device interface is shown in Figure 2.

HANDLE
OpenMissileDeviceViaInterface(VOID)
{
    HDEVINFO                         devInfo;
    SP_DEVICE_INTERFACE_DATA         devInterfaceData;
    PSP_DEVICE_INTERFACE_DETAIL_DATA devInterfaceDetailData = NULL;
    ULONG                            devIndex;
    ULONG                            requiredSize;
    ULONG                            code;
    HANDLE                           handle;
 
    devInfo = SetupDiGetClassDevs(&GUID_DEVINTERFACE_OSR_MISSILE,
                                  NULL,
                                  NULL,
                                  DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
 
    if (devInfo == INVALID_HANDLE_VALUE) {
 
        printf("SetupDiGetClassDevs failed with error 0x%x\n", GetLastError());
 
        return INVALID_HANDLE_VALUE;
    }
 
    devInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
 
    devIndex = 0;
 
    if (!SetupDiEnumDeviceInterfaces(devInfo,
                                     NULL,
                                     &GUID_DEVINTERFACE_OSR_MISSILE,
                                     devIndex++,
                                     &devInterfaceData)) {
 
        code = GetLastError();
 
        if (code != ERROR_INSUFFICIENT_BUFFER) {
 
            printf("SetupDiGetDeviceInterfaceDetail failed with error 0x%x\n", code);
 
            SetupDiDestroyDeviceInfoList(devInfo);
 
            return INVALID_HANDLE_VALUE;
        }
    }
 
    if (!SetupDiGetDeviceInterfaceDetail(devInfo,
                                         &devInterfaceData,
                                         NULL,
                                         0,
                                         &requiredSize,
                                         NULL)) {
 
        code = GetLastError();
 
        if (code != ERROR_INSUFFICIENT_BUFFER) {
 
            printf("SetupDiGetDeviceInterfaceDetail failed with error 0x%x\n", code);
 
            SetupDiDestroyDeviceInfoList(devInfo);
 
            return INVALID_HANDLE_VALUE;
        }
     }
 
    devInterfaceDetailData =
        (PSP_DEVICE_INTERFACE_DETAIL_DATA)malloc(requiredSize);
HANDLE
OpenMissileDeviceViaInterface(VOID)
{
    HDEVINFO                         devInfo;
    SP_DEVICE_INTERFACE_DATA         devInterfaceData;
    PSP_DEVICE_INTERFACE_DETAIL_DATA devInterfaceDetailData = NULL;
    ULONG                            devIndex;
    ULONG                            requiredSize;
    ULONG                            code;
    HANDLE                           handle;
 
    devInfo = SetupDiGetClassDevs(&GUID_DEVINTERFACE_OSR_MISSILE,
                                  NULL,
                                  NULL,
                                  DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
 
    if (devInfo == INVALID_HANDLE_VALUE) {
 
        printf("SetupDiGetClassDevs failed with error 0x%x\n", GetLastError());
 
        return INVALID_HANDLE_VALUE;
    }
 
    devInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
 
    devIndex = 0;
 
    if (!SetupDiEnumDeviceInterfaces(devInfo,
                                     NULL,
                                     &GUID_DEVINTERFACE_OSR_MISSILE,
                                     devIndex++,
                                     &devInterfaceData)) {
 
        code = GetLastError();
 
        if (code != ERROR_INSUFFICIENT_BUFFER) {
 
            printf("SetupDiGetDeviceInterfaceDetail failed with error 0x%x\n", code);
 
            SetupDiDestroyDeviceInfoList(devInfo);
 
            return INVALID_HANDLE_VALUE;
        }
    }
 
    if (!SetupDiGetDeviceInterfaceDetail(devInfo,
                                         &devInterfaceData,
                                         NULL,
                                         0,
                                         &requiredSize,
                                         NULL)) {
 
        code = GetLastError();
 
        if (code != ERROR_INSUFFICIENT_BUFFER) {
 
            printf("SetupDiGetDeviceInterfaceDetail failed with error 0x%x\n", code);
 
            SetupDiDestroyDeviceInfoList(devInfo);
 
            return INVALID_HANDLE_VALUE;
        }
     }
 
    devInterfaceDetailData =
        (PSP_DEVICE_INTERFACE_DETAIL_DATA)malloc(requiredSize);
 
    if (!devInterfaceDetailData) {
 
        printf("Unable to allocate resources...Exiting\n");
 
        SetupDiDestroyDeviceInfoList(devInfo);
 
        return INVALID_HANDLE_VALUE;
    }
 
    devInterfaceDetailData->cbSize =
        sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
 
 
    if (!SetupDiGetDeviceInterfaceDetail(devInfo,
                                         &devInterfaceData,
                                         devInterfaceDetailData,
                                         requiredSize,
                                         &requiredSize,
                                         NULL)) {
 
        printf("SetupDiGetDeviceInterfaceDetail failed with error 0x%x\n",
                                             GetLastError());
 
        SetupDiDestroyDeviceInfoList(devInfo);
 
        free(devInterfaceDetailData);
 
        return INVALID_HANDLE_VALUE;
    }
 
    printf("Device found! %ls\n", devInterfaceDetailData->DevicePath);
 
    SetupDiDestroyDeviceInfoList(devInfo);
 
    if (devInterfaceDetailData == NULL) {
 
        printf("Unable to find any matching devices!\n");
 
        return INVALID_HANDLE_VALUE;
    }
 
    handle = CreateFile(devInterfaceDetailData->DevicePath,
                        GENERIC_READ | GENERIC_WRITE,
                        0,
                        0,
                        OPEN_EXISTING,
                        0,
                        0);
 
    free(devInterfaceDetailData);
 
    return handle;
}

 

Kernel-mode modules can also access your Device Object in various ways using the Device Interface GUID you’ve established.  That means if you follow best practices and do not name your FDO, there are still ways other kernel-mode modules can find and access your device.

Security on Open – But on WHICH Device Object?

You’ll note that we’ve been very specific in this article about which Device Object (the PDO or the FDO) a given access option points to.  The reason we’ve done this is because it matters.  Not understanding which Device Object a given access point is referring to is the basis for most problems in terms of Device Object protection.

When an application calls CreateFile, the only security descriptor that is checked to determine if the CreateFile will succeed is the security descriptor on the Device Object that is being directly opened.  This means, for example, that if the user attempts to access a Device Object via a link that points to the FDO, the access check that will take place will be based on the user’s security credentials and the security descriptor on the FDO.  Similarly, if the user calls CreateFile to access a device via its PDO, the user’s credentials are checked against the security descriptor on the PDO.

And remember earlier we said that regardless of whether I/O is sent to the PDO or to the FDO, the I/O operations always are routed first to the top of the Device Stack.  So, as far as the user-mode application’s ability to send I/O requests to a driver is concerned, it doesn’t matter whether it opens the PDO or the FDO.  All I/O operations will go to the FDO (all other things being equal).

Where Are We?

Let’s see if we can summarize some of the rules and best practices we’ve discussed so far.  Let’s assume you’re writing a function driver.  As part of creating your FDO:

  • We suggest, as a best practice, you specify a Device Setup Class for your FDO by calling WdfDeviceInitSetDeviceClass. This isn’t a big deal, really, but it does help to ensure the Device Object protections are “harmonized” across all the Device Objects in your device stack in certain instances.  There are no downsides to specifying it, and if you just always do it, you’ll find a few edge cases dealing with protection in your device stack are properly handled.
  • You may optionally specify an internal name for your FDO, however doing so it not recommended unless absolutely necessary (and is not a best practice).
  • If you do choose to name your FDO, you can optionally specify a last-chance default security descriptor for the FDO… but, again, it is best practice to not do this.
  • If you choose to not specify a last-chance default security descriptor for your FDO, the protection applied to your FDO will be depend:
    • If there is a default security descriptor stored in the Registry for your device or your Device Setup Class, then that security descriptor will be applied to all Device Objects in your device stack (including the FDO you are creating).
    • If there is no default security descriptor stored in the Registry for your device’s Device Setup Class, then:
  • If you have not specified a Device Setup Class, then your FDO will receive the default protection provided by the Framework (all access for system and administrators but no access for any other groups) and your PDO will receive the default protection provided by Windows (system and admin all access, everyone else read and write access).
  • If you have specified a Device Setup Class (by calling WdfDeviceInitSetDeviceClass) the default protection provided by the Framework will be applied to your FDO and the other Device Objects in your device stack (including the PDO).

Once your FDO has been created, you can make it accessible to user-mode applications by:

  • Creating a symbolic link in the Object Manager’s \DosDevices\ name space. User mode applications can open the device via the symbolic link name you provide.  If your FDO is named, the symbolic link will point to the FDO.  If you did not name your FDO, the symbolic link will point to the PDO.
  • Associating your FDO with a Device Interface. This approach has a number of advantages, but requires user-mode applications to use the SetupDiXxx functions with your Device Interface GUID to retrieve the opaque name associated with a specific instance of the Device Interface to open your device.  The opaque name used to open a device by Device Interface GUID always points to the PDO.

You can make your device accessible using either of the above two methods, or both of them.  Here at OSR, we typically use both methods (as do many Windows in-box drivers).

Some Examples

Let’s look at a couple of examples as a way to more fully understand the options available to us, and to help us understand the impact of those options.

First, let’s say we do not provide an internal name our FDO.  We create a symbolic link to allow applications to open our device by name, and we also associate our FDO with a unique Device Interface GUID.  We do not specify a last-chance security descriptor for our FDO, nor do we specify a Device Setup Class.  The security descriptors on both our PDO and FDO will default to whatever is provided by the system.  In this case, we wind up with a device stack like that shown in Figure 3.

Figure 3—Unnamed FDO, Device Interface Specified, Default Protections

Looking at Figure 3, you can see that both our Device Interface and our symbolic link name point to the PDO.  The symbolic link name we provide by calling WdfDeviceCreateSymbolicLink points to the PDO because we did not specify an internal name for our FDO.  Recall that regardless of whether the names point to the FDO or PDO, all I/O operations from the application will go to the top of the device stack.  So all I/O requests (such as read, write, and DeviceIoControl) sent by the application will always to go the FDO first.

Now, let’s see what happens when we choose to provide an internal name for our FDO, by calling WdfDeviceInitAssignName.  This name will allow other kernel-mode modules to access our device.  In this case, we’ll do everything the same as we did in Figure 3:  We create a symbolic link to allow our device to be opened by name from user-mode applications, and we associate our device with a Device Interface GUID.  And, once again, we do not specify a last-chance security descriptor and we do not specify a Device Setup Class.  Again, the security descriptors on both our PDO and FDO will default to those provided by the system.  In this case, we wind up with a device stack like that shown in Figure 4.

There are several important changes from Figure 3 to notice in Figure 4.  First, notice that the symbolic link name (ML1) now points to the FDO instead of to the PDO.  This is because we chose to provide an internal name for our FDO.  The Device Interface still points to the PDO (as it always will).

Next, notice the security descriptors for the FDO and PDO are different.  If we name our FDO, and there’s no Device Setup Class security descriptor available, the Framework provides an “administrator only” security descriptor for our FDO by default.

Figure 4—Named FDO, Device Interface Specified, Default Protections

As a result of the above two decisions, which security descriptor applies when an application issues a CreateFile will depend on whether the application attempts to open our device by symbolic link name or by the opaque name obtained via the Device Interface GUID.  To be able to successfully open our device by symbolic link name, an application will require administrator privileges (that is, the app will need to be run elevated, “as administrator”).  This is because the symbolic link name points to the FDO, and the security descriptor on the FDO requires administrator access.

However, that same application would not need administrator privileges to be able to open the device using the Device Interface.  Because the Device Interface points to the PDO, and the security descriptor on the PDO allows all applications read and write access.

Once again recall that – once the application has successfully opened some Device Object in the device stack — all I/O operations from the application will always go to the top of the device stack.  So, once again, requests are sent to the FDO regardless of whether the PDO or FDO was used.

Another example?  In this case, we’ll do exactly what we did in the example shown in Figure 4 (named FDO, symbolic link created to FDO, device interface specified, no last-chance security descriptor provided) but this time we will specify a Device Setup Class by calling WdfDeviceInitSetDeviceClass.  The results are shown in Figure 5.

Figure 5—Named FDO, Device Interface Specified, Device Setup Class Specified

Note that in Figure 5, the security descriptor that the Framework provided for the FDO is now harmonized across the device stack, and has been applied to the PDO.  This ensures that applications encounter the same security descriptor when accessing the device, regardless of whether they access the device via the provided symbolic link name or the device interface.

Let’s look at one more example. In this case, we’ll do exactly what we did in the example shown in Figure 5, but we’ll assume a security descriptor for the Device Setup Class has been stored in the registry.  This security descriptor could have been specified during device installation via the INF file, or subsequently by the system administrator.  The security descriptor happens to specify all access to administrators, and read access to everyone else.

The resulting device stack is shown in Figure 6.

The key thing to notice in Figure 6 is that the security descriptor is the same on both the FDO and PDO.  This is because Windows ensures that any security descriptor that is associated with a Device Setup Class is applied uniformly across the device stack.  Note that, when there is a security descriptor present in the registry for your device or for your device’s Device Setup Class, this harmonization happens even if your driver hasn’t called WdfDeviceInitSetDeviceClass.  It doesn’t hurt to specify your Device Setup Class, but Windows already knows it and applies the Device Object protection consistently across the device stack.

And again, whether an application choses to open the device via its symbolic link name or its Device Interface, the result is that the security check is consistent.  And recall it makes no difference in terms of processing I/O requests which Device Object the application opens.  In all cases, I/O operations from the application will be sent to the top of the device stack.

 So What Have We Learned?

Figure 6—Named FDO, Device Interface Specified, pre-defined security descriptor in the Registry

At a very minimum, I hope that you learned that this topic is more complex than it might first appear.   There are also some “best practice” guidelines that I think we can confidently state as a result of our discussion of this topic:

  • Don’t provide an internal name for your FDO unless your device needs to be found using that name by another kernel mode module, and you can’t take the chance the other kernel-mode module will use some other method (such as finding your device by Device Interface GUID).
  • If you want user-mode applications to be able to open your device by name, it’s perfectly fine to create a symbolic link. Be aware that where that symbolic link points will vary, depending on whether you’ve provided an internal name for your FDO.
  • Creating a Device Interface GUID for your device allows many convenient options for accessing your device, from both user-mode and kernel-mode. You can both create a symbolic link and provide a Device Interface GUID for your device to maximize your user’s options.  The Device Interface GUID will always point to the PDO.
  • Specifying a Device Setup Class when you create your FDO is a best practice, because doing so will cause the protections applied to your device stack to be harmonized if there is no default security descriptor specified in the registry for your device or your Device Setup Class.
  • Do not specify a security descriptor for your FDO from within your driver. That is, do not call WdfDeviceInitAssignSDDLString.  Where and if this security descriptor is used depends on too many other factors.  If you need specific protection for your Device Object, specify it in your INF file.
  • If you need to set a specific protection for your device, absolutely the best place to do that is in your INF file. You can specify a protection for your specific device or for all devices in your Device Setup Class.

Now you see why what we thought would be a simple article, on a simple topic, became a much more involved examination of device availability and protection.  Follow the guidelines, and you can’t go wrong!  (Again, See Specifying Security In The INF File, below).

Specifying Security in the INF File

Your first and best option for specifying protection for the Device Objects in your device stack is using the INF file. Note that an end-user will not be able to hack the INF file and change the protection you specify, as long as your driver install package is signed.

When you specify access controls in your INF, the protection that you specify will be propagated throughout the device stack as described in the larger article. Thus, when you specify protection in your INF, that same protection will be applied to the PDO, FDO, and all filter driver Device Objects that appear in your device stack.

INF Class-Wide Access Controls

Probably the most common way that a specific security descriptor (SD) is set on a device stack is by specifying a default security descriptor for all device in your device’s Device Setup Class. This is specified in the INF that defines the device install class via the Security value in the addreg section pointed to from the ClassInstall32 section. Here’s an extract from an INF file that defines the OsrExample install class and specifies a default security descriptor for the class:

[Version]
Signature=”$WINDOWS NT$”
Class=OsrExample
ClassGuid={cab15040-5cc7-11d3-b194-0060b0efd4fd}
Provider=”OSR Open Systems Resources, Inc.”
DriverVer=2/13/2017,7.1.2
catalogfile=wdfdio.cat

[ClassInstall32]
Addreg=OsrHwClass

[OsrHwClass]
HKR,,,,%ClassName%
HKR,,Icon,,”-5″
HKR,,Security,,”D:P(A;;GA;;;SY)(A;;GA;;;BA)” ;System and Admin only access

As previously described, the security descriptor supplied in the INF is defined using Security Descriptor Definition Language (SDDL).

When you specify an SD for your Device Setup Class in your INF, the security descriptor is stored in the registry, in the Security value of the Properties key under the software (A.K.A. driver) key for your driver. Your device’s software key will be:

HKLM\SYSTEM\CCS\CONTROL\CLASS\class-guid\instance

Look under this key for the key named “Properties.” You will need to change the access to the Properties key to be able to see the value named Security. Yes, this is true even if you’re an administrator on the box.

INF Per-Device Access Controls

If the device for which your driver is being installed requires different access controls from those specified for your installation class, you can specify a per-device security descriptor in your INF file. A per-device security descriptor is specified in the addreg section invoked from the ddinstall.HW section of your INF. Following is an extract from an INF file that defines a security descriptor for a particular device within a larger class. You notice that this security descriptor is also defined using SDDL:

[MfgDeviceSection]
%DeviceDesc% = WdfDio, PCI\VEN_135E&DEV_8008&SUBSYS_8008135E&REV_01
%DeviceDesc% = WdfDio, PCI\VEN_135E&DEV_8018&SUBSYS_8018135E&REV_01

[WdfDio]
CopyFiles=@WdfDio.sys

[WDFDIO.HW]
addreg=DIOSD

[DIOSD]
HKR,,Security,,”D:P(A;;GR;;WD)(A;;GA;;BU)(A;;GA;;;SY)(A;;GR;;;WD)”

When you specify a per-device security descriptor in your INF, it is stored in the Registry, in the Security value of the Properties key of your device’s hardware (A.K.A device) key. Your device’s device key will be:

HKLM\SYSTEM\CCS\ENUM\enumerator\device-id

Again, you’ll need to change the protection on the Properties key to be able to view this entry.

Note that specifying per-device access controls in your INF overrides for your device stack only any class-wide access controls that might have been specified when the device class was defined. This is true regardless of whether the security descriptor you supply is more or less secure than the default protection. Thus, specifying a per-device security descriptor for a device allows you to specify precisely the protection that your device should have, without affecting the devices created by any other drivers in the class.

Summary
Article Name
Making Device Objects Accessible...and Safe
Author
OSR