//go:build linux package instancefs import ( "errors" "os" "path/filepath" "syscall" "testing" "time" ) // OpenDirBeneath and OpenRootBeneath open a directory as OpenBeneath opens // a file: no symlink at the name or anywhere in dir, never waiting on a // FIFO, and nothing but a directory accepted. func TestOpenDirBeneathRefusesWhatATenantCanPlant(t *testing.T) { opens := map[string]func(dir, name string) (func(), error){ "OpenDirBeneath": func(dir, name string) (func(), error) { f, err := OpenDirBeneath(dir, name) if err != nil { return nil, err } return func() { f.Close() }, nil }, "OpenRootBeneath": func(dir, name string) (func(), error) { r, err := OpenRootBeneath(dir, name) if err != nil { return nil, err } return func() { r.Close() }, nil }, } for fn, openIt := range opens { for _, tc := range []struct { name string plant func(t *testing.T, parent string) dir func(parent string) string want error }{ {"a symlink at the name", func(t *testing.T, parent string) { if err := os.Symlink(t.TempDir(), filepath.Join(parent, "d")); err != nil { t.Fatal(err) } }, nil, ErrSymlink}, {"a symlinked parent", func(t *testing.T, parent string) { if err := os.Mkdir(filepath.Join(parent, "d"), 0o700); err != nil { t.Fatal(err) } if err := os.Symlink(parent, parent+".link"); err != nil { t.Fatal(err) } }, func(parent string) string { return parent + ".link" }, ErrSymlink}, {"a FIFO", func(t *testing.T, parent string) { if err := syscall.Mkfifo(filepath.Join(parent, "d"), 0o600); err != nil { t.Fatal(err) } }, nil, nil}, {"a regular file", func(t *testing.T, parent string) { if err := os.WriteFile(filepath.Join(parent, "d"), nil, 0o600); err != nil { t.Fatal(err) } }, nil, nil}, } { t.Run(fn+"/"+tc.name, func(t *testing.T) { parent := filepath.Join(t.TempDir(), "inst") if err := os.Mkdir(parent, 0o700); err != nil { t.Fatal(err) } tc.plant(t, parent) dir := parent if tc.dir != nil { dir = tc.dir(parent) } done := make(chan error, 1) go func() { c, err := openIt(dir, "d") if c != nil { c() } done <- err }() select { case err := <-done: // nil want is "not a directory": O_DIRECTORY answers // ENOTDIR, and the fstat check ErrNotDir behind it. notDir := errors.Is(err, syscall.ENOTDIR) || errors.Is(err, ErrNotDir) if (tc.want == nil && !notDir) || (tc.want != nil && !errors.Is(err, tc.want)) { t.Fatalf("got %v, want %v", err, tc.want) } case <-time.After(20 * time.Second): t.Fatal("the open blocked on what was planted") } }) } } } // What OpenRootBeneath hands back is the directory it vetted, even once // the path names something else. func TestOpenRootBeneathIsTheDirectoryItOpened(t *testing.T) { parent := t.TempDir() d := filepath.Join(parent, "d") if err := os.Mkdir(d, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(d, "mine"), []byte("x"), 0o600); err != nil { t.Fatal(err) } r, err := OpenRootBeneath(parent, "d") if err != nil { t.Fatal(err) } defer r.Close() if err := os.Rename(d, d+".moved"); err != nil { t.Fatal(err) } if err := os.Symlink(t.TempDir(), d); err != nil { t.Fatal(err) } if _, err := r.Stat("mine"); err != nil { t.Fatalf("the root is not the directory that was opened: %v", err) } f, err := OpenDirBeneath(parent, ".") if err != nil { t.Fatalf("dir itself: %v", err) } f.Close() } // The root is the directory OpenDirBeneath vetted, never the path opened // again: a link swapped in between the two is not followed. func TestOpenRootBeneathNeverOpensThePathAgain(t *testing.T) { parent := t.TempDir() d := filepath.Join(parent, "d") if err := os.Mkdir(d, 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(d, "mine"), []byte("x"), 0o600); err != nil { t.Fatal(err) } prev := betweenOpenAndRoot betweenOpenAndRoot = func() { if err := os.Rename(d, d+".moved"); err != nil { t.Error(err) } if err := os.Symlink(t.TempDir(), d); err != nil { t.Error(err) } } t.Cleanup(func() { betweenOpenAndRoot = prev }) r, err := OpenRootBeneath(parent, "d") if err != nil { t.Fatal(err) } defer r.Close() if _, err := r.Stat("mine"); err != nil { t.Fatalf("the root followed a link swapped in after the open: %v", err) } }