Yes, it is possible to write a user-mode file system for Windows in managed code, such as C#. This is often referred to as a «file system in user space» or «user-mode file system.» However, it’s important to note that this won’t be a full file system at the disk level, but rather a file system driver that runs in user space and interacts with the Windows API.
To achieve this, you can create a Windows Filter Driver using the File System Filter Driver (FSFD) in the Windows Driver Kit (WDK). However, writing a filter driver in C# is not directly supported by Microsoft, so you would need to use a managed/unmanaged interop technique, such as P/Invoke or a managed wrapper library, to interact with the native APIs from your C# code.
Here’s a high-level outline of the steps you might take:
-
Set up your development environment:
- Install Visual Studio.
- Install the Windows Driver Kit (WDK).
- Install the Windows Software Development Kit (SDK).
-
Write a native (C/C++) user-mode application to create and manage the file system.
- Implement file system operations using Windows API functions.
- Use P/Invoke or a managed wrapper library to call native functions from your C# code.
-
Write a native (C/C++) FSFD to interact with the Windows kernel.
- Register filter callbacks to handle file system events.
- Implement I/O request handling.
-
Integrate the user-mode application with the FSFD:
- Use named pipes or shared memory for inter-process communication (IPC) between the user-mode app and the FSFD.
-
Implement file system operations in your C# code:
- Map user-mode file system operations to the corresponding native functions.
- Use P/Invoke or a managed wrapper library to call native functions.
Here’s a simple example of a C# function that uses P/Invoke to create a file:
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern IntPtr CreateFile(
string lpFileName,
[MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess,
[MarshalAs(UnmanagedType.U4)] FileShare dwShareMode,
IntPtr lpSecurityAttributes,
[MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition,
[MarshalAs(UnmanagedType.U4)] FileAttributes dwFlagsAndAttributes,
IntPtr hTemplateFile);
public static FileStream CreateFileStream(string path, FileMode mode)
{
IntPtr fileHandle = CreateFile(
path,
(FileAccess)0x4, // Generic Read
(FileShare)0x4, // Read Shared
IntPtr.Zero,
mode,
0, // No Attributes
IntPtr.Zero // No Template File
);
if (fileHandle.ToInt32() == -1)
{
throw new Win32Exception();
}
return new FileStream(fileHandle, FileAccess.ReadWrite);
}
This example demonstrates the creation of a file using the CreateFile
Win32 API function. You can extend this example to handle other file system operations and integrate it with your FSFD.
Keep in mind that writing a user-mode file system is a complex task and requires a deep understanding of Windows internals, I/O operations, and the Windows kernel. This high-level overview should give you a starting point for your project. Good luck!
In order to overcome the issue with launching multiple instances of a particular service, WinFsp provides a generic launcher named WinFsp.Launcher. The WinFsp.Launcher is itself a Windows service that can be used to launch and control other services, provided that they fulfill the following requirements:
-
That they are marked as console executables.
-
That they can be parameterized using the command line.
-
That they respond to the CTRL-BREAK console control event and terminate timely.
Services that wish to be controlled by the WinFsp.Launcher must add themselves under the following registry key:
HKEY_LOCAL_MACHINE\Software\WinFsp\Services
Note |
Please note that in a 64-bit system the actual location is HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\WinFsp\Services .
|
For example, the MEMFS sample adds the following registry entries in a 64-bit system:
[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\WinFsp\Services\memfs32] "Executable"="C:\\Program Files (x86)\\WinFsp\\bin\\memfs-x86.exe" "CommandLine"="-i -F NTFS -n 65536 -s 67108864 -u %1 -m %2" "Security"="D:P(A;;RPWPLC;;;WD)" "JobControl"=dword:00000001
[HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\WinFsp\Services\memfs64] "Executable"="C:\\Program Files (x86)\\WinFsp\\bin\\memfs-x64.exe" "CommandLine"="-i -F NTFS -n 65536 -s 67108864 -u %1 -m %2" "Security"="D:P(A;;RPWPLC;;;WD)" "JobControl"=dword:00000001
When the WinFsp.Launcher starts up it creates a named pipe that applications can use to start, stop, get information about and list service instances. A small command line utility (launchctl
) can be used to issue those commands. The CallNamedPipeW
API can be used as well.
One final note regarding security. Notice the Security
registry value in the example above. This registry value uses SDDL syntax to instruct WinFsp.Launcher to allow Everyone (WD
) to start (RP
), stop (WP
) and get information (LC
) about the service instance. If the Security
registry value is missing the default is to allow only LocalSystem and Administrators to control the service instance.
Dokany
What is Dokan
When you want to create a new file system on Windows, other than FAT or NTFS,
you need to develop a file system driver. Developing a device driver that works
in kernel mode on windows is extremely technical. By using Dokan, you can create
your own file systems very easily without writing device drivers. Dokan is
similar to FUSE (Linux file system in user space) but works on Windows. Moreover,
dokany includes a FUSE wrapper
that helps you to port your FUSE filesystems without changes.
What is Dokany
Dokany is a fork of Dokan 0.6.0 with bug fixes, clean change history and
updated to build with latest tools.
Because the original Dokan Legacy (< 0.6.0) project is no longer maintained.
Since version 0.8.0, dokany broke compatibility with the dokan API. See
Choose a version
for more information.
The API has then again changed over time in 1.1.0 and 2.0.0.
Benchmark v1.5.1.1000 vs v2.0.3.1000
A benchmark that is testing multiple scenarios repeaditly and sequentially was run 5 times against the memfs
sample of v1.5.1.1000 and v2.0.3.1000 in an idle environment to precise results.
The detail results can be seen in this spreadsheet here.
As better threading and memory poll were added in v2, it is expected that concurrent scenarios (like those tests) would be even more highly improved.
A sample of the results:
Create New | +13.55% | List | +60.69% | GetAttributes | +48.78% | Read | +18-42% |
Open/Overwrite | +153.41% | ListExactFile | +131.91% | SetAttributes | +120.91% | Write | +10-32% |
RandomOpenClose | +173.05% | | | Delete | +90.83% | | |
Licensing
Dokan contains LGPL and MIT licensed programs.
- user-mode library (dokan2.dll) LGPL
- driver (dokan2.sys) LGPL
- network library (dokannp2.dll) LGPL
- fuse library (dokanfuse2.dll) LGPL
- installer (DokanSetup.exe) LGPL
- control program (dokanctl.exe) MIT
- samples (mirror.exe / memfs.exe) MIT
For details, please check the license files.
- LGPL license.lgpl.txt
- MIT license.mit.txt
You can obtain source files from https://dokan-dev.github.io
Environment
Dokan works on
- Windows Server 2022 / 2019 / 2016 / 2012 (R2) / 2008 R2 SP1
- Windows 11 / 10 / 8.1 / 8 / 7 SP1
Platform
- x86
- x64
- ARM
- ARM64
Signed Release and Debug drivers are provided at each release for all platforms.
How it works
Dokan library contains a user mode DLL (dokan2.dll) and a kernel mode file
system driver (dokan2.sys). Once the Dokan file system driver is installed, you can
create file systems which can be seen as normal file systems in Windows. The
application that creates file systems using Dokan library is called File system
application.
File operation requests from user programs (e.g., CreateFile, ReadFile,
WriteFile, …) will be sent to the Windows I/O subsystem (runs in kernel mode)
which will subsequently forward the requests to the Dokan file system driver
(dokan2.sys). By using functions provided by the Dokan user mode library
(dokan2.dll), file system applications are able to register callback functions
to the file system driver. The file system driver will invoke these callback
routines in order to respond to the requests it received. The results of the
callback routines will be sent back to the user program.
For example, when Windows Explorer requests to open a directory, the CreateFile
with Direction option request will be sent to Dokan file system driver and the
driver will invoke the CreateFile callback provided by the file system
application. The results of this routine are sent back to Windows Explorer as
the response to the CreateFile request. Therefore, the Dokan file system driver
acts as a proxy between user programs and file system applications. The
advantage of this approach is that it allows programmers to develop file systems
in user mode which is safe and easy to debug.
To learn more about Dokan file system development, see the
and the samples, especially dokan_memfs.
Build
In short, download and install the
Visual Studio 2019, select Windows 10 SDK component during the install or from the Tools menu &
install the WDK 10
For details, see the
build page.
Installation
The latest official and signed build can be downloaded from:
- Github release page
choco install dokany2
For manual installation, see the
installation page.
Contribute
You want Dokan to get better? Contribute!
Learn the code and suggest your changes on
GitHub repository.
Detect defects and report them on
GitHub issue tracker.
Ask and answer questions on
Github Discussions or
Google discussion group.
In one of our previous posts, we explained how to protect valuable user data with file encryption. Using the file system minifilter approach, we implemented a driver that can encrypt files on the fly and ensure per-process access restrictions.
While we provided a detailed description of the driver implementation process, there are still some challenges you may face when using minifilter drivers for file encryption. So in this article, we go over six challenges you may face when developing a user mode file system solution and provide some tips on how to overcome them.
This article will be helpful for developers who want to know more about ways to create a fake file system and the pitfalls of this process.
Contents:
- 1. Can you not use cache during the paging I/O process?
- 2. How to avoid double ciphering?
- 3. How to solve problems with sharing modes?
- 4. How to address difficulties with driver callbacks?
- 5. What architecture to use?
- 6. How to ensure the compatibility of the fake file system?
- Conclusion
NTFS has a standard encryption mechanism — Encrypting File System (EFS) — that is used for encrypting separate parts of the logical drive. The main problem with EFS is that if a user has permission to decrypt protected data, then all applications running in the session started by this user get access to the decrypted data. Thus, sensitive data is left unprotected against malicious applications.
To solve this issue, we decided to develop a user mode file system that can transparently encrypt selected objects. Similar to the standard EFS, our fake file system can automatically encrypt and decrypt data. The difference is that our solution splits all applications in the same user session into two groups:
- Permitted applications, which receive unencrypted data
- Prohibited applications, which receive encrypted data
To learn more about this process, read our previous post on building an encryption driver with per-process access restrictions.
When implementing an encryption driver, you may face a number of challenges. In this article, we cover the six most common questions that may arise during user mode file system driver development:
- Can we use cache during the paging input/output (I/O) process?
- How can we avoid double ciphering?
- How can we solve problems with sharing modes?
- How can we address difficulties with driver callbacks?
- What architecture should we use?
- How can we ensure compatibility of the fake file system?
Let’s dig deeper and find answers to each of these questions!
1. Can we not use cache during the paging I/O process?
File systems have three common pairs of read/write operations:
The Cache Manager typically uses paging I/O read/write operations for moving data to the cache or writing it to a file. For decrypted information, you need to maintain a separate cache for encrypting that data on the fly when writing it directly to disk — for instance, when Cache Manager flushes dirty pages to the disk due to lack of memory.
Here, you may face a challenge: since our driver isn’t a data repository, all flushed data will be redirected to the real file system. But we don’t know how the real file system will behave — it fully depends on the way we build our interaction with it.
So what you need to do is flush data to the real file system in a way that allows you to control — or at least accurately predict — its caching. Otherwise, our fake file system’s data flushed by Cache Manager will get back to Cache Manager again, but Cache Manager will see this data as data received from a different file system.
Unfortunately, Cache Manager can’t distinguish cached data received from different file systems. Furthermore, its internal implementation contains a number of blockings that prohibit any recursions. As a result, the process of flushing data goes as follows:
- Cache Manager starts flushing decrypted pages and blocks its internal structure
- Cache Manager initiates writing data to our device
- The device initiates writing data to the real file system
- The real file system tries to move data to the locked cache
- The system gets into a deadlock
There are several possible ways of solving this problem:
- Calculate the amount of available cache space
- Determine how much cache space the file system will need for writing data
- Use end-to-end non-cached data writing to the final file system when other methods can’t be used
The best solution will fully depend on the particular use case. For instance, the use of non-cached write-through I/O on the final file system will harm system performance, especially when working with small files. On the other hand, cache calculations provide you with information that quickly becomes irrelevant if there are other consumers of cache space.
2. How can we avoid double ciphering?
So far, we’ve managed to make our target application write all valuable data only to our fake file system. This allows us to transfer our crypto containers over the network without risking leaking any sensitive information. However, at this point, we face a problem of double ciphering. To avoid this, we need to make our application accept and save the sent crypto containers without encrypting the data twice. Let’s look closer at this process.
We compose larger individual blocks out of small network packets and encrypt the data with ciphering algorithms before saving it to a file. The problem is that the data initially transferred across the network has already been encrypted. So in the end, we get a file that’s been ciphered twice, and if we try to open this file through our file system, we’ll only get unreadable data.
What can be done to solve this problem?
1) Delay writing data
The first option is to delay the process of writing data until we have a fully aggregated buffer with the whole potential header of a crypto container. You need to identify the type of data header contains and write data as is if it’s already encrypted.
Usually, this trick works. But it requires injecting the code at the user space level. Furthermore, we need to have a workaround for writing data directly without encryption when needed.
However, if we’re working with a torrent file, for instance, we’ll have to cache the entire file, since we don’t know when exactly we’ll receive the whole header of the crypto container.
2) Decrypt fully downloaded data
The second option is to try to detect double ciphering after we download the whole file and perform one-time decryption. The main problem here is determining whether the file has been fully downloaded.
Some browsers intercept the downloading process to rename a temporary file and move it to the Downloads folder or any other place specified by the user. Only after that does the download resume.
The general approach here is to consider a crypto container as being invalid if the size of its declared header is less than the actual size of the downloaded file. However, if a part from the middle of the file wasn’t fully downloaded and the file was temporarily closed, we can mistakenly consider the download as finished and lift the unnecessary encryption. And if later the download agent adds the rest of the encrypted data with additional encryption, the file will become corrupted.
As you can see, neither of these methods will be helpful when it comes to partially downloading a range of bytes to a new file from the middle of an existing file. In this case, we’ll always end up with unreadable information.
3. How can we solve problems with sharing modes?
Now, let’s get back to our user mode file system on Windows. As you probably know, Windows supports sharing modes that allow you to not only open files exclusively but also work with them collectively. Furthermore, you can specify what types of operations can be performed in a shared more: read, write, or delete.
There are two standard scenarios for the situation when a file is opened repeatedly from the same or a different application with the same access permission:
- Access to the file is granted in accordance with the set permissions.
- Access to the file is prohibited and the user receives a message about the sharing violation error.
Of course, we need to imitate this behavior in our fake file system. And this is where we may face a number of challenges.
Usually, an application can open a file for reading attributes and, say, determining the size of the file. Reading attributes belongs to the class of operations that don’t require additional permissions and always can be performed. However, we need to return not the current size of the file but the size of the unencrypted file. And for that, we need to have reading rights.
Here’s one more example. The LastAccessTime attribute shows when a particular file was last accessed. But we may need to store extended information related to file access, such as the LastUserAccessed, in our crypto container. These two attributes are synchronous. But if the LastAccessTime attribute is updated automatically, even when reading the file, the LastUserAccessed attribute can only be saved in the header of the crypto container. So if you open the file with reading rights, you may need writing rights to save the LastUserAccessed attribute, and this will lead to a sharing violation error.
To solve this problem at the level of our fake file system, we’ll need to implement so-called superhandles.
A superhandle is a handle that can only be opened when all other handles are closed. So to use it, you need to take three steps:
- Close all other handles opened for the current file.
- Perform the operation.
- Re-open the closed handles.
Superhandles are helpful when dealing with simple situations similar to the one we’ve described. However, this approach may be useless in some cases. For instance, if a file is exclusively opened directly from an untrusted application, we have no control over its handle in our file system. So even if we close our handles, we won’t be able to perform an exclusive operation.
Such a problem may occur when working with antivirus applications or if there’s a clear Inter Process Communication (IPC) between a trusted and untrusted application that both perform some actions on the same file.
4. How can we address difficulties with driver callbacks?
Another common challenge you may face hides in the process of handling driver callbacks. When we create a user mode file system, we only leave the minimum necessary set of functions in the kernel for transferring requests to the service. This model works fine for dealing with fake file systems in the cloud and secured network storage, when we don’t care much about the behavior of the local machine once the request is sent further across the network.
However, when we’re talking about encrypting local files, the behavior of the local machine becomes critical. When processing a request in user mode, we may find ourselves needing to go back to the driver and perform an additional operation, such as flush the cache.
The problem can be solved with the help of custom input-output control (IOCTL) calls to our driver. But since our driver is only capable of transferring requests to the user mode, when we call it, the driver will send new requests with the updated kernel state to the user mode. And since the current thread is already busy waiting for the driver response, these requests will have to be processed by other service threads.
As a result, what we get is the need to support multithreading and take into account all possible blockings. And if you need to add an option for returning a request to the kernel, you’ll have to check all possible execution paths and blockings for deadlocks.
Also, when processing a secondary request in the service, you may need to return to the kernel once more. In this case, the code will become less and less comprehensible due to the increased number of required threads.
The only way to avoid such complexity is to implement our solution solely in the kernel.
5. What architecture should we use?
We offer the following architecture for a user mode file system solution:
- One driver
- One service
- Multiple storage plugins
Such a solution can be used as an ultimate mechanism for encrypting selected local files or connecting to arbitrary storage systems, such as cloud or secured network storage. In this case, the driver won’t even need to know where the data is sent for storage, as all data processing will be performed in user mode.
We can further improve this principle by splitting the user mode logic into two parts:
- General logic for working with all types of storage
- Specialized plugins for working with specific types of storage
Surprisingly, the standard service allows us to implement a lot of logic. The common service will take over communication with the driver and handle common problems that arise when taking out user mode requests from the kernel, including:
- Impersonating requests — We should act on behalf of the user mode service using the credentials of the user who called the driver.
- Caching credentials — We should cache the credentials provided by the kernel when we open the file, as we’ll need them later for multiple operations.
- Translating communications between the kernel and plugins — We need to translate requests and responses from ones that can be processed by the kernel to ones optimized for the plugins (and back). Such communication is often used for translating error codes.
On the other hand, plugins can be fully isolated from communicating with the kernel. In this case, they’ll be developed, tested, and used separately from the service.
If you’re also interested in improving your application’s cybersecurity, check out our article about preventing heap spraying attacks.
6. How can we ensure the compatibility of the fake file system?
In order for encrypted files to be available for AppContainer applications (Universal Windows Platform applications), we need to make sure that the current operating system doesn’t prohibit reparsing to our fake file system. To do this, we need to implement minimal functionality that would allow us to:
- consider our device as a local DiskDrive device
- use this device
There are several levels of the Windows storage architecture that an I/O request should pass through. The following table illustrates the standard Windows storage stack:
Application | Initiates I/O request |
I/O subsystem | Sends I/O to the file system |
Minifilters | Offer various functionality |
FileSystem | Provides file structures |
Volume manager | Presents volumes |
Partition manager | Manages disk partitions |
Class driver | Manages specific device type |
Port/Miniport driver | Port driver manages specific transport, e.g. SCSI port and Storport. Storport miniport driver is a vendor-supplied functionality. |
Bus driver (disk subsystem) | Satisfies I/O requests |
Generally, we need to follow this scheme. But we have nothing below the level of the fake file system. Therefore, we need to implement a custom bus driver that meets the requirements of one of the standard port protocols, such as Small Computer Systems Interface (SCSI). We also need to use standard system class and port drivers.
At the same time, we should return some fake information about the properties of the volume and partition on our device, ignore requests for data reformatting, and prevent the mounting and direct use of the data. All these operations can be assigned to a minifilter.
If you want to learn more about minifilters, check out our Windows minifilter driver tutorial.
Conclusion
Coordinating the way different applications handle encrypted data is challenging. In this article, we described a number of pitfalls you may encounter when implementing a user mode file system for Windows. Keeping these challenges in mind and planning ahead will help you get the most out of implementing your fake file system in user mode.
At Apriorit, we have vast experience developing data encryption and data management solutions. Get in touch with us and we’ll help you bring to life even the most challenging ideas.
This project provides an infrastructure to build user mode file system:
- A kernel mode file system driver is responsible of re-routing IRP to user mode code,
- A library provide a low level abstraction to build user mode file system. This part make the glu with previous kernel driver. A FUSE compatible layer is also provided,
- a sample fs, memfs: an in memory file system,
- A Windows Service called WinFsp Launcher to launch multiple user mode file system.
Source: Main | WinFsp
EDIT: Yet another solution for developping user mode filesystem with FUSE support: https://github.com/dokan-dev/dokany with a .Net binding.