The two finds on the control-node sit in the operator's home of mode 0700, which the agent account cannot search. The search keeps every find; each judgement reads the modes on the way again, since an account can open its own home without root. A find in a home is said by count, its path in the journal alone. Also from review: a checkpoint racing SearchNow forgets the walk it just kept, and a walk kept by an older engine says its counts as lower bounds.
116 lines
4.3 KiB
Go
116 lines
4.3 KiB
Go
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")
|
|
}
|
|
}
|