Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

1//go:build linux && integration 2 3package sandbox 4 5import ( 6 "fmt" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "strings" 11 "testing" 12 13 "github.com/landlock-lsm/go-landlock/landlock" 14) 15 16// This file exercises actual Landlock enforcement. It works by re-execing the 17// test binary with an env var that makes it run the "child" code path: the 18// child applies the ruleset built by BuildRuleSpec and attempts a specific 19// filesystem operation, exiting 0 (allowed) or non-zero (denied). 20// 21// Build tag: linux && integration. Run with: 22// go test -tags integration ./knotserver/sandbox/... 23 24const ( 25 childEnv = "TANGLED_SANDBOX_INT_CHILD" 26 childRepo = "TANGLED_SANDBOX_INT_REPO" 27 childTarget = "TANGLED_SANDBOX_INT_TARGET" 28 childOp = "TANGLED_SANDBOX_INT_OP" 29) 30 31func TestMain(m *testing.M) { 32 if os.Getenv(childEnv) == "1" { 33 runChild() 34 return 35 } 36 os.Exit(m.Run()) 37} 38 39func runChild() { 40 repoPath := os.Getenv(childRepo) 41 target := os.Getenv(childTarget) 42 op := os.Getenv(childOp) 43 44 spec := BuildRuleSpec([]string{repoPath}) 45 rules := []landlock.Rule{ 46 landlock.RODirs(spec.SystemRO...).IgnoreIfMissing(), 47 landlock.RWFiles(spec.DevRW...).WithIoctlDev().IgnoreIfMissing(), 48 landlock.RWDirs(spec.TmpRW...).IgnoreIfMissing(), 49 } 50 if spec.GitConfigRO != "" { 51 rules = append(rules, landlock.ROFiles(spec.GitConfigRO).IgnoreIfMissing()) 52 } 53 for _, p := range spec.RepoRW { 54 rules = append(rules, landlock.RWDirs(p).WithRefer()) 55 } 56 if err := landlock.V8.BestEffort().RestrictPaths(rules...); err != nil { 57 fmt.Fprintf(os.Stderr, "restrict failed: %v\n", err) 58 os.Exit(2) 59 } 60 61 var opErr error 62 switch op { 63 case "read": 64 _, opErr = os.ReadFile(target) 65 case "write": 66 opErr = os.WriteFile(target, []byte("x"), 0644) 67 case "list": 68 _, opErr = os.ReadDir(target) 69 default: 70 fmt.Fprintf(os.Stderr, "unknown op %q\n", op) 71 os.Exit(3) 72 } 73 74 if opErr != nil { 75 fmt.Fprintln(os.Stderr, opErr) 76 os.Exit(1) 77 } 78 os.Exit(0) 79} 80 81// runUnderSandbox spawns the test binary as a child, applies Landlock with 82// the given repoPath as the granted RW path, then attempts op on target. 83// Returns true if the op was allowed, false if denied. 84func runUnderSandbox(t *testing.T, repoPath, target, op string) (allowed bool, output string) { 85 t.Helper() 86 cmd := exec.Command(os.Args[0]) 87 cmd.Env = append(os.Environ(), 88 childEnv+"=1", 89 childRepo+"="+repoPath, 90 childTarget+"="+target, 91 childOp+"="+op, 92 ) 93 out, err := cmd.CombinedOutput() 94 if err == nil { 95 return true, string(out) 96 } 97 if exit, ok := err.(*exec.ExitError); ok && exit.ExitCode() == 1 { 98 return false, string(out) 99 } 100 t.Fatalf("child exited unexpectedly: %v\noutput: %s", err, out) 101 return false, "" 102} 103 104func skipIfNoLandlock(t *testing.T) { 105 t.Helper() 106 if !probeLandlock() { 107 t.Skip("Landlock not available on this kernel") 108 } 109} 110 111func TestSandboxIntegration_AllowsOwnRepoRead(t *testing.T) { 112 skipIfNoLandlock(t) 113 114 root := t.TempDir() 115 repo := filepath.Join(root, "did:plc:abc") 116 target := filepath.Join(repo, "HEAD") 117 mustMkdirAll(t, repo) 118 mustWriteFile(t, target, "ref: refs/heads/main\n") 119 120 allowed, out := runUnderSandbox(t, repo, target, "read") 121 if !allowed { 122 t.Errorf("expected read of own repo to be allowed; child said: %s", strings.TrimSpace(out)) 123 } 124} 125 126func TestSandboxIntegration_DeniesOtherRepoRead(t *testing.T) { 127 skipIfNoLandlock(t) 128 129 root := t.TempDir() 130 myRepo := filepath.Join(root, "did:plc:abc") 131 otherRepo := filepath.Join(root, "did:plc:xyz") 132 secret := filepath.Join(otherRepo, "secret-key") 133 mustMkdirAll(t, myRepo) 134 mustMkdirAll(t, otherRepo) 135 mustWriteFile(t, secret, "TOPSECRET\n") 136 137 allowed, out := runUnderSandbox(t, myRepo, secret, "read") 138 if allowed { 139 t.Errorf("expected read of other repo to be denied; child read: %s", strings.TrimSpace(out)) 140 } 141} 142 143func TestSandboxIntegration_AllowsGlobalConfigRead(t *testing.T) { 144 skipIfNoLandlock(t) 145 146 root := t.TempDir() 147 myRepo := filepath.Join(root, "did:plc:abc") 148 cfgDir := filepath.Join(root, ".config", "git") 149 cfg := filepath.Join(cfgDir, "config") 150 mustMkdirAll(t, myRepo) 151 mustMkdirAll(t, cfgDir) 152 mustWriteFile(t, cfg, "[user]\n\tname = test\n") 153 154 // the child reads $HOME via BuildRuleSpec, so set HOME for it explicitly. 155 t.Setenv("HOME", root) 156 157 allowed, out := runUnderSandbox(t, myRepo, cfg, "read") 158 if !allowed { 159 t.Errorf("expected read of $HOME/.config/git/config to be allowed; child said: %s", strings.TrimSpace(out)) 160 } 161} 162 163func TestSandboxIntegration_DeniesGlobalConfigSibling(t *testing.T) { 164 skipIfNoLandlock(t) 165 166 root := t.TempDir() 167 myRepo := filepath.Join(root, "did:plc:abc") 168 cfgDir := filepath.Join(root, ".config", "git") 169 sibling := filepath.Join(cfgDir, "attributes") 170 mustMkdirAll(t, myRepo) 171 mustMkdirAll(t, cfgDir) 172 mustWriteFile(t, sibling, "*.bin -text\n") 173 174 t.Setenv("HOME", root) 175 176 allowed, out := runUnderSandbox(t, myRepo, sibling, "read") 177 if allowed { 178 t.Errorf("expected read of sibling in .config/git/ to be denied (only the config file is granted); child read: %s", strings.TrimSpace(out)) 179 } 180} 181 182func TestSandboxIntegration_DeniesScanPathSibling(t *testing.T) { 183 skipIfNoLandlock(t) 184 185 root := t.TempDir() 186 myRepo := filepath.Join(root, "did:plc:abc") 187 dbFile := filepath.Join(root, "knotserver.db") 188 mustMkdirAll(t, myRepo) 189 mustWriteFile(t, dbFile, "fake db contents\n") 190 191 allowed, out := runUnderSandbox(t, myRepo, dbFile, "read") 192 if allowed { 193 t.Errorf("expected read of sibling file (knotserver.db) to be denied; child read: %s", strings.TrimSpace(out)) 194 } 195} 196 197func TestSandboxIntegration_DeniesScanPathListing(t *testing.T) { 198 skipIfNoLandlock(t) 199 200 root := t.TempDir() 201 myRepo := filepath.Join(root, "did:plc:abc") 202 mustMkdirAll(t, myRepo) 203 204 allowed, out := runUnderSandbox(t, myRepo, root, "list") 205 if allowed { 206 t.Errorf("expected listing scan path to be denied; child output: %s", strings.TrimSpace(out)) 207 } 208} 209 210func TestSandboxIntegration_AllowsOwnRepoWrite(t *testing.T) { 211 skipIfNoLandlock(t) 212 213 root := t.TempDir() 214 repo := filepath.Join(root, "did:plc:abc") 215 target := filepath.Join(repo, "new-file") 216 mustMkdirAll(t, repo) 217 218 allowed, out := runUnderSandbox(t, repo, target, "write") 219 if !allowed { 220 t.Errorf("expected write to own repo to be allowed; child said: %s", strings.TrimSpace(out)) 221 } 222} 223 224func TestSandboxIntegration_DeniesSystemWrite(t *testing.T) { 225 skipIfNoLandlock(t) 226 227 root := t.TempDir() 228 repo := filepath.Join(root, "did:plc:abc") 229 mustMkdirAll(t, repo) 230 231 // /etc is granted RO; writes must be denied. 232 allowed, out := runUnderSandbox(t, repo, "/etc/sandbox-test-should-fail", "write") 233 if allowed { 234 t.Errorf("expected write to /etc to be denied; child output: %s", strings.TrimSpace(out)) 235 } 236} 237 238func mustMkdirAll(t *testing.T, p string) { 239 t.Helper() 240 if err := os.MkdirAll(p, 0o755); err != nil { 241 t.Fatal(err) 242 } 243} 244 245func mustWriteFile(t *testing.T, p, content string) { 246 t.Helper() 247 if err := os.WriteFile(p, []byte(content), 0o644); err != nil { 248 t.Fatal(err) 249 } 250}