为什么 DeepSeek Harness 里的 PowerShell 命令一条都跑不了?一次 Windows 权限故障的来龙去脉

技术
发布于 10/10/2026, 06:46 PM14 人读过

注意:本文完全由AI生成且未经过完整的人工审查,请谨慎参考其中内容

我在 Windows 上用 DeepSeek Harness(下称 DSH)写代码时,碰到一个很奇怪的事:所有命令全部失败,连 pwd 这种纯查询都失败。不是某条命令报错,而是这个工作区压根没法用。

下面把这件事讲清楚:报错是什么意思、DSH 的沙箱在 Windows 上到底做了什么、为什么偏偏这个目录出问题、以及怎么修。


先说结论

  • 报错长这样:

    SetNamedSecurityInfoW failed (Win32 5): grantWrite(D:\Projects\<项目>)
    
  • 原因:DSH 给工作区目录开沙箱权限时,少了一项叫 WRITE_OWNER 的权限。这个目录只继承了"可修改"权限,而沙箱要的比"可修改"多一项。

  • 为什么连 pwd 都失败:失败发生在启动进程之前。DSH 的规矩是"拿不到隔离就绝不启动",所以不是命令被拒,是命令根本没跑。

  • 怎么修:在上一级目录上给自己一次性加上可继承的 WRITE_OWNER(或者继续用 DSH 自带的诊断脚本,每个新工作区修一次)。


一、出事时的三个现象

这三个现象决定了后面怎么查:

  1. 不是命令的问题,是目录的问题。 把工作区换到用户目录(%USERPROFILE% 下面)里,一切正常;在数据盘上换个新目录,立刻复现。
  2. 失败在进程创建之前。 报错来自"给沙箱发权限"这一步,不是命令本身。
  3. Win32 5 就是"拒绝访问"。 函数名 SetNamedSecurityInfoW 说明它在写某个对象的权限信息。

要串起这三条,得先懂 Windows 怎么管权限。


二、先搞懂 Windows 怎么判"能不能写"

2.1 一个比喻:房间和保安

把每个文件、文件夹想成一间房间,Windows 就是门口的保安。你想进房间干活,得先过保安。后面的术语都挂在这套比喻上。

2.2 SID:身份证号

Windows 内部不认名字,只认号码。那串像乱码的东西就是号码:

号码 是谁
S-1-5-21-…-1001 某个用户账号
S-1-5-18 SYSTEM(系统自己)
S-1-5-32-544 管理员组
S-1-5-11 "所有已登录用户"
S-1-1-0 Everyone(所有人)
S-1-4-… 谁都不是——可以是沙箱按路径算出来的一张"通行证编号"

为什么用号码?因为名字能改、会重、还可能被系统按语言翻译。

2.3 令牌:你的工作证

你登录后系统发你一张工作证:你是谁、属于哪些组、有哪些特权、可信等级多高。你启动的每个程序都带着这张证的复印件,保安只看证,不看你是谁。

DSH 的沙箱做的事就是:复印你的工作证,在上面加一句"只准进另写的几个房间",再把等级降一级。所以同一个程序,在沙箱里跑和在外面跑,拿的不是同一张证。这种证有个学名叫受限令牌,那句"只准进哪些房间"叫 restricting SIDs。

2.4 DACL 和 ACE:访客名单,和名单上的每一行

每个房间门口贴一张访客名单:

  • 整张名单叫 DACL。
  • 名单上每一行叫一个 ACE,一行写四件事:谁的号码、允许还是拒绝、能干什么、要不要让子房间也照办。
号码: S-1-5-32-544   允许   全部   子房间照办

规则是白名单:名单上写了"允许"才能干,没写就是不准;"拒绝"行最优先。名单里存的全是号码,名字只是给你看的。

2.5 继承:名单上的"子房间照此办理"

某一行可以带一句批注:"子房间照此办理"。带了这句,所有子文件夹会自动抄一份到自己的名单里,抄的那份会标上"这是继承来的"。

icacls 输出里的记号:

记号 意思
(OI) 房间里的文件也照办
(CI) 房间里的子文件夹也照办
(IO) 只给子对象,对本房间自己不生效
(I) 这行是继承来的
(NP) 再往下就不传了

