Skip to content

Harden GVFS.Mount against native ProjFS failures under memory pressure (0xC000CE01)#2042

Draft
tyrielv wants to merge 3 commits into
microsoft:masterfrom
tyrielv:tyrielv/fix-projfs-mount-oom-crash
Draft

Harden GVFS.Mount against native ProjFS failures under memory pressure (0xC000CE01)#2042
tyrielv wants to merge 3 commits into
microsoft:masterfrom
tyrielv:tyrielv/fix-projfs-mount-oom-crash

Conversation

@tyrielv

@tyrielv tyrielv commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Problem

GVFS.Mount crashes are being reported that surface downstream as GetOfficialBranch.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 returns 0xC000CE01 until remount.

Watson attributes the crashes to radar_high_memory projectedfslib.dll!prjcompletecommand, alongside System.OutOfMemoryException buckets and GC access violations. The dominant managed signatures are NullReferenceException surfaced from the native ProjFS command-completion (PrjCompleteCommand, via StartDirectoryEnumerationAsyncHandler) and delete (PrjDeleteFile) paths — the native code faults under memory exhaustion and the CLR raises a (catchable) NullReferenceException on the worker thread, which today reaches ExecuteFileOrNetworkRequest and calls Environment.Exit.

VFS for Git is already on the latest Microsoft.Windows.ProjFS (2.1.0), so the native projectedfslib.dll fault 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.

Not a v2.0 regression: the crash-prone call sites and the activeEnumerations leak date to 2018 (785ccb4a, .NET Framework era). The v2.0 pure-C# ProjFS rewrite (66334474) made the same latent native fault legible as a managed NRE in GVFS telemetry — so the recent spike in reports is largely improved visibility, which B’s telemetry is designed to quantify.

A — Bound the activeEnumerations leak

ProjFS does not always deliver EndDirectoryEnumeration for a StartDirectoryEnumeration (the long-standing // TODO: Need ProjFS 13150199 note). The corresponding ActiveEnumeration — and the List<ProjectedFileInfo> it pins — then leaks for the life of the mount.

  • ActiveEnumeration tracks a monotonic LastActivityTickCount (Environment.TickCount64), set on creation and every GetDirectoryEnumeration read.
  • Once activeEnumerations grows 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).
  • Evicting a live-but-idle enumeration only fails that one directory listing with InternalError — never a crash — and every eviction is reported via telemetry.
  • Monotonic clocks are used for both the throttle and the staleness check so NTP/wall-clock steps can’t disturb them.

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, with activeEnumerations/activeCommands counts and GC.GetTotalMemory), and returns HResult.InternalError so the caller maps it to a failure result and keeps serving.

Routed through the swallow-and-continue helper: CompleteCommand (via TryCompleteCommand — covers the completion leg of all async callbacks: enumeration, placeholder-info, file-data), DeleteFile, WritePlaceholderFile, WritePlaceholderDirectory, UpdatePlaceholderIfNeeded, ClearNegativePathCache.

MarkDirectoryAsPlaceholder mutates projection state, so swallowing is unsafe. It uses a sibling helper InvokeProjFSOrThrow that emits the same *_NativeFailure telemetry (so the memory diagnostics are captured) and then rethrows — the exception still reaches LogUnhandledExceptionAndExit, preserving fail-fast. Both helpers share one LogProjFSNativeFailure.

Left as-is (already non-fatal): GetFileStreamHandlerAsyncHandler already fails a single hydration on any exception (completes the command with FileNotAvailable, no exit), so CreateWriteBuffer/WriteFileData are already non-fatal.

The exception is demonstrably catchable — it already reaches GVFS’s LogUnhandledExceptionAndExit today (the source of the existing VFS.Error "exiting process" events). B intercepts it one frame earlier and either declines to exit or (for state-mutating ops) enriches the crash with memory telemetry first.

Tests

  • DeleteFileReturnsIOErrorWhenVirtualizationInstanceThrows, OutboundProjFSOperationsReturnFailureWhenVirtualizationInstanceThrows (ClearNegativePathCache, UpdatePlaceholderIfNeeded), WritePlaceholderOperationsReturnFailureWhenVirtualizationInstanceThrows (tester-based) — a throwing native call yields IOError, not a propagated exception.
  • RecordActivityUpdatesLastActivityTime — covers the A monotonic-timestamp building block.
  • (InvokeProjFSOrThrow’s rethrow path ends in Environment.Exit, so it isn’t safely unit-testable in-process; it’s symmetric with the tested TryInvokeProjFS.)
  • Full unit suite: 881 passed, 0 failed (11 skipped); clean build.

Open questions for review (draft)

  1. Eviction heuristic. Threshold (1024) / stale-timeout (5 min) are conservative guesses. Tunable, or tied to a real signal? Is the rare "evict a live idle enumeration → one failed listing" tradeoff acceptable?
  2. Swallow vs. controlled remount. B fails the single op and keeps serving. If the native context is genuinely dead, every op fails 0xC000CE01 anyway — would a deliberate remount on repeated *_NativeFailure be better recovery than limping?
  3. Is A even the dominant leak? The B telemetry (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

tyrielv added 3 commits July 6, 2026 16:33
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>
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