#665·winfsp

FileInfoTimeout::Infinite + cache-manager paging-IO retention: delete-class IRPs fail until cache release; consider splitting DirInfoTimeout from FileInfoTimeout

Author: jase-perfCreated Apr 26, 2026Updated Apr 30, 2026

Summary

I'm building a content-addressed dedup filesystem on top of WinFsp. FileInfoTimeout = INFINITE is the only switch I've found that activates the FSD's DirInfoCache, and on my workload that's the difference between 105 s and ~20 s cold project-open (5×). Under INFINITE the NT cache manager attaches and retains paging-IO references on the FileNode that survive my cb_close. The FSD then refuses delete-class IRPs against the same path with STATUS_INVALID_DEVICE_REQUEST (Win32 ERROR_INVALID_FUNCTION / "Incorrect function") until the cache manager releases its reference (~500 ms later). I'd love guidance on how to coordinate with cache-manager retention from user-mode at cleanup time, and to ask whether DirInfoTimeout could become an independent knob from FileInfoTimeout.

Profile evidence

My workload is copying a 3,105-file Unreal Engine project. From a probe-instrumented run:

  • intra-copy-ue-project: 104.4 s warm cold-open, 30 files/s.
  • cb_read_directory = 89.6% of the per-file budget (30,123 µs of 33,640 µs). One CopyFileEx fans out to ~81 cb_read_directory IRPs because no dir cache is attached under finite FileInfoTimeout.
  • get_file_info_total = 4 over the whole bench matrix confirms the FileInfoCache is also unattached.
  • Projected under INFINITE: ~6 ms/file, ~20 s total.

What I tried first (so you know I did the work)

I rebuilt my open/handle model on memfs's shape: one Arc<PathContext> per path, refcounted across opens, stored as the FSD UserContext (UmFileContextIsUserContext2 = 0). All 12 of my callbacks dispatch through that pointer.

Along the way I found several mistakes that were my own, but it's been a slow process. These were the issues I found in my own code:

  1. Stat-after-cleanup-pre-COMMIT staleness window — added synchronous write-through to a per-path cached_meta mirror at every size-mutation site.
  2. cb_flush was returning a hardcoded zero-size FILE_INFOFlushFileBuffers was reverting FileSize under INFINITE.
  3. cb_set_file_size was treating FileAllocationInformation as a truncate — .NET's FileStream.SetLength() issues a FileEndOfFileInformation followed by FileAllocationInformation, and my code was treating both as the same op, so the second IRP undid the first. Persisted the wrong size on disk under any timeout.
  4. A cb_close-only finalize race — I was releasing the writer lock before the SQL commit landed; a concurrent open within that window returned Busy → user-visible "Incorrect function".
  5. Duplicate cb_cleanup underflow — under INFINITE, the cache manager dispatches a second cb_cleanup on the same FileContext (flags 0xa0 = FspCleanupSetAllocationSize | FspCleanupSetArchiveBit) when its paging handle releases. My write_opens decrement underflowed 0 → u64::MAX and leaked the PathContext forever. Fixed by making the cleanup arm idempotent at the call site.

After all five fixes, my code is honoring the contract as I understand it. The remaining failure (below) seems to be in a corner I can't reach from user mode.

The remaining blocker — minimal reproducer

I isolated the failure to a small standalone PowerShell script (no benchmark harness needed):

powershell
# Mount my user-space file system at H:\ with FileInfoTimeout = INFINITE.
$dir = "H:\test-a"
$file = Join-Path $dir "seqwrite.bin"
$buf = New-Object byte[] 1MB
(New-Object System.Random 42).NextBytes($buf)
foreach ($run in 1..2) {
  if (Test-Path $dir) {
    Remove-Item -Recurse -Force $dir   # <-- run 2: throws "Incorrect function"
  }
  New-Item -ItemType Directory -Path $dir | Out-Null
  $fs = [System.IO.File]::Create($file, 1MB)
  try {
    $written = 0L
    while ($written -lt 512MB) { $fs.Write($buf, 0, 1MB); $written += 1MB }
    $fs.Flush($true)
  } finally { $fs.Dispose() }
}

Timing matrix (sleep inserted between Run 1 dispose and Run 2 Remove-Item):

Sleep Outcome
0 ms FAIL — Remove-Item throws Incorrect function
100 ms FAIL
500 ms PASS
1000 ms PASS

The 500 ms cadence aligns with the cache manager's lazy-writer / paging-IO release period. Under Ms(999_999) the same script passes at all sleep values, but then I don't get DirInfoCache.

My cb_cleanup returns STATUS_SUCCESS, my cb_close runs, and the FileContext is released cleanly (verified via probe trace — no underflows or leaks after the 1c.8 fix). User-mode CloseHandle returns to the script. Then Remove-Item calls DeleteFileW, which goes through cb_can_delete and cb_cleanup(FspCleanupDelete). Under INFINITE, that delete-class IRP returns STATUS_INVALID_DEVICE_REQUEST for ~500 ms after the prior close, then succeeds. Under finite timeout, no delay.

I couldn't find a user-mode hook to either (a) wait for the cache manager to release its paging-IO references at cleanup time, or (b) discover whether such references are present so I could surface a retryable error rather than fail. That's where I'm stuck.

Asks

  1. Primary — cache-manager paging-IO coordination at cleanup. Under FileInfoTimeout = INFINITE, what's the user-mode FS's contract for handling delete-class IRPs that arrive while the cache manager still holds paging-IO references on the FileNode? Is there a user-mode helper I should be calling to drain the cache (a FspFileSystemFlushCache-shaped thing?), or a way to learn that paging-IO references are outstanding so I could return a retryable error instead of failing? I'd assume memfs runs under INFINITE without hitting this — is there a contract or pattern I'm missing?

  2. Secondary — split DirInfoTimeout from FileInfoTimeout. If the primary can't be addressed in the current ABI, could DirInfoTimeout become independently configurable? The DirInfoCache is the only lever I need for my cold project-open speedup; the FileInfoCache + cache-manager attach are what introduces the retention timing. I'd happily accept FSD-side dir caching while letting per-file metadata pass through every IRP.

Test environment

  • Windows 11 22H2 dev VM (Hyper-V); WinFsp 2.1.25156 (per installed fsctl.h); hand-rolled Rust FFI (no winfsp-sys/winfsp-rs — wrote my own bindings).
  • The filesystem is content-addressed dedup over local NTFS (NT-handle pass-through reads), SQLite adjacency-list index, per-path Arc<PathContext> shared via UserContext, all 12 callbacks Arc-shared per path.
  • Targeting game-dev workstations.

Happy to put together a minimal repro repo, write a memfs patch that adds the INFINITE + back-to-back-write-and-delete test, or both. I can test candidate patches on my VM and report bench numbers from the UE-project workload — just let me know what would be most useful. Thanks for taking a look.