Skip to content

Fix async STATUS_PENDING and add WinFsp.Net integration tests#5

Merged
hooyao merged 16 commits intomainfrom
feature/winfsp-net-binding
Apr 10, 2026
Merged

Fix async STATUS_PENDING and add WinFsp.Net integration tests#5
hooyao merged 16 commits intomainfrom
feature/winfsp-net-binding

Conversation

@hooyao
Copy link
Copy Markdown
Owner

@hooyao hooyao commented Apr 6, 2026

Summary

  • Fix STATUS_PENDING response handling in FileSystemHost (Read/Write/ReadDirectory)
  • Zero-copy async I/O: remove PinnedBufferPool intermediate copy
  • Add WinFsp.Net.Tests with 30 integration tests covering Sync/SyncCompleted/TrueAsync paths
  • Add WinFsp installation to CI workflow

Test plan

  • CI passes with WinFsp installed on windows-latest runner
  • All 30 integration tests pass (10 per async mode)

hooyao and others added 16 commits April 5, 2026 15:17
WinFsp.Net: Modern .NET 10 binding for WinFSP (Windows File System Proxy).
Two-layer API design:
- Low-level: WinFspFileSystem — thin shell, user fills delegate function
  pointers into 64-slot interface struct, zero abstraction
- High-level: IFileSystem + FileSystemHost — Dokan-style async interface,
  ValueTask, Memory<byte>, automatic STATUS_PENDING management

Key implementation details:
- All P/Invoke via LibraryImport (AOT-compatible source-gen)
- Callbacks via [UnmanagedFunctionPointer] delegate +
  Marshal.GetFunctionPointerForDelegate<T> (AOT-safe generic version)
- DLL resolution: NativeLibrary + Windows Registry fallback
- UnmanagedBufferPool: NativeMemory.AlignedAlloc pooled buffers, zero GC
- Per-handle CancellationToken (auto-cancelled on Cleanup)

HelloFs example: Read-only FS with one file (hello.txt = "example").
Verified working in both JIT and Native AOT (2 MB binary).

Design docs in docs/winfsp-net-design.md and docs/winfsp-net-redesign.md
document architecture, WinFSP pitfalls, and debugging lessons learned.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace DokanNet with custom WinFsp.Net binding library
  - FileSystemHost high-level API with IFileSystem interface
  - WinFspFileSystem low-level API for direct function pointer manipulation
  - Full AOT-compatible P/Invoke via [LibraryImport] source generators
  - PinnedBufferPool with two-level cache (ThreadLocal + ConcurrentQueue)
- Implement WinFspRamAdapter bridging RamFileSystem to WinFsp callbacks
- Add zero-copy synchronous I/O path (NativeBufferMemory wraps kernel buffer)
- Mount strategy: try Mount Manager (\.\R:) first, fallback to DefineDosDevice
- Auto-configure MountUseMountmgrFromFSD registry key when running as admin
- Add Setup.bat for one-time WinFsp installation and system configuration
- Add BenchmarkDotNet suite (Core / OnRead / E2E layers)
- Update README, CLAUDE.md, release workflow, and docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Under kernel cache mode (FileInfoTimeout=-1), Windows cache manager calls
SetFileSize to extend the file to its final size *before* issuing WriteFile
calls. The old SetLength only set _length without checking capacity (sparse
extension), allowing files to claim more space than the disk has. When
subsequent WriteFile calls returned STATUS_DISK_FULL, the kernel cache
masked the error — ReadFile served data from cache, making sha256 appear
correct while native memory never received the data.

Fix: PagePool gains Reserve/Unreserve for lightweight capacity accounting.
SetLength extension calls Reserve() to claim capacity without allocating
physical pages. Write() consumes reservations as it rents actual pages.
If capacity is insufficient, SetFileSize returns STATUS_DISK_FULL before
the cache manager begins writing.

Also: CreateFile/OverwriteFile no longer use allocationSize to set _length.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…on tests

- Fix STATUS_PENDING response handling: build fresh FspTransactRsp on stack
  instead of reusing dispatcher's Response pointer (which may be invalidated
  after returning STATUS_PENDING). Follows MEMFS async pattern.
- Zero-copy async Read/Write: wrap kernel buffer directly with
  NativeBufferMemory instead of renting from PinnedBufferPool + memcpy.
- Remove PinnedBufferPool from FileSystemHost (no longer used).
- Add WinFsp.Net.Tests project with TestMemFs (lightweight in-memory IFileSystem
  supporting Sync/SyncCompleted/TrueAsync modes) and 30 integration tests
  covering all three FileSystemHost code paths via real WinFsp mounts.
