package reportbuf_test import ( "errors" "io/fs" "os" "os/user" "path/filepath" "strconv" "syscall" "testing" "sneak.berlin/go/netwatch/internal/reportbuf" ) // reports is the last part of DATA_DIR in these tests, as in the // image's /data/reports. const reports = "reports" // currentUser is the user the test runs as, the only owner a test not // run as root can give files to. func currentUser() *user.User { return &user.User{ Uid: strconv.Itoa(os.Getuid()), Gid: strconv.Itoa(os.Getgid()), } } // tempDirMode700 is a new directory in a t.TempDir with mode 0700, so // a test can tell that PrepareDataDir left its mode alone. func tempDirMode700(t *testing.T) string { t.Helper() dir := filepath.Join(t.TempDir(), "d") err := os.Mkdir(dir, 0o700) if err != nil { t.Fatal(err) } return dir } func requireMode(t *testing.T, path string, want fs.FileMode) { t.Helper() info, err := os.Stat(path) if err != nil { t.Fatal(err) } if info.Mode() != want { t.Errorf("%s: mode %v, want %v", path, info.Mode(), want) } } func requireMissing(t *testing.T, path string) { t.Helper() _, err := os.Lstat(path) if !errors.Is(err, fs.ErrNotExist) { t.Errorf("%s: created, or Lstat failed: %v", path, err) } } func requireOwner(t *testing.T, path string, uid, gid uint32) { t.Helper() info, err := os.Lstat(path) if err != nil { t.Fatal(err) } stat, _ := info.Sys().(*syscall.Stat_t) if stat.Uid != uid || stat.Gid != gid { t.Errorf("%s: owner %d:%d, want %d:%d", path, stat.Uid, stat.Gid, uid, gid) } } func TestPrepareDataDirCreatesDataDir(t *testing.T) { t.Parallel() volume := t.TempDir() dir := filepath.Join(volume, "a", reports) err := reportbuf.PrepareDataDir(volume, dir, currentUser()) if err != nil { t.Fatal(err) } requireMode(t, volume, fs.ModeDir|0o750) requireMode(t, dir, fs.ModeDir|0o750) } // TestPrepareDataDirSetsModeOfExistingDataDir: a DATA_DIR already on // the host with another mode gets the mode too, not only a new one. func TestPrepareDataDirSetsModeOfExistingDataDir(t *testing.T) { t.Parallel() volume := t.TempDir() dir := filepath.Join(volume, reports) err := os.Mkdir(dir, 0o700) if err != nil { t.Fatal(err) } err = reportbuf.PrepareDataDir(volume, dir, currentUser()) if err != nil { t.Fatal(err) } requireMode(t, dir, fs.ModeDir|0o750) } func TestPrepareDataDirTakesTheVolumeItself(t *testing.T) { t.Parallel() volume := t.TempDir() err := reportbuf.PrepareDataDir(volume, volume, currentUser()) if err != nil { t.Fatal(err) } requireMode(t, volume, fs.ModeDir|0o750) } // TestPrepareDataDirRefusesDataDirOutsideVolume covers a DATA_DIR that // is relative, outside the volume, or not written in full. func TestPrepareDataDirRefusesDataDirOutsideVolume(t *testing.T) { t.Parallel() volume := tempDirMode700(t) for _, dir := range []string{ reports, volume + "/../new", volume + "/", volume + "//" + reports, volume + "/./" + reports, volume + "/" + reports + "/..", volume + "x", "/etc", } { err := reportbuf.PrepareDataDir(volume, dir, currentUser()) if !errors.Is(err, reportbuf.ErrDataDirOutsideVolume) { t.Errorf("%q: error = %v, want ErrDataDirOutsideVolume", dir, err) } } requireMissing(t, filepath.Join(filepath.Dir(volume), "new")) requireMode(t, volume, fs.ModeDir|0o700) } // TestPrepareDataDirRefusesLinkOutOfVolume puts a symbolic link to a // directory outside the volume on the path to DATA_DIR, written as a // full path and as one that climbs out with '..', and as DATA_DIR // itself, where the mode would be set through it. func TestPrepareDataDirRefusesLinkOutOfVolume(t *testing.T) { t.Parallel() for _, tc := range []struct { name string climbsOut bool link, dir string }{ {"full path", false, "x", "x/reports"}, {"climbs out", true, "x", "x/reports"}, {"DATA_DIR itself", false, reports, reports}, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() outside := tempDirMode700(t) volume := t.TempDir() climbOut, err := filepath.Rel(volume, outside) if err != nil { t.Fatal(err) } target := outside if tc.climbsOut { target = climbOut } err = os.Symlink(target, filepath.Join(volume, tc.link)) if err != nil { t.Fatal(err) } err = reportbuf.PrepareDataDir(volume, filepath.Join(volume, tc.dir), currentUser()) if err == nil { t.Error("no error") } requireMissing(t, filepath.Join(outside, reports)) requireMode(t, outside, fs.ModeDir|0o700) }) } } // TestPrepareDataDirRefusesDanglingLink: DATA_DIR is a symbolic link // to a name in the volume that does not exist, which is not created. func TestPrepareDataDirRefusesDanglingLink(t *testing.T) { t.Parallel() volume := t.TempDir() err := os.Symlink("missing", filepath.Join(volume, reports)) if err != nil { t.Fatal(err) } err = reportbuf.PrepareDataDir(volume, filepath.Join(volume, reports), currentUser()) if err == nil { t.Error("no error") } requireMissing(t, filepath.Join(volume, "missing")) } // TestPrepareDataDirTakesDirectoryNamedInLatin1: a host directory can // hold names that are not valid UTF-8, here "café" written in Latin-1. // A test not run as root can only check that PrepareDataDir goes into // such a directory and gives it, and what it holds, to the current // user. func TestPrepareDataDirTakesDirectoryNamedInLatin1(t *testing.T) { t.Parallel() volume := t.TempDir() latin1 := filepath.Join(volume, "caf\xe9") err := os.Mkdir(latin1, 0o700) if err != nil { t.Fatal(err) } err = os.WriteFile(filepath.Join(latin1, "f"), nil, 0o600) if err != nil { t.Fatal(err) } owner := currentUser() err = reportbuf.PrepareDataDir(volume, filepath.Join(volume, reports), owner) if err != nil { t.Fatal(err) } uid, _ := strconv.ParseUint(owner.Uid, 10, 32) gid, _ := strconv.ParseUint(owner.Gid, 10, 32) requireOwner(t, latin1, uint32(uid), uint32(gid)) requireOwner(t, filepath.Join(latin1, "f"), uint32(uid), uint32(gid)) } // TestPrepareDataDirGivesVolumeToOwner gives everything in the volume // to a uid and gid that own nothing, which only root can do. A // symbolic link in the volume to a directory outside it is given to // them itself; what it points to is left as it was. func TestPrepareDataDirGivesVolumeToOwner(t *testing.T) { t.Parallel() if os.Geteuid() != 0 { t.Skip("only root can give files to another uid") } outside := t.TempDir() volume := t.TempDir() old := filepath.Join(volume, "old") err := os.WriteFile(filepath.Join(outside, "f"), nil, 0o600) if err != nil { t.Fatal(err) } err = os.Mkdir(old, 0o700) if err != nil { t.Fatal(err) } err = os.WriteFile(filepath.Join(old, "f"), nil, 0o600) if err != nil { t.Fatal(err) } err = os.Symlink(outside, filepath.Join(old, "link")) if err != nil { t.Fatal(err) } err = reportbuf.PrepareDataDir(volume, filepath.Join(volume, reports), &user.User{Uid: "4242", Gid: "4343"}) if err != nil { t.Fatal(err) } for _, path := range []string{ volume, filepath.Join(volume, reports), old, filepath.Join(old, "f"), filepath.Join(old, "link"), } { requireOwner(t, path, 4242, 4343) } requireOwner(t, outside, 0, 0) requireOwner(t, filepath.Join(outside, "f"), 0, 0) }