Subscribe to receive all latest blog updates

This tutorial provides you with easy to understand steps for a simple file system filter driver development. As a result, obtained demo driver is intended to print names of open files to debug output.

This article would be interesting for developers with basic Windows driver experience as well as C/C++ knowledge. In addition, it could be also useful to people without Windows driver development deep understanding.

Written by:
Sergey Podobriy,
Leader of Driver Team

What is Windows file system filter driver?

Windows file system filter driver is called during each file system I/O operation (create, read, write, rename, etc.). Therefore, it is able to modify behavior of file system. File system filter drivers are comparable to legacy drivers, although they require several special development steps. Security, backup and snapshot software, as well as anti-viruses use such drivers.

Developing a Simple File System Filter Driver

Before starting development

First, in order to develop a file system filter driver, you need WDK or IFS Kit from the Microsoft website to build any file system filter driver. You also have to set the %WINDDK% environment variable to the path, where you have installed WDK/IFS Kit.

Attention: Even the smallest error in file system driver can cause BSOD or instability of the system.

Main.c

File system filter driver entry

It is an access point of any driver, including file system filter one. The first thing we should do is storing DriverObject as a global variable (we'll use it later).

//////////////////////////////////////////////////////////////////////////
// Global data
PDRIVER_OBJECT   g_fsFilterDriverObject = NULL;
//////////////////////////////////////////////////////////////////////////
// DriverEntry - Entry point of the driver
NTSTATUS DriverEntry(
    __inout PDRIVER_OBJECT  DriverObject,
    __in    PUNICODE_STRING RegistryPath
    )
{    
    NTSTATUS status = STATUS_SUCCESS;
    ULONG    i      = 0;
    //ASSERT(FALSE); // This will break to debugger
    //
    // Store our driver object.
    //
    g_fsFilterDriverObject = DriverObject;
    ...
}

Setting IRP dispatch table

The next step of developing file system filter driver is populating the IRP dispatch table with function pointers to IRP handlers. We have a generic pass-through IRP handler (it sends request further) in our filter driver. We will also need a handler for IRP_MJ_CREATE to retrieve names of the open files. IRP handlers implementation will be considered later.

//////////////////////////////////////////////////////////////////////////
// DriverEntry - Entry point of the driver
NTSTATUS DriverEntry(
    __inout PDRIVER_OBJECT  DriverObject,
    __in    PUNICODE_STRING RegistryPath
    )
{    
    ...
    //
    //  Initialize the driver object dispatch table.
    //
    for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; ++i) 
    {
        DriverObject->MajorFunction[i] = FsFilterDispatchPassThrough;
    }
    DriverObject->MajorFunction[IRP_MJ_CREATE] = FsFilterDispatchCreate;
    ...
}

Setting Fast-IO dispatch table

File system filter driver requires fast-IO dispatch table. Not setting it up would lead to crash of the system. Fast- IO is a different way to initiate I/O operations (by the way, faster than IRP). Fast- IO operations are always synchronous. If fast- IO handler returns FALSE, then we cannot use fast- IO way. In this case, IRP will be created.

//////////////////////////////////////////////////////////////////////////
// Global data
FAST_IO_DISPATCH g_fastIoDispatch =
{
    sizeof(FAST_IO_DISPATCH),
    FsFilterFastIoCheckIfPossible,
    ...
};
//////////////////////////////////////////////////////////////////////////
// DriverEntry - Entry point of the driver
NTSTATUS DriverEntry(
    __inout PDRIVER_OBJECT  DriverObject,
    __in    PUNICODE_STRING RegistryPath
    )
{    
    ...
    //
    // Set fast-io dispatch table.
    //
    DriverObject->FastIoDispatch = &g_fastIoDispatch;
    ...
}

Registering notification about file system changes

While developing file system filter driver, we should register a notification about file system changes. It is crucial to track if file system is being activated or deactivated to perform attaching/detaching of our file system filter driver. Below you can see the way of tracking file system changes.

//////////////////////////////////////////////////////////////////////////
// DriverEntry - Entry point of the driver
NTSTATUS DriverEntry(
    __inout PDRIVER_OBJECT  DriverObject,
    __in    PUNICODE_STRING RegistryPath
    )
{    
    ...
    //
    //  Registered callback routine for file system changes.
    //
    status = IoRegisterFsRegistrationChange(DriverObject, FsFilterNotificationCallback); 
    if (!NT_SUCCESS(status)) 
    {
        return status;
    }
    ...
}

Setting driver unload routine

