Skip to content

Fix clone ownership under Windows Administrator Protection#2039

Merged
tyrielv merged 1 commit into
microsoft:masterfrom
tyrielv:tyrielv/fix-ap-clone-ownership
Jul 6, 2026
Merged

Fix clone ownership under Windows Administrator Protection#2039
tyrielv merged 1 commit into
microsoft:masterfrom
tyrielv:tyrielv/fix-ap-clone-ownership

Conversation

@tyrielv

@tyrielv tyrielv commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Problem

Under Windows Administrator protection (AP), an elevated process runs as a hidden, profile-separated shadow admin account (MACHINE\admin_<user>) whose SID is a local account (S-1-5-21-…) that differs from the real, non-elevated user's SID.

When gvfs clone runs elevated, the enlistment's src and .git directories are created owned by the Administrators group. EnsureDirectoryIsOwnedByCurrentUser then reassigned ownership to the current user to satisfy git's dubious-ownership check. Under AP the "current user" is the shadow admin, so ownership was set to admin_<user>.

The real (non-elevated) user then hits:

fatal: detected dubious ownership in repository

because git's Administrators-group-membership grace does not cover another specific user's SID, and the shadow admin cannot SetOwner to the real user's SID either.

Fix

Detect the AP shadow admin from the running process's own token (its effective elevation identity) and, when present, leave the directory owner as the Administrators group instead of reassigning it to the shadow admin.

An Administrators-owned directory is accepted by modern git and the libgit2 non-elevated-admin-owner overlay for any Administrators member — the real user, the shadow admin, and SYSTEM (for automount) alike. So skipping the reassignment for a shadow admin is correct for every consumer. When the current user is not a shadow admin, behavior is unchanged (owner reassigned to the current user, as before).

Why token-based, not TypeOfAdminApprovalMode

An earlier revision read the TypeOfAdminApprovalMode policy value. That reflects the pending policy, not the effective state: across an AP enable/disable that hasn't been rebooted, the policy value and the actual elevation behavior disagree, so the guard would misfire (e.g. policy reads 1 but elevation still yields a shadow admin → bug persists). Inspecting the process token is reboot-independent and targets the actual failure condition.

A shadow admin is identified as a local account SID (S-1-5-21-…, IsAccountSid()) whose ProfileList profile directory is ADMIN_-prefixed. The real interactive user (a domain / Entra ID account, e.g. S-1-12-1-…) is excluded.

Why not also set safe.directory

Considered and deliberately rejected. safe.directory would let stock libgit2 consumers (those lacking the non-elevated-admin-owner overlay) open an Administrators-owned repo regardless of owner. But:

  • git ignores safe.directory in repo-local config (security), so it only works from global/user or system config — there is no clean repo-scoped place to write it.
  • --global under an elevated clone lands in the shadow admin's profile (ADMIN_<user>\.gitconfig), which does the real user no good.
  • Covering the real user would require the elevated shadow-admin process to mutate the real user's global gitconfig (invasive) or write machine-wide system config (per-repo pollution affecting all users).
  • The GVFS-supported toolchain already works with Administrators ownership: microsoft/git has the Administrators-membership fix, and GVFS's bundled libgit2 carries the overlay. Only non-GVFS third-party stock-libgit2 tools are unaffected by the fix, and the non-AP elevated-clone path likewise relies purely on ownership (no safe.directory).

Leaving safe.directory untouched keeps the AP and non-AP paths symmetric and avoids cross-user config mutation.

Changes

  • GVFS.Platform.Windows/WindowsPlatform.cs — add IsCurrentUserAdminProtectionShadowAccount() (inspects the current token + ProfileList).
  • GVFS.Platform.Windows/WindowsFileSystem.cs — in EnsureDirectoryIsOwnedByCurrentUser, skip the Administrators→current-user reassignment when the current user is an AP shadow admin.

Testing

Builds clean (GVFS.Platform.Windows, 0 warnings / 0 errors).

Detection logic validated on a real AP machine against the live SIDs:

  • real user S-1-12-1-…IsAccountSid=False → not shadow (reassignment proceeds).
  • shadow admin S-1-5-21-…-1001 → profile ADMIN_<user> → shadow (reassignment skipped).

End-to-end, on a real AP machine in the effective-AP state, using an installed build of this branch:

