Skip to content

Commit ed355ca

Browse files
fix(sandbox): answer the open review threads on the runtime root
Resolve the stamp reader before the stamp is opened. FILE_OVERWRITE_IF truncates at open time, so a reader that could not be established returned an error with the previous stamp already emptied, or a new empty one left behind. The pathname entry point now delegates to the handle writer instead of carrying its own copy of the sequence. Take the Windows user scope for the fallback runtime root from the token's user SID rather than USERNAME. Setup and a later command derive that root in two processes with independent environments, and a USERNAME that differed between them made the command reject the root setup had recorded. On Unix, report a created runtime directory that exists but cannot be identified instead of treating every lookup failure as a clean undo, which is the line the Windows build already draws. Move runtimeTailNotOwned and windowsSameRuntimeRootPath beside their only, Windows-only callers so they are no longer dead code on other platforms, replace a deprecated filepath.HasPrefix in a Unix test, and bind the junction test's planted flag to mklink's result so a failed mklink skips instead of failing a correct acquisition.
1 parent 87124fb commit ed355ca

10 files changed

Lines changed: 281 additions & 88 deletions

‎internal/sandbox/runtime_compensation_other.go‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package sandbox
44

55
import (
6+
"errors"
67
"fmt"
78
"os"
89
"path/filepath"
@@ -42,7 +43,15 @@ func compensateRuntimeStampBound(root string, identity string, name string, prio
4243
func removeCreatedRuntimeDirBound(path string, identity string) error {
4344
current, ok := runtimeDirIdentity(path)
4445
if !ok {
45-
return nil
46+
// ONLY ABSENCE MEANS THERE IS NOTHING TO UNDO. Any lookup failure used to
47+
// return here, so a directory that exists but could not be identified was
48+
// reported as cleanly removed while it stayed on disk. The Windows build
49+
// already draws this line, and the rollback it serves promises to report
50+
// what it could not remove. Reported by CodeRabbit.
51+
if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
52+
return nil
53+
}
54+
return fmt.Errorf("sandbox runtime root %s was created by this run but could not be identified for removal; leaving it in place", path)
4655
}
4756
if current != identity {
4857
return fmt.Errorf("sandbox runtime root %s is no longer the directory this run created; "+
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//go:build !windows
2+
3+
package sandbox
4+
5+
import (
6+
"errors"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"testing"
11+
)
12+
13+
// ONLY ABSENCE IS NOTHING TO UNDO.
14+
//
15+
// removeCreatedRuntimeDirBound returned success on any failure to identify the
16+
// directory, so one that was still there, but could not be looked at, was
17+
// reported as removed. Here the parent loses search permission, which makes the
18+
// directory exist and be unidentifiable at the same time.
19+
func TestRemovingAnUnidentifiableCreatedDirectoryIsReported(t *testing.T) {
20+
if os.Geteuid() == 0 {
21+
t.Skip("root bypasses directory search permission, so the lookup cannot be made to fail")
22+
}
23+
parent := filepath.Join(t.TempDir(), "zero")
24+
created := filepath.Join(parent, "runtime")
25+
if err := os.MkdirAll(created, 0o700); err != nil {
26+
t.Fatalf("create the directory: %v", err)
27+
}
28+
identity, ok := runtimeDirIdentity(created)
29+
if !ok {
30+
t.Fatal("identify the created directory")
31+
}
32+
if err := os.Chmod(parent, 0o600); err != nil {
33+
t.Fatalf("withdraw search permission: %v", err)
34+
}
35+
t.Cleanup(func() { _ = os.Chmod(parent, 0o700) })
36+
if _, err := os.Lstat(created); err == nil || errors.Is(err, os.ErrNotExist) {
37+
t.Fatalf("SETUP INVALID: the directory should exist and be unidentifiable, Lstat said %v", err)
38+
}
39+
40+
err := removeCreatedRuntimeDirBound(created, identity)
41+
if err == nil {
42+
t.Fatal("a created directory that could not be identified was reported as removed")
43+
}
44+
if !strings.Contains(err.Error(), "could not be identified") {
45+
t.Errorf("the error does not say why the directory was left: %v", err)
46+
}
47+
48+
// And it is still there, which is why the error is owed.
49+
if err := os.Chmod(parent, 0o700); err != nil {
50+
t.Fatalf("restore search permission: %v", err)
51+
}
52+
if _, err := os.Lstat(created); err != nil {
53+
t.Fatalf("the directory was gone after all, so the error was not owed: %v", err)
54+
}
55+
}
56+
57+
// Absence stays a clean undo, or the test above would be satisfied by a function
58+
// that reports every lookup failure.
59+
func TestRemovingACreatedDirectoryThatIsAlreadyGoneSucceeds(t *testing.T) {
60+
created := filepath.Join(t.TempDir(), "runtime")
61+
if err := os.Mkdir(created, 0o700); err != nil {
62+
t.Fatalf("create the directory: %v", err)
63+
}
64+
identity, ok := runtimeDirIdentity(created)
65+
if !ok {
66+
t.Fatal("identify the created directory")
67+
}
68+
if err := os.Remove(created); err != nil {
69+
t.Fatalf("remove the directory: %v", err)
70+
}
71+
if err := removeCreatedRuntimeDirBound(created, identity); err != nil {
72+
t.Fatalf("a created directory that is already gone was reported as a failed undo: %v", err)
73+
}
74+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//go:build windows
2+
3+
package sandbox
4+
5+
import "testing"
6+
7+
// THE COMMAND HAS TO RECOGNISE THE ROOT SETUP RECORDED, WHATEVER ITS
8+
// ENVIRONMENT SAYS.
9+
//
10+
// Setup derives the fallback root in an elevated terminal and records it; a
11+
// command re-derives it in an ordinary one to decide whether the record belongs
12+
// to this workspace. The user scope in that derivation used to come from
13+
// USERNAME, so a value that differed between the two terminals made the command
14+
// reject the record and select a root with no capability ACE on it.
15+
func TestRecordedFallbackRootSurvivesADifferentUsernameVariable(t *testing.T) {
16+
workspace := t.TempDir()
17+
tempHome := t.TempDir()
18+
t.Setenv("TMP", tempHome)
19+
t.Setenv("TEMP", tempHome)
20+
21+
t.Setenv("USERNAME", "setup-terminal")
22+
recorded, err := fallbackSandboxRuntimeRoot(workspace)
23+
if err != nil {
24+
t.Fatalf("derive the fallback root as setup: %v", err)
25+
}
26+
if !ownedFallbackRuntimeRecord(workspace, recorded) {
27+
t.Fatalf("SETUP INVALID: the fallback root is not recognised even in the environment that derived it: %s", recorded)
28+
}
29+
30+
t.Setenv("USERNAME", "command-terminal")
31+
if !ownedFallbackRuntimeRecord(workspace, recorded) {
32+
t.Fatalf("once USERNAME changed, the command no longer recognises the fallback root setup recorded: %s", recorded)
33+
}
34+
derived, err := fallbackSandboxRuntimeRoot(workspace)
35+
if err != nil {
36+
t.Fatalf("derive the fallback root as the command: %v", err)
37+
}
38+
if !sameWindowsRuntimeRootPath(derived, recorded) {
39+
t.Fatalf("setup and the command derived different fallback roots for one workspace:\n setup: %s\n command: %s", recorded, derived)
40+
}
41+
}

‎internal/sandbox/runtime_lease_junction_windows_test.go‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,32 @@ func TestRuntimeLeaseRefusesAJunctionPlantedAfterTheCheck(t *testing.T) {
5454
// the pathname lease open used to follow it.
5555
previous := runtimeLeasePreCreateBarrier
5656
t.Cleanup(func() { runtimeLeasePreCreateBarrier = previous })
57-
planted := false
57+
attempted, planted := false, false
5858
runtimeLeasePreCreateBarrier = func() {
59-
if planted {
59+
if attempted {
6060
return
6161
}
62-
planted = true
62+
attempted = true
6363
if out, err := exec.Command("cmd", "/c", "mklink", "/J", owned, target).CombinedOutput(); err != nil {
6464
t.Logf("mklink /J unavailable: %v: %s", err, out)
65+
return
6566
}
67+
planted = true
6668
}
6769

6870
lease, _, err := prepareSandboxRuntimeLeaseRecording(root)
6971
if lease != nil {
7072
lease.release()
7173
}
72-
if !planted {
74+
if !attempted {
7375
t.Skip("the pre-create barrier never ran, so the race was not reproduced")
7476
}
77+
// Bound to mklink's result, not to the attempt. When it fails, acquisition
78+
// goes on to create an ordinary directory at the same name, the check below
79+
// finds it, and a correct acquisition would be failed as a junction bypass.
80+
if !planted {
81+
t.Skip("mklink /J could not create the junction here, so the race was not reproduced")
82+
}
7583
if _, statErr := os.Lstat(owned); statErr != nil {
7684
t.Skipf("the junction could not be created here: %v", statErr)
7785
}

‎internal/sandbox/runtime_lease_link_unix_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func TestRuntimeLeaseRefusesALinkPlantedAfterTheCheck(t *testing.T) {
6565
root := unixLeaseRootUnder(t, base)
6666

6767
owned := filepath.Join(canonicalSandboxWorkspaceRoot(base), "zero")
68-
if !filepath.HasPrefix(root, owned) {
68+
if !pathWithinRoot(owned, root) {
6969
t.Fatalf("SETUP INVALID: %s does not sit under the owned component %s", root, owned)
7070
}
7171

‎internal/sandbox/runtime_root_guard_windows.go‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,22 @@ func refuseForeignRuntimeComponent(string, os.FileInfo) error {
2222
// Windows TEMP is already per-user, so this is belt and braces rather than the
2323
// load-bearing separation it is on Unix. Kept so the derived path has the same
2424
// shape on every platform and one code path builds it.
25+
//
26+
// FROM THE TOKEN, NOT THE ENVIRONMENT. Setup derives the fallback root in the
27+
// elevated terminal and records it; a command later re-derives it in an
28+
// ordinary one to decide whether that record belongs to this workspace. Those
29+
// are two processes with independent environments, so a USERNAME that differed
30+
// between them made the command reject the root setup had provisioned and
31+
// select one that carries no capability ACE. The token's user SID is the same
32+
// in both, because setup refuses to run as any account other than the one that
33+
// will use Zero. The Unix build reads the uid for the same reason. Reported by
34+
// CodeRabbit.
2535
func sandboxRuntimeUserScope() string {
26-
name := strings.TrimSpace(os.Getenv("USERNAME"))
27-
if name == "" {
36+
sid, err := currentProcessSID()
37+
if err != nil {
2838
return "u"
2939
}
30-
return "u" + strings.ToLower(name)
40+
return "u" + strings.ToLower(sid)
3141
}
3242

3343
// sandboxRuntimeFallbackOwnedNames are the components the temp-derived runtime

‎internal/sandbox/windows_acl_apply_windows.go‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"os"
9+
"path/filepath"
910
"sort"
1011
"strings"
1112
"unsafe"
@@ -147,6 +148,19 @@ func verifyStampRootIdentity(handle windows.Handle, path string, stamp *windowsA
147148
// Nil in production.
148149
var windowsACLStampIdentitySwapHook func(path string)
149150

151+
// windowsSameRuntimeRootPath compares two runtime roots the way the filesystem
152+
// does, so the stamp rides along with the right target regardless of spelling.
153+
// Beside its only caller: in the shared file it was dead code on every other
154+
// platform.
155+
func windowsSameRuntimeRootPath(left, right string) bool {
156+
left = filepath.Clean(strings.TrimSpace(left))
157+
right = filepath.Clean(strings.TrimSpace(right))
158+
if left == "" || right == "" {
159+
return false
160+
}
161+
return strings.EqualFold(left, right)
162+
}
163+
150164
// writeRidingStamp writes the stamp through the handle the capability ACE was
151165
// applied on, or through the test hook when one is installed.
152166
func writeRidingStamp(handle windows.Handle, path string, planHash string) error {

‎internal/sandbox/windows_runtime_tail.go‎

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ package sandbox
22

33
import (
44
"errors"
5-
"fmt"
65
"path/filepath"
7-
"runtime"
86
"strings"
97
)
108

@@ -65,24 +63,6 @@ func windowsSandboxRuntimeOwnedTail(root string) (string, []string, bool) {
6563
// Callers must fail rather than quietly opening it by name.
6664
var errRuntimeTailNotOwned = errors.New("path is not a sandbox runtime root")
6765

68-
// windowsSameRuntimeRootPath compares two runtime roots the way the filesystem
69-
// does, so the stamp rides along with the right target regardless of spelling.
70-
func windowsSameRuntimeRootPath(left, right string) bool {
71-
left = filepath.Clean(strings.TrimSpace(left))
72-
right = filepath.Clean(strings.TrimSpace(right))
73-
if left == "" || right == "" {
74-
return false
75-
}
76-
if runtime.GOOS == "windows" {
77-
return strings.EqualFold(left, right)
78-
}
79-
return left == right
80-
}
81-
82-
func runtimeTailNotOwned(root string) error {
83-
return fmt.Errorf("%w: %s", errRuntimeTailNotOwned, root)
84-
}
85-
8666
// windowsRuntimeOwnedNameMatches reports whether one component of a candidate
8767
// tail is a name Zero owns at that position.
8868
//

0 commit comments

Comments
 (0)