Skip to content

Commit 4e580fa

Browse files
committed
ProjFS: tolerate ACCESS_DENIED on FilterAttach, remove dead bundled-driver code
The ProjFS filter driver and native library are no longer bundled with the GVFS installer — ProjFS is required as a Windows Optional Feature (available since Windows 10 1809, our minimum supported OS). Fix FilterAttach ACCESS_DENIED (bug 62349777): - Add TryAttachToVolume() that tolerates ACCESS_DENIED when the ProjFS service is already running (the filter is already attached to the volume but the caller lacks SE_LOAD_DRIVER_PRIVILEGE). - The service-side EnableAndAttachProjFSHandler.Run() now uses TryAttachToVolume instead of raw TryAttach, matching the tolerance already present in the client-side IsReady() path. - Fix Run() to preserve the first error from TryEnablePrjFlt instead of potentially overwriting it with a subsequent attach error. Remove dead non-inbox ProjFS code paths: - Remove TryInstallProjFSViaINF (referenced {app}\Filter\prjflt.inf which is no longer shipped). - Remove TryCopyNativeLibIfDriverVersionsMatch and supporting methods (referenced {app}\Filter\prjflt.sys and {app}\ProjFS\ProjectedFSLib.dll which are no longer shipped). - Simplify TryEnableOrInstallDriver to always use the Windows Optional Feature path; keep build number logging as best-effort telemetry. - Simplify IsNativeLibInstalled to require System32; warn if stale app-local DLL found from a legacy non-inbox install. Improve error messaging: - When ProjFS native library is missing, error now includes the PowerShell command to enable the optional feature. - When ProjFS cannot be enabled, error directs user to the optional feature instead of referencing non-existent bundled files. Assisted-by: Claude Opus 4.6 Signed-off-by: Tyler Vella <tyrielv@gmail.com>
1 parent 1e748bb commit 4e580fa

3 files changed

Lines changed: 77 additions & 409 deletions

File tree

GVFS/GVFS.Platform.Windows/ProjFSFilter.cs

Lines changed: 55 additions & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using Microsoft.Windows.ProjFS;
66
using System;
77
using System.ComponentModel;
8-
using System.Diagnostics;
98
using System.IO;
109
using System.Linq;
1110
using System.Runtime.InteropServices;
@@ -27,7 +26,6 @@ public class ProjFSFilter : IKernelDriver
2726
private const string PrjFltAutoLoggerStartValue = "Start";
2827

2928
private const string System32LogFilesRoot = @"%SystemRoot%\System32\LogFiles";
30-
private const string System32DriversRoot = @"%SystemRoot%\System32\drivers";
3129

3230
// From "Autologger" section of prjflt.inf
3331
private const string FilterLoggerGuid = "ee4206ff-4a4d-452f-be56-6bd0ed272b44";
@@ -87,6 +85,34 @@ public static bool TryAttach(string enlistmentRoot, out string errorMessage)
8785
return true;
8886
}
8987

