On main at f30f550, gitMetadataWriteCarveouts in internal/sandbox/profile.go emits <root>/.git/hooks and <root>/.git/config as write-denied carveouts. The Windows ACL apply then opens those paths with FILE_FLAG_OPEN_REPARSE_POINT, which guards only the final path component. A junction at <root>/.git is traversed as an intermediate, so the deny-write ACEs land on <junction-target>/hooks and <junction-target>/config, outside the workspace.
mklink /J needs no privilege, so any local user with write access to a workspace that has no .git yet can plant one.
Evidence, with provenance. I confirmed the carveout shape by reading the code on main: the two children are emitted unconditionally, with no lstat of .git at all. The traversal itself was probed during the review of #805: a junction created unelevated with mklink /J, then both <root>\.git\hooks through the junction and <junction-target>\hooks directly opened with the apply's exact CreateFile flags, and both resolved to the same volume serial and file index. Opening <root>\.git itself reports reparse=true and is refused, but with these carveouts that path is never opened.
Consequences differ by path. On the unelevated per-command path this only touches objects the user could already re-ACL, and it is rolled back after the command. On zero sandbox setup, which requires Administrator, the same redirect happens with an elevated token: the same TOCTOU class as #728, one component further up.
#805 does not introduce or widen this; it improves the regular-file case and leaves the junction case exactly as main has it. It also does not close it, which is why this is its own issue against the ACL apply rather than against that PR. The fix belongs in the apply: refuse to write an ACE through any owned intermediate that is a reparse point, which is the rule the rooted descent in #901 already follows for the runtime tree.
On
mainat f30f550,gitMetadataWriteCarveoutsininternal/sandbox/profile.goemits<root>/.git/hooksand<root>/.git/configas write-denied carveouts. The Windows ACL apply then opens those paths withFILE_FLAG_OPEN_REPARSE_POINT, which guards only the final path component. A junction at<root>/.gitis traversed as an intermediate, so the deny-write ACEs land on<junction-target>/hooksand<junction-target>/config, outside the workspace.mklink /Jneeds no privilege, so any local user with write access to a workspace that has no.gityet can plant one.Evidence, with provenance. I confirmed the carveout shape by reading the code on main: the two children are emitted unconditionally, with no lstat of
.gitat all. The traversal itself was probed during the review of #805: a junction created unelevated withmklink /J, then both<root>\.git\hooksthrough the junction and<junction-target>\hooksdirectly opened with the apply's exact CreateFile flags, and both resolved to the same volume serial and file index. Opening<root>\.gititself reports reparse=true and is refused, but with these carveouts that path is never opened.Consequences differ by path. On the unelevated per-command path this only touches objects the user could already re-ACL, and it is rolled back after the command. On
zero sandbox setup, which requires Administrator, the same redirect happens with an elevated token: the same TOCTOU class as #728, one component further up.#805 does not introduce or widen this; it improves the regular-file case and leaves the junction case exactly as main has it. It also does not close it, which is why this is its own issue against the ACL apply rather than against that PR. The fix belongs in the apply: refuse to write an ACE through any owned intermediate that is a reparse point, which is the rule the rooted descent in #901 already follows for the runtime tree.