Monorepo for Tangled
tangled.org
1//go:build linux
2
3package sandbox
4
5import (
6 "os/exec"
7 "strings"
8 "syscall"
9 "testing"
10)
11
12func TestLandlockBackend_Name(t *testing.T) {
13 if (&LandlockBackend{}).Name() != "landlock" {
14 t.Error("Name should return \"landlock\"")
15 }
16}
17
18func TestLandlockBackend_WrapMulti_NoPaths(t *testing.T) {
19 sb := &LandlockBackend{selfExe: "/proc/self/exe"}
20 cmd := exec.Command("git", "status")
21
22 wrapped, err := sb.WrapMulti(nil, cmd)
23 if err != nil {
24 t.Fatalf("WrapMulti: %v", err)
25 }
26 if wrapped != cmd {
27 t.Error("empty paths should return the original cmd unchanged")
28 }
29}
30
31func TestLandlockBackend_WrapMulti_ArgsConstruction(t *testing.T) {
32 sb := &LandlockBackend{selfExe: "/path/to/knot"}
33 cmd := exec.Command("git", "upload-pack", "--stateless-rpc", ".")
34 cmd.Env = []string{"GIT_PROTOCOL=version=2"}
35 cmd.Stdin = strings.NewReader("input")
36
37 wrapped, err := sb.WrapMulti([]string{"/repos/a", "/repos/b"}, cmd)
38 if err != nil {
39 t.Fatalf("WrapMulti: %v", err)
40 }
41
42 // argv[0] is the selfExe
43 if wrapped.Path != "/path/to/knot" {
44 t.Errorf("Path = %q, want %q", wrapped.Path, "/path/to/knot")
45 }
46
47 // argv should be: [knot, sandbox-exec, --repo-path=/repos/a, --repo-path=/repos/b, --, <abs-git>, upload-pack, ...]
48 args := wrapped.Args
49 if len(args) < 6 {
50 t.Fatalf("Args too short: %v", args)
51 }
52 if args[0] != "/path/to/knot" {
53 t.Errorf("Args[0] = %q, want %q", args[0], "/path/to/knot")
54 }
55 if args[1] != "sandbox-exec" {
56 t.Errorf("Args[1] = %q, want %q", args[1], "sandbox-exec")
57 }
58 if args[2] != "--repo-path=/repos/a" {
59 t.Errorf("Args[2] = %q, want --repo-path=/repos/a", args[2])
60 }
61 if args[3] != "--repo-path=/repos/b" {
62 t.Errorf("Args[3] = %q, want --repo-path=/repos/b", args[3])
63 }
64 if args[4] != "--" {
65 t.Errorf("Args[4] = %q, want --", args[4])
66 }
67 // args[5] is the resolved absolute git path; just check it ends with /git
68 if !strings.HasSuffix(args[5], "/git") && args[5] != "git" {
69 t.Errorf("Args[5] = %q, want path ending in /git or bare \"git\"", args[5])
70 }
71 if got := args[len(args)-1]; got != "." {
72 t.Errorf("last arg = %q, want %q", got, ".")
73 }
74
75 // Dir should be the first repo path so the kernel chdirs there
76 // after setuid, before execve.
77 if wrapped.Dir != "/repos/a" {
78 t.Errorf("Dir = %q, want %q", wrapped.Dir, "/repos/a")
79 }
80
81 // Env propagated
82 if len(wrapped.Env) != 1 || wrapped.Env[0] != "GIT_PROTOCOL=version=2" {
83 t.Errorf("Env = %v, want [GIT_PROTOCOL=version=2]", wrapped.Env)
84 }
85
86 // Stdio propagated
87 if wrapped.Stdin != cmd.Stdin {
88 t.Error("Stdin not propagated to wrapped cmd")
89 }
90}
91
92func TestLandlockBackend_WrapMulti_NoLookupNoCredential(t *testing.T) {
93 sb := &LandlockBackend{selfExe: "/path/to/knot"} // no lookup
94 cmd := exec.Command("git", "status")
95
96 wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd)
97 if err != nil {
98 t.Fatalf("WrapMulti: %v", err)
99 }
100 if wrapped.SysProcAttr != nil && wrapped.SysProcAttr.Credential != nil {
101 t.Error("no lookup configured; Credential should not be set")
102 }
103}
104
105func TestLandlockBackend_WrapMulti_LookupSetsCredential(t *testing.T) {
106 // lookup deliberately returns a different gid (the service group) to
107 // confirm that WrapMulti ignores it and uses uid as the primary gid.
108 // see the comment in sandbox_linux.go for why this matters.
109 sb := &LandlockBackend{
110 selfExe: "/path/to/knot",
111 lookup: func(repoPath string) (uint32, uint32, error) {
112 if repoPath != "/repos/a" {
113 t.Errorf("lookup called with %q, want /repos/a", repoPath)
114 }
115 return 100042, 1234, nil
116 },
117 }
118 cmd := exec.Command("git", "status")
119
120 wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd)
121 if err != nil {
122 t.Fatalf("WrapMulti: %v", err)
123 }
124 if wrapped.SysProcAttr == nil || wrapped.SysProcAttr.Credential == nil {
125 t.Fatal("Credential should be set when lookup returns uid > 0")
126 }
127 cred := wrapped.SysProcAttr.Credential
128 if cred.Uid != 100042 {
129 t.Errorf("Credential.Uid = %d, want 100042", cred.Uid)
130 }
131 if cred.Gid != 100042 {
132 t.Errorf("Credential.Gid = %d, want 100042 (must equal Uid, not lookup's gid 1234)", cred.Gid)
133 }
134 if !cred.NoSetGroups {
135 t.Error("NoSetGroups should be true")
136 }
137}
138
139func TestLandlockBackend_WrapMulti_LookupErrSkipsCredential(t *testing.T) {
140 sb := &LandlockBackend{
141 selfExe: "/path/to/knot",
142 lookup: func(string) (uint32, uint32, error) {
143 return 0, 0, syscall.ENOENT
144 },
145 }
146 cmd := exec.Command("git", "status")
147
148 wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd)
149 if err != nil {
150 t.Fatalf("WrapMulti: %v", err)
151 }
152 if wrapped.SysProcAttr != nil && wrapped.SysProcAttr.Credential != nil {
153 t.Error("lookup errored; Credential should not be set")
154 }
155}
156
157func TestLandlockBackend_WrapMulti_LookupZeroSkipsCredential(t *testing.T) {
158 // uid == 0 is treated as "don't drop" so we never accidentally drop to
159 // root. Verify Credential isn't set in that case.
160 sb := &LandlockBackend{
161 selfExe: "/path/to/knot",
162 lookup: func(string) (uint32, uint32, error) {
163 return 0, 0, nil
164 },
165 }
166 cmd := exec.Command("git", "status")
167
168 wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd)
169 if err != nil {
170 t.Fatalf("WrapMulti: %v", err)
171 }
172 if wrapped.SysProcAttr != nil && wrapped.SysProcAttr.Credential != nil {
173 t.Error("lookup returned uid=0; Credential should not be set")
174 }
175}