detached 5ac9bb1

fix(sandbox): make the job's /tmp sticky

commit
5ac9bb1494e30a14c2d7120c1e6d86376e7fbd17
author
brice · · 2026-10-14 15:40 UTC
committer
bnema · · 2026-10-14 15:40 UTC
parent
2e816bd
signature
SSH UnverifiedSHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivA The key is unknown
4 files · +39 -2
M internal/guest/vmsetup.go +1 -1
@@ -140,7 +140,7 @@ func setupVM(log zerowrap.Logger, args map[string]string) error {
140140 log.Warn().Err(err).Str("target", m.dst).Msg("init: mount")
141141 }
142142 }
143- _ = os.Chmod("/tmp", 0o1777)
143+ _ = os.Chmod("/tmp", sandbox.TmpMode)
144144
145145 gw := args[sandbox.CmdlineGateway]
146146 if err := writeResolvConf(gw); err != nil {
@@ -140,7 +140,7 @@ func setupVM(log zerowrap.Logger, args map[string]string) error {
140 log.Warn().Err(err).Str("target", m.dst).Msg("init: mount")140 log.Warn().Err(err).Str("target", m.dst).Msg("init: mount")
141 }141 }
142 }142 }
143- _ = os.Chmod("/tmp", 0o1777)143+ _ = os.Chmod("/tmp", sandbox.TmpMode)
144 144
145 gw := args[sandbox.CmdlineGateway]145 gw := args[sandbox.CmdlineGateway]
146 if err := writeResolvConf(gw); err != nil {146 if err := writeResolvConf(gw); err != nil {
M internal/sandbox/container/driver.go +1 -1
@@ -414,7 +414,7 @@ func (s *containerSandbox) runScratch(ctx context.Context, spec sandbox.JobSpec)
414414 return err
415415 }
416416 }
417- return os.Chmod(filepath.Join(mnt, "tmp"), 0o1777)
417+ return os.Chmod(filepath.Join(mnt, "tmp"), sandbox.TmpMode)
418418 }
419419
420420 // runNetwork creates the job's bridge and installs the firewall policy.
@@ -414,7 +414,7 @@ func (s *containerSandbox) runScratch(ctx context.Context, spec sandbox.JobSpec)
414 return err414 return err
415 }415 }
416 }416 }
417- return os.Chmod(filepath.Join(mnt, "tmp"), 0o1777)417+ return os.Chmod(filepath.Join(mnt, "tmp"), sandbox.TmpMode)
418 }418 }
419 419
420 // runNetwork creates the job's bridge and installs the firewall policy.420 // runNetwork creates the job's bridge and installs the firewall policy.
M internal/sandbox/contract.go +8 -0
@@ -2,6 +2,8 @@
22
33 package sandbox
44
5+import "os"
6+
57 // The host/guest contract shared by the drivers and the guest side
68 // (internal/guest). Changing a value here changes both sides at once because
79 // the host always attaches its own executable to the sandbox.
@@ -37,3 +39,9 @@ const (
3739 // CmdlineExternals=1: the externals disk is attached as the fourth drive.
3840 CmdlineExternals = "ermite.externals"
3941 )
42+
43+// TmpMode is the mode of the job's /tmp, as on GitHub's runners: everyone may
44+// write, only the owner may remove or rename (sticky). os.FileMode has its own
45+// sticky flag: the octal 0o1777 would give a plain 0777, which programs that
46+// check their temp directory refuse as replaceable.
47+const TmpMode = 0o777 | os.ModeSticky
@@ -2,6 +2,8 @@
2 2
3 package sandbox3 package sandbox
4 4
5+import "os"
6+
5 // The host/guest contract shared by the drivers and the guest side7 // The host/guest contract shared by the drivers and the guest side
6 // (internal/guest). Changing a value here changes both sides at once because8 // (internal/guest). Changing a value here changes both sides at once because
7 // the host always attaches its own executable to the sandbox.9 // the host always attaches its own executable to the sandbox.
@@ -37,3 +39,9 @@ const (
37 // CmdlineExternals=1: the externals disk is attached as the fourth drive.39 // CmdlineExternals=1: the externals disk is attached as the fourth drive.
38 CmdlineExternals = "ermite.externals"40 CmdlineExternals = "ermite.externals"
39 )41 )
42+
43+// TmpMode is the mode of the job's /tmp, as on GitHub's runners: everyone may
44+// write, only the owner may remove or rename (sticky). os.FileMode has its own
45+// sticky flag: the octal 0o1777 would give a plain 0777, which programs that
46+// check their temp directory refuse as replaceable.
47+const TmpMode = 0o777 | os.ModeSticky
A internal/sandbox/tmpmode_test.go +29 -0
@@ -0,0 +1,29 @@
1+// SPDX-License-Identifier: MIT
2+
3+package sandbox
4+
5+import (
6+ "os"
7+ "path/filepath"
8+ "testing"
9+)
10+
11+// The job's /tmp is world-writable and sticky, as on GitHub's runners: a
12+// program that refuses a replaceable temp directory (a writable ancestor
13+// without the sticky bit) must accept it.
14+func TestTmpModeIsStickyAndWorldWritable(t *testing.T) {
15+ dir := filepath.Join(t.TempDir(), "tmp")
16+ if err := os.Mkdir(dir, 0o755); err != nil {
17+ t.Fatal(err)
18+ }
19+ if err := os.Chmod(dir, TmpMode); err != nil {
20+ t.Fatal(err)
21+ }
22+ fi, err := os.Stat(dir)
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ if fi.Mode()&os.ModeSticky == 0 || fi.Mode().Perm() != 0o777 {
27+ t.Fatalf("mode %v, want sticky and 0777", fi.Mode())
28+ }
29+}
@@ -0,0 +1,29 @@
1+// SPDX-License-Identifier: MIT
2+
3+package sandbox
4+
5+import (
6+ "os"
7+ "path/filepath"
8+ "testing"
9+)
10+
11+// The job's /tmp is world-writable and sticky, as on GitHub's runners: a
12+// program that refuses a replaceable temp directory (a writable ancestor
13+// without the sticky bit) must accept it.
14+func TestTmpModeIsStickyAndWorldWritable(t *testing.T) {
15+ dir := filepath.Join(t.TempDir(), "tmp")
16+ if err := os.Mkdir(dir, 0o755); err != nil {
17+ t.Fatal(err)
18+ }
19+ if err := os.Chmod(dir, TmpMode); err != nil {
20+ t.Fatal(err)
21+ }
22+ fi, err := os.Stat(dir)
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ if fi.Mode()&os.ModeSticky == 0 || fi.Mode().Perm() != 0o777 {
27+ t.Fatalf("mode %v, want sticky and 0777", fi.Mode())
28+ }
29+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close