反过来,目录也能被设成"到此为止,别往下传"。

2.6 权限位:那一栏"能干什么"

这是最关键的一节。保安不是只问"让不让进",而是一项一项查:

icacls 写法 意思 掩码
RX / R 能看 —
M(Modify) 能看、能改、能删内容 0x1301BF
WDAC 能改门口这张名单 0x40000
WO 能改"房主登记" 0x80000
D 能删这间房间 0x10000
DC 能删房间里里面的东西 0x40
F 以上全部 0x1F01FF

这里就是坑:M(能改内容)不包含 WO。

2.7 房主:有一条免检特权,但只到"改名单"为止

每个房间登记一个房主。房主有一条白送的特权:改门口的访客名单,不需要名单上写他的名字(相当于白送 WDAC)。

但这条特权到"改名单"就没了——不包括"改房主登记"(WO)。

2.8 完整性标签、MIC、SACL:和名单无关的第二道锁

只有名单还不够,Windows 还有第二道锁:

  • 每个文件、文件夹有个机密等级:Low(低)/ Medium(中)/ High(高)。你的程序是 Medium。
  • 规则一句话:等级低的,不能往等级高的地方写。 读随便。
  • 这套机制叫 MIC(强制完整性控制),那个标记叫完整性标签,就是几个特殊号码:S-1-16-4096=Low,S-1-16-8192=Medium,S-1-16-12288=High。

为什么名单不够用? 因为名单是白名单,而"所有已登录用户"这种大组经常被允许一大堆东西。沙箱里那张"复印件工作证"还带着这些大组身份,光看名单拦不住。等级这道锁绕不过去:把子进程降成 Low 之后,凡是没被标成 Low 的地方,它一律写不了,名单怎么写都没用。

而这个标签不在访客名单里,在另一张表上——那张表叫 SACL(平时装的是"谁碰过这个文件"的审计记录)。

2.9 为什么"能读不能写"

受限令牌多了"只准进另写的房间"这一句之后,Windows 对写操作查两遍:

  1. 先用你原来的身份查名单;
  2. 再用那张通行证编号查一遍。

两遍都过才给写。 读只查一遍。所以这类沙箱是"读随便、写受限"。

2.10 术语速查

术语 一句话
SID 身份证号,Windows 只认号码
令牌 / 受限令牌 工作证 / 上面多印了"只准进指定房间"并降级的复印件
DACL / ACE 访客名单 / 名单上的一行
继承 名单上"子房间照此办理"那句话
owner 房主;白送"改名单",但不含"改房主登记"
WRITE_DAC(WDAC) 能改访客名单
WRITE_OWNER(WO) 能改"房主登记",也是贴完整性标签要用的权限
SACL 名单之外的另一张表,装审计记录和完整性标签
MIC / 完整性标签 机密等级,以及"低的不能往高的写"这条规则
Win32 5 就是"拒绝访问"

三、DSH 的沙箱在 Windows 上具体做了什么

3.1 三档模式

模式 能写哪里
read-only(默认) 哪里都不能写
workspace-write 只能写工作区根目录 + 一个会话私有的临时目录;进程启动前,TMP/TEMP 会被改到那个临时目录
danger-full-access 不限制

3.2 Windows 后端:受限令牌 + 给目录发权限

它不用命名空间(那是 Linux 的做法),而是"复印工作证 + 把工作区目录授权给证上的能力身份":

  1. 复印当前进程的工作证,标记成 WRITE_RESTRICTED,在 restricting SIDs 里放两个能力身份:
    • 工作区 SID:由工作区路径算出来,所以同一个工作区在哪个会话里都算出同一个;
    • 临时目录 SID:每个"会话 × 工作区"随机生成,互不通用。
  2. 工作证降级到 Low。
  3. 于是为了让子进程能写工作区,必须在工作区目录上一次写入三样东西:
写什么 为什么
给能力 SID 的允许行 让第二遍检查(restricting SID)放行写
给所有人(world)的 FILE_DELETE_CHILD 拒绝 Windows 允许凭父目录的权限删子项,而这个"凭父目录"的授权不需要通行证副署。不拒掉,子进程就能删掉另一个工作区里的文件。拒掉之后,能删东西的唯一依据就是那张通行证
Low 完整性标签 满足"Low 写 Low"

