Application-level Printer Driver Isolation

TL;DR: You can isolate printer drivers from your applications with a small change to your application manifests, resulting in increased stability.

No one likes uninvited guests, especially if they are rude, or don’t play by the rules. They come over, tamper with things that shouldn’t be tampered with, they sometimes burn down your house, or even prevent you from dying in peace. On Windows, there are many types of uninvited guests, such as shell extensions, programs using DLL injection, and printer drivers.

Applications have very little control over these components, so their quality affects your software products’ quality as well. Users don’t care if your application crashes regularly because of some sloppy shell extension, as at the end of the day, it’s your product that crashed. In this blog post, I would like to bring your attention to a somewhat obscure feature of Windows: application-level printer driver isolation.

The application I work on on a daily basis at my day job is a CAD program, so inherently, printing is an often used and important feature of the product. Users use all kinds of printers, ranging from regular consumer models to industrial quality plotter machines costing thousands of dollars. Unfortunately, this level of usage and variety means that the product in question suffers more from buggy printer drivers than an average application. If the printer driver crashes, it brings down the whole program, as it’s loaded inside the address space of the application. Or worse, if it corrupts the heap, it might only cause an error later, making the problem much harder to diagnose.

An example crash

Before getting to the mitigation, let’s see an actual printer driver crash. Below you can find code for a little test program1 that causes a very specific version of HP’s PCL 5 printer driver to corrupt the heap. Reproducing this crash by yourself requires some preparation (you don’t have to print or even connect a printer to your computer, though):

  1. Download version 5.9.0.18326 of HP’s x64 PCL 5 driver from HP’s FTP
  2. Install it, when presented with the option, select “Dynamic mode”
  3. Open “Control Panel”/”Devices and Printers”
  4. Right click “HP Universal Printing PCL 5”, and select “Set as default printer”
  5. Compile (x64) and run the code snippet below
#include <iostream>

#include <windows.h>
#include <wingdi.h>

int main ()
{
  PRINTDLGW dialogResult = {};
  dialogResult.lStructSize = sizeof dialogResult;
  dialogResult.Flags = PD_RETURNDEFAULT;

  PrintDlgW (&dialogResult);

  DEVMODEW* pDefaultDevMode =
    (DEVMODEW*)GlobalLock (dialogResult.hDevMode);
  unsigned char* pDriverPrivateData =
    (unsigned char*)pDefaultDevMode + sizeof (DEVMODEW);

  // Magic offsets...
  for (size_t i = 3290; i < 3300; ++i)
    pDriverPrivateData[i] = 0;

  HDC hDC = CreateICW (L"winspool",
                       pDefaultDevMode->dmDeviceName,
                       L"LPT1:",
                       pDefaultDevMode);

  std::cout << "CreateICW " <<
    (hDC ? "succeeded!" : "failed!") << std::endl;
}

If I run this program on my machine, I get a crash approximately 1 out of 10 times at a late point in execution. For the sake of better reproducibility, let’s turn the Page Heap on for this executable (use gflags or Application Verifier from the Windows SDK), as this is a heap corruption after all. That makes it a 100% reproducible: an access violation is hit2 every time the highlighted line executes. I’d like to stress that it’s not the loop that causes the actual corruption, that just merely damages the driver-private data that gets fed to the driver with the CreateIC function. The heap corruption happens as a consequence of this corrupted data in the driver while serving the CreateIC call.

This practically emulates a real-world scenario very well: an application meets a faulty driver which brings down the entire process. Our goal is to make this program not crash despite the ill-behaving printer driver.

Let’s mitigate

Good news: there is a way to ask the system to load printer drivers into a separate process. All we have to do is provide a so-called application manifest for the executable, with the printerDriverIsolation attribute set to true (here’s the documentation).

Create a file with the “.manifest” extension, and the following content:

<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
  <asmv3:application>
    <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2011/WindowsSettings">
      <printerDriverIsolation>true</printerDriverIsolation>
    </asmv3:windowsSettings>
  </asmv3:application>
</assembly>

In Visual Studio, go to your project properties, then under “Manifest Tool”/”Input and Output”, add the path of this manifest file to the “Additional Manifest Files” property. This will add the manifest as a resource into the executable.

If you did everything correctly, then after rebuilding and running the project, you should notice the following changes in behavior:

  1. The application doesn’t crash anymore (not even with the Page Heap turned on)
  2. When running the application, a new process named splwow64.exe is spawned, and the printer driver will be loaded into that process
The test program without and with application-level printer driver isolation

And that’s basically it, all that was needed was a very simple change to mitigate a complex issue (don’t forget to uninstall the driver3, if you’ve followed along).

Closing thoughts

If you google “printer driver isolation”, you’ll find some blog posts, and the official documentation. These describe a feature that isolates printer drivers from the print spooler process (spoolsv.exe). This is a different feature from the one I’ve just demonstrated, which is application-level printer driver isolation.

I remember quite clearly that I stumbled upon this feature by accident, when I had to look up something else on the application manifest documentation page. I wish Microsoft had created a dedicated documentation page for this feature, as right now there is very little information available, and it’s easy to confuse the two different kinds of printer driver isolation.

For example, on the page describing spooler-level isolation, it is stated that it requires support from the drivers themselves, which they have to declare in their INF files. Is that also the case for application-level isolation? The answer is no, but instead of getting that information from the documentation, I had to make sure of that myself with an experiment4.

1 For conciseness, error handling and resource deallocations are omitted.

2 Notice how I avoided saying “crash”. That’s becuase unfortunately, the access violation is swallowed by winspool.drv (an exception handler in the function CallDrvDocumentEventNative, to be exact), which is the component that contains the implementation of printing API functions and interfaces to the spooler. That’s a shame (better a crash today, than a heap corruption tomorrow), but If I’d have to make a guess, it’s most likely due to compatibility reasons. Throughout the rest of the post, when I say crash, I mean access violation that gets swallowed.

3 It’s harder and more tedious than you think:

  1. Open “Control Panel”/”Devices and Printers”
  2. Right-click “HP Universal Printing PCL 5”, select “Remove device”
  3. Select another printer, and click on “Print server properties”
  4. On the “Drivers” tab, locate and select “HP Universal Printing 5”
  5. Click on “Remove”, select “Remove driver and driver package”, press OK
  6. Click on “Yes” on the confirmation dialog
  7. Click the “Delete” button on the new dialog
  8. The driver is removed (even if the dialog complains about the unability to access the driver)

4 Here’s what I did:

  1. I downloaded a printer driver (any would do)
  2. Before installing it, I located all “DriverIsolation” entries in *.inf files in the package, and modified their values to zero
  3. As in this state the signing check would have failed on the driver, I rebooted my machine in test mode (in an admin command prompt, I typed bcdedit.exe -set TESTSIGNING ON, then restarted my computer)
  4. Installed the driver, made the new printer the default one
  5. Ran the test program with the application manifest embedded
  6. I noted that application-level isolation still worked
  7. Uninstalled the driver
  8. Turned off test signing mode (in an admin command prompt, I typed bcdedit.exe -set TESTSIGNING OFF, then restarted my computer)

Author: Donpedro

C++ programmer with an interest in operating systems and everything low level.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s