88+
/// <summary>
89+
/// Attempts to attach the ProjFS filter driver to the volume containing the enlistment.
90+
/// If FilterAttach returns ACCESS_DENIED but the ProjFS service is already running,
91+
/// the filter is presumed to already be attached and the call succeeds.
92+
/// </summary>
93+
public static bool TryAttachToVolume(string enlistmentRoot, ITracer tracer, out string errorMessage)
94+
{
95+
if (TryAttach(enlistmentRoot, out errorMessage))
96+
{
97+
return true;
98+
}
99+
100+
// FilterAttach requires SE_LOAD_DRIVER_PRIVILEGE, which is typically only
101+
// granted to administrators. When the caller lacks this privilege but the
102+
// ProjFS service is confirmed running, the filter is already attached to
103+
// the volume — treat ACCESS_DENIED as success in that case.
104+
if (errorMessage != null
105+
&& errorMessage.Contains(AccessDeniedResult.ToString())
106+
&& IsServiceRunning(tracer))
107+
{
108+
tracer.RelatedInfo($"{nameof(TryAttachToVolume)}: FilterAttach returned ACCESS_DENIED, but ProjFS service is running. Proceeding.");
109+
errorMessage = null;
110+
return true;
111+
}
112+
113+
return false;
114+
}
115+
90116
public static bool IsServiceRunning(ITracer tracer)
91117
{
92118
try
@@ -243,128 +269,45 @@ public static bool TryEnableOrInstallDriver(
243269
out bool isProjFSFeatureAvailable)
244270
{
245271
isProjFSFeatureAvailable = false;
246-
if (!TryGetIsInboxProjFSFinalAPI(tracer, out windowsBuildNumber, out isInboxProjFSFinalAPI))
272+
273+
// Log build number for telemetry. ProjFS is inbox on all supported OS versions
274+
// (Windows 10 1809 / build 17763+), so this is informational only.
275+
if (!TryGetWindowsBuildNumber(tracer, out windowsBuildNumber))
247276
{
248-
return false;
277+
tracer.RelatedWarning($"{nameof(TryEnableOrInstallDriver)}: Could not determine Windows build number");
249278
}
250279

251-
if (isInboxProjFSFinalAPI)
252-
{
253-
if (TryEnableProjFSOptionalFeature(tracer, fileSystem, out isProjFSFeatureAvailable))
254-
{
255-
return true;
256-
}
280+
isInboxProjFSFinalAPI = true;
257281

258-
return false;
282+
if (TryEnableProjFSOptionalFeature(tracer, fileSystem, out isProjFSFeatureAvailable))
283+
{
284+
return true;
259285
}
260286

261-
return TryInstallProjFSViaINF(tracer, fileSystem);
287+
return false;
262288
}
263289

264290
public static bool IsNativeLibInstalled(ITracer tracer, PhysicalFileSystem fileSystem)
265291
{
266292
string system32Path = Path.Combine(Environment.SystemDirectory, ProjFSNativeLibFileName);
267293
bool existsInSystem32 = fileSystem.FileExists(system32Path);
268294

269-
string gvfsAppDirectory = ProcessHelper.GetCurrentProcessLocation();
270-
string nonInboxNativeLibInstallPath;
271-
string packagedNativeLibPath;
272-
GetNativeLibPaths(gvfsAppDirectory, out packagedNativeLibPath, out nonInboxNativeLibInstallPath);
273-
bool existsInAppDirectory = fileSystem.FileExists(nonInboxNativeLibInstallPath);
274-
275295
EventMetadata metadata = CreateEventMetadata();
276296
metadata.Add(nameof(system32Path), system32Path);
277297
metadata.Add(nameof(existsInSystem32), existsInSystem32);
278-
metadata.Add(nameof(gvfsAppDirectory), gvfsAppDirectory);
279-
metadata.Add(nameof(nonInboxNativeLibInstallPath), nonInboxNativeLibInstallPath);
280-
metadata.Add(nameof(packagedNativeLibPath), packagedNativeLibPath);
281-
metadata.Add(nameof(existsInAppDirectory), existsInAppDirectory);
282-
tracer.RelatedEvent(EventLevel.Informational, nameof(IsNativeLibInstalled), metadata);
283-
return existsInSystem32 || existsInAppDirectory;
284-
}
285-
286-
public static bool TryCopyNativeLibIfDriverVersionsMatch(ITracer tracer, PhysicalFileSystem fileSystem, out string copyNativeDllError)
287-
{
288-
string system32NativeLibraryPath = Path.Combine(Environment.SystemDirectory, ProjFSNativeLibFileName);
289-
if (fileSystem.FileExists(system32NativeLibraryPath))
290-
{
291-
copyNativeDllError = $"{ProjFSNativeLibFileName} already exists at {system32NativeLibraryPath}";
292-
return false;
293-
}
294-
295-
string gvfsProcessLocation = ProcessHelper.GetCurrentProcessLocation();
296-
string nonInboxNativeLibInstallPath;
297-
string packagedNativeLibPath;
298-
GetNativeLibPaths(gvfsProcessLocation, out packagedNativeLibPath, out nonInboxNativeLibInstallPath);
299-
if (fileSystem.FileExists(nonInboxNativeLibInstallPath))
300-
{
301-
copyNativeDllError = $"{ProjFSNativeLibFileName} already exists at {nonInboxNativeLibInstallPath}";
302-
return false;
303-
}
304-
305-
if (!fileSystem.FileExists(packagedNativeLibPath))
306-
{
307-
copyNativeDllError = $"{packagedNativeLibPath} not found, no {ProjFSNativeLibFileName} available to copy";
308-
return false;
309-
}
310-
311-
string packagedPrjfltDriverPath = Path.Combine(gvfsProcessLocation, "Filter", DriverFileName);
312-
if (!fileSystem.FileExists(packagedPrjfltDriverPath))
313-
{
314-
copyNativeDllError = $"{packagedPrjfltDriverPath} not found, unable to validate that packaged driver matches installed driver";
315-
return false;
316-
}
317-
318-
string system32PrjfltDriverPath = Path.Combine(Environment.ExpandEnvironmentVariables(System32DriversRoot), DriverFileName);
319-
if (!fileSystem.FileExists(system32PrjfltDriverPath))
320-
{
321-
copyNativeDllError = $"{system32PrjfltDriverPath} not found, unable to validate that packaged driver matches installed driver";
322-
return false;
323-
}
324-
325-
FileVersionInfo packagedDriverVersion;
326-
FileVersionInfo system32DriverVersion;
327-
try
328-
{
329-
packagedDriverVersion = fileSystem.GetVersionInfo(packagedPrjfltDriverPath);
330-
system32DriverVersion = fileSystem.GetVersionInfo(system32PrjfltDriverPath);
331-
if (!fileSystem.FileVersionsMatch(packagedDriverVersion, system32DriverVersion))
332-
{
333-
copyNativeDllError = $"Packaged sys FileVersion '{packagedDriverVersion.FileVersion}' does not match System32 sys FileVersion '{system32DriverVersion.FileVersion}'";
334-
return false;
335-
}
336-
337-
if (!fileSystem.ProductVersionsMatch(packagedDriverVersion, system32DriverVersion))
338-
{
339-
copyNativeDllError = $"Packaged sys ProductVersion '{packagedDriverVersion.ProductVersion}' does not match System32 sys ProductVersion '{system32DriverVersion.ProductVersion}'";
340-
return false;
341-
}
342-
}
343-
catch (FileNotFoundException e)
344-
{
345-
EventMetadata metadata = CreateEventMetadata(e);
346-
tracer.RelatedWarning(
347-
metadata,
348-
$"{nameof(TryCopyNativeLibIfDriverVersionsMatch)}: Exception caught while comparing sys versions");
349-
copyNativeDllError = $"Exception caught while comparing sys versions: {e.Message}";
350-
return false;
351-
}
352-
353-
EventMetadata driverVersionMetadata = CreateEventMetadata();
354-
driverVersionMetadata.Add($"{nameof(packagedDriverVersion)}.FileVersion", packagedDriverVersion.FileVersion.ToString());
355-
driverVersionMetadata.Add($"{nameof(system32DriverVersion)}.FileVersion", system32DriverVersion.FileVersion.ToString());
356-
driverVersionMetadata.Add($"{nameof(packagedDriverVersion)}.ProductVersion", packagedDriverVersion.ProductVersion.ToString());
357-
driverVersionMetadata.Add($"{nameof(system32DriverVersion)}.ProductVersion", system32DriverVersion.ProductVersion.ToString());
358-
tracer.RelatedInfo(driverVersionMetadata, $"{nameof(TryCopyNativeLibIfDriverVersionsMatch)}: Copying native library");
359298

360-
if (!TryCopyNativeLibToNonInboxInstallLocation(tracer, fileSystem, gvfsProcessLocation))
299+
// Check for stale app-local native library from legacy non-inbox installs.
300+
// This file should not exist on current builds; warn if found so admins can clean it up.
301+
string appLocalPath = Path.Combine(ProcessHelper.GetCurrentProcessLocation(), ProjFSNativeLibFileName);
302+
bool staleAppLocalLibExists = fileSystem.FileExists(appLocalPath);
303+
if (staleAppLocalLibExists)
361304
{
362-
copyNativeDllError = "Failed to copy native library";
363-
return false;
305+
metadata.Add(nameof(appLocalPath), appLocalPath);
306+
metadata.Add(TracingConstants.MessageKey.WarningMessage, "Stale app-local ProjectedFSLib.dll found from legacy non-inbox ProjFS install");
364307
}
365308

366-
copyNativeDllError = null;
367-
return true;
309+
tracer.RelatedEvent(EventLevel.Informational, nameof(IsNativeLibInstalled), metadata);
310+
return existsInSystem32;
368311
}
369312

370313
public bool IsGVFSUpgradeSupported()
@@ -469,23 +412,14 @@ public bool IsReady(JsonTracer tracer, string enlistmentRoot, TextWriter output,
469412

470413
if (!IsNativeLibInstalled(tracer, new PhysicalFileSystem()))
471414
{
472-
error = "ProjFS native library is not installed";
415+
error = "ProjFS native library (ProjectedFSLib.dll) is not installed. "
416+
+ "Ensure the Windows Projected File System optional feature is enabled: "
417+
+ "Enable-WindowsOptionalFeature -Online -FeatureName Client-ProjFS";
473418
return false;
474419
}
475420

476-
if (!TryAttach(enlistmentRoot, out error))
421+
if (!TryAttachToVolume(enlistmentRoot, tracer, out error))
477422
{
478-
// FilterAttach requires SE_LOAD_DRIVER_PRIVILEGE (admin). When running
479-
// non-elevated on a machine where ProjFS is already set up, the filter
480-
// is already attached to the volume and the only failure is ACCESS_DENIED.
481-
// Allow the mount to proceed in that specific case.
482-
if (error.Contains(AccessDeniedResult.ToString()))
483-
{
484-
tracer.RelatedInfo($"IsReady: TryAttach returned ACCESS_DENIED, but ProjFS service is running. Proceeding.");
485-
error = string.Empty;
486-
return true;
487-
}
488-
489423
return false;
490424
}
491425

@@ -508,134 +442,20 @@ private static bool IsInboxAndEnabled()
508442
return getOptionalFeatureResult.ExitCode == (int)ProjFSInboxStatus.Enabled;
509443
}
510444

511-
private static bool TryGetIsInboxProjFSFinalAPI(ITracer tracer, out uint windowsBuildNumber, out bool isProjFSInbox)
445+
private static bool TryGetWindowsBuildNumber(ITracer tracer, out uint windowsBuildNumber)
512446
{
513-
isProjFSInbox = false;
514447
windowsBuildNumber = 0;
515448
try
516449
{
517450
windowsBuildNumber = Common.NativeMethods.GetWindowsBuildNumber();
518-
tracer.RelatedInfo($"{nameof(TryGetIsInboxProjFSFinalAPI)}: Build number = {windowsBuildNumber}");
519-
}
520-
catch (Win32Exception e)
521-
{
522-
tracer.RelatedError(CreateEventMetadata(e), $"{nameof(TryGetIsInboxProjFSFinalAPI)}: Exception while trying to get Windows build number");
523-
return false;
524-
}
525-
526-
const uint MinRS4inboxVersion = 17121;
527-
const uint FirstRS5Version = 17600;
528-
const uint MinRS5inboxVersion = 17626;
529-
isProjFSInbox = !(windowsBuildNumber < MinRS4inboxVersion || (windowsBuildNumber >= FirstRS5Version && windowsBuildNumber < MinRS5inboxVersion));
530-
return true;
531-
}
532-
533-
private static bool TryInstallProjFSViaINF(ITracer tracer, PhysicalFileSystem fileSystem)
534-
{
535-
string gvfsAppDirectory = ProcessHelper.GetCurrentProcessLocation();
536-
if (!TryCopyNativeLibToNonInboxInstallLocation(tracer, fileSystem, gvfsAppDirectory))
537-
{
538-
return false;
539-
}
540-
541-
ProcessResult result = ProcessHelper.Run("RUNDLL32.EXE", $"SETUPAPI.DLL,InstallHinfSection DefaultInstall 128 {gvfsAppDirectory}\\Filter\\prjflt.inf");
542-
if (result.ExitCode == 0)
543-
{
544-
tracer.RelatedInfo($"{nameof(TryInstallProjFSViaINF)}: Installed PrjFlt via INF");
451+
tracer.RelatedInfo($"{nameof(TryGetWindowsBuildNumber)}: Build number = {windowsBuildNumber}");
545452
return true;
546453
}
547-
else
548-
{
549-
EventMetadata metadata = CreateEventMetadata();
550-
metadata.Add("resultExitCode", result.ExitCode);
551-
metadata.Add("resultOutput", result.Output);
552-
tracer.RelatedError(metadata, $"{nameof(TryInstallProjFSViaINF)}: RUNDLL32.EXE failed to install PrjFlt");
553-
}
554-
555-
return false;
556-
}
557-
558-
private static bool TryCopyNativeLibToNonInboxInstallLocation(ITracer tracer, PhysicalFileSystem fileSystem, string gvfsAppDirectory)
559-
{
560-
string packagedNativeLibPath;
561-
string nonInboxNativeLibInstallPath;
562-
GetNativeLibPaths(gvfsAppDirectory, out packagedNativeLibPath, out nonInboxNativeLibInstallPath);
563-
564-
EventMetadata pathMetadata = CreateEventMetadata();
565-
pathMetadata.Add(nameof(gvfsAppDirectory), gvfsAppDirectory);
566-
pathMetadata.Add(nameof(packagedNativeLibPath), packagedNativeLibPath);
567-
pathMetadata.Add(nameof(nonInboxNativeLibInstallPath), nonInboxNativeLibInstallPath);
568-
569-
if (fileSystem.FileExists(packagedNativeLibPath))
570-
{
571-
tracer.RelatedEvent(EventLevel.Informational, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}_CopyingNativeLib", pathMetadata);
572-
573-
try
574-
{
575-
fileSystem.CopyFile(packagedNativeLibPath, nonInboxNativeLibInstallPath, overwrite: true);
576-
577-
try
578-
{
579-
fileSystem.FlushFileBuffers(nonInboxNativeLibInstallPath);
580-
}
581-
catch (Win32Exception e)
582-
{
583-
EventMetadata metadata = CreateEventMetadata(e);
584-
metadata.Add(nameof(nonInboxNativeLibInstallPath), nonInboxNativeLibInstallPath);
585-
metadata.Add(nameof(packagedNativeLibPath), packagedNativeLibPath);
586-
tracer.RelatedWarning(metadata, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: Win32Exception while trying to flush file buffers", Keywords.Telemetry);
587-
}
588-
}
589-
catch (UnauthorizedAccessException e)
590-
{
591-
EventMetadata metadata = CreateEventMetadata(e);
592-
tracer.RelatedError(metadata, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: UnauthorizedAccessException caught while trying to copy native lib");
593-
return false;
594-
}
595-
catch (DirectoryNotFoundException e)
596-
{
597-
EventMetadata metadata = CreateEventMetadata(e);
598-
tracer.RelatedError(metadata, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: DirectoryNotFoundException caught while trying to copy native lib");
599-
return false;
600-
}
601-
catch (FileNotFoundException e)
602-
{
603-
EventMetadata metadata = CreateEventMetadata(e);
604-
tracer.RelatedError(metadata, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: FileNotFoundException caught while trying to copy native lib");
605-
return false;
606-
}
607-
catch (IOException e)
608-
{
609-
EventMetadata metadata = CreateEventMetadata(e);
610-
tracer.RelatedWarning(metadata, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: IOException caught while trying to copy native lib");
611-
612-
if (fileSystem.FileExists(nonInboxNativeLibInstallPath))
613-
{
614-
tracer.RelatedWarning(
615-
CreateEventMetadata(),
616-
"Could not copy native lib to app directory, but file already exists, continuing with install",
617-
Keywords.Telemetry);
618-
}
619-
else
620-
{
621-
tracer.RelatedError($"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: Failed to copy native lib to app directory");
622-
return false;
623-
}
624-
}
625-
}
626-
else
454+
catch (Win32Exception e)
627455
{
628-
tracer.RelatedError(pathMetadata, $"{nameof(TryCopyNativeLibToNonInboxInstallLocation)}: Native lib does not exist in install directory");
456+
tracer.RelatedWarning(CreateEventMetadata(e), $"{nameof(TryGetWindowsBuildNumber)}: Exception while trying to get Windows build number");
629457
return false;
630458
}
631-
632-
return true;
633-
}
634-
635-
private static void GetNativeLibPaths(string gvfsAppDirectory, out string packagedNativeLibPath, out string nonInboxNativeLibInstallPath)
636-
{
637-
packagedNativeLibPath = Path.Combine(gvfsAppDirectory, "ProjFS", ProjFSNativeLibFileName);
638-
nonInboxNativeLibInstallPath = Path.Combine(gvfsAppDirectory, ProjFSNativeLibFileName);
639459
}
640460

641461
private static bool TryEnableProjFSOptionalFeature(ITracer tracer, PhysicalFileSystem fileSystem, out bool isProjFSFeatureAvailable)

0 commit comments

Comments
 (0)