Harden GVFS.Mount against native ProjFS failures under memory pressure (0xC000CE01)#2042
Draft
tyrielv wants to merge 3 commits into
Draft
Harden GVFS.Mount against native ProjFS failures under memory pressure (0xC000CE01)#2042tyrielv wants to merge 3 commits into
tyrielv wants to merge 3 commits into
Conversation
GVFS.Mount crashes are being reported that manifest downstream as STATUS_FILE_SYSTEM_VIRTUALIZATION_UNAVAILABLE (0xC000CE01): once the mount process dies, the ProjFS virtualization root is orphaned and every subsequent placeholder access fails. Watson attributes the crashes to radar_high_memory projectedfslib.dll!prjcompletecommand, with the dominant managed signatures being NullReferenceException surfaced from the native ProjFS command-completion (PrjCompleteCommand) and delete (PrjDeleteFile) paths under memory pressure. VFS for Git is already on the latest Microsoft.Windows.ProjFS package, so the native projectedfslib.dll fault cannot be fixed here directly; the only levers on the managed side are (A) reducing the memory pressure that triggers the native fault, and (B) not converting a single failed ProjFS call into a whole-mount crash. A. Bound the activeEnumerations leak. ProjFS does not always deliver EndDirectoryEnumeration for a StartDirectoryEnumeration (for example when an enumeration is cancelled), which leaks the corresponding ActiveEnumeration - and the projected item list it pins - in this.activeEnumerations. Over long-lived mounts this unbounded growth contributes to the memory pressure that ultimately trips the native crash. ActiveEnumeration now tracks a LastActivityTimeUtc (set on creation and on every GetDirectoryEnumeration read). Once activeEnumerations grows past a threshold far larger than any realistic count of concurrent live enumerations, a throttled sweep evicts entries that have seen no activity for a long time. Evicting a live (but idle) enumeration only fails that one directory listing with InternalError - never a crash - and eviction is reported via telemetry so its frequency is measurable. B. Do not crash the mount on a native ProjFS failure. TryCompleteCommand and DeleteFile now catch exceptions thrown by the native virtualizationInstance calls, emit high-signal telemetry (including live enumeration/command counts and GC memory), and fail just that operation instead of letting the exception reach ExecuteFileOrNetworkRequest and call Environment.Exit. DeleteFile returns IOError (callers already treat a non-Ok result as a delete failure and continue); a failed CompleteCommand is dropped. This keeps the virtualization root alive and, crucially, replaces the opaque Watson crash bucket with actionable GVFS telemetry at the exact failure point. Adds unit tests: DeleteFile returns IOError when the virtualization instance throws, and ActiveEnumeration records activity time. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
…sweep clock Follow-up to the initial DeleteFile/CompleteCommand hardening. Guarding only DeleteFile among the GVFS-initiated ProjFS operations was inconsistent: WritePlaceholderFile, WritePlaceholderDirectory, UpdatePlaceholderIfNeeded, and ClearNegativePathCache make the same kind of direct native call and can fault identically under memory pressure. Nothing is special about DeleteFile beyond it being one of the two dominant crash signatures in telemetry. Introduce a single TryInvokeProjFS(Func<HResult>, operationName, ...) boundary helper that catches a native failure, emits the shared *_NativeFailure telemetry (with live enumeration/command counts and GC memory), and returns HResult.InternalError so the caller maps it to a failure result. Route every outbound override and TryCompleteCommand through it. Notification handlers, which mutate projection state, deliberately remain fail-fast; the file-stream handler already fails a single hydration on any exception, so it is left as-is. Also switch the enumeration-eviction throttle and ActiveEnumeration activity timestamp from DateTime.UtcNow to Environment.TickCount64, so wall-clock adjustments (NTP steps) cannot disturb the sweep interval or cause a live-but-idle enumeration to be misjudged as stale. Tests: add coverage for the newly guarded outbound operations returning IOError when the virtualization instance throws (direct construction for ClearNegativePathCache/UpdatePlaceholderIfNeeded, tester-based for the WritePlaceholder* operations that require FileSystemCallbacks). Full unit suite: 881 passed, 0 failed. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
…ethrowing MarkDirectoryAsPlaceholder mutates projection state, so a native failure there must stay fail-fast (swallowing could leave the projection inconsistent). But it should still emit the same high-signal *_NativeFailure telemetry (memory + counts) as the swallow-and-continue operations, so the crash carries the memory-pressure diagnostics. Add InvokeProjFSOrThrow, a log-then-rethrow sibling of TryInvokeProjFS sharing a single LogProjFSNativeFailure helper, and route MarkDirectoryAsPlaceholder's native call through it. The rethrown exception still reaches NotifyNewFileCreatedHandler's LogUnhandledExceptionAndExit, preserving fail-fast. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.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.
Problem
GVFS.Mountcrashes are being reported that surface downstream asGetOfficialBranch.exe(and other tools) exiting with-1073689087=0xC000CE01=STATUS_FILE_SYSTEM_VIRTUALIZATION_UNAVAILABLE. The chain is: GVFS.Mount crashes → the ProjFS virtualization root is orphaned → every subsequent placeholder access on that enlistment returns0xC000CE01until remount.Watson attributes the crashes to
radar_high_memory projectedfslib.dll!prjcompletecommand, alongsideSystem.OutOfMemoryExceptionbuckets and GC access violations. The dominant managed signatures areNullReferenceExceptionsurfaced from the native ProjFS command-completion (PrjCompleteCommand, viaStartDirectoryEnumerationAsyncHandler) and delete (PrjDeleteFile) paths — the native code faults under memory exhaustion and the CLR raises a (catchable)NullReferenceExceptionon the worker thread, which today reachesExecuteFileOrNetworkRequestand callsEnvironment.Exit.VFS for Git is already on the latest
Microsoft.Windows.ProjFS(2.1.0), so the nativeprojectedfslib.dllfault cannot be fixed from here. The only managed-side levers are (A) reduce the memory pressure that triggers the native fault, and (B) stop converting a single failed native ProjFS call into a whole-mount crash. This PR does both. It does not claim to eliminate the native OOM fault itself.A — Bound the
activeEnumerationsleakProjFS does not always deliver
EndDirectoryEnumerationfor aStartDirectoryEnumeration(the long-standing// TODO: Need ProjFS 13150199note). The correspondingActiveEnumeration— and theList<ProjectedFileInfo>it pins — then leaks for the life of the mount.ActiveEnumerationtracks a monotonicLastActivityTickCount(Environment.TickCount64), set on creation and everyGetDirectoryEnumerationread.activeEnumerationsgrows past a threshold far larger than any realistic count of concurrent live enumerations (1024), a throttled sweep (≤ once/min) evicts entries idle longer than a timeout (5 min).InternalError— never a crash — and every eviction is reported via telemetry.B — Don’t crash the mount on a native ProjFS failure (generalized)
A single boundary helper,
TryInvokeProjFS(Func<HResult>, operationName, …), wraps each GVFS-initiated native ProjFS call: it catches the exception, emits high-signal telemetry (*_NativeFailure, withactiveEnumerations/activeCommandscounts andGC.GetTotalMemory), and returnsHResult.InternalErrorso the caller maps it to a failure result and keeps serving.Routed through the swallow-and-continue helper:
CompleteCommand(viaTryCompleteCommand— covers the completion leg of all async callbacks: enumeration, placeholder-info, file-data),DeleteFile,WritePlaceholderFile,WritePlaceholderDirectory,UpdatePlaceholderIfNeeded,ClearNegativePathCache.MarkDirectoryAsPlaceholdermutates projection state, so swallowing is unsafe. It uses a sibling helperInvokeProjFSOrThrowthat emits the same*_NativeFailuretelemetry (so the memory diagnostics are captured) and then rethrows — the exception still reachesLogUnhandledExceptionAndExit, preserving fail-fast. Both helpers share oneLogProjFSNativeFailure.Left as-is (already non-fatal):
GetFileStreamHandlerAsyncHandleralready fails a single hydration on any exception (completes the command withFileNotAvailable, no exit), soCreateWriteBuffer/WriteFileDataare already non-fatal.Tests
DeleteFileReturnsIOErrorWhenVirtualizationInstanceThrows,OutboundProjFSOperationsReturnFailureWhenVirtualizationInstanceThrows(ClearNegativePathCache, UpdatePlaceholderIfNeeded),WritePlaceholderOperationsReturnFailureWhenVirtualizationInstanceThrows(tester-based) — a throwing native call yieldsIOError, not a propagated exception.RecordActivityUpdatesLastActivityTime— covers the A monotonic-timestamp building block.InvokeProjFSOrThrow’s rethrow path ends inEnvironment.Exit, so it isn’t safely unit-testable in-process; it’s symmetric with the testedTryInvokeProjFS.)Open questions for review (draft)
0xC000CE01anyway — would a deliberate remount on repeated*_NativeFailurebe better recovery than limping?activeEnumerations/memory) is partly here to answer that. Worth landing B first purely for diagnostics before committing to A’s heuristic?Assisted-by: Claude Opus 4.8