3.3 关键点:为什么这次合并写入需要 WO

第三样东西——完整性标签——在 SACL 里。而这三样要在同一次权限写入里一起生效,DSH 的文档写明了:这一步需要对该目录的 WRITE_OWNER。

用户目录下的目录天然带"完全控制",所以从来没人注意过;而**"名单只给 Modify"的目录会直接报错,不会静默跳过**。

3.4 它是真沙箱,但有边界

  • 这是内核级拦截,不是路径匹配。 同一个命令、同一个字符串,能不能写取决于目标对象的权限信息,不取决于路径怎么写。所以用目录联接、符号链接绕不过去——检查发生在目标对象上。
  • 只限写,不限读。 读文件、联网、看进程都不受限。
  • 硬链接是同一个文件的别名,授权会落到同一个文件上,所以同一份内容可能从外部别名写进去。
  • Low 标签是常驻的。 会话结束后它还在,所以其他跑在 Low 等级的程序也能写这个工作区——这是写边界的代价。
  • NUL 设备两种模式下都能写;CIM/WMI 那类命令在受限模式下用不了。

四、为什么偏偏这个目录出问题

4.1 数据盘的默认权限

一台普通的 Windows 机器上,新格式化的数据盘根目录默认大概是这样:

D:\
  房主 = SYSTEM
  管理员组                 : 完全控制(只对本对象)
  SYSTEM                  : 完全控制(只对本对象)
  所有已登录用户            : Modify + "子房间照办"
  普通用户组                : 读+执行

注意:只有 Modify,没有完全控制,也没有任何一行是写给你本人的。数据盘里的项目目录会把这一整套继承下来:

D:\Projects\<项目>
  房主 = 你本人
  名单上写给你本人的行:0 条(全是继承来的)

4.2 把探测结果和机制对上

用"直接问内核"的方式探测这个目录(方法见第五节),结果是:

房主                = 你本人
名单上写给你本人的行  = 0 条
WRITE_DAC   = True     ← 你是房主,白送
WRITE_OWNER = False    ← 就是它缺了

于是整件事对上了:

沙箱要往工作区目录写"允许行 + 拒绝项 + Low 标签"
        ↓  这次合并写入需要 WRITE_OWNER
这个目录只有继承来的 Modify,没有针对本人的显式行
        ↓
SetNamedSecurityInfoW 返回 Win32 5(拒绝访问)
        ↓
DSH 直接放弃:进程根本没被创建
        ↓
所有命令,包括 pwd,全部失败

4.3 三个不是答案的直觉

直觉 为什么不对
"C 盘行、D 盘不行" 盘符不是判据。 系统盘根目录、C:\Users 本身同样缺 WO。真正的原因是用户目录上有一条显式的、可继承的完全控制,所以它下面的新目录天然满足
"目录是我的,凭什么不能改" 房主身份不是判据。 房主只白送"改名单",不送"改房主登记"。反例更清楚:C:\Users 的房主是管理员组(不是你),可它因为有一条显式允许行而满足条件
"我明明能在这个目录里随便写文件" "我能写文件"和"沙箱能接管这个目录"是两件事。 前者只要 Modify,后者还要能改权限信息(含 WO)

唯一管用的判据:这条继承链上,有没有一条对当前账号生效的、带 WO 的行。


五、怎么自己查一个目录行不行

5.1 别靠读名单去猜

人肉读 ACE 再推断"我能不能改权限"很容易翻车,因为要考虑继承、阻断、拒绝行优先、组成员身份、特权、完整性等级、房主隐含位。

正确做法:直接问内核。 请求恰好那一位权限,让系统回答。DSH 的诊断脚本内部就是这么干的,本质等于:

// 请求 WRITE_DAC(0x40000) 或 WRITE_OWNER(0x80000),打开已存在的对象
CreateFileW(path, access, share = 7, NULL, OPEN_EXISTING /*3*/,
            FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT /*0x02200000*/,
            NULL);
// 返回 5(拒绝访问)→ 这一位没有
// 返回别的错误 → 是探测本身失败,不能当成"没有"

