mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestTheSetuidSearchRunsInTheBackgroundAndIsKeptForItsInterval (0.00s)
mesh/delivery superseded: a newer head of the same pull request
After the first apply the record is no longer used as found, so the directory read as the mesh's own and later applies set its mode and owner by path: a parent swapped for a link after the hand-over took carried root's chmod and chown to its target. The hand-over is now checked before that shortcut, so a handed-over directory always goes through applyHandedOver. The test swaps the parent and the grandparent after an apply already gave the directory its mode; both cases fail with the old order.
258 lines
10 KiB
Go
258 lines
10 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
// A directory used as found is never quiet, and a person at the machine can hand it over (novox/hq issue 339).
|
|
func TestAFoundDirectoryIsSaidAndCanBeHandedOver(t *testing.T) {
|
|
dir := t.TempDir()
|
|
found := filepath.Join(dir, "found")
|
|
if err := os.Mkdir(found, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
decl := `{"id":"notes.data","type":"directory","path":"` + found + `","mode":"0755"}`
|
|
var said []string
|
|
log := func(line string) { said = append(said, line) }
|
|
_, state, err := Apply(context.Background(), archHost(t), declare(t, decl), store.State{}, store.OriginDeclared,
|
|
noServices, log, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(strings.Join(said, "\n"), "WARNING notes.data") {
|
|
t.Fatalf("the apply did not warn of a directory used as found: %q", said)
|
|
}
|
|
if a, _ := state.Find("notes.data"); !a.AsFound || !strings.Contains(a.Declared, "755") {
|
|
t.Fatalf("the record does not say what was declared: %+v", a)
|
|
}
|
|
// Said again at the next apply: a warning once is a warning missed.
|
|
said = nil
|
|
if _, state, err = Apply(context.Background(), archHost(t), declare(t, decl), state, store.OriginDeclared,
|
|
noServices, log, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(strings.Join(said, "\n"), "WARNING notes.data") {
|
|
t.Fatalf("the next apply did not warn again: %q", said)
|
|
}
|
|
|
|
// Handed over: the next apply gives it the declared mode and it is the mesh's.
|
|
before := HandedOver
|
|
HandedOver = func() map[string]bool { return map[string]bool{handOverKey("notes.data", found): true} }
|
|
t.Cleanup(func() { HandedOver = before })
|
|
if _, state, err = Apply(context.Background(), archHost(t), declare(t, decl), state, store.OriginDeclared,
|
|
noServices, nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info, _ := os.Stat(found); info.Mode().Perm() != 0o755 {
|
|
t.Fatalf("a directory handed over was not given its declared mode: %o", info.Mode().Perm())
|
|
}
|
|
if a, _ := state.Find("notes.data"); a.AsFound {
|
|
t.Fatal("a directory handed over is still recorded as found")
|
|
}
|
|
}
|
|
|
|
// The review's additions to the guard: the spool, /opt, the container runtimes' data wherever the runtime keeps
|
|
// it, and an account's .ssh as an access or a mount, or as a directory anywhere but its own home.
|
|
func TestTheRuntimesDataAndAnySSHAreRefused(t *testing.T) {
|
|
cfg := filepath.Join(t.TempDir(), "daemon.json")
|
|
if err := os.WriteFile(cfg, []byte(`{"data-root":"/storage/docker"}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := runtimeConfigs
|
|
runtimeConfigs = runtimeFiles{daemonJSON: cfg}
|
|
t.Cleanup(func() { runtimeConfigs = before })
|
|
for _, kind := range []placing{placingDirectory, placingAccess, placingMount} {
|
|
for _, path := range []string{"/var/spool/cron", "/opt/app", "/var/lib/docker/volumes/x",
|
|
"/var/lib/containers/storage", "/storage/docker", "/storage/docker/overlay2"} {
|
|
if err := refusePath(path, kind, "notes", ""); err == nil {
|
|
t.Errorf("%v at %s was taken", kind, path)
|
|
}
|
|
}
|
|
}
|
|
for _, path := range []string{"/srv/backup/.ssh", "/srv/backup/.ssh/keys"} {
|
|
for _, kind := range []placing{placingDirectory, placingAccess, placingMount} {
|
|
if err := refusePath(path, kind, "notes", ""); err == nil {
|
|
t.Errorf("%v at %s was taken", kind, path)
|
|
}
|
|
}
|
|
}
|
|
if err := refusePath("/storage/dockerish", placingAccess, "notes", ""); err != nil {
|
|
t.Errorf("a path beside the data root: %v", err)
|
|
}
|
|
}
|
|
|
|
// When a directory was found is kept from the first apply that found it (novox/hq issue 339): the controller
|
|
// tells a directory a send found from one found before it by that time, so it must not move with every apply.
|
|
func TestWhenADirectoryWasFoundIsKept(t *testing.T) {
|
|
dir := t.TempDir()
|
|
found := filepath.Join(dir, "found")
|
|
if err := os.Mkdir(found, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
decl := `{"id":"notes.data","type":"directory","path":"` + found + `","mode":"0755"}`
|
|
_, state, err := Apply(context.Background(), archHost(t), declare(t, decl), store.State{}, store.OriginDeclared,
|
|
noServices, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first, _ := state.Find("notes.data")
|
|
if first.FoundAt.IsZero() {
|
|
t.Fatal("no time the directory was found")
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
_, state, err = Apply(context.Background(), archHost(t), declare(t, decl), state, store.OriginDeclared, noServices, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if again, _ := state.Find("notes.data"); !again.FoundAt.Equal(first.FoundAt) {
|
|
t.Fatalf("found at %v, then %v: the time moved with the apply", first.FoundAt, again.FoundAt)
|
|
}
|
|
}
|
|
|
|
// A hand-over is for the resource that was used as found there: another declaration that later points another
|
|
// resource at the same path is not handed anything (novox/hq issue 339).
|
|
func TestAHandOverIsForTheResourceThatAskedForIt(t *testing.T) {
|
|
dir := t.TempDir()
|
|
found := filepath.Join(dir, "found")
|
|
if err := os.Mkdir(found, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := HandedOver
|
|
HandedOver = func() map[string]bool { return map[string]bool{handOverKey("notes.data", found): true} }
|
|
t.Cleanup(func() { HandedOver = before })
|
|
other := `{"id":"other.data","type":"directory","path":"` + found + `","mode":"0755"}`
|
|
if _, _, err := Apply(context.Background(), archHost(t), declare(t, other), store.State{}, store.OriginDeclared,
|
|
noServices, nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info, _ := os.Stat(found); info.Mode().Perm() != 0o700 {
|
|
t.Fatalf("a hand-over for notes.data gave other.data the directory: mode %o", info.Mode().Perm())
|
|
}
|
|
}
|
|
|
|
// **A handed-over directory is changed through no link, at the moment of use** (review of issue 356). The
|
|
// hand-over refused a link when it was recorded; between then and the apply its parent is swapped for a link to
|
|
// a victim tree. The apply refuses, and the victim's directory keeps its mode — and so does a directory whose own
|
|
// name became a link, and one whose grandparent did.
|
|
func TestAHandedOverDirectoryIsChangedThroughNoLink(t *testing.T) {
|
|
for _, c := range []struct {
|
|
swap string
|
|
applyOnce bool
|
|
}{{"parent", false}, {"grandparent", false}, {"itself", false}, {"parent", true}, {"grandparent", true}} {
|
|
swap, applyOnce := c.swap, c.applyOnce
|
|
name := swap
|
|
if applyOnce {
|
|
name += " after an apply gave it its owner"
|
|
}
|
|
t.Run(name, func(t *testing.T) {
|
|
dir, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
found := filepath.Join(dir, "top", "parent", "data")
|
|
if err := os.MkdirAll(found, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
decl := `{"id":"notes.data","type":"directory","path":"` + found + `","mode":"0755"}`
|
|
_, state, err := Apply(context.Background(), archHost(t), declare(t, decl), store.State{}, store.OriginDeclared,
|
|
noServices, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a, _ := state.Find("notes.data"); !a.AsFound {
|
|
t.Fatalf("not used as found: %+v", a)
|
|
}
|
|
before := HandedOver
|
|
HandedOver = func() map[string]bool { return map[string]bool{handOverKey("notes.data", found): true} }
|
|
t.Cleanup(func() { HandedOver = before })
|
|
if applyOnce {
|
|
// The hand-over takes: the directory is given its mode, and the record is no longer as found.
|
|
if _, state, err = Apply(context.Background(), archHost(t), declare(t, decl), state, store.OriginDeclared,
|
|
noServices, nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a, _ := state.Find("notes.data"); a.AsFound {
|
|
t.Fatalf("still as found after the hand-over took: %+v", a)
|
|
}
|
|
if info, _ := os.Stat(found); info.Mode().Perm() != 0o755 {
|
|
t.Fatalf("the hand-over did not take: %o", info.Mode().Perm())
|
|
}
|
|
}
|
|
|
|
// The victim: a tree of the same shape somewhere else, its data directory mode 0700.
|
|
victim := filepath.Join(dir, "victim", "top", "parent", "data")
|
|
if err := os.MkdirAll(victim, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var swapped, target string
|
|
switch swap {
|
|
case "parent":
|
|
swapped, target = filepath.Join(dir, "top", "parent"), filepath.Join(dir, "victim", "top", "parent")
|
|
case "grandparent":
|
|
swapped, target = filepath.Join(dir, "top"), filepath.Join(dir, "victim", "top")
|
|
case "itself":
|
|
swapped, target = found, victim
|
|
}
|
|
if err := os.Rename(swapped, swapped+".real"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(target, swapped); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _, err = Apply(context.Background(), archHost(t), declare(t, decl), state, store.OriginDeclared,
|
|
noServices, nil, nil)
|
|
if err == nil {
|
|
t.Fatal("a handed-over directory reached through a link was applied")
|
|
}
|
|
// Refused by the path's re-check before any descriptor is opened (a link at the name itself is refused
|
|
// earlier still, by the directory applier's own look at it).
|
|
want := "reached through a link"
|
|
if swap == "itself" {
|
|
want = "symbolic link"
|
|
}
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Fatalf("refused as %v; want %q", err, want)
|
|
}
|
|
if info, _ := os.Stat(victim); info.Mode().Perm() != 0o700 {
|
|
t.Fatalf("the victim's directory was given the declared mode through the link: %o", info.Mode().Perm())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The check at the moment of use, without the path's re-check: a component swapped for a link is refused by the
|
|
// descriptor walk itself, whatever was resolved a moment before.
|
|
func TestGiveHandedOverFollowsNoLink(t *testing.T) {
|
|
dir, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
victim := filepath.Join(dir, "victim", "data")
|
|
if err := os.MkdirAll(victim, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(filepath.Join(dir, "victim"), filepath.Join(dir, "parent")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, _, err := giveHandedOver(filepath.Join(dir, "parent", "data"), 0o755, 0, 0, false); err == nil {
|
|
t.Fatal("a directory reached through a link was changed")
|
|
}
|
|
if info, _ := os.Stat(victim); info.Mode().Perm() != 0o700 {
|
|
t.Fatalf("the victim was changed: %o", info.Mode().Perm())
|
|
}
|
|
mode, _, _, err := giveHandedOver(victim, 0o750, 0, 0, false)
|
|
if err != nil || mode.Perm() != 0o700 {
|
|
t.Fatalf("the directory itself: before %o, %v", mode, err)
|
|
}
|
|
if info, _ := os.Stat(victim); info.Mode().Perm() != 0o750 {
|
|
t.Fatalf("the directory itself was not given its mode: %o", info.Mode().Perm())
|
|
}
|
|
}
|