CE6 Drivers: What you need to know
Posted by: Sue Loh
One of the biggest concerns people have about the new CE6 release is backward compatibility. Every release we try very hard to make existing applications, drivers and OALs as compatible as possible. With CE6 we expect very high compatibility for applications and even OAL code, but unfortunately I can’t say the same about drivers. Many, in fact most, drivers will need modifications in order to run on CE6. While binary compatibility (being able to run the exact same driver without a rebuild) is not likely, we do expect it to be easy to port almost all drivers. That was our goal once we realized many drivers would have to change.
The primary reasons that drivers will need change are:
- Deprecated APIs
- Memory passing
- Asynchronous buffer access
- User interface handling
The biggest difference in CE6 is how drivers access embedded pointers and other data, as I described in detail in my memory marshalling post. There are two main things you need to do to fix memory accesses. First, look through your existing code for calls to mapping APIs like MapCallerPtr or MapPtrToProcess, and convert them to calls to marshalling APIs like CeOpenCallerBuffer / CeCloseCallerBuffer. Second, look for calls to SetKMode and SetProcPermissions. They most likely correspond to asynchronous memory access, for which you’ll now need CeAllocAsynchronousBuffer / CeFreeAsynchronousBuffer.
That will take care of most of the porting work. The other thing to look for is UI functionality. If your driver has any UI, you won’t be able to run it in the kernel. And most CE6 drivers will run in the kernel. Even if your driver will run in user mode, we recommend using the kernel UI handling to maximize portability between user and kernel mode. In CE6, drivers that require UI should break that UI functionality out into a companion user-mode DLL. Move all the resources, shell calls, etc. into the new DLL. Then use the new CeCallUserProc API to call into the user-mode helper.
LPVOID lpInBuffer, DWORD nInBufferSize,
LPVOID lpOutBuffer, DWORD nOutBufferSize,
This is something like a combination of LoadLibrary / GetProcAddress with an IOCTL call. When a kernel-mode driver calls this API, we’ll load the DLL inside an instance of udevice.exe. When a user-mode driver calls this API, the DLL will load in-proc inside the same instance of udevice.exe that the user-mode driver is running in. So drivers that use this API can run in kernel or user mode without change.
The one big difference between CeCallUserProc and an IOCTL is that CeCallUserProc does NOT allow embedded pointers. All arguments must be stored inside the single “in” buffer passed to CeCallUserProc, and return data must be stored in the single “out” buffer. The problem is, if kernel code calls user code, user code cannot use CeOpenCallerBuffer or any other method to get the contents of kernel memory. We never allow user-mode code to access kernel-mode memory.
And, while you are modifying your drivers to use the new marshalling helpers and CeCallUserProc, you might as well check to see if it needs to do any secure-copy or exception handling it never did before… As I outlined in the marshalling post. Remember, now that drivers run in the kernel, you must be more careful than ever to preserve the security and stability of the system.
As we’ve already mentioned, CE6 now supports running drivers inside a user-mode driver host, udevice.exe. User-mode drivers work pretty much the same as kernel-mode drivers: an application calls ActivateDevice(Ex) and DeactivateDevice on the driver. The device manager will check registry settings to see if the driver is supposed to be loaded in user mode. You can also use registry settings to specify an instance “ID” of udevice.exe to use, if you want multiple user-mode drivers to load into the same process.
For example, there is one user-mode driver group with ID 3. Multiple drivers load into this group. If you look inside the CE6 %_WINCEROOT%\public\common\oak\files\common.reg (an unprocessed version of what you get in your release directory), you’ll see how this group is created and a few drivers that belong to it.
; Flags==0x10 is DEVFLAGS_LOAD_AS_USERPROC
If you don’t specify a process group, your driver will be launched inside a unique instance of udevice.exe.
The device manager creates a reflector service object to help the user-mode driver do its job. The reflector service launches udevice.exe, mounts the specified volume and registers the file system volume APIs for communicating with the driver. Communication between applications and the user mode driver pass through the reflector, which helps with buffer marshalling. The reflector also assists the user-mode driver with operations that user-mode code is not normally allowed to make, like mapping physical memory; more on this later.
It is our goal that drivers should be as close to 100% portable between kernel and user mode as possible. However, kernel code will always be more privileged than user code will be. Taking advantage of the increased kernel capabilities will make your kernel-mode driver impossible to port to user mode.
What are some of the incompatibilities you need to know about?
As I explained in the marshalling post, user-mode drivers cannot write back pointer parameters asynchronously. I take it a step further and say that user-mode drivers cannot operate on caller memory asynchronously. That you’re better off keeping such drivers in kernel mode for now, or restructuring their communication with the caller so that nothing is asynchronous.
Another detail you should know about is that user-mode drivers cannot receive embedded pointers from the kernel. This is exactly the same as saying that CeCallUserProc cannot support embedded pointers. If you’re writing a driver that talks to kernel-mode drivers, and those kernel-mode drivers pass you embedded pointers, then your driver may have no choice but to run in kernel mode. If you can reorganize the communication between drivers, you may be able to “flatten” the structure so that, like CeCallUserProc, all the data is stored directly in the IN and OUT buffers instead of referenced via embedded pointers.
There are some APIs which used to require trust that now are (mostly) blocked against use in user mode. One notable example is VirtualCopy, and its wrapper function MmMapIoSpace. Most user-mode code cannot call VirtualCopy. User-mode drivers can, with a little help from the reflector. The reflector can call VirtualCopy on behalf of a user-mode driver, but it will not do so unless it knows the driver is allowed to use the addresses it’s copying. Under each driver setup entry in the registry, there are IOBase and IOLen keys that we use to mark physical memory. When your driver calls VirtualCopy, the reflector will check these values to make sure your driver is allowed to access the physical address. For example, the serial driver might specify a physical address like this:
If you have just one buffer to copy, use DWORD values. Use multi-strings to specify multiple base addresses and sizes.
Since only privileged applications can write to this part of the registry, the registry keys should protect against unprivileged code trying to gain access to these addresses.
Notable APIs that user-mode code cannot call:
- VM APIs: VirtualCopy[Ex], LockPages[Ex], CreateStaticMapping
- Interrupt APIs: InterruptInitialize, InterruptDone, LoadIntChainHandler
- You cannot install IISR directly, though you can install GIISR via the reflector. (GIISR exposes well known interfaces and the reflector can do the required checks on these calls.)
- OAL IOCTLs that are not explicitly permitted by the kernel
Call-backs from a user-mode driver to any process are also prohibited. The most important repercussion of this is, if you move a bus driver to user mode, you’d have to move the client drivers to user mode too. You can’t have the client driver in the kernel since you cannot call back to the bus driver. You may want to put the bus driver and all of its client drivers in the same udevice.exe instance, so that the callbacks are all within a single process.
OEMs can choose to expose additional OAL IOCTLs and APIs to user mode by building a kernel-mode driver that provides these services – by essentially writing their own version of a reflector. There is a kernel-mode driver, the oalioctl driver, that OEMs can extend to this end. Anyone who’s not an OEM would have to write their own kernel-mode driver to do it. But be warned! Using oalioctl or writing new kernel-mode drivers to expose this functionality is essentially opening up a security gap that we (Microsoft) sought to close. Personally I advise against it.
Writing CE5 drivers to be compatible with CE6
I would like to mention that Steve Maillet, one of our eMVPs, had a good suggestion: you can set up abstractions which combine the CE5 and CE6 driver needs, so that all you have to do is reimplement the abstraction layer in order to port from CE5 to CE6. He even presented his abstraction layer at this year’s MEDC (Mobile & Embedded DevCon, 2006). I don’t know if he’s interested in giving it out widely, but you could contact him at EmbeddedFusion (http://www.embeddedfusion.com/), or steal his idea and implement your own layer.
Juggs Ravalia did a Channel 9 interview on the topic of drivers in CE6 – if you don’t like my explanation, maybe you’ll like his better. He knows much more about our user mode driver framework than I do. https://channel9.msdn.com/Showpost.aspx?postid=233119
P.S. Many thanks to Juggs for reviewing this post and my marshalling post! Now I've just gotta get him blogging here too...