Check Result
Elevated gvfs clone (runs as shadow admin) Succeeded, mounted
Owner of src BUILTIN\Administrators (not admin_<user>)
Owner of src\.git BUILTIN\Administrators
Non-elevated real user git status / git rev-parse Exit 0 — no dubious ownership error

Pre-fix, those directories would have been owned by the shadow admin, and the real user would have hit fatal: detected dubious ownership.

Related

@tyrielv tyrielv force-pushed the tyrielv/fix-ap-clone-ownership branch from a251e89 to 8164d13 Compare July 3, 2026 03:39
Under Windows "Administrator protection" (AP), an elevated process runs as a
hidden, profile-separated shadow admin account (MACHINE\admin_<user>) whose SID
is a local account (S-1-5-21-...) that differs from the real (non-elevated)
user's SID.

When `gvfs clone` runs elevated, the enlistment's `src` and `.git` directories
are created owned by the Administrators group. EnsureDirectoryIsOwnedByCurrentUser
then reassigned ownership to the current user to satisfy git's dubious-ownership
check. Under AP the "current user" is the shadow admin, so ownership was set to
admin_<user> — which the real user does not match. The real (non-elevated) user
then hit `fatal: detected dubious ownership`, because git's Administrators-group
membership grace does not cover another specific user's SID, and the shadow admin
cannot SetOwner to the real user's SID either.

Detect the AP shadow admin from the running process's own token — its effective
elevation identity — and, when present, leave the directory owner as the
Administrators group instead of reassigning it to the shadow admin. An
Administrators-owned directory is accepted by modern git and the libgit2
non-elevated-admin-owner overlay for any Administrators member — the real user,
the shadow admin, and SYSTEM (for automount) alike.

Detection inspects the token rather than the TypeOfAdminApprovalMode policy value
so it stays correct across an AP policy change that has not yet been rebooted
(where the policy value is pending but elevation still yields a shadow admin). A
shadow admin is identified as a local account SID (S-1-5-21-...) whose ProfileList
profile directory is ADMIN_-prefixed; the real interactive user (a domain or
Entra ID account) is excluded.

Assisted-by: Claude Opus 4.8
Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
@tyrielv tyrielv force-pushed the tyrielv/fix-ap-clone-ownership branch from 8164d13 to 3cbc75f Compare July 6, 2026 20:27
@tyrielv tyrielv marked this pull request as ready for review July 6, 2026 20:31
@tyrielv tyrielv merged commit 4a17c98 into microsoft:master Jul 6, 2026
35 checks passed
tyrielv added a commit that referenced this pull request Jul 6, 2026
The `non-elevated-admin-owner.diff` overlay patch was copied verbatim from
libgit2 PR #7200. That PR version had four bugs that maintainers fixed
before merging to libgit2 `main`:

- `linked_token` HANDLE from TokenLinkedToken was never CloseHandle'd,
  leaking a handle in the long-lived GVFS mount service (SYSTEM).
- `current_user_sid` used `static HANDLE linked_token` — not thread-safe
  and retained a stale handle across calls; replaced with a per-call
  local initialized to NULL by the caller.
- The TokenLinkedToken failure path nulled the local pointer parameter
  (`linked_token = NULL`) instead of `*linked_token = NULL` (+CloseHandle).
- `current_user_sid()` was only invoked in the CURRENT_USER branch, but
  the admin-membership branch also reads `linked_token`; a caller passing
  only USER_IS_ADMINISTRATOR would read an uninitialized handle. It is now
  called unconditionally before either branch.

Regenerate the patch so that, applied to the pinned libgit2 source, it
produces the same corrected `src/util/fs_path.c` as upstream `main`
(commits cc477ee + f9f36a6 + 44c05e5, merge e805a16, merged 2026-06-07).
The patch is not yet in any libgit2 release, so the overlay remains
necessary.

Also bump the pinned libgit2 version 1.9.3 -> 1.9.4 (matching the official
vcpkg port). fs_path.c is byte-identical between 1.9.3 and 1.9.4, so the
patched region is unaffected and the diff applies cleanly to both.

Verified: Build.bat Debug rebuilds libgit2 for both x64-windows-static-aot
and x64-windows-dynamic triplets, applying non-elevated-admin-owner.diff
with no hunk failures, and the full solution builds to an installer.

Complements #2039 (the GVFS-side AP clone-ownership fix).
Work item: AB#62918022.

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.

2 participants