Monorepo for Tangled tangled.org
5

Configure Feed

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

at icy/lqyotq 7.9 kB View raw
1//go:build linux 2 3package sandbox 4 5import ( 6 "os/exec" 7 "reflect" 8 "strings" 9 "syscall" 10 "testing" 11) 12 13func TestBuildRuleSpec_SingleRepo(t *testing.T) { 14 spec := buildRuleSpec([]string{"/home/git/did:plc:abc"}, "/home/git") 15 16 if got, want := spec.GitConfigRO, "/home/git/.config/git/config"; got != want { 17 t.Errorf("GitConfigRO = %q, want %q", got, want) 18 } 19 if got, want := spec.RepoRW, []string{"/home/git/did:plc:abc"}; !reflect.DeepEqual(got, want) { 20 t.Errorf("RepoRW = %q, want %q", got, want) 21 } 22 if got, want := spec.SystemRO, []string{"/usr", "/bin", "/lib", "/lib64", "/nix", "/etc"}; !reflect.DeepEqual(got, want) { 23 t.Errorf("SystemRO = %q, want %q", got, want) 24 } 25 if got, want := spec.TmpRW, []string{"/tmp"}; !reflect.DeepEqual(got, want) { 26 t.Errorf("TmpRW = %q, want %q", got, want) 27 } 28 if got, want := spec.DevRW, []string{"/dev"}; !reflect.DeepEqual(got, want) { 29 t.Errorf("DevRW = %q, want %q", got, want) 30 } 31} 32 33func TestBuildRuleSpec_GitConfigFollowsHome(t *testing.T) { 34 // the granted git config path must follow $HOME, not the repo path. this 35 // is what makes the merge case work: tmpDir is under /tmp, but the 36 // subprocess still resolves the global config from $HOME/.config/git/config. 37 spec := buildRuleSpec([]string{"/tmp/git-clone-XYZ"}, "/home/git") 38 39 if got, want := spec.GitConfigRO, "/home/git/.config/git/config"; got != want { 40 t.Errorf("GitConfigRO = %q, want %q", got, want) 41 } 42} 43 44func TestBuildRuleSpec_NoHome(t *testing.T) { 45 // empty $HOME should not produce a bogus "/.config/git/config" entry. 46 spec := buildRuleSpec([]string{"/home/git/did:plc:abc"}, "") 47 48 if spec.GitConfigRO != "" { 49 t.Errorf("GitConfigRO = %q, want empty", spec.GitConfigRO) 50 } 51} 52 53func TestBuildRuleSpec_NeverGrantsScanPath(t *testing.T) { 54 // the scan path (the repo's parent) must NEVER appear in any RW or RO 55 // list. granting it would expose other repos and the knot DB via 56 // Landlock RO + DAC group bits. this is the key invariant the rule 57 // tightening was meant to enforce. 58 spec := buildRuleSpec([]string{"/home/git/did:plc:abc"}, "/home/git") 59 60 parent := "/home/git" 61 for _, group := range [][]string{spec.SystemRO, spec.DevRW, spec.TmpRW, spec.RepoRW} { 62 for _, p := range group { 63 if p == parent { 64 t.Errorf("scan path %q must not appear in the ruleset; found in %q", parent, group) 65 } 66 } 67 } 68 if spec.GitConfigRO == parent { 69 t.Errorf("scan path %q must not be granted as GitConfigRO", parent) 70 } 71} 72 73func TestBuildRuleSpec_EmptyInput(t *testing.T) { 74 spec := buildRuleSpec(nil, "") 75 if spec.GitConfigRO != "" { 76 t.Errorf("GitConfigRO should be empty for nil input and no $HOME, got %q", spec.GitConfigRO) 77 } 78 if len(spec.RepoRW) != 0 { 79 t.Errorf("RepoRW should be empty for nil input, got %q", spec.RepoRW) 80 } 81} 82 83func TestLandlockBackend_Name(t *testing.T) { 84 if (&LandlockBackend{}).Name() != "landlock" { 85 t.Error("Name should return \"landlock\"") 86 } 87} 88 89func TestLandlockBackend_WrapMulti_NoPaths(t *testing.T) { 90 sb := &LandlockBackend{selfExe: "/proc/self/exe"} 91 cmd := exec.Command("git", "status") 92 93 wrapped, err := sb.WrapMulti(nil, cmd) 94 if err != nil { 95 t.Fatalf("WrapMulti: %v", err) 96 } 97 if wrapped != cmd { 98 t.Error("empty paths should return the original cmd unchanged") 99 } 100} 101 102func TestLandlockBackend_WrapMulti_ArgsConstruction(t *testing.T) { 103 sb := &LandlockBackend{selfExe: "/path/to/knot"} 104 cmd := exec.Command("git", "upload-pack", "--stateless-rpc", ".") 105 cmd.Env = []string{"GIT_PROTOCOL=version=2"} 106 cmd.Stdin = strings.NewReader("input") 107 108 wrapped, err := sb.WrapMulti([]string{"/repos/a", "/repos/b"}, cmd) 109 if err != nil { 110 t.Fatalf("WrapMulti: %v", err) 111 } 112 113 // argv[0] is the selfExe 114 if wrapped.Path != "/path/to/knot" { 115 t.Errorf("Path = %q, want %q", wrapped.Path, "/path/to/knot") 116 } 117 118 // argv should be: [knot, sandbox-exec, --repo-path=/repos/a, --repo-path=/repos/b, --, <abs-git>, upload-pack, ...] 119 args := wrapped.Args 120 if len(args) < 6 { 121 t.Fatalf("Args too short: %v", args) 122 } 123 if args[0] != "/path/to/knot" { 124 t.Errorf("Args[0] = %q, want %q", args[0], "/path/to/knot") 125 } 126 if args[1] != "sandbox-exec" { 127 t.Errorf("Args[1] = %q, want %q", args[1], "sandbox-exec") 128 } 129 if args[2] != "--repo-path=/repos/a" { 130 t.Errorf("Args[2] = %q, want --repo-path=/repos/a", args[2]) 131 } 132 if args[3] != "--repo-path=/repos/b" { 133 t.Errorf("Args[3] = %q, want --repo-path=/repos/b", args[3]) 134 } 135 if args[4] != "--" { 136 t.Errorf("Args[4] = %q, want --", args[4]) 137 } 138 // args[5] is the resolved absolute git path; just check it ends with /git 139 if !strings.HasSuffix(args[5], "/git") && args[5] != "git" { 140 t.Errorf("Args[5] = %q, want path ending in /git or bare \"git\"", args[5]) 141 } 142 if got := args[len(args)-1]; got != "." { 143 t.Errorf("last arg = %q, want %q", got, ".") 144 } 145 146 // Dir should be the first repo path so the kernel chdirs there 147 // after setuid, before execve. 148 if wrapped.Dir != "/repos/a" { 149 t.Errorf("Dir = %q, want %q", wrapped.Dir, "/repos/a") 150 } 151 152 // Env propagated 153 if len(wrapped.Env) != 1 || wrapped.Env[0] != "GIT_PROTOCOL=version=2" { 154 t.Errorf("Env = %v, want [GIT_PROTOCOL=version=2]", wrapped.Env) 155 } 156 157 // Stdio propagated 158 if wrapped.Stdin != cmd.Stdin { 159 t.Error("Stdin not propagated to wrapped cmd") 160 } 161} 162 163func TestLandlockBackend_WrapMulti_NoLookupNoCredential(t *testing.T) { 164 sb := &LandlockBackend{selfExe: "/path/to/knot"} // no lookup 165 cmd := exec.Command("git", "status") 166 167 wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd) 168 if err != nil { 169 t.Fatalf("WrapMulti: %v", err) 170 } 171 if wrapped.SysProcAttr != nil && wrapped.SysProcAttr.Credential != nil { 172 t.Error("no lookup configured; Credential should not be set") 173 } 174} 175 176func TestLandlockBackend_WrapMulti_LookupSetsCredential(t *testing.T) { 177 // lookup deliberately returns a different gid (the service group) to 178 // confirm that WrapMulti ignores it and uses uid as the primary gid. 179 // see the comment in sandbox_linux.go for why this matters. 180 sb := &LandlockBackend{ 181 selfExe: "/path/to/knot", 182 lookup: func(repoPath string) (uint32, uint32, error) { 183 if repoPath != "/repos/a" { 184 t.Errorf("lookup called with %q, want /repos/a", repoPath) 185 } 186 return 100042, 1234, nil 187 }, 188 } 189 cmd := exec.Command("git", "status") 190 191 wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd) 192 if err != nil { 193 t.Fatalf("WrapMulti: %v", err) 194 } 195 if wrapped.SysProcAttr == nil || wrapped.SysProcAttr.Credential == nil { 196 t.Fatal("Credential should be set when lookup returns uid > 0") 197 } 198 cred := wrapped.SysProcAttr.Credential 199 if cred.Uid != 100042 { 200 t.Errorf("Credential.Uid = %d, want 100042", cred.Uid) 201 } 202 if cred.Gid != 100042 { 203 t.Errorf("Credential.Gid = %d, want 100042 (must equal Uid, not lookup's gid 1234)", cred.Gid) 204 } 205 if !cred.NoSetGroups { 206 t.Error("NoSetGroups should be true") 207 } 208} 209 210func TestLandlockBackend_WrapMulti_LookupErrSkipsCredential(t *testing.T) { 211 sb := &LandlockBackend{ 212 selfExe: "/path/to/knot", 213 lookup: func(string) (uint32, uint32, error) { 214 return 0, 0, syscall.ENOENT 215 }, 216 } 217 cmd := exec.Command("git", "status") 218 219 wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd) 220 if err != nil { 221 t.Fatalf("WrapMulti: %v", err) 222 } 223 if wrapped.SysProcAttr != nil && wrapped.SysProcAttr.Credential != nil { 224 t.Error("lookup errored; Credential should not be set") 225 } 226} 227 228func TestLandlockBackend_WrapMulti_LookupZeroSkipsCredential(t *testing.T) { 229 // uid == 0 is treated as "don't drop" so we never accidentally drop to 230 // root. Verify Credential isn't set in that case. 231 sb := &LandlockBackend{ 232 selfExe: "/path/to/knot", 233 lookup: func(string) (uint32, uint32, error) { 234 return 0, 0, nil 235 }, 236 } 237 cmd := exec.Command("git", "status") 238 239 wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd) 240 if err != nil { 241 t.Fatalf("WrapMulti: %v", err) 242 } 243 if wrapped.SysProcAttr != nil && wrapped.SysProcAttr.Credential != nil { 244 t.Error("lookup returned uid=0; Credential should not be set") 245 } 246}