FILE_FLAG_BACKUP_SEMANTICS 让它能打开目录,FILE_FLAG_OPEN_REPARSE_POINT 让它不跟随快捷方式。

5.2 一段能直接用的自检

function Test-WorkspaceProvisionable {
  param([Parameter(Mandatory)][string]$Path)

  $acl = Get-Acl -LiteralPath $Path
  $me  = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name

  # 只找"在当前令牌下真正生效"的主体:本人、所有已登录用户、Everyone、Users
  $applicable = @($me, 'NT AUTHORITY\Authenticated Users', 'Everyone', 'BUILTIN\Users')

  $WRITE_OWNER = 0x80000
  $ok = $false
  foreach ($ace in $acl.Access) {
    if ($ace.AccessControlType -ne 'Allow') { continue }
    if ($applicable -notcontains $ace.IdentityReference.Value) { continue }
    if (([int]$ace.FileSystemRights -band $WRITE_OWNER) -ne 0) { $ok = $true }
  }

  "{0}  {1}" -f $(if ($ok) { 'OK        ' } else { 'NEEDS-FIX ' }), $Path
}

Test-WorkspaceProvisionable 'D:\Projects\<项目>'

5.3 别把管理员组算进来

这是最容易写错的地方:不能写成"名单里有完全控制就行"。

如果当前程序是普通启动的(没提权),那么 Administrators 和 SYSTEM 的完全控制并不生效——它们不是你。把这两个算进去,会对一个真有问题的目录报出假的 OK。

顺带一句:"我是管理员所以没问题"这个想法也不可靠。 身份在名单里,不等于它在当前令牌里生效。


六、DSH 自带的诊断脚本做了什么

DSH 的沙箱包里带了一个诊断脚本,体检和动手术一次做完,没有模式开关。它做得挺讲究,值得说一下。

6.1 它必须在"没有沙箱"的环境里跑

它要读写权限信息,而受限子进程恰好没有这个能力。所以:

  • 必须在解除沙箱的会话里跑,或者由你在普通终端里跑;
  • 但解除沙箱不等于提权。 它能成功,靠的是普通账号本来就有的"改名单"权限(你是房主),不是管理员身份。

这点很重要:修这个问题一般不需要管理员权限。

6.2 六个阶段

① 准备。 建输出目录,开一份 JSONL 记录(原因见 6.5),检查参数边界。

② 现场编译几个底层调用。 因为 .NET 的现成接口给不了它要的三件事:

  1. 上面那种有效访问探测;

  2. 只写 DACL 的权限写入——注意给"房主、组、SACL"三个参数全部传 NULL:

    SetNamedSecurityInfoW(path, SE_FILE_OBJECT,
        DACL_SECURITY_INFORMATION | PROTECTED_OR_UNPROTECTED,
        NULL, NULL, dacl, NULL);
    

    也就是说:它从不碰房主、从不碰 SACL、从不删任何一行;

  3. 读当前令牌的完整性等级,并且专门注明:不要为了查等级去启动 whoami——在低于 Medium 的令牌里那个程序会初始化失败,还会在你桌面上弹一个错误框。

③ 逐级观察。 对请求的目录和它的每一级上级各读一份"体检报告":名单里的所有行(带继承标记)、房主是谁、用探测拿到的 WRITE_DAC/WRITE_OWNER、完整性标签、以及有没有"别的容器工具留下的通行证行"(S-1-15-2-*,其中两个"全体包"组要排除)。

两个细节值得记:

  • 读名单时用号码,免得被系统语言翻译名字坑到;
  • 读 Low 标签时先按号码匹配(S-1-16-4096),英文名只当兜底,因为 icacls 显示标签名会被本地化。认不出的本地化名字要当"不知道",不能当"没有标签"。

④ 判定。 前提条件只看你指定的那个目录(上级的权限不算,否则每个健康目录都会被误判)。然后分类:

判定 什么时候
INCOMPLETE 有观察失败(缺证据 ≠ 证据表明安全)
CULPRIT / BOTH 发现了容器工具的残留行;同时还缺前提就是 BOTH
UNREADABLE 名单读不出来
PRECONDITION 缺 WRITE_DAC 或 WRITE_OWNER ← 就是这次
NOT_THIS_CLASS 都不是(并声明"没排除其它原因")