The final part of the file system driver initialization is setting an unload routine. It will help you to load or unload file system filter driver multiple times without need to restart OS. Nonetheless, this driver becomes unloadable just for debugging purposes, because it is impossible to unload file system filters in a safe way. It is not recommended to perform it in production code.

//////////////////////////////////////////////////////////////////////////
// DriverEntry - Entry point of the driver
NTSTATUS DriverEntry(
    __inout PDRIVER_OBJECT  DriverObject,
    __in    PUNICODE_STRING RegistryPath
    )
{    
    ...
    //
    // Set driver unload routine (debug purpose only).
    //
    DriverObject->DriverUnload = FsFilterUnload;
    return STATUS_SUCCESS;
}

File system driver unload implementation

Driver unload routine cleans up resources and deallocates them. The next step of file system driver development is unregistering the notification for file system changes.

//////////////////////////////////////////////////////////////////////////
// Unload routine
VOID FsFilterUnload(
    __in PDRIVER_OBJECT DriverObject
    )
{
    ...
    //
    //  Unregistered callback routine for file system changes.
    //
    IoUnregisterFsRegistrationChange(DriverObject, FsFilterNotificationCallback);
    ...
}

After doing that, you should loop through created devices, detach, and remove them. Then wait for 5 seconds until all outstanding IRPs are completed. Notice that it is a debug only solution. It works for the greater number of cases, but there is no guarantee for all of them.

//////////////////////////////////////////////////////////////////////////
// Unload routine
VOID FsFilterUnload(
    __in PDRIVER_OBJECT DriverObject
    )
{
    ...
    for (;;)
    {
        IoEnumerateDeviceObjectList(
            DriverObject,
            devList,
            sizeof(devList),
            &numDevices);
        if (0 == numDevices)
        {
            break;
        }
        numDevices = min(numDevices, RTL_NUMBER_OF(devList));
        for (i = 0; i < numDevices; ++i) 
        {
            FsFilterDetachFromDevice(devList[i]);
            ObDereferenceObject(devList[i]);
        }
        
        KeDelayExecutionThread(KernelMode, FALSE, &interval);
    }
}

IrpDispatch.c

Dispatch pass-through

The only responsibility of this IRP handler is to pass requests further to next driver. Next driver object is stored in our device extension.

///////////////////////////////////////////////////////////////////////////////////////////////////
// PassThrough IRP Handler
NTSTATUS FsFilterDispatchPassThrough(
    __in PDEVICE_OBJECT DeviceObject, 
    __in PIRP           Irp
    )
{
    PFSFILTER_DEVICE_EXTENSION pDevExt = (PFSFILTER_DEVICE_EXTENSION)DeviceObject->DeviceExtension;
    IoSkipCurrentIrpStackLocation(Irp);
    return IoCallDriver(pDevExt->AttachedToDeviceObject, Irp);
}

Dispatch create

Every file create operation invokes this IRP handler. After grabbing a filename from PFILE_OBJECT, we print it to debug output. After that, we call the pass-through handler, which we have described above. Notice that a valid file name exists in PFILE_OBJECT only until the file create operation is finished! There also exist relative opens as well as opens by id. In third-party resources, you can find more details about file names retrieving in those cases.

///////////////////////////////////////////////////////////////////////////////////////////////////
// IRP_MJ_CREATE IRP Handler
NTSTATUS FsFilterDispatchCreate(
    __in PDEVICE_OBJECT DeviceObject,
    __in PIRP           Irp
    )
{
    PFILE_OBJECT pFileObject = IoGetCurrentIrpStackLocation(Irp)->FileObject;
    DbgPrint("%wZ\n", &pFileObject->FileName);
    return FsFilterDispatchPassThrough(DeviceObject, Irp);
}

FastIo.c

As not all of the fast-IO routines should be implemented by the underlying file system, we have to test the fast-IO dispatch table validity for the next driver, using the following macro:

//  Macro to test if FAST_IO_DISPATCH handling routine is valid
#define VALID_FAST_IO_DISPATCH_HANDLER(_FastIoDispatchPtr, _FieldName) \
    (((_FastIoDispatchPtr) != NULL) && \
    (((_FastIoDispatchPtr)->SizeOfFastIoDispatch) >= \
    (FIELD_OFFSET(FAST_IO_DISPATCH, _FieldName) + sizeof(void *))) && \
    ((_FastIoDispatchPtr)->_FieldName != NULL))

Fast-IO pass-through

Unlike IRP requests, passing through fast-IO requests requires a huge amount of code, because there is its own set of parameters for each fast-IO function. Below you can find a common pass-through function example:

