//go:build linux package instancefs import ( "errors" "os" "path/filepath" "strings" "testing" "time" "golang.org/x/sys/unix" ) func TestOpenBeneathReadsAPlainFile(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "h.env"), []byte("A=1\n"), 0o600); err != nil { t.Fatal(err) } got, err := ReadBeneath(dir, "h.env") if err != nil { t.Fatalf("ReadBeneath: %v", err) } if string(got) != "A=1\n" { t.Errorf("got %q", got) } } func TestOpenBeneathRefusesASymlinkAtTheLeaf(t *testing.T) { dir := t.TempDir() secret := filepath.Join(t.TempDir(), "secret") if err := os.WriteFile(secret, []byte("token"), 0o600); err != nil { t.Fatal(err) } if err := os.Symlink(secret, filepath.Join(dir, "h.env")); err != nil { t.Fatal(err) } _, err := ReadBeneath(dir, "h.env") if !errors.Is(err, ErrSymlink) { t.Fatalf("ReadBeneath through a symlink: err = %v, want ErrSymlink", err) } } func TestOpenBeneathRefusesASymlinkedDirectory(t *testing.T) { dir := t.TempDir() other := t.TempDir() if err := os.WriteFile(filepath.Join(other, "x"), []byte("x"), 0o600); err != nil { t.Fatal(err) } if err := os.Symlink(other, filepath.Join(dir, "sub")); err != nil { t.Fatal(err) } _, err := ReadBeneath(dir, "sub/x") if !errors.Is(err, ErrSymlink) { t.Fatalf("err = %v, want ErrSymlink", err) } } // TestOpenBeneathRefusesASymlinkedDir pins the review fix: openDir must // refuse a symlink at dir itself, not only at an intermediate component // under it. A same-uid process that swaps the instance directory for a // symlink before the mount option lands must be caught the same way a // symlink deeper in the path already is. func TestOpenBeneathRefusesASymlinkedDir(t *testing.T) { real := t.TempDir() if err := os.WriteFile(filepath.Join(real, "x"), []byte("x"), 0o600); err != nil { t.Fatal(err) } link := filepath.Join(t.TempDir(), "linked-dir") if err := os.Symlink(real, link); err != nil { t.Fatal(err) } _, err := ReadBeneath(link, "x") if !errors.Is(err, ErrSymlink) { t.Fatalf("ReadBeneath(%q, \"x\"): err = %v, want ErrSymlink", link, err) } } func TestOpenBeneathRefusesDotDotAndAbsolute(t *testing.T) { dir := t.TempDir() for _, name := range []string{"../x", "a/../../x", "/etc/passwd"} { _, err := ReadBeneath(dir, name) if !errors.Is(err, ErrEscapes) { t.Errorf("ReadBeneath(%q): err = %v, want ErrEscapes", name, err) } } } func TestWriteAtomicReplacesAndNeverFollows(t *testing.T) { dir := t.TempDir() if err := WriteAtomic(dir, "h.config.env", []byte("one\n"), 0o600); err != nil { t.Fatalf("WriteAtomic: %v", err) } if err := WriteAtomic(dir, "h.config.env", []byte("two\n"), 0o600); err != nil { t.Fatalf("WriteAtomic (replace): %v", err) } got, err := os.ReadFile(filepath.Join(dir, "h.config.env")) if err != nil || string(got) != "two\n" { t.Fatalf("got %q, %v", got, err) } fi, err := os.Stat(filepath.Join(dir, "h.config.env")) if err != nil || fi.Mode().Perm() != 0o600 { t.Errorf("mode = %v, %v", fi.Mode(), err) } // A symlink planted at the destination is REPLACED as a directory // entry; its target is untouched. victim := filepath.Join(t.TempDir(), "victim") if err := os.WriteFile(victim, []byte("keep"), 0o600); err != nil { t.Fatal(err) } if err := os.Remove(filepath.Join(dir, "h.config.env")); err != nil { t.Fatal(err) } if err := os.Symlink(victim, filepath.Join(dir, "h.config.env")); err != nil { t.Fatal(err) } if err := WriteAtomic(dir, "h.config.env", []byte("three\n"), 0o600); err != nil { t.Fatalf("WriteAtomic over a symlink: %v", err) } if b, _ := os.ReadFile(victim); string(b) != "keep" { t.Errorf("symlink target was written through: %q", b) } if fi, err := os.Lstat(filepath.Join(dir, "h.config.env")); err != nil || fi.Mode()&os.ModeSymlink != 0 { t.Errorf("destination is still a symlink: %v %v", fi, err) } // No temp file is left behind. entries, _ := os.ReadDir(dir) for _, e := range entries { if strings.Contains(e.Name(), ".tmp-") { t.Errorf("temp file left behind: %s", e.Name()) } } } func TestMountNoSymfollowReadsMountinfo(t *testing.T) { // / is a mount and does not carry nosymfollow on any ordinary box; a // path that is not a mountpoint is reported as not a nosymfollow mount // with a nil error (the caller's message names the path). got, err := MountNoSymfollow("/") if err != nil { t.Fatalf("MountNoSymfollow(/): %v", err) } if got { t.Error("/ reported as nosymfollow") } got, err = MountNoSymfollow(t.TempDir()) if err != nil || got { t.Errorf("temp dir: %v %v", got, err) } // The parser, against a fixture line, since a real nosymfollow mount // needs root to create. line := "36 35 98:0 /.instances /data/instances rw,nosuid,nodev,noexec,nosymfollow,relatime - ext4 /dev/root rw" if !mountinfoLineHas(line, "/data/instances", "nosymfollow") { t.Error("parser missed nosymfollow on a fixture line") } if mountinfoLineHas(line, "/data", "nosymfollow") { t.Error("parser matched the wrong mountpoint") } // Stacked mounts at the same mountpoint list oldest first; the LAST // line is the one actually in effect, so the verdict must follow it // even when an earlier line at the same mountpoint disagrees. first := "36 35 98:0 /.instances /data/instances rw,nosuid,nodev,noexec,relatime - ext4 /dev/root rw" last := "37 35 98:0 /.instances /data/instances rw,nosuid,nodev,noexec,nosymfollow,relatime - ext4 /dev/root rw" stacked := strings.NewReader(first + "\n" + last + "\n") got, err = mountOptionOf(stacked, "/data/instances", "nosymfollow") if err != nil || !got { t.Errorf("stacked mounts, last with nosymfollow: got %v, %v, want true, nil", got, err) } stackedReversed := strings.NewReader(last + "\n" + first + "\n") got, err = mountOptionOf(stackedReversed, "/data/instances", "nosymfollow") if err != nil || got { t.Errorf("stacked mounts, last WITHOUT nosymfollow: got %v, %v, want false, nil (the earlier nosymfollow line must not outvote the effective one)", got, err) } } // TestMountOptionOfIsTheGeneralForm pins that MountNoSymfollow is now a // thin wrapper over MountOptionOf, not a separate implementation: asking // for an option other than nosymfollow (here "ro", the option the // cgroup-ro-mount probe check asks about) reads the same field. func TestMountOptionOfIsTheGeneralForm(t *testing.T) { line := "36 35 98:0 / /sys/fs/cgroup ro,nosuid,nodev,noexec - cgroup2 cgroup2 rw" got, err := mountOptionOf(strings.NewReader(line+"\n"), "/sys/fs/cgroup", "ro") if err != nil || !got { t.Errorf("MountOptionOf(/sys/fs/cgroup, ro): got %v, %v, want true, nil", got, err) } got, err = mountOptionOf(strings.NewReader(line+"\n"), "/sys/fs/cgroup", "nosymfollow") if err != nil || got { t.Errorf("MountOptionOf(/sys/fs/cgroup, nosymfollow) on a line without it: got %v, %v, want false, nil", got, err) } got, err = MountOptionOf("/", "ro") if err != nil { t.Fatalf("MountOptionOf(/, ro) against the live mountinfo: %v", err) } _ = got // / is not reliably ro or rw across test hosts; only the absence of an error matters here. } // TestOpenBeneathRefusesAFIFOInsteadOfBlocking pins the review fix: a FIFO // with no writer blocks open(2) indefinitely, and a tenant can plant one at // any name the agent reads by contract. The read must fail fast with // ErrNotRegular rather than hang the caller, so this runs it in a goroutine // with its own timeout: a regression here must fail the test, not wedge it. func TestOpenBeneathRefusesAFIFOInsteadOfBlocking(t *testing.T) { dir := t.TempDir() if err := unix.Mkfifo(filepath.Join(dir, "h.env"), 0o600); err != nil { t.Fatalf("Mkfifo: %v", err) } done := make(chan error, 1) go func() { _, err := ReadBeneath(dir, "h.env") done <- err }() select { case err := <-done: if !errors.Is(err, ErrNotRegular) { t.Fatalf("ReadBeneath a FIFO: err = %v, want ErrNotRegular", err) } case <-time.After(2 * time.Second): t.Fatal("ReadBeneath blocked on a FIFO with no writer instead of refusing it") } } // TestOpenBeneathRefusesAWriteOpenOfAFIFO pins the write-side half of the // same fix: a write-open of a FIFO with no reader on the other end blocks // open(2) indefinitely too, and the admin token file this backs an append // onto is on the same tenant-writable ground as anything OpenBeneath reads. // O_NONBLOCK turns that into an immediate ENXIO instead. func TestOpenBeneathRefusesAWriteOpenOfAFIFO(t *testing.T) { dir := t.TempDir() if err := unix.Mkfifo(filepath.Join(dir, "token"), 0o600); err != nil { t.Fatalf("Mkfifo: %v", err) } done := make(chan error, 1) go func() { _, err := OpenBeneath(dir, "token", os.O_WRONLY|os.O_APPEND, 0) done <- err }() select { case err := <-done: if err == nil { t.Fatal("write-open of a FIFO with no reader succeeded, want an error") } case <-time.After(2 * time.Second): t.Fatal("write-open blocked on a FIFO with no reader instead of refusing it") } } func TestReadBeneathLimitRefusesALargeFile(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "big"), make([]byte, 4096), 0o600); err != nil { t.Fatal(err) } if _, err := ReadBeneathLimit(dir, "big", 1024); !errors.Is(err, ErrTooLarge) { t.Fatalf("err = %v, want ErrTooLarge", err) } got, err := ReadBeneathLimit(dir, "big", 4096) if err != nil || len(got) != 4096 { t.Fatalf("exact limit: %d bytes, %v", len(got), err) } } // TestSelfCheckPassesWhereOpenat2Works pins the shape of the agent's own // openat2 check: a plain directory passes, a symlinked one is refused as a // symlink (the same rule as every other open here), and a missing one // reports its own error rather than ErrOpenat2Unavailable, which is kept // for the one answer that means the call itself was blocked. func TestSelfCheckPassesWhereOpenat2Works(t *testing.T) { dir := t.TempDir() if err := SelfCheck(dir); err != nil { t.Fatalf("SelfCheck(%s) = %v, want nil", dir, err) } link := filepath.Join(dir, "link") if err := os.Symlink(dir, link); err != nil { t.Fatal(err) } if err := SelfCheck(link); !errors.Is(err, ErrSymlink) { t.Errorf("SelfCheck(symlink) = %v, want ErrSymlink", err) } err := SelfCheck(filepath.Join(dir, "missing")) if err == nil || errors.Is(err, ErrOpenat2Unavailable) { t.Errorf("SelfCheck(missing) = %v, want ENOENT and not ErrOpenat2Unavailable", err) } }