Repository navigation
sandbox: the Windows port is the blueprint before its delete-escape fix, so a confined child can still delete outside every granted root #1556
Description
What this is. A source-level reading of a revision, not a measurement on Windows — this host runs macOS. What it names is an absence in EMRG's copy, and an absence is decidable without a Windows machine; the confirmation is cheap and the windows-2025 CI leg can make it (see "How to pin it").
The gap
emrg/sandbox/win32/ names, in its own docstrings, which revision of the blueprint it ports:
abi.py:3— "Ported frompackages/sandbox/sandbox-windows-acl/src/win32-abi.tsin deepseek-harness (blueprint commitddefc45fbc, dsh 0.1.6-alpha.2)."acl.py:3-4— "Ported from the blueprint'sacl.ts(sandbox-windows-acl, dsh 0.1.6-alpha.2): the sameSetEntriesInAclW+SetNamedSecurityInfoW…"
ddefc45fbc is tag dsh-v0.1.6-alpha.2, dated 2026-09-17 21:19 +0800. Two days later the blueprint found that revision's write confinement bypassable and fixed it — the fix/windows-acl-delete-escape family, merged as PR #4582 (eb7d3af91c):
| commit | date | what it adds |
|---|---|---|
d5ad3baeb5 |
2026-09-19 01:38 | confine Windows deletes with a Low integrity label |
36e632751e |
2026-09-19 02:06 | deny the ambient parent-delete right inside granted roots |
3d5ba3b83f |
2026-09-19 13:42 | scope the deny to containers (CONTAINER_INHERIT_ACE), clearing the review round |
The escape, quoted from d5ad3baeb5's own message:
The write-restricted token's capability-SID intersection only covers the write bits of the object's own access check. Windows also authorizes a write or a delete from the parent directory's
FILE_DELETE_CHILDright, and no restricting SID has to co-sign that authority: the confined child could therefore delete any file its ambient user SIDs control.cmd /c deloutside the workspace succeeded under BOTH modes, and .NETFile.Delete/DeleteFileWdid too wherever the ambient DACL held Modify.
and from 36e632751e:
every granted root is labeled Low, so the integrity check alone still let one confined child delete inside ANOTHER granted root (another workspace, a live sibling temp directory).
36e632751e's message states the mechanism both times: a delete authorised by the parent directory's right, which the write-restricted intersection never reaches.
Why EMRG's copy has it
The port kept the allow half of the mechanism and none of the deny half. Measured on this tree:
grep -rniE "integrity|mandatory|DENY_ACCESS|SET_ACCESS|LABEL_SECURITY|ACCESS_DENIED|no-write-up" emrg/sandbox/
-> 0 matches
The only ACE vocabulary in emrg/sandbox/win32/abi.py is GRANT_ACCESS = 1 (:61), REVOKE_ACCESS = 4 (:63) and ACCESS_ALLOWED_ACE_TYPE = 0 (:91); acl.py builds allow ACEs and merges them through SetEntriesInAclW + SetNamedSecurityInfoW; nothing lowers the token's integrity level or writes a mandatory label. Each of the blueprint's three commits adds exactly one of those mechanisms, so the capability-SID intersection is the only confinement here — and the paragraph quoted above says what that leaves open.
GRANT_MASK (abi.py:35) grants DELETE | FILE_DELETE_CHILD inside a root, which is correct and is the counterpart of the missing deny: with only the grant, a delete is authorised from the parent's right as well and no restricting SID co-signs it.
So this is not a port bug in the ordinary sense — the port is faithful to a revision known to be broken. It is the "the change was written, the consumer inventory was not" class this repository has met before, one level out: the blueprint's own later history is a source this port has never re-read. The design document's annotation for this rung ("Windows ACL 照抄移植(无实机 → 标注未验证)") was honest, and this issue is what the annotation was standing in for.
How to pin it (CI can, so "no Windows machine" no longer blocks it)
.github/workflows/test.yml:78 runs pytest on windows-2025 on every PR, and tests/test_bash_v2_win32.py already has the two shapes this needs — test_a_workspace_write_run_inherits_the_capability_and_nothing_outside_it (:809) and test_a_read_only_run_is_refused_inside_the_workspace_too (:843). What is missing is a delete path:
grep -niE "\bdel\b|rmdir|Remove-Item|DeleteFile|unlink" tests/test_bash_v2_win32.py
-> 0 matches
The blueprint's own pin is runner.spec.ts (cmd, .NET, Remove-Item, libuv), so the port has a model to copy rather than a design to invent.
Suggested shape
- Port the three commits: the Low mandatory label applied in the same
SetNamedSecurityInfoWcall that adds the capability ACE, and the inheritableDeny Everyone FILE_DELETE_CHILDscoped withCONTAINER_INHERIT_ACE—3d5ba3b83fmeasured that the unscoped deny reaches files, and0x40is a member ofFILE_ALL_ACCESS, so everyGENERIC_ALLopen inside a granted root starts failing. - Keep the residual cost visible rather than rediscovering it: a
FullControlopen of a directory inside a granted root stays denied by design (the deny contains a right the full-access mask contains). Its revoke path must keep the shared Low label while another capability grant stands on the same directory, so two grants on one path stay independently revocable. - Pin per path on the windows-2025 leg:
cmd /c del, .NETFile.Delete,Remove-Itemand a libuv unlink — each (a) outside every granted root under both modes, (b) inside a sibling granted root (another workspace, a sibling session's temp tree). - Carry
3d5ba3b83f's other halves while porting: it also closed leaks on fail-closed paths (the label ACL on bothSetEntriesInAclWearly exits, the read descriptor when the label build fails, the parsed capability SID when either well-known-SID creation fails) and made the idempotent skip require the exact deny, so a root granted by an earlier build is upgraded on its next provision rather than skipped.
Related
- PR emrg: confine windows commands with an acl-restricted token (bash tool v2, P4) #1542 (P4, merged
5a010e68) is where the port landed; this is a follow-up to it, not a defect in it. - sandbox: a runner's fatal signature is matched on any stderr line, so a command's own output is reported as the runner failing #1543 is the other Windows-leg reading in the same area (a runner's fatal signature matched on any stderr line).
- sandbox: read-only denies the OS temp area too, so no test, gate or tempfile can run at all under the dirty-tree downgrade #1552 / read-only tier: the exit from unique dirt is a git verb the tier refuses, so the state is absorbing #1465 are the read-only tier's own absorbing state, unrelated to this one.
Activity
argszero commentedon Sep 23, 2026
Independent verification on this tree, plus one measurement about the fix's own provenance pin — from cycle cyc20260924-015321 (read-only tier, so the whole reading is source and grep; nothing executed).
The absence claim reproduces, coordinates and all
On HEAD 664ff8b (one commit behind master, #1555):
grep -rniE 'integrity|mandatory|DENY_ACCESS|SET_ACCESS|LABEL_SECURITY|ACCESS_DENIED|no-write-up' emrg/sandbox/ -> 0
emrg/sandbox/win32/abi.py:61,63,91 -> GRANT_ACCESS = 1, REVOKE_ACCESS = 4, ACCESS_ALLOWED_ACE_TYPE = 0 (and no DENY ACE type)
emrg/sandbox/win32/abi.py:35 -> GRANT_MASK = (FILE_GENERIC_WRITE | DELETE | FILE_DELETE_CHILD) & ~STANDARD_RIGHTS_WRITE
grep -niE '\bdel\b|rmdir|Remove-Item|DeleteFile|unlink' tests/test_bash_v2_win32.py -> 0 (of 38 tests)
So the grant half is present and the deny half is absent, exactly as filed, and both test coordinates you name exist with those names: test_a_workspace_write_run_inherits_the_capability_and_nothing_outside_it and test_a_read_only_run_is_refused_inside_the_workspace_too, each under @needs_windows.
The revision pin is prose in 9 of the 10 modules, and names the commit in only 2
This is the half the port will have to carry and no guard can check. Census over emrg/sandbox/win32/:
init.py commit ddefc45fbc + dsh 0.1.6-alpha.2
abi.py commit ddefc45fbc + dsh 0.1.6-alpha.2
acl.py, ffi.py, grants.py, runner.py, sandbox.py, sid.py, token.py dsh 0.1.6-alpha.2 only
spawn.py neither
Six of those nine also state what the file is a port of (acl.ts, win32-process/src/ffi.ts, the chain table, ...), and emrg/sandbox/providers/win32.py adds the provider row with line ranges. Nothing reads any of it: there is no constant, no recorded datum, and no check that re-reads the blueprint — so 'this port is faithful to a revision known to be broken' is not detectable from the tree, which is why re-reading the blueprint's own later history was the only way to find the three commits at all.
Two consequences for whoever ports them:
Scope note, because a port must not symmetrise the other two providers
The parent-directory-right mechanism you quote is Windows-specific, and the tree says so in its own terms. darwin is a path rule — (deny file-write*) with /dev/null as the single literal allow (providers/darwin.py:77-78), i.e. an operation set over paths; linux is a mount namespace — --ro-bind / / then --bind , and providers/linux.py:29 states the refused file is simply absent from the namespace, which is a mechanism no ambient parent right can reach around. So whatever the Windows deny-half becomes, neither neighbour wants a copy of it.
The one thing I could not settle here, and it is the question a port should ask first: whether seatbelt's file-write* operation set covers unlink (delete) as well as content writes. That decides whether the darwin provider's confinement is complete by coverage or complete by accident of the operation sets. It needs an execution, which this tier refuses — flagged rather than claimed.
argszero commentedon Sep 25, 2026
The question this issue flagged as needing an execution is answered:
file-write*covers removal, and it is now a test rather than a sentence.Re-measured this cycle on macOS 26.6.2 (build 25G83), against the production profile builder (
seatbelt_profile_args), one spawn per row:rma file inside the workspaceOperation not permitted, file survivesrma file outside the workspacemva file from the workspace to/tmpchmod 777/>>append on a file insidermthe same inside fileThe fourth row is the control that makes the reading a coverage claim rather than a wording claim: the deciding variable is the grant, not the operation — the same binary, the same file, the same syscall, denied where nothing is granted and allowed inside the granted
(subpath …).So the answer to the question as posed is complete by coverage, not by accident:
file-write*is a wildcard over thefile-write-*operation set, which includes the directory-entry operations (unlink, and the create half of a move) as well as content writes. That means the darwin row does not need the explicit deny-half the Windows port needs, and why not is now a property of the operation set rather than of the profile's phrasing — the neighbour backends stay unsymmetrised for the reason you already state (darwin is an operation set over paths, linux is a namespace where the file is absent).It is pinned, not merely recorded, by
tests/test_bash_v2_boundary.py::test_deletion_is_governed_by_the_same_grant_as_a_write— four rows, the production builder, and the move row kept because a boundary that half-completed it (create refused, unlink already done) would lose the file rather than refuse the operation. Two mutation arms were confirmed to land before being believed: dropping(deny file-write*)fails the read-only row, and emptyingrootsfails the control row.The one consequence I would carry into the delete-escape work: on Windows the equivalent hole is real and needs a deny half, because there the ACL grant is per-object at creation and the parent-directory right is what the blueprint's later commits close — so nothing above transfers, but the test shape does (outside-the-grant removal, the half-completed move, and a granted control).
PR #1614 carries the test. The measurement is a definition, not a change: behaviour is untouched.
— cycle
cyc20260925-152945argszero commentedon Sep 25, 2026
The other halves have coordinates: the blueprint's delete-escape fix is three commits past the port's pin, and two of them read as absent from this host.
Taking this issue's own open item — the port is the blueprint before its delete-escape fix — I re-measured the port against the blueprint's later history, from a local clone (source reads and
git logonly; this cycle is on macOS, nothing executed).The pin, and what the pack gained after it. The port's modules name
dsh 0.1.6-alpha.2=ddefc45fbcas their source revision. Three commits touchpackages/sandbox/sandbox-windows-acl/after that revision, in this order:d5ad3baeb536e632751e3d5ba3b83facl.tsmoves 154 lines in the first and 156 in the second, withffi.ts,grant.ts,token.tsandwin32-abi.tsalongside, plus the pack's own test file; the third is a review-round correction and adds the note.agents/notes/implemented/feature/2026-09-19-windows-acl-mandatory-integrity-confinement.md. So "carry the other halves" is not one constant to copy: the fix has an ABI half, an ACL half and a token half, and one review round on top.Two of the three read as absent here, by census.
emrg/sandbox/win32/abi.py:61,63,91—GRANT_ACCESS = 1,REVOKE_ACCESS = 4,ACCESS_ALLOWED_ACE_TYPE = 0, and no denied-ACE type at all. The blueprint's36e632751eadds exactly that line, with its comment:ACCESS_DENIED_ACE_TYPE = 1— "shares the allowed ACE's Mask/SID layout". Without it a deny ACE cannot be expressed in the port's own ABI, which is the mechanical reason the deny half has no site rather than merely no caller.grep -rniE 'integrity|mandatory|LABEL_SECURITY|no-write-up' emrg/sandbox/→ 0. The Low-integrity-label half (d5ad3baeb5) has no site in the port either, and no module in the tree mentions the mechanism.What this does not say. The blueprint's local clone is at
477b4f4205(dsh-0.1.7-rc.2) — it has moved twice since the pin, so "the blueprint" today names a tree two releases past what the port was taken from. That is a provenance choice rather than an error, and the pin is the right thing to move only in the landing that changes those modules (this issue's own note). And I have not run any of it: the defect itself is the report's Windows measurement, not mine — what is measured here is that the fix exists upstream and that its two mechanisms have no site in this tree.No PR accompanies this comment: with no Windows host the port cannot be verified where it matters, and landing a different ACL shape with a green macOS run is the failure this thread has been documenting.
— cycle
cyc20260925-160939emrg: the boundary states what was measured, not what it assumes (#1614)
argszero commentedon Sep 25, 2026
The fix family is now inside a tag — the re-port has a revision to pin, not just a commit family
Measured this cycle in the blueprint checkout: the three commits this issue's table names are all ancestors of tag
dsh-v0.1.7-rc.2(477b4f4205, an ancestor of the release merge787b746b80), not merely of a branch:Why it matters for this issue rather than being a footnote: the port's docstrings (
abi.py:3,acl.py:3-4) name a blueprint commit (ddefc45fbc=dsh-v0.1.6-alpha.2), which is how the absence this issue describes stayed invisible — a commit comment does not tell a later reader that a newer revision exists. A tag does: the natural pin for the re-port isdsh-v0.1.7-rc.2, which carries the delete-escape family and everything the port already copies faithfully. Same prerelease class as the current pin, so the design's pin convention is unchanged.Scope of this reading. Source-level only (this host is macOS): ancestry of the three commits in the tag, and that the tag's sandbox package is the same package the port names. No Windows measurement, and nothing here changes the issue's suggested shape.
Also worth a look while the revision is being chosen: the newest revision adds one precondition the pin did not have — a granted directory must also carry
WRITE_OWNER, because the mandatory label lives in the SACL, so a directory whose DACL grants only Modify now fails the grant loudly instead of silently skipping the confinement (README.md, "Granted directories must be caller-owned and grantWRITE_OWNER"). That is a host-facing obligation, not just a port detail.— cycle
cyc20260926-030230emrg: the link reading names landed work, and a quoted keyword claims…
argszero commentedon Sep 27, 2026
What the merged referrer answered, and what it leaves — measured by cycle
cyc20260926-220539on master4ca419f8, 2026-09-27.#1614(4fbc4c05, merged 2026-09-25) references this issue and the link reading reports it as landed work carrying this number. It answered one half, and says so in its own body: "darwin: deletion is covered byfile-write*— measured, not assumed (the open question in #1556)" — the macOS provider'sENFORCEMENT = "full"claim is now backed by a measurement that removal is insidefile-write*, so a confined process cannot delete what it cannot write.The half this issue is about is unchanged. Both of this issue's own probes, re-run just now on
4ca419f8:Still zero: no Low mandatory label, no inheritable
Deny Everyone FILE_DELETE_CHILD, no delete path in the win32 suite. So the port is still faithful toddefc45fbc— the revision whose delete escaped5ad3baeb5fixed — and a confined Windows child can still delete via the parent directory'sFILE_DELETE_CHILDright.This issue therefore stays open, and the referrer's row should read as partially answered rather than landed. The remaining work is exactly the "Suggested shape" above, with the pin the issue names (a delete path in
tests/test_bash_v2_win32.py, on the windows-2025 leg).emrg: an issue and its PR must name each other, and a guard reads it (#…
argszero commentedon Sep 28, 2026
Closed — host decision, 2026-09-28: defer until a Windows-side report exists. The finding is valid; what is missing is a user on Windows, not evidence.
Validated before closing (re-measured on
019572f5, all four claims hold):emrg/sandbox/win32/abi.py:3-4— blueprintddefc45fbc(dsh 0.1.6-alpha.2), i.e. before the blueprint's own delete-escape fix;grep -rniE "integrity|mandatory|DENY_ACCESS|LABEL_SECURITY|no-write-up" emrg/sandbox/→ 0 matches; the only ACE vocabulary isGRANT_ACCESS(abi.py:61),REVOKE_ACCESS(:63),ACCESS_ALLOWED_ACE_TYPE(:91), andGRANT_MASK(:35) grantsDELETE | FILE_DELETE_CHILD;grep -niE "\bdel\b|rmdir|Remove-Item|DeleteFile|unlink" tests/test_bash_v2_win32.py→ 0 matches;.github/workflows/test.yml:79runs onwindows-2025.The risk this leaves open, stated so it is not rediscovered as a surprise: at both confined Windows tiers, a command inside a granted root can delete files outside every granted root (and inside a sibling granted root), because a delete is authorised by the parent directory's
FILE_DELETE_CHILDright and no restricting SID has to co-sign it. macOS and Linux are unaffected.Why no successor issue: the host's instruction is to wait for a Windows-side report rather than to carry a fix-up issue with no one to verify it. The material a future report needs is preserved here — the three blueprint commits to port (the Low mandatory label applied in the same
SetNamedSecurityInfoWcall as the capability ACE; the inheritableDeny Everyone FILE_DELETE_CHILDscoped withCONTAINER_INHERIT_ACE; the fail-closed leak halves and the exact-deny idempotence skip), the residual cost that stays by design (FullControlopen of a directory inside a granted root), and the blueprint's own pin to copy rather than invent (runner.spec.ts: cmd, .NET,Remove-Item, libuv, each inside and outside a granted root).If a Windows host reports it, reopen this issue — the measurement and the port plan are in its body, and nothing has changed under it since.