Monorepo for Tangled
Something went wrong. Try again.
8.3 kB · 252 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253//go:build linux
package sandbox
import ( "os/exec" "reflect" "strings" "syscall" "testing")
func TestBuildRuleSpec_SingleRepo(t *testing.T) { spec := buildRuleSpec([]string{"/home/git/did:plc:abc"}, "/home/git")
if got, want := spec.GitConfigRO, "/home/git/.config/git/config"; got != want { t.Errorf("GitConfigRO = %q, want %q", got, want) } if got, want := spec.RepoRW, []string{"/home/git/did:plc:abc"}; !reflect.DeepEqual(got, want) { t.Errorf("RepoRW = %q, want %q", got, want) } if got, want := spec.SystemRO, []string{"/usr", "/bin", "/lib", "/lib64", "/nix", "/etc"}; !reflect.DeepEqual(got, want) { t.Errorf("SystemRO = %q, want %q", got, want) } if got, want := spec.TmpRW, []string{"/tmp"}; !reflect.DeepEqual(got, want) { t.Errorf("TmpRW = %q, want %q", got, want) } if got, want := spec.DevRW, []string{"/dev"}; !reflect.DeepEqual(got, want) { t.Errorf("DevRW = %q, want %q", got, want) }}
func TestBuildRuleSpec_GitConfigFollowsHome(t *testing.T) { // the granted git config path must follow $HOME, not the repo path. this // is what makes the merge case work: tmpDir is under /tmp, but the // subprocess still resolves the global config from $HOME/.config/git/config. spec := buildRuleSpec([]string{"/tmp/git-clone-XYZ"}, "/home/git")
if got, want := spec.GitConfigRO, "/home/git/.config/git/config"; got != want { t.Errorf("GitConfigRO = %q, want %q", got, want) }}
func TestBuildRuleSpec_NoHome(t *testing.T) { // empty $HOME should not produce a bogus "/.config/git/config" entry. spec := buildRuleSpec([]string{"/home/git/did:plc:abc"}, "")
if spec.GitConfigRO != "" { t.Errorf("GitConfigRO = %q, want empty", spec.GitConfigRO) }}
func TestBuildRuleSpec_NeverGrantsScanPath(t *testing.T) { // the scan path (the repo's parent) must NEVER appear in any RW or RO // list. granting it would expose other repos and the knot DB via // Landlock RO + DAC group bits. this is the key invariant the rule // tightening was meant to enforce. spec := buildRuleSpec([]string{"/home/git/did:plc:abc"}, "/home/git")
parent := "/home/git" for _, group := range [][]string{spec.SystemRO, spec.DevRW, spec.TmpRW, spec.RepoRW} { for _, p := range group { if p == parent { t.Errorf("scan path %q must not appear in the ruleset; found in %q", parent, group) } } } if spec.GitConfigRO == parent { t.Errorf("scan path %q must not be granted as GitConfigRO", parent) }}
func TestBuildRuleSpec_EmptyInput(t *testing.T) { spec := buildRuleSpec(nil, "") if spec.GitConfigRO != "" { t.Errorf("GitConfigRO should be empty for nil input and no $HOME, got %q", spec.GitConfigRO) } if len(spec.RepoRW) != 0 { t.Errorf("RepoRW should be empty for nil input, got %q", spec.RepoRW) }}
func TestLandlockBackend_Name(t *testing.T) { if (&LandlockBackend{}).Name() != "landlock" { t.Error("Name should return \"landlock\"") }}
func TestLandlockBackend_WrapMulti_NoPaths(t *testing.T) { sb := &LandlockBackend{selfExe: "/proc/self/exe"} cmd := exec.Command("git", "status")
wrapped, err := sb.WrapMulti(nil, cmd) if err != nil { t.Fatalf("WrapMulti: %v", err) } if wrapped != cmd { t.Error("empty paths should return the original cmd unchanged") }}
func TestLandlockBackend_WrapMulti_ArgsConstruction(t *testing.T) { sb := &LandlockBackend{selfExe: "/path/to/knot"} cmd := exec.Command("git", "upload-pack", "--stateless-rpc", ".") cmd.Env = []string{"GIT_PROTOCOL=version=2"} cmd.Stdin = strings.NewReader("input")
wrapped, err := sb.WrapMulti([]string{"/repos/a", "/repos/b"}, cmd) if err != nil { t.Fatalf("WrapMulti: %v", err) }
// argv[0] is the selfExe if wrapped.Path != "/path/to/knot" { t.Errorf("Path = %q, want %q", wrapped.Path, "/path/to/knot") }
// argv should be: [knot, sandbox-exec, --repo-path=/repos/a, --repo-path=/repos/b, --, <abs-git>, upload-pack, ...] args := wrapped.Args if len(args) < 6 { t.Fatalf("Args too short: %v", args) } if args[0] != "/path/to/knot" { t.Errorf("Args[0] = %q, want %q", args[0], "/path/to/knot") } if args[1] != "sandbox-exec" { t.Errorf("Args[1] = %q, want %q", args[1], "sandbox-exec") } if args[2] != "--repo-path=/repos/a" { t.Errorf("Args[2] = %q, want --repo-path=/repos/a", args[2]) } if args[3] != "--repo-path=/repos/b" { t.Errorf("Args[3] = %q, want --repo-path=/repos/b", args[3]) } if args[4] != "--" { t.Errorf("Args[4] = %q, want --", args[4]) } // args[5] is the resolved absolute git path; just check it ends with /git if !strings.HasSuffix(args[5], "/git") && args[5] != "git" { t.Errorf("Args[5] = %q, want path ending in /git or bare \"git\"", args[5]) } if got := args[len(args)-1]; got != "." { t.Errorf("last arg = %q, want %q", got, ".") }
// Dir should be the first repo path so the kernel chdirs there // after setuid, before execve. if wrapped.Dir != "/repos/a" { t.Errorf("Dir = %q, want %q", wrapped.Dir, "/repos/a") }
// Env propagated if len(wrapped.Env) != 1 || wrapped.Env[0] != "GIT_PROTOCOL=version=2" { t.Errorf("Env = %v, want [GIT_PROTOCOL=version=2]", wrapped.Env) }
// Stdio propagated if wrapped.Stdin != cmd.Stdin { t.Error("Stdin not propagated to wrapped cmd") }}
func TestLandlockBackend_WrapMulti_NoLookupNoCredential(t *testing.T) { sb := &LandlockBackend{selfExe: "/path/to/knot"} // no lookup cmd := exec.Command("git", "status")
wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd) if err != nil { t.Fatalf("WrapMulti: %v", err) } if wrapped.SysProcAttr != nil && wrapped.SysProcAttr.Credential != nil { t.Error("no lookup configured; Credential should not be set") }}
func TestLandlockBackend_WrapMulti_LookupSetsCredential(t *testing.T) { // lookup deliberately returns a different gid (the service group) to // confirm that WrapMulti ignores it and uses uid as the primary gid. // see the comment in sandbox_linux.go for why this matters. sb := &LandlockBackend{ selfExe: "/path/to/knot", lookup: func(repoPath string) (uint32, uint32, error) { if repoPath != "/repos/a" { t.Errorf("lookup called with %q, want /repos/a", repoPath) } return 100042, 1234, nil }, } cmd := exec.Command("git", "status")
wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd) if err != nil { t.Fatalf("WrapMulti: %v", err) } if wrapped.SysProcAttr == nil || wrapped.SysProcAttr.Credential == nil { t.Fatal("Credential should be set when lookup returns uid > 0") } cred := wrapped.SysProcAttr.Credential if cred.Uid != 100042 { t.Errorf("Credential.Uid = %d, want 100042", cred.Uid) } if cred.Gid != 100042 { t.Errorf("Credential.Gid = %d, want 100042 (must equal Uid, not lookup's gid 1234)", cred.Gid) } // NoSetGroups must be false (the default) so the kernel calls // setgroups(0, NULL) and clears supplementary groups. NoSetGroups: true // would let the subprocess inherit the parent's groups (gitGroup). if cred.NoSetGroups { t.Error("NoSetGroups must be false so supplementary groups get cleared") } if len(cred.Groups) != 0 { t.Errorf("Groups = %v, want empty (no supplementary groups granted)", cred.Groups) }}
func TestLandlockBackend_WrapMulti_LookupErrSkipsCredential(t *testing.T) { sb := &LandlockBackend{ selfExe: "/path/to/knot", lookup: func(string) (uint32, uint32, error) { return 0, 0, syscall.ENOENT }, } cmd := exec.Command("git", "status")
wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd) if err != nil { t.Fatalf("WrapMulti: %v", err) } if wrapped.SysProcAttr != nil && wrapped.SysProcAttr.Credential != nil { t.Error("lookup errored; Credential should not be set") }}
func TestLandlockBackend_WrapMulti_LookupZeroSkipsCredential(t *testing.T) { // uid == 0 is treated as "don't drop" so we never accidentally drop to // root. Verify Credential isn't set in that case. sb := &LandlockBackend{ selfExe: "/path/to/knot", lookup: func(string) (uint32, uint32, error) { return 0, 0, nil }, } cmd := exec.Command("git", "status")
wrapped, err := sb.WrapMulti([]string{"/repos/a"}, cmd) if err != nil { t.Fatalf("WrapMulti: %v", err) } if wrapped.SysProcAttr != nil && wrapped.SysProcAttr.Credential != nil { t.Error("lookup returned uid=0; Credential should not be set") }}