- Add WinFsp installation to CI workflow and include integration tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…LA+ verified), add integration tests

- Replace src/WinFsp.Net/ with WinFsp.Native NuGet package (0.1.1)
- Remove examples/HelloFs/, tests/WinFsp.Net.Tests/, docs/winfsp-net-*.md (moved to hooyao/winfsp-native)
- Fix PagedFileContent.Write spurious DISK_FULL: Phase 2 now unreserves own
  reservations before RentBatch so AllocateNewPageIfUnderCapacity succeeds.
  Bug reproduced and fix verified with TLA+ model checker (tla/).
- Add integration test suite (tests/RamDrive.IntegrationTests/):
  TortureTests (10 structured concurrent tests) and ChaosTests (random FS
  operation fuzzer with SHA256 integrity verification, configurable duration)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add UseWindowsService() + EventLog logging for dual-mode exe (console + service)
- Add Inno Setup script bundling WinFsp MSI, with Full/Green/Custom install types
- Update release workflow to build and publish installer alongside zip

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ArchitecturesInstallIn64BitModeOnly was removed in Inno Setup 6.4+,
replaced by ArchitecturesAllowed=x64compatible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sets MountUseMountmgrFromFSD=1 after WinFsp install so non-admin
mounts are visible to all apps (Explorer, benchmark tools, etc).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Wizard page for drive letter and capacity with validation
- Writes user settings to appsettings.jsonc during install
- Start Menu shortcuts: Edit Configuration + Restart Service
- Instructions on how to change settings after install

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sc.exe create with Inno Setup Exec() silently fails when the install
path contains spaces (Program Files). Write service registry keys
directly to avoid quoting issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
32-bit Inno Setup Exec('sc.exe') hits WOW64 file system redirection,
calling the 32-bit sc.exe instead of the native 64-bit one. Use
{sysnative}\sc.exe to bypass this. Falls back to direct registry
writes if sc.exe still fails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New config option (default off) creates a \Temp directory via RamFileSystem
directly after mount, avoiding Windows file API timing issues. Installer
adds a checkbox on the config page for this setting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Security descriptors:
- Store per-file security descriptor on FileNode
- Implement GetFileSecurity/SetFileSecurity with RawSecurityDescriptor merge
- Root gets default SD (O:BAG:BAD:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;WD))
- Enables Recycle Bin and proper ACL support

SetLength/SetFileSize fixes:
- Remove page reservation from SetLength — sparse extension only expands
  page table, pages allocated on demand in Write
- SetAllocationSize now checks free space (best-effort TOCTOU) so Explorer
  reports "not enough space" before starting a copy
- EnableKernelCache defaults to false (stale FreeBytes from old Reserve
  mechanism is now fixed, but conservative default)

ChaosTests fixes:
- Overwrite: SetKnown uses CAS (gen compare) to prevent stale hash
- WriteSeek/Append/Truncate/Extend: call Mutated before file modification
- Rename: invalidate hash to prevent stale-path race with Overwrite
- Overwrite uses FileMode.Create (not Truncate) for correct sharing semantics

TLA+ full-system model (RamDiskSystem.tla):
- Models PagePool, sparse page tables, 3-phase write, Read, CreateFile,
  Extend, Truncate, Delete, SetAllocSize TOCTOU, GetVolumeInfo observer
- Verified: PoolConsistent, NoPageLeak, FreeBytesAccurate, DataIntegrity,
  ReadConsistent, DeadFilesClean, SingleFileCap, WriteTerminates
- Minimal (3p×2f): 3.16M states, 12min — all pass
- Standard (4p×2f): 66.19M states, 5h — all pass (safety + liveness)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ListDirectory now returns children sorted by name (case-insensitive).
Without sorting, Dictionary.Values iteration order is unstable, causing
WinFsp marker-based pagination to return duplicate or missing entries
when the buffer fills and ReadDirectory is called multiple times.

Also update README (Quick Start, Formal Verification section) and
CLAUDE.md (TLA+ download/run/modeling guide, updated config defaults).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SetLength no longer reserves pages, so FreeBytes is never polluted and
kernel cache metadata stays accurate. Safe to re-enable by default (~3x
read throughput). ChaosTests now assert exactly 0 integrity failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@hooyao hooyao merged commit feaa9f9 into main Apr 10, 2026
2 checks passed
@hooyao hooyao deleted the feature/winfsp-net-binding branch April 10, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant