问题描述

  • 在 Fork 中提交时报错:
✖ Failed to back up original state!
✖ lint-staged failed due to a git error.
  • 在 Fork 中对工作区执行 stash 也报错:
Cannot save the current status
  • 同一仓库在普通终端/opencode 终端执行 git stash、npx lint-staged 均正常。
  • opencode 终端修改过的文件,用户无法使用普通权限修改。
  • 仓库是从 D:\Workspace\**** 拷贝到 E:\Workspace\**** 的;D 盘源仓库在 Fork 软件中执行命令正常。

环境

  • Windows 10,NTFS
  • Git for Windows 2.56.0.windows.4
  • Fork 2.23.2(内置 Git 2.50.1)
  • 当前仓库:E:\Workspace\****
  • 源仓库:D:\Workspace\****
  • opencode 终端为 High 完整性级别(管理员权限)
  • Fork 进程为 Medium 完整性级别(普通权限)

可能的原因

推测是 E 盘的 NTFS 权限继承配置与 D 盘不同,导致管理员权限进程创建或替换文件后,普通权限进程无法继续修改这些文件。

Windows 中,文件创建时的 ACL 受到父目录可继承 ACE、创建进程的安全令牌以及创建程序指定的安全描述符影响。

如果管理员权限进程创建了一个新文件,而该文件没有继承当前用户的写入权限,那么普通权限运行的软件可能无法修改该文件。

本次问题涉及的关键机制:

  1. NTFS 权限继承(OI、CI、IO)。
  2. CREATOR OWNER 的替换机制。
  3. Windows Access Token 中的默认 Owner(TOKEN_OWNER)。
  4. UAC 提升令牌与普通权限令牌之间的差异。
  5. Git 使用 lock 文件更新 ref 的机制。

解决方法

方法一:项目级权限修复

修改项目文件夹权限,给当前用户添加可继承的 Modify 权限。

cd "projectPath"$id = "$env:USERDOMAIN\$env:USERNAME"icacls ".\" /grant "${id}:(OI)(CI)(M)" /T /C

参数说明:

  • (OI):文件继承。
  • (CI):子目录继承。
  • (M):Modify,修改权限。
  • /T:递归处理当前目录及其子目录中的现有对象。
  • /C:即使部分对象处理失败,也继续处理其他对象。

该方法适合修复指定项目目录内现有文件和文件夹的权限。

如果只希望为项目目录设置可继承权限,而不显式递归处理整个目录树,可以不使用 /T:

icacls ".\" /grant "${id}:(OI)(CI)(M)"

?> [!NOTE] 注意:不使用 /T 代表 icacls 不会显式遍历并逐个执行权限修改,但 Windows 的自动继承机制仍可能将新增的可继承 ACE 传播到已有子对象。不能保证现存文件完全不受影响。

验证:

icacls .git\refs\stash
icacls .git\logs\refs\stash

应能看到当前用户具有 (M) 或 (F) 权限。

方法二:磁盘级权限配置

修改当前磁盘根目录的继承权限。

!> [!IMPORTANT] 注意:此方法会扩大整个磁盘的修改权限范围,执行前应了解 Windows NTFS 权限机制,并确认其他用户是否应该拥有该权限。

icacls "盘符:\" /grant "Authenticated Users:(OI)(CI)(M)"

例如:

icacls "E:\" /grant "Authenticated Users:(OI)(CI)(M)"

效果:

  • 在磁盘根目录增加 Authenticated Users 的 Modify 权限。
  • (OI) 允许文件继承该权限。
  • (CI) 允许子目录继承该权限。
  • 管理员权限和普通权限进程创建的正常继承文件,都可以通过 Authenticated Users 获得修改权限。

这里不使用 /T,因此没有显式递归遍历磁盘中的所有对象。

但 NTFS 支持自动传播可继承 ACE,因此现存且允许继承的子文件和子目录也可能获得这条继承权限。已经禁用继承的对象不会按普通继承规则获得该权限。

Authenticated Users 不等于当前登录用户。

它包含经过身份验证的用户和计算机身份,包括其他本地账户以及受信任域中的身份主体。因此,如果只希望当前账户获得权限,方法一的授权范围更小。

根因定位

1. 对比 D 盘源仓库和 E 盘副本的 ACL

在拷贝项目后,用管理员权限的 opencode 终端进行文件修改。

使用以下命令检查仓库中的 Git 文件:

icacls .git\refs\stash

发现部分 E 盘文件的 ACL 与 D 盘源仓库不同。

例如,E 盘文件在某些修改前包含:

USERDOMAIN\USERNAME:(I)(F)

而在管理员权限进程操作后,权限变成:

BUILTIN\Administrators:(F)
NT AUTHORITY\SYSTEM:(F)
BUILTIN\Users:(RX)

当前用户的完全控制权限不再存在,普通权限进程因此无法正常修改文件。

2. NTFS 权限继承机制

需要区分以下标记:

标记 含义
I Inherited,当前 ACE 从父对象继承而来
OI Object Inherit,向文件继承
CI Container Inherit,向子目录继承
IO Inherit Only,仅用于继承,不作用于当前对象
NP No Propagate Inherit,不继续向更深层子目录传播

例如:

USERDOMAIN\USERNAME:(I)(F)

如果这条 ACE 出现在一个目录上:

  • (I) 表示它是从父目录继承而来的。
  • 没有 (OI),不会继续传递给文件。
  • 没有 (CI),不会继续传递给子目录。

因此,这条 ACE 只对当前目录生效,不能保证其后代对象也拥有当前用户的完全控制权限。

3. CREATOR OWNER 与 TOKEN_OWNER

原 E 盘目录权限还包含:

CREATOR OWNER:(I)(OI)(CI)(IO)(F)

CREATOR OWNER 是一个特殊 SID(S-1-3-0),它是一个占位符,不是实际用户。

当创建子对象时,CREATOR OWNER 会根据新对象的 Owner 被替换为相应的 SID。

而新对象的默认 Owner 通常来自创建进程 Access Token 中的 TOKEN_OWNER。

例如,假设管理员 PowerShell 的默认 Owner 是:

BUILTIN\Administrators

那么 CREATOR OWNER 继承项可能被映射为:

BUILTIN\Administrators:(I)(F)

而不是:

USERDOMAIN\USERNAME:(I)(F)

这样就可能导致管理员进程创建的文件没有当前用户的独立完全控制权限。

需要注意:管理员进程的默认 Owner 不一定都是 Administrators,应通过实际 Token 和创建后的文件 Owner 验证。

可以使用以下命令检查当前进程的默认 Owner:

$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()$identity.Owner.Translate([System.Security.Principal.NTAccount]
).Value

检查文件实际 Owner:

(Get-Acl -LiteralPath ".git\refs\stash").Owner

分别在普通 PowerShell 和管理员 PowerShell 中执行,进行比较。

4. UAC、完整性级别与 Administrators SID

本次环境中:

进程 完整性级别
opencode 终端 High
Fork Medium

两种进程虽然使用同一个 **** 账户,但使用的访问令牌并不完全相同。

UAC 的正常机制是:

  • 普通权限进程通常使用经过筛选的访问令牌。
  • 管理员权限进程使用提升后的访问令牌。
  • 在普通权限令牌中,Administrators 组 SID 通常处于 Deny only 状态。
  • 在提升后的令牌中,Administrators 组 SID 通常处于启用状态。

因此,假设一个文件只有以下权限:

BUILTIN\Administrators:(F)
BUILTIN\Users:(RX)

那么:

  • 管理员 PowerShell 可以通过 Administrators 的完全控制权限修改文件。
  • 普通权限 Fork 无法利用 Administrators 的允许 ACE。
  • BUILTIN\Users 只有读取、执行权限。
  • 如果没有其他可用的写入权限,Fork 就无法修改文件。

注意:完整性级别与 Administrators SID 是否启用是两个不同的概念。实际排查时应同时检查。

可以使用:

whoami /groups
whoami /integritylevel

检查当前进程的组 SID 状态和完整性级别。

5. Git ref 更新与 lock 文件

Git 更新 ref 时,并不一定直接修改原文件。

例如,Git 使用 lock 文件机制更新 ref:

  1. 创建 ref 对应的 lock 文件。
  2. 将新的 ref 内容写入 lock 文件。
  3. 完成更新后,用新文件替换原 ref 文件。

例如:

.git/refs/stash
.git/refs/stash.lock

Git 更新 ref 时需要具备创建 lock 文件、写入内容以及完成替换操作的权限。

新建 lock 文件时,会根据其父目录的可继承 ACE 生成 ACL。

因此,即使原来的 refs/stash 有当前用户的完全控制权限,新创建的 lock 文件也不一定拥有相同的 ACL。

在更新完成后,原文件被新文件替换,最终的 ACL 也可能随之改变。

此外,Git 更新 stash 时还需要处理 reflog:

.git/logs/refs/stash

因此,如果这些文件的权限不正确,就可能导致 stash 或 ref 更新失败。

6. 为什么 lint-staged 和 Fork 会失败?

Fork 以普通权限运行,而 opencode 终端以 High 完整性级别运行。

当 Git 需要更新:

.git/refs/stash
.git/logs/refs/stash
.git/ORIG_HEAD

等文件时,如果当前进程没有相应的写入、创建或删除权限,就可能失败。

其中:

  • Fork 执行 stash 时,无法正常完成 ref 或相关文件的更新。
  • lint-staged 在备份工作区状态时使用 Git 的 stash 相关机制,因此可能出现 Failed to back up original state!。
  • --quiet 会减少正常输出,但不能仅凭它断言底层权限错误被吞掉;目前看到的是 lint-staged 返回的通用备份失败提示。
  • 管理员权限 opencode 终端可以利用 Administrators 的完全控制权限,因此相关命令行测试可能正常。

解决过程

  1. 复现并确认:.git 中没有残留 *.lock,命令行 git stash / npx lint-staged 正常。

  2. 捕获 Fork 的 hook 环境,确认 Fork 使用内置 Git 2.50.1、GIT_INDEX_FILE=.git/index 等环境信息。

  3. 对比 D 盘源仓库和 E 盘副本的 ACL,发现 refs/stash、logs/refs/stash、ORIG_HEAD 权限不同。

  4. 检查进程 Token:opencode 终端为 High,Fork 为 Medium,确认 Fork 使用普通权限令牌。

  5. 执行 icacls 修复 .git 权限。

  6. 使用 runas /trustlevel:0x20000 进行受限权限测试:

    • git stash store 成功。
    • npx lint-staged 成功,显示 Done backing up original state。

    注意:runas /trustlevel 使用 SAFER 机制,不等同于完整模拟 UAC 筛选令牌。该测试用于提供受限权限下的对照结果,不能单独证明与 Fork 的 Token 完全相同。

  7. 对其他 D 盘拷贝过来的文件夹,以及 E 盘新建的文件夹进行测试:普通权限创建文件后,再使用管理员 PowerShell 创建文件,并比较 ACL。

    发现该现象不依赖于从 D 盘拷贝项目,在 E 盘新建的文件夹中也能复现,问题主要与 E 盘的权限继承配置有关。

  8. 对测试文件夹添加:

USERDOMAIN\USERNAME:(OI)(CI)(M)

发现管理员权限进程在这些文件夹内创建或修改文件后,普通权限用户也能够正常修改。

  1. 对 E 盘根目录添加:
Authenticated Users:(OI)(CI)(M)

在正常继承该权限的对象上,管理员和普通权限进程均可通过 Authenticated Users 获得修改权限,解决了本次 Git 文件权限冲突。

经验

  • 从其他磁盘拷贝 Git 仓库后,如果 GUI 工具出现 stash、ref 相关错误,优先检查 .git/refs、.git/logs/refs 及其父目录的 ACL。
  • 不要认为管理员权限创建的文件,普通权限的同一用户就一定拥有修改权限。
  • 避免使用管理员权限运行 Git 或其他会修改项目文件的工具,尤其是在日常使用的 GUI 工具以普通权限运行时。
  • 如果必须同时使用管理员和普通权限工具,应提前为工作目录设计合理的可继承权限。
  • CREATOR OWNER 不等于固定的当前登录用户,其继承结果受到新对象 Owner 的影响。
  • 检查权限时,不仅要检查文件本身,还要检查直接父目录的可继承 ACE。
  • (I) 表示继承而来,不代表该 ACE 可以继续向下传递;是否继续传递要看 (OI)、(CI)。
  • icacls /T 表示显式递归处理现有目录树;没有 /T 不代表 Windows 不会自动传播继承权限。
  • Authenticated Users 可以避免依赖创建者 Owner SID,但授权范围比当前用户更广,应根据磁盘的实际使用场景选择。

参考资料

  1. Microsoft Learn - icacls https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/icacls

  2. Microsoft Learn - ACE Inheritance Rules https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-inheritance-rules

  3. Microsoft Learn - Automatic Propagation of Inheritable ACEs https://learn.microsoft.com/en-us/windows/win32/secauthz/automatic-propagation-of-inheritable-aces

  4. Microsoft Learn - Owner of a New Object https://learn.microsoft.com/en-us/windows/win32/secauthz/owner-of-a-new-object

  5. Microsoft Learn - Access Tokens https://learn.microsoft.com/en-us/windows/win32/secauthz/access-tokens

  6. Git Documentation - git-update-ref https://git-scm.com/docs/git-update-ref.html