Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions GVFS/GVFS.Platform.Windows/WindowsFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,10 @@ public bool IsFileSystemSupported(string path, out string error)
/// Also, even if the fix were in place, automount would still fail because it runs under SYSTEM account.
///
/// This method ensures that the directory is owned by the current user (which is verified to work for SYSTEM account for automount).
///
/// Exception: under Windows "Administrator protection" (AP), an elevated process runs as a profile-separated shadow admin
/// account (MACHINE\admin_<user>) whose SID differs from the real (non-elevated) user's. In that case the directory owner is
/// left as the Administrators group rather than being reassigned to the shadow admin (see the AP note inline).
/// </summary>
public void EnsureDirectoryIsOwnedByCurrentUser(string directoryPath)
{
Expand All @@ -339,6 +343,25 @@ public void EnsureDirectoryIsOwnedByCurrentUser(string directoryPath)
SecurityIdentifier administratorsSid = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null);
if (directoryOwner == administratorsSid)
{
// Under Windows "Administrator protection" (AP), an elevated process runs as a profile-separated shadow
// admin account (MACHINE\admin_<user>) whose SID differs from the real (non-elevated) user's. Reassigning
// ownership to the current user here would set the owner to that shadow admin, causing the real user to hit
// "fatal: detected dubious ownership" — git's Administrators-membership grace does not cover another specific
// user's SID. The shadow admin also cannot SetOwner to the real user's SID. Leaving the owner as the
// Administrators group is accepted by modern git and the libgit2 non-elevated-admin-owner overlay for any
// Administrators member (real user, shadow admin, and SYSTEM for automount alike), so skip the reassignment.
//
// Note: a libgit2 consumer that lacks the non-elevated-admin-owner patch (i.e. stock libgit2 without the
// Administrators-membership fix) will not be able to open an Administrators-owned repo. Addressing that would
// require setting git's safe.directory for the real user, but safe.directory is only honored from global/system
// config (never repo-local), and an elevated clone runs as the shadow admin — so covering the real user would
// mean mutating the real user's global gitconfig or machine-wide system config from the shadow admin. After
// consideration, GVFS is not the right place to do that; unpatched third-party libgit2 consumers are out of scope.
if (WindowsPlatform.IsCurrentUserAdminProtectionShadowAccount())
{
return;
}

WindowsIdentity currentUser = WindowsIdentity.GetCurrent();
directorySecurity.SetOwner(currentUser.User);
directoryInfo.SetAccessControl(directorySecurity);
Expand Down
44 changes: 44 additions & 0 deletions GVFS/GVFS.Platform.Windows/WindowsPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public partial class WindowsPlatform : GVFSPlatform
private const string BuildLabRegistryValue = "BuildLab";
private const string BuildLabExRegistryValue = "BuildLabEx";

private const string ProfileListRegistryKey = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList";
private const string ProfileImagePathRegistryValue = "ProfileImagePath";
private const string AdminProtectionProfilePrefix = "ADMIN_";

public WindowsPlatform() : base(underConstruction: new UnderConstructionFlags())
{
}
Expand Down Expand Up @@ -76,6 +80,46 @@ public static object GetValueFromRegistry(RegistryHive registryHive, string key,
return value;
}

/// <summary>
/// Detects whether the current process token belongs to a Windows "Administrator protection" (AP)
/// shadow admin account.
///
/// Under AP, elevating produces a token whose user is a hidden, system-managed local account
/// (MACHINE\admin_&lt;user&gt;, SID S-1-5-21-...) with its own user profile named ADMIN_&lt;user&gt;. That
/// account's SID differs from the real (interactive) user's SID, so treating it as "the current user"
/// for file-ownership purposes is wrong.
///
/// This inspects the running process's own token (its effective elevation identity) rather than the
/// TypeOfAdminApprovalMode policy value, so it is correct even across an AP policy change that has not
/// yet been rebooted (where the policy value is pending but elevation still yields a shadow admin).
/// </summary>
public static bool IsCurrentUserAdminProtectionShadowAccount()
{
using (WindowsIdentity currentUser = WindowsIdentity.GetCurrent())
{
SecurityIdentifier userSid = currentUser.User;

// AP shadow accounts are local machine accounts (S-1-5-21-...); the real interactive user is a
// domain or Entra ID account (e.g. S-1-12-1-...), which is not an "account SID" in this sense.
if (userSid == null || !userSid.IsAccountSid())
{
return false;
}

string profilePath = GetStringFromRegistry(
$"{ProfileListRegistryKey}\\{userSid.Value}",
ProfileImagePathRegistryValue);
if (string.IsNullOrEmpty(profilePath))
{
return false;
}

string profileFolderName = Path.GetFileName(
profilePath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
return profileFolderName.StartsWith(AdminProtectionProfilePrefix, StringComparison.OrdinalIgnoreCase);
}
}

public static bool TrySetDWordInRegistry(RegistryHive registryHive, string key, string valueName, uint value)
{
RegistryKey localKey = RegistryKey.OpenBaseKey(registryHive, RegistryView.Registry64);
Expand Down
Loading