check / check (push) Waiting to run
A hard link in /data shares its owner with a file that can be anywhere on the same filesystem, so chowning it gave that file to the netwatch user. A file with more than one name now keeps its owner and its path is logged. Owners are now set on the opened directory, file or link after checking it, as chmodDir sets modes, so a hard link swapped in afterwards is caught. Every other file is opened without following it; a symbolic link is given to the netwatch user itself, and devices, named pipes and sockets keep their owner. This needs Linux, the image's system; elsewhere it returns an error. A failed owner change names the file. Model: opus-5-5
340 lines
8.0 KiB
Go
340 lines
8.0 KiB
Go
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 the directories, regular
|
|
// files and symbolic links 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. A hard link to a file outside the volume, one to a
|
|
// symbolic link outside it and one to a named pipe outside it are left
|
|
// as they were, and so is a named pipe in the volume.
|
|
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.Symlink("f", filepath.Join(outside, "link"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = syscall.Mkfifo(filepath.Join(outside, "fifo"), 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 = syscall.Mkfifo(filepath.Join(old, "fifo"), 0o600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// os.Link does not follow a symbolic link: it gives the link itself
|
|
// a second name.
|
|
for _, name := range []string{"f", "link", "fifo"} {
|
|
err = os.Link(filepath.Join(outside, name),
|
|
filepath.Join(old, "hardlink-to-"+name))
|
|
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)
|
|
}
|
|
|
|
for _, path := range []string{
|
|
outside, filepath.Join(outside, "f"), filepath.Join(outside, "link"),
|
|
filepath.Join(outside, "fifo"), filepath.Join(old, "fifo"),
|
|
} {
|
|
requireOwner(t, path, 0, 0)
|
|
}
|
|
}
|