mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 05:01:28 -10:00
* fix(sandbox): keep a derived ACL target inside the write root it came from FILE_FLAG_OPEN_REPARSE_POINT refuses a reparse point at the final path component and resolves every component above it, which is right for a path the operator named and not enough for one this package derived. The write-root carveouts are derived: <root>/.git/hooks and <root>/.git/config are constructed from a root, and <root>/.git is a name an unprivileged workspace writer can create before setup runs. mklink /J needs no privilege, so a junction there had the apply open <junction-target>/hooks, an ordinary directory with nothing wrong about its final component, and zero sandbox setup wrote a deny ACE on it as Administrator, outside the workspace. A derived entry now carries the root it came from, and the apply requires the object it finally holds to still live under it. The check is on the handle rather than the name: GetFinalPathNameByHandle answers where the open object actually is, and both sides go through it so the spelling normalizes the same way. Materialization is checked before it creates, since os.MkdirAll follows the same reparse points. Deliberately not a refusal of every reparse point on the path. Above the write root the path is the operator's, who may keep a workspace under a junction or a mapped directory; only the tail the sandbox appended is held strict. Closes #1024 * fix(sandbox): reject the reparse ancestor itself before materializing The pre-create check asked where the deepest existing ancestor lives, and a junction answers with its own path: the open does not follow a final-component reparse point, so <root>/.git came back as <root>/.git and matched. os.MkdirAll does follow it. What disqualifies that ancestor is that it IS a reparse point, not where it reports living. Found by reverting the check and watching the test still pass, which said the guard was doing nothing rather than that the test was weak. Pinned against the function now, because through the whole apply the create is made and then removed on the failure path, so the filesystem afterwards looks identical either way. The plan wiring gets its own test for the same reason: every apply-level case here hands the group an anchor directly, so none of them would notice the builder never setting one. * fix(sandbox): anchor only the carveouts that are under the root, and put the DACL back Two from review, both right. ReadOnlySubpaths is a profile field an operator can set to any path, and one placed outside the write root is a configuration that works today; anchoring it unconditionally turned that into a containment refusal. Only paths lexically under the root are anchored now, which is where the derived carveouts are anyway, and anything else keeps the final-component guard it always had. The successful-apply tests left their deny ACE in place, so t.TempDir could fail to remove the tree. Restoring the snapshot exposed the sharper half: the ACE denied the group the test runs as, which revoked its own WRITE_DAC and left the rollback unable to reopen the target. The tests deny a group this process is not a member of instead, which is what a capability SID is in production, and roll back afterwards. * test(sandbox): check the plan anchoring on every platform, out-of-root case included The plan builder is cross-platform and its anchor tests were in a Windows-only file, so nothing checked the wiring on Linux or macOS. Moved beside the other BuildWindowsACLPlan tests, with the case review raised: an out-of-root ReadOnlySubpath stays unanchored, and an in-root one is still held to its write root. * test(sandbox): give the cross-platform anchor tests native paths Moving these beside the other BuildWindowsACLPlan tests made them run on Linux and macOS, where their Windows path literals stop meaning what they say: pathWithinRoot is filepath.Rel underneath, a backslash is an ordinary character off Windows, and C:\workspace\.git\hooks is then one component that is not under C:\workspace, so every anchor came back empty. The neighbouring tests get away with such literals because they only compare strings they built the same way and never ask whether one contains another. Paths are built with filepath.Join from temp roots now, so the containment question is asked in the separator the running platform actually uses.