Logo
blank Skip to main content

Windows File System Filter Driver Development: Tutorial and Examples

This tutorial provides you with easy to understand steps for a simple file system filter driver development. The demo driver that we show you how to create prints names of open files to debug output.

This article is written for engineers with basic Windows device driver development experience as well as knowledge of C/C++. In addition, it could also be useful for people without a deep understanding of Windows driver development.

What is Windows file system filter driver?

A 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 the behavior of the file system. File system filter drivers are comparable to legacy drivers, although they require several special development steps. Security, backup, snapshot, and anti-viruse software uses such drivers.

Developing a Simple File System Filter Driver

Before starting development

First, Windows file system driver development requires the IFS or WDK kit from the Microsoft website. You also have to set the %WINDDK% environment variable to the path, where you have installed the WDK/IFS kit.

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

Main.c

File system filter driver entry

It is an access point for any driver, for example, for file system filter driver. The first thing we should do is store DriverObject as a global variable (we’ll use it later):

C
//
// 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;
    ...
}

Looking for niche driver development experts?

The Apriorit team has been working on challenging driver and kernel tasks for over 20 years. Let us take your development project to a new level.

Setting the IRP dispatch table

The next step in developing a file system filter driver is populating the IRP dispatch table with function pointers to IRP handlers. We’ll have a generic pass-through IRP handler in our filer driver that sends requests further. We’ll also need a handler for IRP_MJ_CREATE to retrieve the names of open files. We’ll consider the implementation of IRP handlers later.

C
//
// 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 I/O dispatch table

File system filter driver requires fast I/O dispatch table. Not setting up this table would lead to the system crashing. Fast I/O is a different way to initiate I/O operations that’s faster than IRP. Fast I/O operations are always synchronous. If the fast I/O handler returns FALSE, then we cannot use fast I/O. In this case, IRP will be created.

C
//
// 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 notifications about file system changes

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

C
//
// 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. This routine will help you to load and unload your file system filter driver without needing to reboot. Nonetheless, this driver only truly becomes unloadable for debugging purposes, as it’s impossible to unload file system filters safely. It’s not recommended to perform unloading in production code.

C
//
// 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

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

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

After unregistering the notification, you should loop through created devices and detach and remove them. Then wait for five seconds until all outstanding IRPs have completed. Note that this is a debug-only solution. It works in the greater number of cases, but there’s no guarantee that it will work in all of them.

C
//
// 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);
    }
}

Read also

How to Develop a Windows File System Minifilter Driver: Complete Tutorial

Follow our step-by-step tutorial to build a simple file system minifilter driver and see how you can manage file system I/O operations in Windows with it.

Learn more
Developing a Windows File System Minifilter Driver

IrpDispatch.c

Dispatch pass-through

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

C
//
// 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 the debug output. After that, we call the pass-through handler that we’ve 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 retrieving file names in those cases.

C
//
// 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

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

C
//  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 I/O pass-through

Unlike IRP requests, passing through fast-IO requests requires a huge amount of code because each fast I/O function has its own set of parameters. Below you can find an example of a common pass-through function:

C
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 I/O detach device

Detach device is a specific fast I/O request that we should handle without calling the next driver. We should delete our filter device after detaching it from the file system device stack. Below you can find example code demonstrating how to easily manage this request:

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

Related project

Improving a Windows Audio Driver to Obtain a WHQL Release Signature

By working closely with Apriorit, IRIS ensured that their AI-powered noise-canceling app delivers a consistent cross-platform user experience. As a result, IRIS expanded their audience while maintaining high standards of performance and security.

Project details
Improving a Windows Audio Driver to Obtain a WHQL Release Signature

Notification.c

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

A callback is invoked for all active file systems each time a file system either registers or unregisters itself as active. This is a great place for attaching or detaching our file system filter device. When the file system activates itself, we attach to its control device (if not already attached) and enumerate its volume devices and attach to them too. While deactivating the file system, we examine its control device stack, find our device, and detach it. Detaching from file system volume devices is performed in the FsFilterFastIoDetachDevice routine that we described earlier.

C
//
// 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 attach, we need to call IoCreateDevice to create a 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 case of failure. Our attachment request can fail if the device object has not finished initializating. This can happen if we try to mount the volume-only filter. After attaching, we save the โ€œattached toโ€ device object to the device extension and clear the DO_DEVICE_INITIALIZING flag. Below you can see the device extension:

C
//
// 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, that we attached to and then call IoDetachDevice and IoDeleteDevice.

C
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

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

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

Sources and makefile

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

Contents of the sources file:

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

The makefile is standard:

C
!include $(NTMAKEENV)makefile.def

MSVC makefile project build command line is:

C
call $(WINDDK)binsetenv.bat $(WINDDK) chk wxpncd /d $(ProjectDir)nbuild.exe โ€“I

Related project

Developing Drivers for Low Latency Virtual Reality Headsets

By partnering with Apriorit, our client achieved the high-speed data transfer needed for their industrial VR headset. We developed custom PCI Express drivers that ensured stable performance, low latency, and data transmission speeds up to 10 Gbps โ€” crucial for the success of their product.

Project details
Developing Drivers for Low Latency Virtual Reality Headsets

How To Install a 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 filter driver

To install the file system filter driver, call:

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

This will create a new service entry with the name FsFilter with a service type of filesystem and a binary path of c:FsFilter.sys.

Start file system filter driver

To start the file system filter driver, call:

ShellScript
sc start FsFilter

The FsFilter service will be started.

Stop file system driver

To stop the file system filter driver, call:

ShellScript
sc stop FsFilter

The FsFilter service will be stopped.

Uninstall file system filter driver

To uninstall the file system filter driver, call:

ShellScript
sc delete FsFilter

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

Resulting script

We can put all those commands into a single batch file to make driver testing easier. Below are the contents of our Install.cmd command file:

ShellScript
sc create FsFilter type= filesys binPath= c:FsFilter.sysnsc start FsFilternpausensc stop FsFilternsc delete FsFilternpause

Running a Sample of the File System Filter Driver

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 view devices and drivers.

First, letโ€™s build the driver. After that, we’ll copy the resultant FsFilter.sys file and the Install.cmd script to the root of the C drive.

Building the driver

 

File system filter driver and the install script on the C drive.

Now we’ll run Install.cmd, which installs and starts the file system driver, and then waits for user input.

Installing the driver

 

The file system filter driver has been successfully installed and started.

Now we should start the DebugView utility.

Debug output monitoring.

 

Debug output monitoring.

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

The device tree.

 

Our filter driver in the device tree.

There are various devices created by our driver. Letโ€™s open the NTFS driver and take a look at the device tree:

Attachment to NTFS.

 

Our filter is attached to NTFS.

We’re attached now. Letโ€™s take a look at other file systems:

Attachment to other file system

 

Our filter is also attached to other file systems.

Finally, we can press any key to continue our install script, stopping and uninstalling the driver.

Stopping and uninstalling the driver

 

Our file system filter driver has been stopped and uninstalled.

We can press F5 to refresh the device tree list:

Removing the driver from device tree

 

Our filter devices are no longer in the device tree.

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

Getting more advanced

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

Conclusion

In our tutorial, we’ve provided you with simple steps for Windows file system filter driver development. We’ve shown how to install, start, stop, and uninstall a file system filter driver using the command line. Other file system filter driver issues have also been discussed. We’ve considered the file system device stack with attached filters and have discussed how to monitor debug output from the driver. You can use the resources in this article as a skeleton for developing your own file system filter driver and modify its behavior according to your needs.

Check out our another article about virtual disk driver development if you want to learn how to build such a driver for Windows.

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.

Hope you enjoyed our Windows driver development tutorial. Ready to hire an experienced team to work on your project like file system filter driver development? Just contact us and we will provide you all details!

This tutorial shows you how to write a driver for Windows. The demo driver that we show you how to create prints names of open files to debug output.

This article is written for engineers with basic Windows device driver development experience as well as knowledge of C/C++. In addition, it could also be useful for people without a deep understanding of Windows driver development.

Ready to level up your driver development?

Let Apriorit experts assess your project, propose an efficient development plan, and implement it without any risks

Have a question?

Ask our expert!

Michael-Teslia
Michael Teslia

Program Manager

Tell us about
your project

...And our team will:

  • Process your request within 1-2 business days.
  • Get back to you with an offer based on your project's scope and requirements.
  • Set a call to discuss your future project in detail and finalize the offer.
  • Sign a contract with you to start working on your project.

Do not have any specific task for us in mind but our skills seem interesting? Get a quick Apriorit intro to better understand our team capabilities.