BOOLEAN FsFilterFastIoQueryBasicInfo(
    __in PFILE_OBJECT       FileObject,
    __in BOOLEAN            Wait,
    __out PFILE_BASIC_INFORMATION Buffer,
    __out PIO_STATUS_BLOCK  IoStatus,
    __in PDEVICE_OBJECT     DeviceObject
    )
{
    //
    //  Pass through logic for this type of Fast I/O
    //
    PDEVICE_OBJECT    nextDeviceObject = ((PFSFILTER_DEVICE_EXTENSION)
DeviceObject->DeviceExtension)->AttachedToDeviceObject;
    PFAST_IO_DISPATCH fastIoDispatch = nextDeviceObject->DriverObject
->FastIoDispatch;
    if (VALID_FAST_IO_DISPATCH_HANDLER(fastIoDispatch, FastIoQueryBasicInfo)) 
    {
        return (fastIoDispatch->FastIoQueryBasicInfo)(
            FileObject,
            Wait,
            Buffer,
            IoStatus,
            nextDeviceObject);
    }
    
    return FALSE;
}

Fast-IO detach device

That is a specific fast-IO request, which we should handle ourselves without calling next driver. We should delete our filter device after detaching it from file system device stack. Below you can find code example demonstrating the way to easily manage it:

VOID FsFilterFastIoDetachDevice(
    __in PDEVICE_OBJECT     SourceDevice,
    __in PDEVICE_OBJECT     TargetDevice
    )
{
    //
    //  Detach from the file system's volume device object.
    //
    IoDetachDevice(TargetDevice);
    IoDeleteDevice(SourceDevice);
}
    

Notification.c

Common file system consists of control device and volume ones. Volume devices are attached to the storage device stack. Control device is registered as a file system.

Figure 1 - Common file system devices

There is a callback that is being invoked for all active file systems each time file system has earlier registered or unregistered itself as active one. It is a great place for attaching or detaching our file system filter device. When file system activates itself, we are attaching to its control device (in case if we are not already attached), enumerating its volume devices and attaching to them too. While deactivating file system, we examine its control device stack, find our device, and detach it. Detaching from file system volume devices is performed in FsFilterFastIoDetachDevice routine, which was described earlier.

//////////////////////////////////////////////////////////////////////
// This routine is invoked whenever a file system has either registered or
// unregistered itself as an active file system.
VOID FsFilterNotificationCallback(
    __in PDEVICE_OBJECT DeviceObject,
    __in BOOLEAN        FsActive
    )
{
    //
    //  Handle attaching/detaching from the given file system.
    //
    if (FsActive)
    {
        FsFilterAttachToFileSystemDevice(DeviceObject);
    }
    else
    {
        FsFilterDetachFromFileSystemDevice(DeviceObject);
    }
}

AttachDetach.c

This file contains helper routines for attaching, detaching, and checking if our filter is already attached.

Attaching

In order to perform attaching, we should call IoCreateDevice to create new device object with device extension, and then propagate device object flags from the device object we are trying to attach to (DO_BUFFERED_IO, DO_DIRECT_IO, FILE_DEVICE_SECURE_OPEN). Then we call IoAttachDeviceToDeviceStackSafe in a loop with delay in the case of failure. Our attachment request can fail, as the device object has not finished initialization. It can happen if we try to mount the volume only filter. After attaching, we save “attached to” device object to the device extension and clear DO_DEVICE_INITIALIZING flag. Below you can see the device extension:

//////////////////////////////////////////////////////////////////////////
// Structures
typedef struct _FSFILTER_DEVICE_EXTENSION
{
    PDEVICE_OBJECT AttachedToDeviceObject;
} FSFILTER_DEVICE_EXTENSION, *PFSFILTER_DEVICE_EXTENSION;

Detaching

Detaching is rather simple. From the device extension, we get the device, we attached to, and call IoDetachDevice and IoDeleteDevice.

void FsFilterDetachFromDevice(
    __in PDEVICE_OBJECT DeviceObject
    )
{    
    PFSFILTER_DEVICE_EXTENSION pDevExt = (PFSFILTER_DEVICE_EXTENSION)
DeviceObject->DeviceExtension;
    
    IoDetachDevice(pDevExt->AttachedToDeviceObject);
    IoDeleteDevice(DeviceObject);
}

Checking if our device is attached

