WinFsp Tutorial

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).

Note
The file system that we build in this tutorial is suitable for education purposes. If you are looking for a production quality pass through file system please see the ntptfs sample.

Prerequisites

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/winfsp/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.

WinFsp Installer

With those prerequisites out of the way we are now ready to start creating our first file system.

Note
The file system that we will create is included as a sample with the WinFsp installer. Look in the samples\passthrough directory.

Create the project skeleton

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.

Visual Studio New Project
Note
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)
}
  1. Include WinFsp header file.

  2. Service entry point.

  3. Service exit point.

  4. 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

Note
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:

Missing WinFsp DLL

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.

Note
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:

First Run

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!

File system entry/exit points

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.

Entry point

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)
  1. Create the passthrough file system.

  2. Start the file system dispatcher.

  3. 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;
}
  1. Get the full path name of the passthrough directory. This allows the file system to change directories safely (if it so chooses).

  2. Get the creation time of the passthrough directory. We will use this time as the volume creation time.

  3. Allocate memory for the passthrough file system main structure and for the passthrough directory path.

  4. 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 the FileContext parameter as a "file descriptor".

  5. Create the WinFsp FileSystem object.

  6. Set the mount point. It can be a drive or directory.

  7. Set debug log flags. Specify 0 to disable logging. Specify -1 to enable all logging.

Exit point

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)
  1. Get the passthrough file system from the service instance.

  2. Stop the file system dispatcher.

  3. 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)
}
  1. Delete the WinFsp FileSystem object.

  2. Free any remaining memory.

Test run

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.

Entry/exit test run
Note
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.

File system operations

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,
};

GetSecurityByName / Open / Close

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;
}
  1. Get file attributes.

  2. 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)
}
  1. Create the FileContext object. This is used to track an open file instance.

  2. Allow opening of directories (FILE_FLAG_BACKUP_SEMANTICS).

  3. 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 simplify Cleanup for this simple file system.

  4. 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.

  5. 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)
}
  1. Close the file handle.

  2. Delete the directory buffer (if there is one).

  3. 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;

ReadDirectory

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;
}
  1. Acquire a directory buffer if there is not one or if Marker == 0.

  2. Iterate over all directory entries and buffer them.

  3. Release the directory buffer.

  4. Copy the buffered directory contents into the specified Buffer.

GetVolumeInfo

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;
}
  1. Total size in bytes.

  2. Free size in bytes.

  3. We do not support volume labels so we simply return the default (blank) volume label.

GetFileInfo / GetSecurity

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;
}

Read / Write

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;
}
  1. Specify the Offset to read in an OVERLAPPED 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);
}
  1. If ConstrainedIo is set we must restrict Write to not extend file size.

  2. Specify the Offset to write in an OVERLAPPED structure. Note that the Offset will be (UINT64)-1 when WriteToEndOfFile is set, which achieves the desired effect.

SetBasicInfo / SetFileSize / SetSecurity

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;
}

Flush

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

Create

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)
}
  1. Create the FileContext object. This is used to track an open file instance.

  2. Allow creation of directories using the flags FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_POSIX_SEMANTICS.

  3. 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 simplify Cleanup for this simple file system.

  4. 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.

  5. Use GetFileInfoInternal to return information about the file.

Overwrite

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);
}
  1. If ReplaceFileAttributes is true, set the file’s attributets to the specified ones (this is a "supersede" operation).

  2. If ReplaceFileAttributes is false, merge the specified file attributes with the existing ones (this is an "overwrite" operation).

  3. Set the underlying file’s allocation size to 0, which also sets the file size to 0, thus truncating the file.

Cleanup

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)
    }
}
  1. Only close the underlying file’s handle if our file system’s file instance has been marked for deletion.

  2. 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.

Note
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.

CanDelete

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;
}
  1. Mark the underlying file system’s file for deletion.

Rename

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;
}

Testing the file system

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.

Note

There is some additional functionality which WinFsp supports but our file system does not implement:

  • Open, create, close, delete, query named streams.

  • Reparse points and symbolic links.

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.

Note
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 ---
  1. Run winfsp-tests with --external, --resilient switches which instructs it to run its external file system tests.

  2. 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).

Running the file system as a service

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.

First Run

Alternatively one can use the Windows explorer.

First Run

Conclusion

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.