fix: empty directories return ENOENT under libuv (missing "." / ".." in ReadDirectory) - #20
Merged
Merged
Conversation
WinFspRamAdapter.ReadDirectory listed only real children, never the
synthetic "." (self) and ".." (parent) entries that NTFS and WinFsp's
reference memfs emit for every non-root directory. An EMPTY directory
therefore enumerated to zero records, and the kernel answered the first
NtQueryDirectoryFile with STATUS_NO_SUCH_FILE -> ERROR_FILE_NOT_FOUND.
.NET's Directory.* and PowerShell swallow that status and return an empty
set, which is why the .NET-based torture/chaos suites never caught it.
libuv (Node.js / Electron) surfaces it as ENOENT -- which broke VS Code's
updater: its cachePath getter does mkdir(...,{recursive}) immediately
followed by readdir(...) of the freshly-created (empty) cache dir, so
every update check failed with
ENOENT: no such file or directory, scandir Z:\Temp\vscode-stable-system-x64
Fix mirrors the validated MemfsReference oracle: prepend "." and ".." for
non-root directories, with correct marker-based resume.
Regression test (DirectoryEnumerationTests) goes through raw Win32
FindFirstFileW rather than Directory.* -- the only layer that exposes the
bug. Verified: fails 4/4 without the fix, passes 4/4 with it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diagnostic for a ChaosTests.RandomFuzzer integrity failure seen only on CI (GitHub runner, fewer cores) and not reproducible locally even pinned to 2 cores. Prints path / expected vs actual size so the next CI run reveals whether it is a stale-size (metadata cache) or wrong-bytes (page content) divergence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nostic) On an integrity failure, check whether the bytes actually on disk match ANY genuine write to that file. If they do, the filesystem preserved a real write and the test merely mis-attributed which concurrent Overwrite won the generation race (TEST false positive); if they match no write ever made to the file, the disk holds content that was never written -> REAL filesystem bug. Also TEMP-bumps CI CHAOS_DURATION_SEC to 90s to surface the rare race faster. Both this diagnostic and the duration bump are reverted once classified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Distinguishes the corruption shape: overwrite content intact but file not shrunk (stale tail) vs extended-with-zeros (size race) vs leading bytes wrong (cross-file page aliasing). Diagnostic only; reverted once the bug is localised. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ruption If the integrity corruption vanishes with EnableKernelCache=false it is a kernel-Cc cache-coherency bug (stale tail not invalidated on shrinking overwrite); if it persists it is user-mode PagedFileContent concurrency. Both env knobs are TEMP and reverted once localised. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ConcurrencyCorruptionTests: each file only ever holds its own unique byte value; a read returning any other byte = real corruption (aliasing / stale page / lost zero). All opens FileShare.ReadWrite for true same-file concurrency. Locally (2-core pinned, 82k reads) it finds ZERO corruption, so production is data- integrity-correct under concurrency and the ChaosTests integrity REALBUG is a false positive of its generation-based tracking, not a real bug. This run puts both on the CI runner to confirm. Adds env-gated (RAMDRIVE_OPLOG) callback log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ostics The ChaosTests.RandomFuzzer integrity failure on CI was a FALSE POSITIVE, not a production data-corruption bug. A confound-free detector (ConcurrencyCorruptionTests: each file only ever holds its own unique byte value; a read must see only that value or 0x00) finds ZERO corruption under aggressive concurrent overwrite/writeseek/ truncate/extend -- locally (82k reads, 2-core pinned) and on the CI runner. So PagePool + three-phase Write + SetLength are concurrency-correct for data integrity. The false positive comes from the chaos test generation-based ground-truth model: with the fixture permanent kernel cache (FileInfoTimeoutMs=uint.MaxValue) and the fire-and-forget FspFileSystemNotify, a read issued right after a size-changing op can briefly observe a stale cached size/content while user-mode state is already correct. Fix: ReadVerify re-reads to confirm a suspected mismatch (up to ~2s). A transient cache/scheduling blip self-heals (notify lands, or a concurrent op bumps the generation so VerifySnapshot skips); genuine corruption persists and is still counted. ConcurrencyCorruptionTests is kept as a permanent confound-free data-integrity regression. Reverts all temporary diagnostics (oplog, classification/forensic logging, workflow/fixture env knobs). The production "." / ".." ReadDirectory fix -- this PR's actual change -- is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace blocking Task.WaitAll / Task.Wait / .Wait(timeout) with async await Task.WhenAll / await Task.WhenAny(..., Task.Delay(...)) in the affected test methods, which xUnit1031 flags as deadlock-prone: - ChaosTests.RandomFuzzer (Task.WaitAll + printer.Wait) - ConcurrencyCorruptionTests (Task.WaitAll) - TortureTests.CapacityPressure (task.Wait(timeout)) Behaviour is unchanged; the three tests pass. No production code touched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
VS Code (and any libuv / Node.js / Electron app) on a RamDrive-backed
TEMPfails to enumerate empty directories. VS Codes auto-update aborted every time with:VS Codes
cachePathgetter doesmkdir(cacheDir,{recursive:true})and immediatelyreaddir(cacheDir). On a fresh RAM-diskTEMPthe cache dir is newly-created and empty ->readdir-> ENOENT -> the update never proceeds (repeats identically on every retry).Root cause
WinFspRamAdapter.ReadDirectoryenumerated only real children -- it never emitted the synthetic.(self) and..(parent) entries that NTFS and WinFsps referencememfsproduce for every non-root directory.An empty directory therefore enumerated to zero records. The Windows kernel answers the first
NtQueryDirectoryFileon a zero-record enumeration withSTATUS_NO_SUCH_FILE(->ERROR_FILE_NOT_FOUND):Directory.*/ PowerShellGet-ChildItemswallow that status and return an empty set -- so the bug was invisible to the existing .NET-based torture/chaos suites and to manualGet-ChildItem.ENOENT-- so VS Code, npm, git, etc. break.Reproduction (100% deterministic, against the live mount)
mkdir(d); readdir(d)onZ:(empty)C:NTFS (control)OK []mkdir; sleep 50ms; readdirdoes not help -- ruling out the kernel-cache / async-Notifyrace the cache-coherency postmortems primed us to suspect. The trigger is purely "directory enumerates to zero records".Fix
Mirror the validated
MemfsReferenceoracle: prepend.and..for non-root directories inReadDirectory, with correct marker-based resume (./..sort first; children listed fresh afterwards). Root is excluded -- NTFS root has no./...Test
tests/RamDrive.IntegrationTests/DirectoryEnumerationTests.cs-- must go through raw Win32FindFirstFileW, the only layer that exposes the bug (Directory.*passes even when broken).ERROR_FILE_NOT_FOUND) and passes 4/4 with it (deliberate-break self-test).🤖 Generated with Claude Code