In this tutorial we describe the process of creating a simple user mode file system using WinFsp. The file system we will create is called "passthrough", because it simply passes through the file system operations requested by the kernel to an underlying file system (usually NTFS).
This tutorial assumes that you have WinFsp and Visual Studio 2015 installed. The WinFsp installer can be downloaded from the WinFsp GitHub repository: https://github.com/billziss-gh/winfsp. The Microsoft Visual Studio Community 2015 can be downloaded for free from Microsoft’s web site.
When installing WinFsp make sure to choose "Developer" to ensure that all necessary header and library files are included in the installation.
With those prerequisites out of the way we are now ready to start creating our first file system.
ℹ️
|
The file system that we will create is included as a sample with the WinFsp installer. Look in the samples\passthrough directory.
|
We first start by creating the Visual Studio project. Choose "Win32 Console Application" and then select our preferred settings. For this project we will select empty project, because we will add all files ourselves.
ℹ️
|
A user mode file system services requests from the operating system. Therefore it becomes an important system component and must service requests timely. In general it should be a console mode application, not block for user input after it has been initialized, and not expose a GUI. This also allows the user mode file system to be converted into a Windows service easily or to be controlled by the WinFsp.Launcher service (see the WinFsp Service Architecture document). |
Now create and add a file passthrough.c with the following contents:
passthrough.c
#include <winfsp/winfsp.h> // (1)
#define PROGNAME "passthrough"
static
NTSTATUS SvcStart(FSP_SERVICE *Service, ULONG argc, PWSTR *argv) // (2)
{
return STATUS_NOT_IMPLEMENTED;
}
static
NTSTATUS SvcStop(FSP_SERVICE *Service) // (3)
{
return STATUS_NOT_IMPLEMENTED;
}
int wmain(int argc, wchar_t **argv)
{
return FspServiceRun(L"" PROGNAME, SvcStart, SvcStop, 0); // (4)
}
-
Include WinFsp header file.
-
Service entry point.
-
Service exit point.
-
Run file system as a service.
This simple template allows a user mode file system to be run as a console application, as a Windows service, or as a "sub-service" controlled by the WinFsp launcher.
If we try to build the program, it will fail. We must set up the locations where Visual Studio can find the WinFsp headers and libraries. The following settings must be made:
-
C/C++ > General > Additional Include Directories:
$(MSBuildProgramFiles32)\WinFsp\inc
-
Linker > Input > Additional Dependencies:
$(MSBuildProgramFiles32)\WinFsp\lib\winfsp-$(PlatformTarget).lib
ℹ️
|
These settings assume that WinFsp has been installed in the default location under "Program Files". |
We are now able to build this program. But if we try to run it:
We must make the WinFsp DLL available. There are multiple ways of doing this, but my preferred way is to delay load the DLL and then load the correct version of the DLL at run-time. This is explained below.
ℹ️
|
WinFsp made a conscious decision not to install the WinFsp DLL in one of the Windows system directories. Applications that do not ship with the operating system should not be installing components in the system directories in this author’s opinion. |
First add the WinFsp DLL as a delay loaded DLL. Open the project properties and change the following setting:
-
Linker > Input > Delay Loaded Dll’s:
winfsp-$(PlatformTarget).dll
Then add the following lines in the beginning of wmain
:
wmain excerpt
if (!NT_SUCCESS(FspLoad(0)))
return ERROR_DELAY_LOAD_FAILED;
Running this now results in a console window:
The message is The service passthrough has failed to start (Status=c0000002).
The status c0000002
is STATUS_NOT_IMPLEMENTED
, which is what we return from SvcStart
. This means that our program has actually run and we are ready to start building our passthrough file system!
We now turn our attention to the file system entry/exit points. Recall that passthrough is written as a service and its entry and exit points are SvcStart
and SvcStop
respectively.
We start with the entry point SvcStart
and first consider command line handling. We want the passthrough file system to be used as follows:
usage
usage: passthrough OPTIONS options: -d DebugFlags [-1: enable all debug logs] -D DebugLogFile [file path; use - for stderr] -u \Server\Share [UNC prefix (single backslash)] -p Directory [directory to expose as pass through file system] -m MountPoint [X:|*|directory]
The full code to handle these command line parameters is straight forward and is omitted for brevity. It can be found in the passthrough.c sample file that ships with the WinFsp installer. The code sets a number of variables that are used to configure each run of the passthrough file system.
SvcStart excerpt
PWSTR DebugLogFile = 0;
ULONG DebugFlags = 0;
PWSTR VolumePrefix = 0;
PWSTR PassThrough = 0;
PWSTR MountPoint = 0;
The variable DebugLogFile
is used to control the WinFsp debug logging mechanism. This mechanism can send messages to the debugger for display or log them into a file. The behavior is controlled by a call to FspDebugLogSetHandle
: if this call is not made any debug log messages will be sent to the debugger; if this call is made debug log messages will be logged into the specified file handle.
SvcStart excerpt
if (0 != DebugLogFile)
{
if (0 == wcscmp(L"-", DebugLogFile))
DebugLogHandle = GetStdHandle(STD_ERROR_HANDLE);
else
DebugLogHandle = CreateFileW(
DebugLogFile,
FILE_APPEND_DATA,
FILE_SHARE_READ | FILE_SHARE_WRITE,
0,
OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
0);
if (INVALID_HANDLE_VALUE == DebugLogHandle)
{
fail(L"cannot open debug log file");
goto usage;
}
FspDebugLogSetHandle(DebugLogHandle);
}
The remaining variables are used to create and start an instance of the passthrough file system.
SvcStart excerpt
Result = PtfsCreate(PassThrough, VolumePrefix, MountPoint, DebugFlags,
&Ptfs); // (1)
if (!NT_SUCCESS(Result))
{
fail(L"cannot create file system");
goto exit;
}
Result = FspFileSystemStartDispatcher(Ptfs->FileSystem, 0); // (2)
if (!NT_SUCCESS(Result))
{
fail(L"cannot start file system");
goto exit;
}
...
Service->UserContext = Ptfs; // (3)
-
Create the passthrough file system.
-
Start the file system dispatcher.
-
Associate the passthrough file system with the service instance.
We now consider the code for PtfsCreate
:
PtfsCreate
typedef struct
{
FSP_FILE_SYSTEM *FileSystem;
PWSTR Path;
} PTFS;
...
static NTSTATUS PtfsCreate(PWSTR Path, PWSTR VolumePrefix, PWSTR MountPoint, UINT32 DebugFlags,
PTFS **PPtfs)
{
WCHAR FullPath[MAX_PATH];
ULONG Length;
HANDLE Handle;
FILETIME CreationTime;
DWORD LastError;
FSP_FSCTL_VOLUME_PARAMS VolumeParams;
PTFS *Ptfs = 0;
NTSTATUS Result;
*PPtfs = 0;
Handle = CreateFileW(
Path, FILE_READ_ATTRIBUTES, 0, 0,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
if (INVALID_HANDLE_VALUE == Handle)
return FspNtStatusFromWin32(GetLastError());
Length = GetFinalPathNameByHandleW(Handle,
FullPath, FULLPATH_SIZE - 1, 0); // (1)
if (0 == Length)
{
LastError = GetLastError();
CloseHandle(Handle);
return FspNtStatusFromWin32(LastError);
}
if (L'\\' == FullPath[Length - 1])
FullPath[--Length] = L'\0';
if (!GetFileTime(Handle, &CreationTime, 0, 0)) // (2)
{
LastError = GetLastError();
CloseHandle(Handle);
return FspNtStatusFromWin32(LastError);
}
CloseHandle(Handle);
/* from now on we must goto exit on failure */
Ptfs = malloc(sizeof *Ptfs); // (3)
if (0 == Ptfs)
{
Result = STATUS_INSUFFICIENT_RESOURCES;
goto exit;
}
memset(Ptfs, 0, sizeof *Ptfs);
Length = (Length + 1) * sizeof(WCHAR);
Ptfs->Path = malloc(Length); // (3)
if (0 == Ptfs->Path)
{
Result = STATUS_INSUFFICIENT_RESOURCES;
goto exit;
}
memcpy(Ptfs->Path, FullPath, Length);
memset(&VolumeParams, 0, sizeof VolumeParams); // (4)
VolumeParams.SectorSize = ALLOCATION_UNIT;
VolumeParams.SectorsPerAllocationUnit = 1;
VolumeParams.VolumeCreationTime = ((PLARGE_INTEGER)&CreationTime)->QuadPart;
VolumeParams.VolumeSerialNumber = 0;
VolumeParams.FileInfoTimeout = 1000;
VolumeParams.CaseSensitiveSearch = 0;
VolumeParams.CasePreservedNames = 1;
VolumeParams.UnicodeOnDisk = 1;
VolumeParams.PersistentAcls = 1;
VolumeParams.PostCleanupWhenModifiedOnly = 1; // (4)
VolumeParams.UmFileContextIsUserContext2 = 1; // (4)
if (0 != VolumePrefix)
wcscpy_s(VolumeParams.Prefix, sizeof VolumeParams.Prefix / sizeof(WCHAR), VolumePrefix);
wcscpy_s(VolumeParams.FileSystemName, sizeof VolumeParams.FileSystemName / sizeof(WCHAR),
L"" PROGNAME);
Result = FspFileSystemCreate(
VolumeParams.Prefix[0] ? L"" FSP_FSCTL_NET_DEVICE_NAME : L"" FSP_FSCTL_DISK_DEVICE_NAME,
&VolumeParams,
&PtfsInterface,
&Ptfs->FileSystem); // (5)
if (!NT_SUCCESS(Result))
goto exit;
Ptfs->FileSystem->UserContext = Ptfs; // (5)
Result = FspFileSystemSetMountPoint(Ptfs->FileSystem, MountPoint); // (6)
if (!NT_SUCCESS(Result))
goto exit;
FspFileSystemSetDebugLog(Ptfs->FileSystem, DebugFlags); // (7)
Result = STATUS_SUCCESS;
exit:
if (NT_SUCCESS(Result))
*PPtfs = Ptfs;
else if (0 != Ptfs)
PtfsDelete(Ptfs);
return Result;
}
-
Get the full path name of the passthrough directory. This allows the file system to change directories safely (if it so chooses).
-
Get the creation time of the passthrough directory. We will use this time as the volume creation time.
-
Allocate memory for the passthrough file system main structure and for the passthrough directory path.
-
Initialize the file system
VolumeParams
. We want the file system to post Cleanup requests only when a file is modified (this avoids unnecessary Cleanup requests thus improving performance). We also want to treat theFileContext
parameter as a "file descriptor". -
Create the WinFsp
FileSystem
object. -
Set the mount point. It can be a drive or directory.
-
Set debug log flags. Specify 0 to disable logging. Specify -1 to enable all logging.
We now consider the exit point SvcStop
. The code for this is simple:
SvcStop excerpt
PTFS *Ptfs = Service->UserContext; // (1)
FspFileSystemStopDispatcher(Ptfs->FileSystem); // (2)
PtfsDelete(Ptfs); // (3)
-
Get the passthrough file system from the service instance.
-
Stop the file system dispatcher.
-
Delete the file system.
Finally the code for PtfsDelete
:
PtfsDelete
static VOID PtfsDelete(PTFS *Ptfs)
{
if (0 != Ptfs->FileSystem)
FspFileSystemDelete(Ptfs->FileSystem); // (1)
if (0 != Ptfs->Path)
free(Ptfs->Path); // (2)
free(Ptfs); // (2)
}
-
Delete the WinFsp
FileSystem
object. -
Free any remaining memory.
We can now run the program from Visual Studio or the command line. The program starts and waits for file system requests from the operating system (although we do not yet service any). Press Ctrl-C to stop the file system.
ℹ️
|
Pressing Ctrl-C orderly stops the file system (by calling SvcStop ). It is however possible to forcibly stop a file system, e.g. by killing the process in the debugger. This is fine with WinFsp as all associated resources will be automatically cleaned up. This includes resources that WinFsp knows about such as kernel memory, volume devices, etc. It does not include resources that it has no knowledge about such as temporary files, network registrations, etc.
|
We now start implementing the actual file system operations. These operations are the ones found in FSP_FILE_SYSTEM_INTERFACE
. We first create stubs for all operations that our file system is going to support.
File system operations stubs
static NTSTATUS GetVolumeInfo(FSP_FILE_SYSTEM *FileSystem,
FSP_FSCTL_VOLUME_INFO *VolumeInfo)
{
return STATUS_INVALID_DEVICE_REQUEST;
}
static NTSTATUS SetVolumeLabel_(FSP_FILE_SYSTEM *FileSystem,
PWSTR VolumeLabel,
FSP_FSCTL_VOLUME_INFO *VolumeInfo)
{
return STATUS_INVALID_DEVICE_REQUEST;
}
...
static FSP_FILE_SYSTEM_INTERFACE PtfsInterface =
{
GetVolumeInfo,
SetVolumeLabel_,
GetSecurityByName,
Create,
Open,
Overwrite,
Cleanup,
Close,
Read,
Write,
Flush,
GetFileInfo,
SetBasicInfo,
SetFileSize,
CanDelete,
Rename,
GetSecurity,
SetSecurity,
ReadDirectory,
};
At a minimum a file system needs to support GetSecurityByName
, Open
and Close
. This allows one to use the command prompt to switch to the drive, but not much more. [Strictly speaking it is possible to not implement GetSecurityByName, but the file system will perform no access checks in that case.]
GetSecurityByName
is used by WinFsp to retrieve essential metadata about a file to be opened, such as its attributes and security descriptor.
GetSecurityByName
static NTSTATUS GetSecurityByName(FSP_FILE_SYSTEM *FileSystem,
PWSTR FileName, PUINT32 PFileAttributes,
PSECURITY_DESCRIPTOR SecurityDescriptor, SIZE_T *PSecurityDescriptorSize)
{
PTFS *Ptfs = (PTFS *)FileSystem->UserContext;
WCHAR FullPath[FULLPATH_SIZE];
HANDLE Handle;
FILE_ATTRIBUTE_TAG_INFO AttributeTagInfo;
DWORD SecurityDescriptorSizeNeeded;
NTSTATUS Result;
if (!ConcatPath(Ptfs, FileName, FullPath))
return STATUS_OBJECT_NAME_INVALID;
Handle = CreateFileW(FullPath,
FILE_READ_ATTRIBUTES | READ_CONTROL, 0, 0,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
if (INVALID_HANDLE_VALUE == Handle)
{
Result = FspNtStatusFromWin32(GetLastError());
goto exit;
}
if (0 != PFileAttributes)
{
if (!GetFileInformationByHandleEx(Handle,
FileAttributeTagInfo, &AttributeTagInfo, sizeof AttributeTagInfo))
{
Result = FspNtStatusFromWin32(GetLastError());
goto exit;
}
*PFileAttributes = AttributeTagInfo.FileAttributes; // (1)
}
if (0 != PSecurityDescriptorSize)
{
if (!GetKernelObjectSecurity(Handle,
OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
SecurityDescriptor, (DWORD)*PSecurityDescriptorSize, &SecurityDescriptorSizeNeeded))
{
*PSecurityDescriptorSize = SecurityDescriptorSizeNeeded;
Result = FspNtStatusFromWin32(GetLastError());
goto exit;
}
*PSecurityDescriptorSize = SecurityDescriptorSizeNeeded; // (2)
}
Result = STATUS_SUCCESS;
exit:
if (INVALID_HANDLE_VALUE != Handle)
CloseHandle(Handle);
return Result;
}
-
Get file attributes.
-
Get file security.
The next call to implement is Open
. Open
is used to open existing files and should never create or overwrite files.
Open
static NTSTATUS Open(FSP_FILE_SYSTEM *FileSystem,
PWSTR FileName, UINT32 CreateOptions, UINT32 GrantedAccess,
PVOID *PFileContext, FSP_FSCTL_FILE_INFO *FileInfo)
{
PTFS *Ptfs = (PTFS *)FileSystem->UserContext;
WCHAR FullPath[FULLPATH_SIZE];
ULONG CreateFlags;
PTFS_FILE_CONTEXT *FileContext;
if (!ConcatPath(Ptfs, FileName, FullPath))
return STATUS_OBJECT_NAME_INVALID;
FileContext = malloc(sizeof *FileContext); // (1)
if (0 == FileContext)
return STATUS_INSUFFICIENT_RESOURCES;
memset(FileContext, 0, sizeof *FileContext);
CreateFlags = FILE_FLAG_BACKUP_SEMANTICS; // (2)
if (CreateOptions & FILE_DELETE_ON_CLOSE)
CreateFlags |= FILE_FLAG_DELETE_ON_CLOSE; // (3)
FileContext->Handle = CreateFileW(FullPath,
GrantedAccess, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0,
OPEN_EXISTING, CreateFlags, 0); // (4)
if (INVALID_HANDLE_VALUE == FileContext->Handle)
{
free(FileContext);
return FspNtStatusFromWin32(GetLastError());
}
*PFileContext = FileContext;
return GetFileInfoInternal(FileContext->Handle, FileInfo); // (5)
}
-
Create the
FileContext
object. This is used to track an open file instance. -
Allow opening of directories (
FILE_FLAG_BACKUP_SEMANTICS
). -
Include the FILE_FLAG_DELETE_ON_CLOSE flag. File systems do not normally have to track this flag as WinFsp will track it and post the appropriate
Cleanup
request. Passing it to the underlying file system here allows us to simplifyCleanup
for this simple file system. -
Use OPEN_EXISTING to open existing files only. Allow full sharing (
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE
) as WinFsp performs its own sharing checks. -
Use
GetFileInfoInternal
to return information about the file (see below).
After the completion of many file system operations the kernel needs to have an accurate view of the file system metadata. [This is also the case with Open
.] We create a helper function GetFileInfoInternal
for this purpose.
GetFileInfoInternal
static NTSTATUS GetFileInfoInternal(HANDLE Handle, FSP_FSCTL_FILE_INFO *FileInfo)
{
BY_HANDLE_FILE_INFORMATION ByHandleFileInfo;
if (!GetFileInformationByHandle(Handle, &ByHandleFileInfo))
return FspNtStatusFromWin32(GetLastError());
FileInfo->FileAttributes = ByHandleFileInfo.dwFileAttributes;
FileInfo->ReparseTag = 0;
FileInfo->FileSize =
((UINT64)ByHandleFileInfo.nFileSizeHigh << 32) | (UINT64)ByHandleFileInfo.nFileSizeLow;
FileInfo->AllocationSize = (FileInfo->FileSize + ALLOCATION_UNIT - 1)
/ ALLOCATION_UNIT * ALLOCATION_UNIT;
FileInfo->CreationTime = ((PLARGE_INTEGER)&ByHandleFileInfo.ftCreationTime)->QuadPart;
FileInfo->LastAccessTime = ((PLARGE_INTEGER)&ByHandleFileInfo.ftLastAccessTime)->QuadPart;
FileInfo->LastWriteTime = ((PLARGE_INTEGER)&ByHandleFileInfo.ftLastWriteTime)->QuadPart;
FileInfo->ChangeTime = FileInfo->LastWriteTime;
FileInfo->IndexNumber = 0;
FileInfo->HardLinks = 0;
return STATUS_SUCCESS;
}
Every Open
(or Create
) is always matched by Close
. Close
is the final call that will be received for an open file instance.
Close
static VOID Close(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext0)
{
PTFS_FILE_CONTEXT *FileContext = FileContext0;
HANDLE Handle = HandleFromContext(FileContext);
CloseHandle(Handle); // (1)
FspFileSystemDeleteDirectoryBuffer(&FileContext->DirBuffer); // (2)
free(FileContext); // (3)
}
-
Close the file handle.
-
Delete the directory buffer (if there is one).
-
Free the
FileContext
object.
For completeness the definition of PTFS_FILE_CONTEXT
is included here:
PTFS_FILE_CONTEXT
#define HandleFromContext(FC) (((PTFS_FILE_CONTEXT *)(FC))->Handle)
typedef struct
{
HANDLE Handle;
PVOID DirBuffer;
} PTFS_FILE_CONTEXT;
Our simple file system can only open and close existing files. Supporting the Windows explorer is somewhat more involved. It requires implementation of ReadDirectory
.
ReadDirectory
is conceptually simple: given a Marker
file name within the directory fill the specified Buffer
with directory contents. The idea here is that a directory can be viewed as a file with directory entries, the Marker
is used to specify where in the file to start reading. Only files with names that are greater than (not equal to) the Marker
(in the directory order determined by the file system) should be returned. If the Marker
is NULL
it means to start at the beginning of the directory file.
This scheme is simple and flexible in that it allows arbitrarily large directories to be read in chunks. If implemented correctly it can also cope with concurrent modifications to the directory (like file creations, deletions).
Not all file systems maintain a consistent directory order or are able to seek by file name within a directory. For these file systems a simple strategy is to buffer all directory contents when they receive a NULL
Marker
.
This is how we implement ReadDirectory
for our passthrough file system.
ReadDirectory
static NTSTATUS ReadDirectory(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext0, PWSTR Pattern, PWSTR Marker,
PVOID Buffer, ULONG BufferLength, PULONG PBytesTransferred)
{
PTFS *Ptfs = (PTFS *)FileSystem->UserContext;
PTFS_FILE_CONTEXT *FileContext = FileContext0;
HANDLE Handle = HandleFromContext(FileContext);
WCHAR FullPath[FULLPATH_SIZE];
ULONG Length, PatternLength;
HANDLE FindHandle;
WIN32_FIND_DATAW FindData;
union
{
UINT8 B[FIELD_OFFSET(FSP_FSCTL_DIR_INFO, FileNameBuf) + MAX_PATH * sizeof(WCHAR)];
FSP_FSCTL_DIR_INFO D;
} DirInfoBuf;
FSP_FSCTL_DIR_INFO *DirInfo = &DirInfoBuf.D;
NTSTATUS DirBufferResult;
DirBufferResult = STATUS_SUCCESS;
if (FspFileSystemAcquireDirectoryBuffer(&FileContext->DirBuffer, 0 == Marker,
&DirBufferResult)) // (1)
{
if (0 == Pattern)
Pattern = L"*";
PatternLength = (ULONG)wcslen(Pattern);
Length = GetFinalPathNameByHandleW(Handle, FullPath, FULLPATH_SIZE - 1, 0);
if (0 == Length)
DirBufferResult = FspNtStatusFromWin32(GetLastError());
else if (Length + 1 + PatternLength >= FULLPATH_SIZE)
DirBufferResult = STATUS_OBJECT_NAME_INVALID;
if (!NT_SUCCESS(DirBufferResult))
{
FspFileSystemReleaseDirectoryBuffer(&FileContext->DirBuffer);
return DirBufferResult;
}
if (L'\\' != FullPath[Length - 1])
FullPath[Length++] = L'\\';
memcpy(FullPath + Length, Pattern, PatternLength * sizeof(WCHAR));
FullPath[Length + PatternLength] = L'\0';
FindHandle = FindFirstFileW(FullPath, &FindData); // (2)
if (INVALID_HANDLE_VALUE != FindHandle)
{
do
{
memset(DirInfo, 0, sizeof *DirInfo);
Length = (ULONG)wcslen(FindData.cFileName);
DirInfo->Size = (UINT16)(FIELD_OFFSET(FSP_FSCTL_DIR_INFO, FileNameBuf) + Length * sizeof(WCHAR));
DirInfo->FileInfo.FileAttributes = FindData.dwFileAttributes;
DirInfo->FileInfo.ReparseTag = 0;
DirInfo->FileInfo.FileSize =
((UINT64)FindData.nFileSizeHigh << 32) | (UINT64)FindData.nFileSizeLow;
DirInfo->FileInfo.AllocationSize = (DirInfo->FileInfo.FileSize + ALLOCATION_UNIT - 1)
/ ALLOCATION_UNIT * ALLOCATION_UNIT;
DirInfo->FileInfo.CreationTime = ((PLARGE_INTEGER)&FindData.ftCreationTime)->QuadPart;
DirInfo->FileInfo.LastAccessTime = ((PLARGE_INTEGER)&FindData.ftLastAccessTime)->QuadPart;
DirInfo->FileInfo.LastWriteTime = ((PLARGE_INTEGER)&FindData.ftLastWriteTime)->QuadPart;
DirInfo->FileInfo.ChangeTime = DirInfo->FileInfo.LastWriteTime;
DirInfo->FileInfo.IndexNumber = 0;
DirInfo->FileInfo.HardLinks = 0;
memcpy(DirInfo->FileNameBuf, FindData.cFileName, Length * sizeof(WCHAR));
if (!FspFileSystemFillDirectoryBuffer(&FileContext->DirBuffer, DirInfo,
&DirBufferResult)) // (2)
break;
} while (FindNextFileW(FindHandle, &FindData)); // (2)
FindClose(FindHandle);
}
FspFileSystemReleaseDirectoryBuffer(&FileContext->DirBuffer); // (3)
}
if (!NT_SUCCESS(DirBufferResult))
return DirBufferResult;
FspFileSystemReadDirectoryBuffer(&FileContext->DirBuffer,
Marker, Buffer, BufferLength, PBytesTransferred); // (4)
return STATUS_SUCCESS;
}
-
Acquire a directory buffer if there is not one or if
Marker == 0
. -
Iterate over all directory entries and buffer them.
-
Release the directory buffer.
-
Copy the buffered directory contents into the specified
Buffer
.
The Windows explorer will often query a volume (file system) for information about it. Implementation of GetVolumeInfo
allows us to return information about the total and free space in the file system and its volume label.
GetVolumeInfo
static NTSTATUS GetVolumeInfo(FSP_FILE_SYSTEM *FileSystem,
FSP_FSCTL_VOLUME_INFO *VolumeInfo)
{
PTFS *Ptfs = (PTFS *)FileSystem->UserContext;
WCHAR Root[MAX_PATH];
ULARGE_INTEGER TotalSize, FreeSize;
if (!GetVolumePathName(Ptfs->Path, Root, MAX_PATH))
return FspNtStatusFromWin32(GetLastError());
if (!GetDiskFreeSpaceEx(Root, 0, &TotalSize, &FreeSize))
return FspNtStatusFromWin32(GetLastError());
VolumeInfo->TotalSize = TotalSize.QuadPart; // (1)
VolumeInfo->FreeSize = FreeSize.QuadPart; // (2)
// (3)
return STATUS_SUCCESS;
}
-
Total size in bytes.
-
Free size in bytes.
-
We do not support volume labels so we simply return the default (blank) volume label.
If we right click on a file and choose "Properties" on the Windows explorer, it will interrogate the file system for the file metadata. This metadata includes file information such as file size, attributes, times, etc. and security information such as ACL’s.
The GetFileInfo
operation allows the kernel to query/refresh its view of the file metadata.
GetFileInfo
static NTSTATUS GetFileInfo(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext,
FSP_FSCTL_FILE_INFO *FileInfo)
{
HANDLE Handle = HandleFromContext(FileContext);
return GetFileInfoInternal(Handle, FileInfo);
}
The GetSecurity
operation is used to return a file’s security descriptor. [Please note that file systems that do not support ACL’s need not implement this function.]
GetSecurity
static NTSTATUS GetSecurity(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext,
PSECURITY_DESCRIPTOR SecurityDescriptor, SIZE_T *PSecurityDescriptorSize)
{
HANDLE Handle = HandleFromContext(FileContext);
DWORD SecurityDescriptorSizeNeeded;
if (!GetKernelObjectSecurity(Handle,
OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
SecurityDescriptor, (DWORD)*PSecurityDescriptorSize, &SecurityDescriptorSizeNeeded))
{
*PSecurityDescriptorSize = SecurityDescriptorSizeNeeded;
return FspNtStatusFromWin32(GetLastError());
}
*PSecurityDescriptorSize = SecurityDescriptorSizeNeeded;
return STATUS_SUCCESS;
}
Files in our file system can now be listed (ReadDirectory
) and queried for their metadata (GetFileInfo
, GetSecurity
). However files cannot be read or written yet!
Implementing Read
is simple for our file system. Here is the implementation.
Read
static NTSTATUS Read(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext, PVOID Buffer, UINT64 Offset, ULONG Length,
PULONG PBytesTransferred)
{
HANDLE Handle = HandleFromContext(FileContext);
OVERLAPPED Overlapped = { 0 };
Overlapped.Offset = (DWORD)Offset; // (1)
Overlapped.OffsetHigh = (DWORD)(Offset >> 32);
if (!ReadFile(Handle, Buffer, Length, PBytesTransferred, &Overlapped))
return FspNtStatusFromWin32(GetLastError());
return STATUS_SUCCESS;
}
-
Specify the
Offset
to read in anOVERLAPPED
structure.
Implementing Write
is also simple, although more involved. This is because Write
has more complex semantics and supports a ConstrainedIo
mode in which the file system is not allowed to extend the file size during a Write
.
Write
static NTSTATUS Write(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext, PVOID Buffer, UINT64 Offset, ULONG Length,
BOOLEAN WriteToEndOfFile, BOOLEAN ConstrainedIo,
PULONG PBytesTransferred, FSP_FSCTL_FILE_INFO *FileInfo)
{
HANDLE Handle = HandleFromContext(FileContext);
LARGE_INTEGER FileSize;
OVERLAPPED Overlapped = { 0 };
if (ConstrainedIo) // (1)
{
if (!GetFileSizeEx(Handle, &FileSize))
return FspNtStatusFromWin32(GetLastError());
if (Offset >= (UINT64)FileSize.QuadPart)
return STATUS_SUCCESS;
if (Offset + Length > (UINT64)FileSize.QuadPart)
Length = (ULONG)((UINT64)FileSize.QuadPart - Offset);
}
Overlapped.Offset = (DWORD)Offset; // (2)
Overlapped.OffsetHigh = (DWORD)(Offset >> 32);
if (!WriteFile(Handle, Buffer, Length, PBytesTransferred, &Overlapped))
return FspNtStatusFromWin32(GetLastError());
return GetFileInfoInternal(Handle, FileInfo);
}
-
If
ConstrainedIo
is set we must restrictWrite
to not extend file size. -
Specify the
Offset
to write in anOVERLAPPED
structure. Note that theOffset
will be(UINT64)-1
whenWriteToEndOfFile
is set, which achieves the desired effect.
Along with the ability to write a file, we also want the ability to update its metadata. This is accomplished by implementing the SetBasicInfo
, SetFileSize
, and SetSecurity
operations. [The SetSecurity
operation is not necessary if the file system does not support ACL’s.]
The SetBasicInfo
operation is used to update a file’s attributes and times. The implementation follows:
SetBasicInfo
static NTSTATUS SetBasicInfo(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext, UINT32 FileAttributes,
UINT64 CreationTime, UINT64 LastAccessTime, UINT64 LastWriteTime, UINT64 ChangeTime,
FSP_FSCTL_FILE_INFO *FileInfo)
{
HANDLE Handle = HandleFromContext(FileContext);
FILE_BASIC_INFO BasicInfo = { 0 };
if (INVALID_FILE_ATTRIBUTES == FileAttributes)
FileAttributes = 0;
else if (0 == FileAttributes)
FileAttributes = FILE_ATTRIBUTE_NORMAL;
BasicInfo.FileAttributes = FileAttributes;
BasicInfo.CreationTime.QuadPart = CreationTime;
BasicInfo.LastAccessTime.QuadPart = LastAccessTime;
BasicInfo.LastWriteTime.QuadPart = LastWriteTime;
//BasicInfo.ChangeTime = ChangeTime;
if (!SetFileInformationByHandle(Handle,
FileBasicInfo, &BasicInfo, sizeof BasicInfo))
return FspNtStatusFromWin32(GetLastError());
return GetFileInfoInternal(Handle, FileInfo);
}
The SetFileSize
operation is used to change a file’s sizes. Files in a Windows file system can have two sizes: an "EndOfFile" size or FileSize
and an AllocationSize
. The FileSize
is the number of bytes contained in a file. The AllocationSize
is a concept that many file systems can safely ignore (or not expose to the kernel): it is the actual number of bytes that a file occupies on its storage medium.
Although some file systems may have an internal block / chunk / cluster / sector that they use as their basic AllocationUnit
, it is not necessary to expose this information to the kernel. The advantage to exposing it is that applications can use (little documented) file system API’s to preallocate files.
Regardless of whether a file system exposes AllocationSize
it must obey the following rule: it must always be that FileSize <= AllocationSize
. In general the WinFsp driver also assumes that the AllocationSize
is a multiple of the AllocationUnit
; in this case the AllocationUnit
is the product of SectorSize * SectorsPerAllocationUnit
.
SetFileSize
static NTSTATUS SetFileSize(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext, UINT64 NewSize, BOOLEAN SetAllocationSize,
FSP_FSCTL_FILE_INFO *FileInfo)
{
HANDLE Handle = HandleFromContext(FileContext);
FILE_ALLOCATION_INFO AllocationInfo;
FILE_END_OF_FILE_INFO EndOfFileInfo;
if (SetAllocationSize)
{
/*
* This file system does not maintain AllocationSize, although NTFS clearly can.
* However it must always be FileSize <= AllocationSize and NTFS will make sure
* to truncate the FileSize if it sees an AllocationSize < FileSize.
*
* If OTOH a very large AllocationSize is passed, the call below will increase
* the AllocationSize of the underlying file, although our file system does not
* expose this fact. This AllocationSize is only temporary as NTFS will reset
* the AllocationSize of the underlying file when it is closed.
*/
AllocationInfo.AllocationSize.QuadPart = NewSize;
if (!SetFileInformationByHandle(Handle,
FileAllocationInfo, &AllocationInfo, sizeof AllocationInfo))
return FspNtStatusFromWin32(GetLastError());
}
else
{
EndOfFileInfo.EndOfFile.QuadPart = NewSize;
if (!SetFileInformationByHandle(Handle,
FileEndOfFileInfo, &EndOfFileInfo, sizeof EndOfFileInfo))
return FspNtStatusFromWin32(GetLastError());
}
return GetFileInfoInternal(Handle, FileInfo);
}
Finally the SetSecurity
operation is used to update a file’s security information.
SetSecurity
static NTSTATUS SetSecurity(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext,
SECURITY_INFORMATION SecurityInformation, PSECURITY_DESCRIPTOR ModificationDescriptor)
{
HANDLE Handle = HandleFromContext(FileContext);
if (!SetKernelObjectSecurity(Handle, SecurityInformation, ModificationDescriptor))
return FspNtStatusFromWin32(GetLastError());
return STATUS_SUCCESS;
}
Windows file systems are free to cache file information in order to speed up operations. In some cases it is important to ensure that all caches have been "flushed" and all information has been persisted in the final storage medium. Windows provides the FlushFileBuffers
API for this purpose. User mode file systems that support flushing must implement the Flush
operation.
The Flush
operation is used to flush a single file or the whole volume (file system). At the time the Flush
call arrives the kernel has already flushed all its file caches (by calling Write
for all dirty data in its caches). If the file system performs additional caching it should flush its own caches at this point.
The implementation of Flush
for our passthrough file system follows:
Flush
NTSTATUS Flush(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext,
FSP_FSCTL_FILE_INFO *FileInfo)
{
HANDLE Handle = HandleFromContext(FileContext);
/* we do not flush the whole volume, so just return SUCCESS */
if (0 == Handle)
return STATUS_SUCCESS;
if (!FlushFileBuffers(Handle))
return FspNtStatusFromWin32(GetLastError());
return GetFileInfoInternal(Handle, FileInfo);
}
Our file system is now functional, but it still misses an important ability: the ability to create and delete files. We will tackle creating files first.
The Create
operation is used to create files and directories. A file or directory should be created only if it does not already exist. Whether to create a file or directory is controlled by the FILE_DIRECTORY_FILE
flag.
The implementation of Create
follows:
Create
static NTSTATUS Create(FSP_FILE_SYSTEM *FileSystem,
PWSTR FileName, UINT32 CreateOptions, UINT32 GrantedAccess,
UINT32 FileAttributes, PSECURITY_DESCRIPTOR SecurityDescriptor, UINT64 AllocationSize,
PVOID *PFileContext, FSP_FSCTL_FILE_INFO *FileInfo)
{
PTFS *Ptfs = (PTFS *)FileSystem->UserContext;
WCHAR FullPath[FULLPATH_SIZE];
SECURITY_ATTRIBUTES SecurityAttributes;
ULONG CreateFlags;
PTFS_FILE_CONTEXT *FileContext;
if (!ConcatPath(Ptfs, FileName, FullPath))
return STATUS_OBJECT_NAME_INVALID;
FileContext = malloc(sizeof *FileContext); // (1)
if (0 == FileContext)
return STATUS_INSUFFICIENT_RESOURCES;
memset(FileContext, 0, sizeof *FileContext);
SecurityAttributes.nLength = sizeof SecurityAttributes;
SecurityAttributes.lpSecurityDescriptor = SecurityDescriptor;
SecurityAttributes.bInheritHandle = FALSE;
CreateFlags = FILE_FLAG_BACKUP_SEMANTICS; // (2)
if (CreateOptions & FILE_DELETE_ON_CLOSE)
CreateFlags |= FILE_FLAG_DELETE_ON_CLOSE; // (3)
if (CreateOptions & FILE_DIRECTORY_FILE)
{
/*
* It is not widely known but CreateFileW can be used to create directories!
* It requires the specification of both FILE_FLAG_BACKUP_SEMANTICS and
* FILE_FLAG_POSIX_SEMANTICS. It also requires that FileAttributes has
* FILE_ATTRIBUTE_DIRECTORY set.
*/
CreateFlags |= FILE_FLAG_POSIX_SEMANTICS; // (2)
FileAttributes |= FILE_ATTRIBUTE_DIRECTORY;
}
else
FileAttributes &= ~FILE_ATTRIBUTE_DIRECTORY;
if (0 == FileAttributes)
FileAttributes = FILE_ATTRIBUTE_NORMAL;
FileContext->Handle = CreateFileW(FullPath,
GrantedAccess, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &SecurityAttributes,
CREATE_NEW, CreateFlags | FileAttributes, 0); // (4)
if (INVALID_HANDLE_VALUE == FileContext->Handle)
{
free(FileContext);
return FspNtStatusFromWin32(GetLastError());
}
*PFileContext = FileContext;
return GetFileInfoInternal(FileContext->Handle, FileInfo); // (5)
}
-
Create the
FileContext
object. This is used to track an open file instance. -
Allow creation of directories using the flags
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_POSIX_SEMANTICS
. -
Include the FILE_FLAG_DELETE_ON_CLOSE flag. File systems do not normally have to track this flag as WinFsp will track it and post the appropriate
Cleanup
request. Passing it to the underlying file system here allows us to simplifyCleanup
for this simple file system. -
Use CREATE_NEW to create new files only. Allow full sharing (
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE
) as WinFsp performs its own sharing checks. -
Use
GetFileInfoInternal
to return information about the file.
Another special operation for Windows file systems is the ability to "overwrite" or "supersede" files. This operation is used (for example) when an application calls CreateFileW
with the CREATE_ALWAYS
flag.
Overwrite
must truncate the file to zero size. It must also replace or merge the file’s attributes according to the ReplaceFileAttributes
parameter. The implementation of Overwrite
for our file system follows.
Overwrite
static NTSTATUS Overwrite(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext, UINT32 FileAttributes, BOOLEAN ReplaceFileAttributes, UINT64 AllocationSize,
FSP_FSCTL_FILE_INFO *FileInfo)
{
HANDLE Handle = HandleFromContext(FileContext);
FILE_BASIC_INFO BasicInfo = { 0 };
FILE_ALLOCATION_INFO AllocationInfo = { 0 };
FILE_ATTRIBUTE_TAG_INFO AttributeTagInfo;
if (ReplaceFileAttributes)
{
if (0 == FileAttributes)
FileAttributes = FILE_ATTRIBUTE_NORMAL;
BasicInfo.FileAttributes = FileAttributes; // (1)
if (!SetFileInformationByHandle(Handle,
FileBasicInfo, &BasicInfo, sizeof BasicInfo))
return FspNtStatusFromWin32(GetLastError());
}
else if (0 != FileAttributes)
{
if (!GetFileInformationByHandleEx(Handle,
FileAttributeTagInfo, &AttributeTagInfo, sizeof AttributeTagInfo))
return FspNtStatusFromWin32(GetLastError());
BasicInfo.FileAttributes =
FileAttributes | AttributeTagInfo.FileAttributes; // (2)
if (BasicInfo.FileAttributes ^ FileAttributes)
{
if (!SetFileInformationByHandle(Handle,
FileBasicInfo, &BasicInfo, sizeof BasicInfo))
return FspNtStatusFromWin32(GetLastError());
}
}
if (!SetFileInformationByHandle(Handle,
FileAllocationInfo, &AllocationInfo, sizeof AllocationInfo)) // (3)
return FspNtStatusFromWin32(GetLastError());
return GetFileInfoInternal(Handle, FileInfo);
}
-
If
ReplaceFileAttributes
is true, set the file’s attributets to the specified ones (this is a "supersede" operation). -
If
ReplaceFileAttributes
is false, merge the specified file attributes with the existing ones (this is an "overwrite" operation). -
Set the underlying file’s allocation size to 0, which also sets the file size to 0, thus truncating the file.
One of the important file system operations that we have not discussed so far is Cleanup
. Cleanup
is called whenever a file is about to be closed (when an application that opened a file calls CloseHandle
). If the VolumeParams
PostCleanupWhenModifiedOnly
flag is set, then Cleanup
is posted only when the file was modified or deleted. As such Cleanup
support is essential if a file system supports deleting files.
Our Cleanup
implementation is minimal. We present it below and we discuss it afterwards.
Cleanup
static VOID Cleanup(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext, PWSTR FileName, ULONG Flags)
{
HANDLE Handle = HandleFromContext(FileContext);
if (Flags & FspCleanupDelete) // (1)
{
CloseHandle(Handle);
/* this will make all future uses of Handle to fail with STATUS_INVALID_HANDLE */
HandleFromContext(FileContext) = INVALID_HANDLE_VALUE; // (2)
}
}
-
Only close the underlying file’s handle if our file system’s file instance has been marked for deletion.
-
This invalidates the underlying file’s handle, thus ensuring that additional file operations will fail with
STATUS_INVALID_HANDLE
.
If our open file instance is not marked for deletion we do not CloseHandle
the underlying handle; we will do so at a later time when we receive the Close
request. This allows the file system to receive additional requests (for example, Write
requests from the kernel lazy writer if kernel caching is enabled for this file system).
If our open file instance is marked for deletion we CloseHandle
the underlying handle, and we invalidate the handle. By calling CloseHandle
we ensure that the underlying file system can now delete a file that has been previously marked for deletion by the FILE_FLAG_DELETE_ON_CLOSE
flag or a FileDispositionInfo
call (see CanDelete
below). By invalidating the handle we ensure that no additional file operations can be performed on this file instance (they will fail with STATUS_INVALID_HANDLE
). We will still receive a Close
operation for our open file instance which calls CloseHandle
again, but this is safe to do with INVALID_HANDLE_VALUE.
ℹ️
|
The WinFsp kernel driver maintains a DeletePending flag for every open file. This flag becomes true when a file is opened with FILE_FLAG_DELETE_ON_CLOSE or when FileDispositionInfo is set. The WinFsp kernel driver sets FspCleanupDelete when it receives the last CloseHandle for a file that is being deleted. The user mode file system need not maintain its own DeletePending flag.
|
There are two ways for deleting a file or directory on Windows. One is to supply the FILE_FLAG_DELETE_ON_CLOSE
flag during a CreateFileW
call. The other one is to use the FileDispositionInfo
information class with a call to SetInformationByHandle
(which is what DeleteFileW
and RemoveDirectoryW
effectively do). [It is also possible to delete an (unopened) file using Rename
by we will ignore this case here.]
CanDelete
is called in the FileDispositionInfo
case (only). In general CanDelete
needs to check whether deleting the file or directory is allowed and return STATUS_SUCCESS
or an appropriate status code. Most file systems need only check whether a directory is empty and disallow deletion by returning STATUS_DIRECTORY_NOT_EMPTY
if it is not. CanDelete
need not mark a file for deletion, this flag is maintained by the WinFsp kernel driver.
In this implementation of CanDelete
we take advantage of the fact that the underlying Windows file system already knows how to handle a FileDispositionInfo
call.
CanDelete
static NTSTATUS CanDelete(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext, PWSTR FileName)
{
HANDLE Handle = HandleFromContext(FileContext);
FILE_DISPOSITION_INFO DispositionInfo;
DispositionInfo.DeleteFile = TRUE; // (1)
if (!SetFileInformationByHandle(Handle,
FileDispositionInfo, &DispositionInfo, sizeof DispositionInfo))
return FspNtStatusFromWin32(GetLastError());
return STATUS_SUCCESS;
}
-
Mark the underlying file system’s file for deletion.
Our file system is almost fully functional. There remains one operation to implement: Rename
.
Rename
can be hard to implement for a general purpose file system, but in our case things are simple, because the underlying Windows file system will take care of the details.
Rename
static NTSTATUS Rename(FSP_FILE_SYSTEM *FileSystem,
PVOID FileContext,
PWSTR FileName, PWSTR NewFileName, BOOLEAN ReplaceIfExists)
{
PTFS *Ptfs = (PTFS *)FileSystem->UserContext;
WCHAR FullPath[FULLPATH_SIZE], NewFullPath[FULLPATH_SIZE];
if (!ConcatPath(Ptfs, FileName, FullPath))
return STATUS_OBJECT_NAME_INVALID;
if (!ConcatPath(Ptfs, NewFileName, NewFullPath))
return STATUS_OBJECT_NAME_INVALID;
if (!MoveFileExW(FullPath, NewFullPath, ReplaceIfExists ? MOVEFILE_REPLACE_EXISTING : 0))
return FspNtStatusFromWin32(GetLastError());
return STATUS_SUCCESS;
}
We now have a functional file system. It supports the following Windows file system functionality:
-
Query volume information.
-
Open, create, close, delete, rename files and directories.
-
Query and set file and directory information.
-
Query and set security information (ACL’s).
-
Read and write files.
-
Memory mapped I/O.
-
Directory change notifications.
-
Lock and unlock files.
-
Opportunistic locks.
ℹ️
|
There is some additional functionality which WinFsp supports but our file system does not implement:
|
The question is: how can we develop the confidence that our file system works as a "proper" Windows file system?
WinFsp includes a number of test suites that are used for testing its components and its reference file system MEMFS. The primary test suite is called winfsp-tests
and is a comprehensive test suite that exercises all aspects of Windows file system functionality that WinFsp supports. Winfsp-tests
can be run in a special --external
mode where it can be used to test other WinFsp-based file systems. We will use it in this case to test our passthrough file system.
ℹ️
|
Winfsp-tests is not included with the WinFsp installer. In order to use winfsp-tests one must first clone the WinFsp repository and build the WinFsp Visual Studio solution. The steps to do so are not included in this tutorial.
|
Winfsp-tests
exercises some esoteric aspects of Windows file system functionality, so we do not expect all the tests to pass. For example, our simple file system does not maintain AllocationSize
; we therefore expect related tests to fail. As another example, the passthrough file system uses normal Windows file API’s to implement its functionality, as such some security tests are expected to fail if the file system runs under a normal account.
In order to test our file system we create a drive Y:
using the command line passthrough-x64 -p C:\...\passthrough-x64 -m Y:
and then execute the command.
winfsp-tests run
Y:\>C:\...\winfsp-tests-x64 --external --resilient --case-insensitive-cmp -create_allocation_test -getfileinfo_name_test -delete_access_test -rename_flipflop_test -rename_mmap_test -reparse* -stream* (1) (2) [snip irrelevant tests] create_test............................ OK 0.03s create_related_test.................... OK 0.00s create_sd_test......................... OK 0.03s create_notraverse_test................. OK 0.00s create_backup_test..................... OK 0.00s create_restore_test.................... OK 0.00s create_share_test...................... OK 0.00s create_curdir_test..................... OK 0.00s create_namelen_test.................... OK 0.02s getfileinfo_test....................... OK 0.00s setfileinfo_test....................... OK 0.01s delete_test............................ OK 0.00s delete_pending_test.................... OK 0.00s delete_mmap_test....................... OK 0.02s rename_test............................ OK 0.06s rename_open_test....................... OK 0.00s rename_caseins_test.................... OK 0.02s getvolinfo_test........................ OK 0.00s setvolinfo_test........................ OK 0.00s getsecurity_test....................... OK 0.00s setsecurity_test....................... OK 0.01s rdwr_noncached_test.................... OK 0.02s rdwr_noncached_overlapped_test......... OK 0.03s rdwr_cached_test....................... OK 0.02s rdwr_cached_append_test................ OK 0.01s rdwr_cached_overlapped_test............ OK 0.03s rdwr_writethru_test.................... OK 0.06s rdwr_writethru_append_test............. OK 0.01s rdwr_writethru_overlapped_test......... OK 0.00s rdwr_mmap_test......................... OK 0.23s rdwr_mixed_test........................ OK 0.03s flush_test............................. OK 0.06s flush_volume_test...................... OK 0.00s lock_noncached_test.................... OK 0.02s lock_noncached_overlapped_test......... OK 0.02s lock_cached_test....................... OK 0.05s lock_cached_overlapped_test............ OK 0.02s querydir_test.......................... OK 0.39s querydir_expire_cache_test............. OK 0.00s querydir_buffer_overflow_test.......... OK 0.00s dirnotify_test......................... OK 1.01s --- COMPLETE ---
-
Run
winfsp-tests
with--external
,--resilient
switches which instructs it to run its external file system tests. -
Disable tests that are not expected to pass because they test functionality that either we did not implement (
-reparse*
,-stream*
) or is esoteric (-create_allocation_test
,-getfileinfo_name_test
,-rename_flipflop_test
,-rename_mmap_test
) or requires that the file system is run under an account with sufficient security rights (-delete_access_test
).
Our final task is to discuss how to convert our file system into a service that can be managed by the WinFsp launcher. This allows our file system to provide file services to all processes in the system.
An important thing to consider is that our file system will be running in the SYSTEM account security context, which is different from the security context of any processes that want to use this file system. Recall that the passthrough file system is a simple layer over an underlying file system, therefore how the underlying file system handles security becomes important, particularly when the underlying file system is NTFS.
For this reason we modify the passthrough file system to enable the "backup" and "restore" privileges which are available to a process running under the SYSTEM account. Enabling these privileges allows us to circumvent some NTFS access checks and simply use NTFS as a storage medium. With the EnableBackupRestorePrivileges
implementation in place all that remains is to call it from SvcStart
.
EnableBackupRestorePrivileges
static NTSTATUS EnableBackupRestorePrivileges(VOID)
{
union
{
TOKEN_PRIVILEGES P;
UINT8 B[sizeof(TOKEN_PRIVILEGES) + sizeof(LUID_AND_ATTRIBUTES)];
} Privileges;
HANDLE Token;
Privileges.P.PrivilegeCount = 2;
Privileges.P.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
Privileges.P.Privileges[1].Attributes = SE_PRIVILEGE_ENABLED;
if (!LookupPrivilegeValueW(0, SE_BACKUP_NAME, &Privileges.P.Privileges[0].Luid) ||
!LookupPrivilegeValueW(0, SE_RESTORE_NAME, &Privileges.P.Privileges[1].Luid))
return FspNtStatusFromWin32(GetLastError());
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &Token))
return FspNtStatusFromWin32(GetLastError());
if (!AdjustTokenPrivileges(Token, FALSE, &Privileges.P, 0, 0, 0))
{
CloseHandle(Token);
return FspNtStatusFromWin32(GetLastError());
}
CloseHandle(Token);
return STATUS_SUCCESS;
}
We are now ready to register our file system to be managed by the WinFsp launcher. For this purpose we will use the fsreg.bat
utility which can be found in the WinFsp bin
directory. Fsreg.bat
will create all necessary entries in the Windows registry.
From an administrator prompt switch to the passthrough directory and run:
fsreg.bat invocation
fsreg.bat passthrough build\Debug\passthrough-x64.exe "-u %1 -m %2" "D:P(A;;RPWPLC;;;WD)"
With this step complete we can now launch our file system from any command prompt.
Alternatively one can use the Windows explorer.
In less than 1000 lines of C code we have written a Windows file system. Our file system implements all commonly used file functionality on Windows. It integrates fully with the OS and has been tested to give us reasonable confidence that it works as expected under many scenarios.
Time to go on and create your own file system! Some ideas for quick gratification:
-
RegFs: Create a file system view of the registry. Bonus points if you make it read/write and if you find creative ways of handling different registry value types.
-
WinObjFs: Are you familiar with WinObj from SysInternals? It’s a fantastic app to explore the NTOS object namespace. Create a file system that presents this namespace as a file system. Make it read-only!
-
ProcFs: Create something akin to procfs for Windows.