另外:如果目标缺权限、或者目标就是被授权的根,它会在同一次运行里扫一遍子树(最多 2000 个目录,跳过快捷方式类和受管理目录,读不了的只计数不猜),把容器工具残留行的来源全找出来。目的是"一次审批就把深层问题查完,别让你批第二次"。

⑤ 动手前的闸门。 这是它最讲究的部分:

  • 要改的目录必须在允许范围内;
  • 不能是盘根、用户目录根、系统目录、Packages、WindowsApps 这类危险位置;
  • 路径上任何一级都不能是快捷方式/联接(那会把授权搬到别处);
  • 任何要改的目录只要缺"改名单"权限,就在动手之前整体放弃——否则会改一半,回滚也做不成;
  • 只要有一个目标越界或观察不完整,整条路径直接放弃,绝不改一半。

⑥ 改 → 复验 → 能撤销。

  • 先备份:只抄"这个对象自己拥有的行"(继承来的行跟着上级走,抄回去反而会把它钉死),同时保留"是否阻断继承"的标志;写成 JSON,并且把脚本自身复制一份到备份目录——因为技能的临时目录会随注册失效被删掉,回滚脚本必须能独立存在;
  • 改:给当前账号追加一条完全控制允许行,然后只写 DACL;
  • 复验:改完重新探测一次 WRITE_DAC/WRITE_OWNER,两位都为真才算成功(不是"接口没报错就算成功");
  • 回滚:任何异常或拒绝导致非零退出时,按相反顺序把改过的目录名单恢复回去,恢复本身也验证。

6.3 为什么"给自己加一条"就合法,而且不用提权

你是房主
  → 白送"改名单"权限
  → 你有权改这张名单
  → 在名单上给自己写一条"完全控制"
  → 完全控制里含 WO
  → 缺的那一项补上了

用本来就有的"改名单"权限,补上缺的"改房主登记"。这就是整个修复的本质。也正因如此,脚本把"缺改名单权限"当成硬性放弃条件——没有它就没法自举。

6.4 一个容易忽略的细节:它补的那行不传播

脚本用的是三参数的构造方式:

$acl.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new(
    $sid, [System.Security.AccessControl.FileSystemRights]::FullControl, 'Allow'))

三参数版本不带"子房间照办"标记,所以这行只作用于那一个目录本身,不会让同级目录或以后新建的目录免疫。

你在 icacls 里能直接看出来:写进去的那行是 账号:(F),没有 (OI)(CI);在它下面新建的子目录不会继承到它。

这就解释了那个常见困惑:"我修好过一个目录,为什么换个新目录又不行?"——因为它治的是那一棵,不是一整片。

6.5 为什么输出是 JSONL 加一行 RECAP

命令行工具的屏幕输出只保留末尾。所以脚本把每条记录(观察、判定、动作、验证、恢复)同时写进 acl-report-*.jsonl,最后再补一行 RECAP 和 SUMMARY FIXED=… GRANTED=… REFUSED=… RESTORED=…。

注意 FIXED 和 GRANTED 是两回事:前者数"删掉了几个容器工具残留行",后者数"给几个目录补了前提权限"。这次是 GRANTED=1 FIXED=0——它唯一的改动就是补前提。


七、怎么修

7.1 方案 A:在上一级目录做一次可继承授权(推荐)

思路很简单:既然每次新建工作区都缺同一项,那就在上一级目录上一次性把权限补齐,并加上"子房间照办"。

但有个前提,我实测踩过:这条命令不能在 DSH 的受限会话里跑。 受限会话里连新建目录都改不了权限:

icacls <临时目录> /grant '…'
  → <路径>: Access is denied.

原因和前面一样:沙箱发给你的通行证上写的是"写内容 + 删除",没有"改名单"这一项。

所以两种跑法:自己开一个普通 PowerShell 窗口跑,或者让 DSH 用一次"解除沙箱"的审批来跑(仍然不需要提权)。

# 自动拼出当前账号,免得手写错
$me = "$env:USERDOMAIN\$env:USERNAME"

# ① 先看现状(不改任何东西)
icacls 'D:\Projects'

