package accounts import ( "io/fs" "os" "path/filepath" "strings" "testing" ) // A setuid program no package owns counts against an account only where it can execute it, read at every // judgement from the modes on the way (novox/hq issue 361): the operator's copies in a home of mode 0700 are no // way for the agent account, and become one the moment the home is opened, without a new search. const homeFind = "/home/op/usr/bin/mount" func homeMachine(home FileMode) *agentMachine { m := clean() m.setuid = homeFind + "\n" m.nodes = map[string]FileMode{"/home/op": home} return m } func TestReachabilityDecidesWhetherAFindCounts(t *testing.T) { op := func(perm fs.FileMode) FileMode { return FileMode{UID: 1500, GID: 1500, Perm: fs.ModeDir | perm} } for _, c := range []struct { name string set func(*agentMachine) counted bool }{ {"a home of 0700", func(m *agentMachine) {}, false}, {"a home of 0755", func(m *agentMachine) { m.nodes["/home/op"] = op(0o755) }, true}, {"a home its group may search, the agent in it", func(m *agentMachine) { m.nodes["/home/op"], m.gids = op(0o710), "1600 1500" }, true}, {"a home its group may search, the agent not in it", func(m *agentMachine) { m.nodes["/home/op"] = op(0o710) }, false}, {"an ACL on the home", func(m *agentMachine) { m.files["/home/op"] = FileMode{UID: 1500, GID: 1500, Perm: 0o700} m.acls = map[string][]ACLEntry{"/home/op": {{}}} }, true}, {"a link on the way", func(m *agentMachine) { m.nodes["/home/op"] = FileMode{UID: 1500, GID: 1500, Perm: fs.ModeSymlink | 0o777} }, true}, {"a file the agent may not execute", func(m *agentMachine) { m.nodes["/home/op"] = op(0o755) m.nodes[homeFind] = FileMode{Perm: fs.ModeSetuid | 0o4750} }, false}, } { t.Run(c.name, func(t *testing.T) { m := homeMachine(FileMode{UID: 1500, GID: 1500, Perm: fs.ModeDir | 0o700}) c.set(m) v := lookAgent(t, m) if c.counted != (v.State == Unhealthy) { t.Fatalf("counted %v: %+v", c.counted, v) } if strings.Contains(v.Reason, "/home/op") { t.Fatalf("a path in a home was said: %s", v.Reason) } if c.counted && !strings.Contains(v.Reason, "a setuid-root program no package owns in an account's home, which agent can execute") { t.Fatalf("the reason: %s", v.Reason) } }) } } // The search keeps every find; whether one counts is read again at every look. func TestAHomeOpenedAfterTheSearchCountsWithoutANewSearch(t *testing.T) { m := homeMachine(FileMode{UID: 1500, GID: 1500, Perm: fs.ModeDir | 0o700}) c := &SetuidCache{} j := New(Exec{Run: m.run, Walk: m.walk, Stat: m.stat, ReadFile: m.readFile, ReadDir: m.readDir, ACL: m.acl, Lstat: m.lstat, Cache: c}) j.Set([]Account{agent}) j.Look(t.Context()) c.Wait() if st, _ := j.Look(t.Context()); st.Accounts[0].State != Healthy { t.Fatalf("a find in a closed home: %+v", st.Accounts[0]) } m.nodes["/home/op"] = FileMode{UID: 1500, GID: 1500, Perm: fs.ModeDir | 0o755} st, _ := j.Look(t.Context()) if st.Accounts[0].State != Unhealthy || starts(c) != 1 { t.Fatalf("the home opened: %+v, %d searches", st.Accounts[0], starts(c)) } m.nodes["/home/op"] = FileMode{UID: 1500, GID: 1500, Perm: fs.ModeDir | 0o700} if st, _ := j.Look(t.Context()); st.Accounts[0].State != Healthy || starts(c) != 1 { t.Fatalf("the home closed again: %+v", st.Accounts[0]) } } // On the machine's own modes, as their owner (the directories above a test's are its own): a directory without // its owner's search bit hides what is in it. func TestExecutableReadsTheMachinesModes(t *testing.T) { if os.Getuid() == 0 { t.Skip("root searches every directory") } top := tree(t, "home/bin/x!") home := filepath.Join(top, "home") me := os.Getuid() noACL := func(string) ([]ACLEntry, error) { return nil, nil } if err := os.Chmod(home, 0o600); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Chmod(home, 0o700) }) if Executable(filepath.Join(home, "bin/x"), me, map[int]bool{}, lstatOf, noACL) { t.Fatal("a file in a directory its account may not search was executable") } if err := os.Chmod(home, 0o700); err != nil { t.Fatal(err) } if !Executable(filepath.Join(home, "bin/x"), me, map[int]bool{}, lstatOf, noACL) { t.Fatal("a file every directory opens was not executable") } if Executable(filepath.Join(home, "bin/gone"), me, map[int]bool{}, lstatOf, noACL) { t.Fatal("a file gone was executable") } }