//go:build linux package hostenv import ( "os" "path/filepath" "strings" "testing" ) // U3 (census condition 2 on runHostEnv): write's path is fixed, and the // file is written only through etc and etc/carlos held by descriptor, // each a real directory this user owns and nobody else can write. func TestWriteGoesOnlyThroughDirectoriesRootAloneCanWrite(t *testing.T) { read := func(root string) string { b, _ := os.ReadFile(filepath.Join(root, "etc", "carlos", "host.env")) return string(b) } t.Run("lands whole, and replaces whole", func(t *testing.T) { root := withHostEnvRoot(t) if err := writeHostEnv(root, []byte("CARLOS_REGION=ie\n")); err != nil { t.Fatal(err) } fi, err := os.Stat(filepath.Join(root, "etc", "carlos")) if err != nil || fi.Mode().Perm()&0o022 != 0 || !fi.IsDir() { t.Fatalf("created directory %v %v", fi, err) } first, err := os.Stat(filepath.Join(root, "etc", "carlos", "host.env")) if err != nil { t.Fatal(err) } if err := writeHostEnv(root, []byte("CARLOS_REGION=uk\n")); err != nil { t.Fatal(err) } f, err := os.Stat(filepath.Join(root, "etc", "carlos", "host.env")) if err != nil || f.Mode().Perm() != 0o600 || read(root) != "CARLOS_REGION=uk\n" { t.Fatalf("host.env %v %v %q", f, err, read(root)) } // Replaced by rename, not rewritten in place: a reader holding the // old file never sees part of the new one. if os.SameFile(first, f) { t.Fatal("host.env was rewritten in place") } ents, _ := os.ReadDir(filepath.Join(root, "etc", "carlos")) if len(ents) != 1 { t.Fatalf("left behind: %v", ents) } }) for name, spoil := range map[string]func(t *testing.T, root string){ "a group-writable etc/carlos": func(t *testing.T, root string) { dir := filepath.Join(root, "etc", "carlos") if err := os.Mkdir(dir, 0o755); err != nil { t.Fatal(err) } if err := os.Chmod(dir, 0o775); err != nil { t.Fatal(err) } }, "a world-writable etc": func(t *testing.T, root string) { if err := os.Chmod(filepath.Join(root, "etc"), 0o757); err != nil { t.Fatal(err) } }, "an etc/carlos another user owns": func(t *testing.T, root string) { if err := os.Mkdir(filepath.Join(root, "etc", "carlos"), 0o755); err != nil { t.Fatal(err) } was := hostEnvOwner hostEnvOwner = func() int { return was() + 1 } t.Cleanup(func() { hostEnvOwner = was }) }, "a symlinked etc/carlos": func(t *testing.T, root string) { elsewhere := safeDir(t) if err := os.Symlink(elsewhere, filepath.Join(root, "etc", "carlos")); err != nil { t.Fatal(err) } }, "a symlinked etc, an ancestor": func(t *testing.T, root string) { elsewhere := safeDir(t) if err := os.Mkdir(filepath.Join(elsewhere, "carlos"), 0o755); err != nil { t.Fatal(err) } if err := os.Remove(filepath.Join(root, "etc")); err != nil { t.Fatal(err) } if err := os.Symlink(elsewhere, filepath.Join(root, "etc")); err != nil { t.Fatal(err) } }, } { t.Run(name, func(t *testing.T) { root := withHostEnvRoot(t) spoil(t, root) err := writeHostEnv(root, []byte("CARLOS_REGION=ie\n")) if err == nil || !strings.Contains(err.Error(), "is not a directory only root can write") { t.Fatalf("%v", err) } if got := read(root); got != "" { t.Fatalf("wrote %q", got) } }) } } // safeDir is a directory that passes every check but the one under test: // this user's, 0755 whatever the umask, so a symlink to it is refused only // for being a symlink. func safeDir(t *testing.T) string { t.Helper() dir := t.TempDir() if err := os.Chmod(dir, 0o755); err != nil { t.Fatal(err) } return dir }