In order to check if we are attached to a device or not, we have to iterate through the device stack using IoGetAttachedDeviceReference and IoGetLowerDeviceObject, and then look for our device there. We can differentiate our devices by comparing device driver object with our driver one (g_fsFilterDriverObject).

//////////////////////////////////////////////////////////////////////////
// Misc
BOOLEAN FsFilterIsMyDeviceObject(
    __in PDEVICE_OBJECT DeviceObject
    )
{
    return DeviceObject->DriverObject == g_fsFilterDriverObject;
}

Sources and makefile

The utility, which builds the driver, uses sources and makefile files. These files contain project settings and source file names.

Content of the sources file:

TARGETNAME  = FsFilter
TARGETPATH  = obj
TARGETTYPE  = DRIVER
DRIVERTYPE  = FS
SOURCES     = \
    Main.c \
    IrpDispatch.c \
    AttachDetach.c \
    Notification.c \
    FastIo.c

The makefile is standard:

!include $(NTMAKEENV)\makefile.def

MSVC makefile project build command line is:

call $(WINDDK)\bin\setenv.bat $(WINDDK) chk wxp
cd /d $(ProjectDir)
build.exe –I

How to install file system filter driver

SC.EXE overview

We will use sc.exe (sc – service control) to manage our driver. We can use this command-line utility to query or modify the installed services database. It is shipped with Windows XP and higher, or you can find it in Windows SDK/DDK.

Install file system driver

To install the file system filter driver, call:

sc create FsFilter type= filesys binPath= c:\FSFilter.sys

Thus, there will be created a new service entry with the FsFilter name, its service type is filesystem and binary path is c:\FsFilter.sys.

Start file system driver

To start file system filter driver, call:

sc start FsFilter

Thus, the FsFilter service will be started.

Stop file system driver

To stop the file system filter driver, call:

sc stop FsFilter

Thus, the FsFilter service will be stopped.

Uninstall file system driver

In order to uninstall file system filter driver, call:

sc delete FsFilter

This command instructs service manager to remove service entry with the FsFilter name.

Resulting script

Put all those commands into a single batch file to make driver testing easier. Below there is a listing of Install.cmd command file:

sc create FsFilter type= filesys binPath= c:\FsFilter.sys
sc start FsFilter
pause
sc stop FsFilter
sc delete FsFilter
pause

Running a sample of file system filter driver

This part of the article is the most interesting and demonstrative. Now we are going to show how the file system filter works. For this purpose, we will use Sysinternals DebugView for Windows to monitor debug output as well as OSR Device Tree to overview devices and drivers.

First, let’s build the driver. After that, copy obtained FsFilter.sys file and Install.cmd script to the root of the C disk.

 

Figure 2 – File system filter driver and the install script on the C disk.

Run Install.cmd, which installs and starts the file system driver, and then waits for user input.

Figure 3 - The file system filter driver is successfully installed and started.

Now we should start the DebugView utility.

Figure 4 - Debug output monitoring.

Now we can see what files were opened! It means that our filter works. Now we should run the device tree utility and locate our driver there.

Figure 5 - Our filter driver in the device tree.

There are various devices created by our driver. Let’s open the NTFS driver and consider the device tree:

Figure 6 - Our filter is attached to NTFS.

We are attached now. Let’s consider other file systems.

Figure 7 - Our filter is also attached to other file systems.

Finally, press any key to continue our install script, so it could stop and uninstall the driver.

Figure 8 - Our file system filter driver is stopped and uninstalled.

Press F5 to refresh the device tree list:

Figure 9 - Our filter devices are no longer in the device tree.

Our file system filter disappeared and the system is running just as before.

Advancement

The file system filter driver described above is very simple, it lacks a number of functions, required for a common driver. The idea of this article was to show the easiest way to create file system filter driver, that’s why we described simple and easy-to-understand development process. You can write IRP_MJ_FILE_SYSTEM_CONTROL handler of your own to track the newly arrived volumes.

Conclusion

Our tutorial provides you with simple steps for simple file system filter driver development. It shows how to install, start, stop, and uninstall it using a command line. Other file system filter driver issues are also discussed there. We considered file system device stack with attached filters and learned the way to monitor debug output from the driver. You can use the sources of this article as a skeleton for your own file system filter driver, and if needed modify its behavior according to your needs.

References

  1. File System Filter Drivers
  2. Content for File System or File System Filter Developers
  3. Windows NT File System Internals (OSR Classic Reprints) (Paperback)
  4. sfilter DDK sample

Download source files of the sample project.

Ready to hire experienced driver development team to work on your project like file system filter driver development? Just contact us and we will provide you all details!