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.
Contents
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):
//
// 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.
//
// 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.
//
// 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.
//
// 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.
//
// 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.
//
// 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.
//
// 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.
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.
//
// 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.
//
// 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:
// 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:
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:
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.
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.
//
// 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:
//
// 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.
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
).
//
// 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:
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)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.
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:
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:
sc start FsFilter
The FsFilter
service will be started.
Stop file system driver
To stop the file system filter driver, call:
sc stop FsFilter
The FsFilter
service will be stopped.
Uninstall file system filter driver
To uninstall the file system filter driver, call:
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:
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.
Now we’ll run Install.cmd, which installs and starts the file system driver, and then waits for user input.
Now we should start the DebugView utility.
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.
There are various devices created by our driver. Letโs open the NTFS driver and take a look at the device tree:
We’re attached now. Letโs take a look at other file systems:
Finally, we can press any key to continue our install script, stopping and uninstalling the driver.
We can press F5 to refresh the device tree list:
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
- File System Filter Drivers
- Content for File System or File System Filter Developers
- Windows NT File System Internals (OSR Classic Reprints) (Paperback)
- 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