# ② 最小剂量:改房主登记 + 改名单,并让子目录照办
icacls 'D:\Projects' /grant "${me}:(OI)(CI)(WO,WDAC)"

# ③ 复核
icacls 'D:\Projects'

撤销(随时能退):

icacls 'D:\Projects' /remove:g $me

7.2 语法坑(实测过)

写法 结果
账号:(OI)(CI)(WO,WDAC) ✅ 可以
账号:(WO,WDAC) ✅ 可以(不带继承)
账号:(OI)(CI)F ✅ 可以(完全控制,含 WO)
账号:(OI)(CI)WO ❌ Invalid parameter

记住:单独写某个特殊权限位必须加括号——(OI)(CI)(WO),不能写成 (OI)(CI)WO。想省事就用 (OI)(CI)F。

另外,不管你写名字还是号码,最后存进名单的都是号码。

7.3 副作用

副作用 有多严重
会立刻把现有子目录和文件逐个改写一遍权限 这就是"子房间照办"的代价。小目录树几秒钟;几十万个文件的目录树会明显变慢。文件内容和修改时间不受影响
对设了"到此为止、别往下传"的目录无效 那里还得单独处理
房主不是你本人的对象可能要再加 WDAC 白送的"改名单"只对房主生效。所以建议 WO,WDAC 一起给,别只给 WO
不会把整个盘变成"低等级保护区" Low 标签是 DSH 贴在真正当工作区用的那一棵上的,只往下传,不会往上传
不会让沙箱里的程序多写一个字节 子进程能不能写,取决于它那张通行证和 Low 标签。你加的这行只是让 DSH 以后能"发证",边界没变
备份/同步类工具会看到一次权限变化 一次性噪音,不是问题
撤销范围要注意 撤掉上一级那行后,子目录上的继承副本会消失;但诊断脚本在某个具体目录上加的那条独立、不带继承的行不会跟着消失,得单独撤
这是机器级设置,不进 git 换机器或重装系统后要重做

7.4 写名字还是写号码?

前提是能解析到同一个号码。 实测:

'<账号>'            → S-1-5-21-…-1001   ✅ 和目标一致
'<机器名>\<账号>'    → S-1-5-21-…-1001   ✅ 一致
'*S-1-5-21-…-1001'   → 走通了(只是卡在沙箱的写入授权上)
'*S-1-5-21-0-0-0-9999'(伪造的) → No mapping between account names and security IDs was done

最后一行很说明问题:写号码也不是随便写个数就行,它同样要被校验成真实账号。所以两种写法都要"解析",只是一个从名字找号码、一个拿号码核对。

差别只在三处:

名字 号码
歧义 光写账号名时,优先匹配本机账号;如果机器加入了域且域里有同名账号,可能指错 没有歧义
重装系统后 名字多半还在,命令还能用 号码会变 → 报 1332(安全地失败,不会误授权)
改账号名 得改命令 不受影响

建议:要复制进文档或脚本,用带机器名/域名的完整名字;要绝对精确、不在意以后能不能复用,就用 *号码。

7.5 另外三种选择

方案 适合什么情况 代价
专门开一个父目录 只给"专门交给 DSH 用"的目录授权(比如 D:\Projects\_workspaces),工作区都放里面 影响范围最小,但你得记得把项目放进去
工作区放用户目录 直接放 %USERPROFILE% 下面 零改动(那里天然有可继承的完全控制);缺点是项目不在你习惯的位置
每次跑诊断脚本 偶尔才新建工作区 只动那一棵,自带备份和回滚命令;但每次多一步

八、顺带一个坑:工具缓存默认写在用户目录

排查时还会撞上同一套边界的另一面:语言和包管理器的缓存默认都在用户目录里。

8.1 现象

# 缓存是热的时候,只是噪音:
go: writing stat cache: open <模块缓存路径>\...tmp: Access is denied.

# 缓存是冷的时候,直接失败:
failed to initialize build cache at <构建缓存路径>: mkdir <构建缓存路径>: Access is denied.
(退出码 1,缓存目录都没建起来)

这解释了一个很容易被骗的假象:某次构建"成功"了,就以为受限会话没问题——其实那只是因为缓存命中(命中只需要读,不需要写)。只要你改一行代码、需要写新缓存,就会失败。

