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 Unverified
SHA256:VOCA5lYARHcbKDZ7GhTV45VmcjWnJ6ZQIBazj17LivAThe 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 { | |||
| 140 | 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 | 145 | gw := args[sandbox.CmdlineGateway] | |
| 146 | 146 | 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) | |||
| 414 | 414 | 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 | 420 | // 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 err | 414 | 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 @@ | |||
| 2 | 2 | ||
| 3 | 3 | package sandbox | |
| 4 | 4 | ||
| 5 | + | import "os" | |
| 6 | + | ||
| 5 | 7 | // The host/guest contract shared by the drivers and the guest side | |
| 6 | 8 | // (internal/guest). Changing a value here changes both sides at once because | |
| 7 | 9 | // the host always attaches its own executable to the sandbox. | |
| @@ -37,3 +39,9 @@ const ( | |||
| 37 | 39 | // CmdlineExternals=1: the externals disk is attached as the fourth drive. | |
| 38 | 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 | |
| @@ -2,6 +2,8 @@ | |||||
| 2 | 2 | ||||
| 3 | package sandbox | 3 | package sandbox | ||
| 4 | 4 | ||||
| 5 | + | import "os" | |||
| 6 | + | ||||
| 5 | // The host/guest contract shared by the drivers and the guest side | 7 | // The host/guest contract shared by the drivers and the guest side | ||
| 6 | // (internal/guest). Changing a value here changes both sides at once because | 8 | // (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 | + | } | |||