Wednesday, December 16, 2009

Driver to Service communication- Don’t reinvent the wheel!

Most of us have at one time or the other implemented "something" that was required for the communication between the user mode service and the kernel mode driver. Things are pretty straightforward when the communication is triggered from the service. For example, the service wants to send a list of files to be excluded from whatever magic the driver is doing. Now, let's talk about the communication originating from the driver. What then? Those who have implemented something like this know that it is not as straightforward as the previous case. Ofcoure, if you are writing a file system minifilter, then you are good. Just call FltSendMessage and you are done.

So, let's see if we can make use of the filter manager's communication port functionality to send and receive messages in our driver even if our driver is not a FS minifilter. Let's try to divide the functionality provided by filter manager in two categories: FS filtering and communication. If you observe carefully, both of them are independent of each other. However, the initial call to register the driver with the filter manager does check some registry entries that are specific to a FS minifilter; for example the altitude. Anyways, I tried to write a driver that received process notifications using the Process manager callbacks. My objective was to be able to receive the callback and send the information to the user mode service. So, I started off by using the INF of nullfilter minifilter sample.

Note that the driver that I tried to write was a "no stack" driver- meaning that it was not attached to network stack or storage stack, etc. Still need to figure out if that will work. Anyways, coming back to the driver, I cooked up the FLT_REGISTRATION structure in the following fashion:

CONST FLT_REGISTRATION registrationData =

{

sizeof (FLT_REGISTRATION),

FLT_REGISTRATION_VERSION,

0,

NULL,

NULL,

FooUnload,

NULL,

NULL,

NULL,

NULL,

NULL,

NULL,

NULL,

};


If you observe carefully, the only non-NULL parameter in this structure (except for the first two) is the FooUnload where I have registerd the unload routine. The idea of using this kind of registration structure was to tell the filter manager that we are not interested in any FS I/O callbacks (create, read, write, etc.) or in attaching to any volumes. Note that the 7th parameter is NULL (used to specify the Instance setup callback) but it does not mean that the filter manager will not create instance corresponding to this filter for the volumes. To be able to tell filter manager not to attach to any volume, you need to register an Instance setup callback and then return STATUS_FLT_DO_NOT_ATTACH for all the instance setup callbacks. I had other reasons for letting filter manager create the instances for me, so set it to NULL. Next interesting thing to note here is that you must specify a unload routine in the registration structure. And this where things change a bit. The signature of the unload routine for a regular driver is different from that of a minifilter. So, you need to use the signature required by filter manager model and simply do all the required cleanup in this unload routine.

Next step is to call FltRegisterFilter which will register the driver with the filter manager. And then you can simply create the communication port using the FltCreateCommunicationPort call. And then? Simply start firing requests as you do in the minifilter.

Couple of points to ponder and explore:

  1. What altitude should we ask from MS for this kind of a filter? We are not going to be layered between other drivers in the FS stack. Well, it doesn't really matter what altitude you get because altitudes define the order in which you will see the I/O requests with respect to other filters. So, simply ask for a activity monitoring range.
  2. What happens if we want to attach to a storage stack? The filter manager might not be avialable that soon. Well, think of it this way, you need to communicate with you service only when the service is up. Right? So, you can delay registering your driver with the filter manager by "somehow" detecting that the user mode environment is ready. Or, if it is acceptable for your driver's functionality, you can simply put a dependency on fltmgr.sys in your driver's registry entry.
  3. Implications of LoadOrderGroup and driver type.

Will try out more experiments for the above points and post them soon.

However, there is an alternate way of achieving this. Make a common communication driver that implements this functionalty and export various communication functions to be used by other drivers that you develop. And I like this one. This export driver can load after the filter manager is up. But your driver can load even before that. And anyways, you can be sure that your communication driver is up when you "actually" want to communicate with your service. Just remember to set the start type of your export driver appropriately.

Thursday, April 2, 2009

FltLockUserBuffer locks the buffer in CORRECT process context- HOW?

The WDK documentation says "The caller can be running in any process context. FltLockUserBuffer automatically locks the buffer in the correct process context." Remember that in our legacy filters we used to call MmProbeAndLockPages in the correct process context to lock the pages? Well, the fact is that we can do the same thing that FltLockUserBuffer does, to lock the pages properly, no matter in which process context we are.

Anyways, let's see how FltLockUserBuffer locks the buffer of a process properly even if it is called from a different process context.

Let's see a portion of the disassembly for FltLockUserBuffer and it becomes obvious as to how it is able to lock the pages in correct process context.


 

f84cc355 ff15b4df4cf8    call dword ptr [fltMgr!_imp__IoThreadToProcess (f84cdfb4)]

f84cc35b 50        push eax

f84cc35c ff36        push dword ptr [esi]

f84cc35e ff1568df4cf8    call dword ptr [fltMgr!_imp__MmProbeAndLockProcessPages (f84cdf68)]

f84cc364 834dfcff    or dword ptr [ebp-4],0FFFFFFFFh

f84cc368 807de700    cmp byte ptr [ebp-19h],0


 

In the disassembly, you can see 2 functions that make things obvious: IoThreadToProcess and MmProbeAndLockProcessPages.

So, what does each function do? Well, IoThreadToProcess returns a PEPROCESS given a PETHREAD. And if you remember, FLT_CALLBACK_DATA structure already has a parameter 'Thread' which identifies the thread that initiated the I/O. So, from this thread, the target process is found using the IoThreadToProcess function. The next function is MmProbeAndLockProcessPages. As the name suggests, it locks the pages of a particular process.

Let's see a portion of the disassembly of MmProbeAndLockProcessPages.

8059c5d8 e85bb0f5ff        call nt!KeStackAttachProcess (804f7638)

8059c5dd c745e401000000    mov dword ptr [ebp-1Ch],1

8059c5e4 8975fc        mov dword ptr [ebp-4],esi

8059c5e7 ff7514            push dword ptr [ebp+14h]

8059c5ea ff7510            push dword ptr [ebp+10h]

8059c5ed ff7508            push dword ptr [ebp+8]

8059c5f0 e8c19bf6ff        call nt!MmProbeAndLockPages (805061b6)


 

As you can see, MmProbeAndLockProcessPages internally calls KeStackAttachProcess to attach to the target process for which the pages have to be locked. Once it gets attached, it then calls MmProbeAndLockPages to lock the pages!


 

Simple and sweet! J