8.2 为什么

可写的地方只有两个:

  • 工作区根目录;
  • 会话私有的临时目录(进程启动前 TMP/TEMP 已经改到那里;注意它是会话级、会轮换、释放时删除的)。

而 Go 默认把缓存放在 %USERPROFILE%\go\pkg\mod 和 %LOCALAPPDATA%\go-build,都在边界之外。

这不是 Windows 独有:Linux/macOS 后端同样只允许"工作区根 + /tmp"(不同后端细节不同),所以 $HOME/go/pkg/mod、$HOME/.cache/go-build 一样写不进去。

8.3 怎么办

推荐:把缓存指到工作区里面。

$env:GOCACHE    = "$PWD\.cache\go\build"
$env:GOMODCACHE = "$PWD\.cache\go\mod"
go build ./...

再配一条 gitignore(.cache/)。实测:冷构建缓存放工作区外 → 失败退出;放工作区内 → 成功,并写进了上千个缓存文件。好处是跨会话保留、不用审批、不碰任何权限设置。

次选:指到 TEMP。 零配置(那里天然可写),但不持久——会话一轮换或 DSH 一重启就没了,大项目不划算。

再次:按命令解除沙箱。 适合偶尔一次的大动作(首次拉全量依赖、装工具链),但那一条命令没有隔离,而且要人工批准。

8.4 两个"此路不通"

  • 没有"额外可写目录"这个配置项。 策略层写明了:每个会话只有一个主要工作区根。
  • 别想用符号链接、目录联接绕。 检查发生在目标对象上,不在路径上,联接只是换个名字指过去。

九、常见误解六个

1. 目录是我的,为什么不能改它的权限? 不是不能改,是改的项不对。房主白送"改名单",你缺的是"改房主登记",而沙箱的合并写入需要它。

2. 为什么 C 盘行、D 盘不行? 不是盘符。系统盘根目录、C:\Users 本身也缺 WO;真正的差别是用户目录上有一条显式的、可继承的完全控制,所以它下面的新目录天然满足。

3. 加了权限,会不会让沙箱里的程序写到更多地方? 不会。子进程能写什么,取决于它那张通行证和 Low 标签。边界没变。

4. 会不会把整个盘变成低等级保护区? 不会。Low 标签只贴在真正当工作区用的那一棵上,只往下传。

5. 修好过一个目录,为什么换新目录又不行? 诊断脚本补的那行不带继承标记,只作用于那一个目录。要么在上一级做可继承授权,要么每个新工作区再修一次。

6. 删除重建、改名、跨盘移动之后还要重修吗?

操作 要重修吗
同一个盘内改名或移动 不用(权限跟着对象走)
删掉再建同名目录 要(显式行随对象没了)
重新克隆/复制到新目录 要(新目录继承的是上级那套)
跨盘移动或复制 看目标盘,可能要
在已修好的工作区里新建子目录 不用(沙箱的常驻授权会往整棵树传播)

十、收尾

三句话记住这件事:

  1. 权限不是"能不能用这个目录",而是"谁、以什么身份、能改哪些项"。 Modify、WRITE_DAC、WRITE_OWNER 是三样不同的东西,但报错都是同一句"拒绝访问"。
  2. 房主身份不是万能钥匙。 它只送你"改名单",不送"改房主登记"——而现代沙箱恰恰需要后者来写完整性标签。
  3. 别靠读名单猜,直接问内核。 一次真实的访问探测,胜过一小时的人肉推理。

下次再遇到类似问题,按这个顺序走:

  • 先分清:是"命令失败",还是"命令根本没启动"?(grantWrite / SetNamedSecurityInfoW 属于后者)
  • 换个位置试(用户目录 vs 数据盘),看能不能复现
  • 用自检脚本看 WRITE_DAC / WRITE_OWNER 这两项
  • 缺 WO → 跑一次诊断脚本(在解除沙箱的会话里,但不需要提权)
  • 以后还会新建工作区 → 在上一级目录做一次可继承授权((OI)(CI)(WO,WDAC)),并留好撤销命令
  • 顺手把语言/包管理器的缓存指到工作区里面,避免"热缓存能过、冷缓存必挂"的假象