package integration
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"reflect"
"runtime"
"syscall"
"testing"
"golang.org/x/sys/unix"
"github.com/bazelbuild/sandboxfs/integration/utils"
)
func TestReadOnly_DirectoryStructure(t *testing.T) {
state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%", "--mapping=ro:/mappings/dir:%ROOT%/mappings/dir", "--mapping=ro:/mappings/scaffold/dir:%ROOT%/mappings/dir")
defer state.TearDown(t)
utils.MustMkdirAll(t, state.RootPath("dir1"), 0755)
utils.MustMkdirAll(t, state.RootPath("dir2"), 0500)
utils.MustMkdirAll(t, state.RootPath("dir3/dir1"), 0700)
utils.MustMkdirAll(t, state.RootPath("dir3/dir2"), 0755)
utils.MustMkdirAll(t, state.RootPath("mappings/dir"), 0555)
utils.MustMkdirAll(t, state.RootPath("mappings/scaffold"), 0555)
if err := os.Chmod(state.RootPath("mappings"), 0555); err != nil {
t.Fatalf("Failed to set permissions on temporary directory: %v", err)
}
defer os.Chmod(state.RootPath("mappings"), 0755)
for _, dir := range []string{"", "dir1", "dir2", "dir3/dir1", "dir3/dir2", "mappings"} {
if err := utils.DirEquals(state.RootPath(dir), state.MountPath(dir)); err != nil {
t.Error(err)
}
}
}
func TestReadOnly_FileContents(t *testing.T) {
state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%")
defer state.TearDown(t)
utils.MustWriteFile(t, state.RootPath("file"), 0400, "foo")
utils.MustMkdirAll(t, state.RootPath("dir1/dir2"), 0755)
utils.MustWriteFile(t, state.RootPath("dir1/dir2/file"), 0600, "bar baz")
for i := 0; i < 1000; i++ {
if err := utils.FileEquals(state.MountPath("file"), "foo"); err != nil {
t.Error(err)
}
if err := utils.FileEquals(state.MountPath("dir1/dir2/file"), "bar baz"); err != nil {
t.Error(err)
}
}
}
func TestReadOnly_ReplaceUnderlyingFile(t *testing.T) {
state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%")
defer state.TearDown(t)
externalFile := state.RootPath("foo")
internalFile := state.MountPath("foo")
utils.MustWriteFile(t, externalFile, 0600, "old contents")
if err := utils.FileEquals(internalFile, "old contents"); err != nil {
t.Fatalf("Test file doesn't match expected contents: %v", err)
}
utils.MustWriteFile(t, externalFile, 0600, "new contents")
err := utils.FileEquals(internalFile, "new contents")
switch runtime.GOOS {
case "darwin":
if err == nil {
t.Fatalf("Test file matches expected contents, but we know it shouldn't have on this platform")
}
case "linux":
if err != nil {
t.Fatalf("Test file doesn't match expected contents: %v", err)
}
default:
t.Fatalf("Don't know how this test behaves in this platform")
}
}
func TestReadOnly_MoveUnderlyingDirectory(t *testing.T) {
state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%")
defer state.TearDown(t)
utils.MustMkdirAll(t, state.RootPath("first/a"), 0755)
utils.MustMkdirAll(t, state.RootPath("first/b"), 0755)
utils.MustMkdirAll(t, state.RootPath("first/c"), 0755)
utils.MustMkdirAll(t, state.RootPath("second/1"), 0755)
if err := utils.DirEquals(state.RootPath("first"), state.MountPath("first")); err != nil {
t.Fatal(err)
}
if err := utils.DirEquals(state.RootPath("second"), state.MountPath("second")); err != nil {
t.Fatal(err)
}
if err := os.Rename(state.RootPath("first"), state.RootPath("third")); err != nil {
t.Fatalf("Failed to move underlying directory away: %v", err)
}
if err := os.Rename(state.RootPath("second"), state.RootPath("first")); err != nil {
t.Fatalf("Failed to replace previous underlying directory: %v", err)
}
if err := utils.DirEquals(state.RootPath("first"), state.MountPath("first")); err != nil {
t.Error(err)
}
if err := utils.DirEquals(state.RootPath("third"), state.MountPath("third")); err != nil {
t.Error(err)
}
}
func TestReadOnly_ReadLargeDir(t *testing.T) {
state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%", "--mapping=ro:/dir:%ROOT%/dir", "--mapping=ro:/scaffold/abc:%ROOT%/dir")
defer state.TearDown(t)
utils.MustMkdirAll(t, state.RootPath("dir"), 0755)
wantNames := make(map[string]bool)
for i := 0; i < 4096; i++ {
name := fmt.Sprintf("this-is-a-long-file-name-%08d", i)
utils.MustWriteFile(t, state.RootPath("dir", name), 0644, "")
wantNames[name] = true
}
entries, err := ioutil.ReadDir(state.MountPath("dir"))
if err != nil {
t.Fatalf("readdir failed: %v", err)
}
names := make(map[string]bool)
for _, entry := range entries {
if _, ok := names[entry.Name()]; ok {
t.Errorf("readdir returned duplicate entry for %s", entry.Name())
}
names[entry.Name()] = true
}
for wantName := range wantNames {
if _, ok := names[wantName]; !ok {
t.Errorf("readdir didn't return entry for %s", wantName)
}
}
for name := range names {
if _, ok := wantNames[name]; !ok {
t.Errorf("readdir returned entry for non-existent entry %s", name)
}
}
}
func TestReadOnly_RepeatedReadDirsWhileDirIsOpen(t *testing.T) {
state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%", "--mapping=ro:/dir:%ROOT%/dir", "--mapping=ro:/scaffold/abc:%ROOT%/dir")
defer state.TearDown(t)
utils.MustMkdirAll(t, state.RootPath("mapped-dir"), 0755)
utils.MustWriteFile(t, state.RootPath("mapped-file"), 0644, "")
utils.MustMkdirAll(t, state.RootPath("dir/mapped-dir-2"), 0755)
utils.MustWriteFile(t, state.RootPath("dir/mapped-file-2"), 0644, "")
testData := []struct {
name string
dir string
wantNames []string }{
{"Root", "/", []string{"dir", "mapped-dir", "mapped-file", "scaffold"}},
{"MappedDir", "/dir", []string{"mapped-dir-2", "mapped-file-2"}},
{"ScaffoldDir", "/scaffold", []string{"abc"}},
}
for _, d := range testData {
t.Run(d.name, func(t *testing.T) {
path := state.MountPath(d.dir)
handle, err := os.OpenFile(path, os.O_RDONLY, 0)
if err != nil {
t.Fatalf("Failed to open directory %s: %v", path, err)
}
defer handle.Close()
for i := 0; i < 5; i++ {
err := utils.DirEntryNamesEqual(path, d.wantNames)
if err != nil {
t.Errorf("Failed iteration %d: %v", i, err)
}
}
})
}
}
func TestReadOnly_Attributes(t *testing.T) {
state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%")
defer state.TearDown(t)
utils.MustMkdirAll(t, state.RootPath("dir"), 0755)
utils.MustWriteFile(t, state.RootPath("file"), 0644, "new content")
utils.MustSymlink(t, "missing", state.RootPath("symlink"))
for _, name := range []string{"dir", "file", "symlink"} {
outerPath := state.RootPath(name)
outerFileInfo, err := os.Lstat(outerPath)
if err != nil {
t.Fatalf("Failed to stat %s: %v", outerPath, err)
}
outerStat := outerFileInfo.Sys().(*syscall.Stat_t)
innerPath := state.MountPath(name)
innerFileInfo, err := os.Lstat(innerPath)
if err != nil {
t.Fatalf("Failed to stat %s: %v", innerPath, err)
}
innerStat := innerFileInfo.Sys().(*syscall.Stat_t)
if innerFileInfo.Mode() != outerFileInfo.Mode() {
t.Errorf("Got mode %v for %s, want %v", innerFileInfo.Mode(), innerPath, outerFileInfo.Mode())
}
if utils.Atime(innerStat) != utils.Atime(outerStat) {
t.Errorf("Got atime %v for %s, want %v", utils.Atime(innerStat), innerPath, utils.Atime(outerStat))
}
if innerFileInfo.ModTime() != outerFileInfo.ModTime() {
t.Errorf("Got mtime %v for %s, want %v", innerFileInfo.ModTime(), innerPath, outerFileInfo.ModTime())
}
if utils.Ctime(innerStat) != utils.Ctime(outerStat) {
t.Errorf("Got ctime %v for %s, want %v", utils.Ctime(innerStat), innerPath, utils.Ctime(outerStat))
}
if innerStat.Nlink != outerStat.Nlink {
t.Errorf("Got nlink %v for %s, want %v", innerStat.Nlink, innerPath, outerStat.Nlink)
}
if innerStat.Rdev != outerStat.Rdev {
t.Errorf("Got rdev %v for %s, want %v", innerStat.Rdev, innerPath, outerStat.Rdev)
}
wantBlksize := outerStat.Blksize switch runtime.GOOS {
case "darwin":
wantBlksize = 65536
case "linux":
wantBlksize = 4096
default:
t.Fatalf("Don't know how this test behaves in this platform")
}
if innerStat.Blksize != wantBlksize {
t.Errorf("Got blocksize %v for %s, want %v", innerStat.Blksize, innerPath, wantBlksize)
}
}
}
func TestReadOnly_Access(t *testing.T) {
mustMkdirAs := func(user *utils.UnixUser, path string, mode os.FileMode) {
cmd := exec.Command("mkdir", path)
utils.SetCredential(cmd, user)
if err := cmd.Run(); err != nil {
t.Fatalf("Failed to mkdir %s as %v: %v", path, user, err)
}
if err := os.Chmod(path, mode); err != nil {
t.Fatalf("Failed to chmod %v %s: %v", mode, path, err)
}
}
testAs := func(user *utils.UnixUser, path string, op string) error {
cmd := exec.Command("test", op, path)
utils.SetCredential(cmd, user)
return cmd.Run()
}
root := utils.RequireRoot(t, "Requires root privileges to test permissions as various user combinations")
user, err := utils.LookupUserOtherThan(root.Username)
if err != nil {
t.Fatal(err)
}
t.Logf("Using unprivileged user: %v", user)
state := utils.MountSetupWithUser(t, root, "--allow=other", "--mapping=ro:/:%ROOT%", "--mapping=ro:/scaffold/dir/foo:%ROOT%/foo")
defer state.TearDown(t)
utils.MustMkdirAll(t, state.RootPath("all"), 0777)
mustMkdirAs(root, state.RootPath("all/root"), 0755)
mustMkdirAs(root, state.RootPath("all/root/self"), 0700)
mustMkdirAs(root, state.RootPath("all/root/self/hidden"), 0500)
mustMkdirAs(root, state.RootPath("all/root/everyone-ro"), 0555)
mustMkdirAs(user, state.RootPath("all/user"), 0755)
mustMkdirAs(user, state.RootPath("all/user/self"), 0700)
mustMkdirAs(user, state.RootPath("all/user/self/hidden"), 0500)
mustMkdirAs(user, state.RootPath("all/user/everyone-ro"), 0555)
testData := []struct {
name string
runAs *utils.UnixUser
testFile string
testOp string
wantOk bool
}{
{"RootCanLookupUser", root, "all/user/self/hidden", "-e", true},
{"RootCanLookupRoot", root, "all/root/self/hidden", "-e", true},
{"RootCanReadUser", root, "all/user/self", "-r", true},
{"RootCanReadRoot", root, "all/root/self", "-r", true},
{"RootCanWriteUser", root, "all/user/self", "-w", true},
{"RootCanWriteRoot", root, "all/root/self", "-w", true},
{"RootCanExecuteUser", root, "all/user/self", "-x", true},
{"RootCanExecuteRoot", root, "all/root/self", "-x", true},
{"RootCanReadOwnReadOnly", root, "all/root/everyone-ro", "-r", true},
{"RootCanWriteOwnReadOnly", root, "all/root/everyone-ro", "-w", true},
{"UserCanLookupUser", user, "all/user/self/hidden", "-e", true},
{"UserCannotLookupRoot", user, "all/root/self/hidden", "-e", false},
{"UserCanReadUser", user, "all/user/self", "-r", true},
{"UserCannotReadRoot", user, "all/root/self", "-r", false},
{"UserCanWriteUser", user, "all/user/self", "-w", true},
{"UserCannotWriteRoot", user, "all/root/self", "-w", false},
{"UserCanExecuteUser", user, "all/user/self", "-x", true},
{"UserCannotExecuteRoot", user, "all/root/self", "-x", false},
{"UserCanReadOwnReadOnly", user, "all/user/everyone-ro", "-r", true},
{"UserCannotWriteOwnReadOnly", user, "all/user/everyone-ro", "-w", false},
{"RootCanLookupScaffoldDir", root, "scaffold/dir", "-e", true},
{"RootCanReadScaffoldDir", root, "scaffold/dir", "-r", true},
{"RootCanWriteScaffoldDir", root, "scaffold/dir", "-w", true},
{"RootCanExecuteScaffoldDir", root, "scaffold/dir", "-x", true},
{"UserCanLookupScaffoldDir", user, "scaffold/dir", "-e", true},
{"UserCanReadScaffoldDir", user, "scaffold/dir", "-r", true},
{"UserCannotWriteScaffoldDir", user, "scaffold/dir", "-w", false},
{"UserCanExecuteScaffoldDir", user, "scaffold/dir", "-x", true},
}
for _, d := range testData {
t.Run(d.name, func(t *testing.T) {
err := testAs(d.runAs, state.MountPath(d.testFile), d.testOp)
if d.wantOk && err != nil {
t.Errorf("Want test %s %s to succeed; got %v", d.testOp, d.testFile, err)
} else if !d.wantOk && err == nil {
t.Errorf("Want test %s %s to fail; got success", d.testOp, d.testFile)
}
})
}
}
func TestReadOnly_HardLinkCountsAreFixed(t *testing.T) {
state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%", "--mapping=ro:/scaffold/dir:%ROOT%/dir")
defer state.TearDown(t)
utils.MustMkdirAll(t, state.RootPath("dir"), 0755)
utils.MustWriteFile(t, state.RootPath("no-links"), 0644, "")
utils.MustWriteFile(t, state.RootPath("name1"), 0644, "")
if err := os.Link(state.RootPath("name1"), state.RootPath("name2")); err != nil {
t.Fatalf("Failed to create hard link in underlying file system: %v", err)
}
testData := []struct {
name string
file string
wantNlink int
}{
{"MappedDir", "dir", 2},
{"FileWithOnlyOneName", "no-links", 1},
{"FileWithManyNames", "name1", 1},
{"ScaffoldDir", "scaffold", 2},
}
for _, d := range testData {
t.Run(d.name, func(t *testing.T) {
fileInfo, err := os.Lstat(state.MountPath(d.file))
if err != nil {
t.Fatalf("Failed to stat %s in mount point: %v", d.file, err)
}
stat := fileInfo.Sys().(*syscall.Stat_t)
if int(stat.Nlink) != d.wantNlink {
t.Errorf("Want hard link count for %s to be %d; got %d", d.file, d.wantNlink, stat.Nlink)
}
})
}
}
func TestReadOnly_ReadFromDirFails(t *testing.T) {
state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%")
defer state.TearDown(t)
utils.MustMkdirAll(t, state.RootPath("dir"), 0755)
fd, err := unix.Open(state.MountPath("dir"), unix.O_RDONLY, 0)
if err != nil {
t.Fatalf("Failed to open directory %s: %v", state.MountPath("dir"), err)
}
defer unix.Close(fd)
buffer := make([]byte, 1024)
_, err = unix.Read(fd, buffer)
if err == nil || err != unix.EISDIR {
t.Errorf("Want error to be EISDIR; got %v", err)
}
}
func TestReadOnly_ReaddirFromFileFails(t *testing.T) {
state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%")
defer state.TearDown(t)
utils.MustWriteFile(t, state.RootPath("file"), 0644, "")
fd, err := unix.Open(state.MountPath("file"), unix.O_RDONLY, 0)
if err != nil {
t.Fatalf("Failed to open file %s: %v", state.MountPath("file"), err)
}
defer unix.Close(fd)
buffer := make([]byte, 1024)
_, err = unix.ReadDirent(fd, buffer)
if err == nil || (err != unix.EINVAL && err != unix.ENOTDIR) {
t.Errorf("Want error to be %v or %v; got %v", unix.EINVAL, unix.ENOTDIR, err)
}
}
func TestReadOnly_Listxattrs(t *testing.T) {
state := utils.MountSetup(t, "--xattrs", "--mapping=ro:/:%ROOT%")
defer state.TearDown(t)
utils.MustMkdirAll(t, state.RootPath("dir"), 0755)
utils.MustWriteFile(t, state.RootPath("file"), 0644, "new content")
utils.MustSymlink(t, "missing", state.RootPath("symlink"))
tests := []string{"dir", "file"}
if runtime.GOOS != "linux" { tests = append(tests, "symlink")
}
for _, name := range tests {
if err := unix.Lsetxattr(state.RootPath(name), "user.first", []byte{}, 0); err != nil {
t.Fatalf("Lsetxattr(%s) failed: %v", name, err)
}
if err := unix.Lsetxattr(state.RootPath(name), "user.second", []byte{}, 0); err != nil {
t.Fatalf("Lsetxattr(%s) failed: %v", name, err)
}
for _, path := range []string{state.MountPath(name), state.RootPath(name)} {
buf := make([]byte, 32)
sz, err := unix.Llistxattr(path, buf)
if err != nil {
t.Fatalf("Llistxattr(%s) failed: %v", path, err)
}
list := buf[0:sz]
wantList := []byte("user.first\000user.second\000")
if !reflect.DeepEqual(list, wantList) {
t.Errorf("Invalid attributes list for %s: got %s, want %s", path, list, wantList)
}
}
}
}
func TestReadOnly_ListxattrsOnScaffoldDirectory(t *testing.T) {
state := utils.MountSetup(t, "--xattrs", "--mapping=ro:/:%ROOT%", "--mapping=ro:/scaffold/dir:%ROOT%")
defer state.TearDown(t)
path := state.MountPath("scaffold")
buf := make([]byte, 32)
sz, err := unix.Llistxattr(path, buf)
if err != nil {
t.Fatalf("Llistxattr(%s) failed: %v", path, err)
}
if sz != 0 {
t.Errorf("Got attributes list for scaffold dir, want nothing")
}
}
func TestReadOnly_ListxattrsDisabled(t *testing.T) {
state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%")
defer state.TearDown(t)
utils.MustMkdirAll(t, state.RootPath("dir"), 0755)
if err := unix.Lsetxattr(state.RootPath("dir"), "user.foo", []byte{}, 0); err != nil {
t.Fatalf("Lsetxattr failed: %v", err)
}
switch runtime.GOOS {
case "darwin":
buf := make([]byte, 32)
sz, err := unix.Llistxattr(state.MountPath("dir"), buf)
if err != nil {
t.Fatalf("Llistxattr failed: %v", err)
}
if sz != 0 {
t.Errorf("Llistxattr should not have returned anything")
}
case "linux":
buf := make([]byte, 32)
if _, err := unix.Llistxattr(state.MountPath("dir"), buf); err != unix.EOPNOTSUPP {
t.Fatalf("Llistxattr should have failed with %v, but got %v", unix.EOPNOTSUPP, err)
}
default:
panic("Don't know how this test behaves on this platform")
}
}
func TestReadOnly_Getxattr(t *testing.T) {
state := utils.MountSetup(t, "--xattrs", "--mapping=ro:/:%ROOT%")
defer state.TearDown(t)
utils.MustMkdirAll(t, state.RootPath("dir"), 0755)
utils.MustWriteFile(t, state.RootPath("file"), 0644, "new content")
utils.MustSymlink(t, "missing", state.RootPath("symlink"))
tests := []string{"dir", "file"}
if runtime.GOOS != "linux" { tests = append(tests, "symlink")
}
for _, name := range tests {
wantValue := []byte("some-value")
if err := unix.Lsetxattr(state.RootPath(name), "user.foo", wantValue, 0); err != nil {
t.Fatalf("Lsetxattr(%s) failed: %v", name, err)
}
for _, path := range []string{state.MountPath(name), state.RootPath(name)} {
buf := make([]byte, 32)
sz, err := unix.Lgetxattr(path, "user.foo", buf)
if err != nil {
t.Fatalf("Lgetxattr(%s) failed: %v", path, err)
}
value := buf[0:sz]
if !reflect.DeepEqual(value, wantValue) {
t.Errorf("Invalid attribute for %s: got %s, want %s", path, value, wantValue)
}
}
}
}
func TestReadOnly_GetxattrOnScaffoldDirectory(t *testing.T) {
state := utils.MountSetup(t, "--xattrs", "--mapping=ro:/:%ROOT%", "--mapping=ro:/scaffold/dir:%ROOT%")
defer state.TearDown(t)
path := state.MountPath("scaffold")
buf := make([]byte, 32)
if _, err := unix.Lgetxattr(path, "user.foo", buf); err != utils.MissingXattrErr {
t.Errorf("Invalid error from Lgetxattr for %s: got %v, want %v", path, err, utils.MissingXattrErr)
}
}
func TestReadOnly_GetxattrMissingErrno(t *testing.T) {
state := utils.MountSetup(t, "--xattrs", "--mapping=ro:/:%ROOT%")
defer state.TearDown(t)
utils.MustMkdirAll(t, state.RootPath("dir"), 0755)
utils.MustWriteFile(t, state.RootPath("file"), 0644, "new content")
utils.MustSymlink(t, "missing", state.RootPath("symlink"))
tests := []string{"dir", "file"}
if runtime.GOOS != "linux" { tests = append(tests, "symlink")
}
for _, name := range tests {
for _, path := range []string{state.MountPath(name), state.RootPath(name)} {
buf := make([]byte, 32)
if _, err := unix.Lgetxattr(path, "user.foo", buf); err != utils.MissingXattrErr {
t.Errorf("Invalid error from Lgetxattr for %s: got %v, want %v", path, err, utils.MissingXattrErr)
}
}
}
}
func TestReadOnly_GetxattrDisabled(t *testing.T) {
state := utils.MountSetup(t, "--mapping=ro:/:%ROOT%")
defer state.TearDown(t)
utils.MustMkdirAll(t, state.RootPath("dir"), 0755)
if err := unix.Lsetxattr(state.RootPath("dir"), "user.foo", []byte{}, 0); err != nil {
t.Fatalf("Lsetxattr failed: %v", err)
}
var wantErr error
switch runtime.GOOS {
case "darwin":
wantErr = utils.MissingXattrErr
case "linux":
wantErr = unix.EOPNOTSUPP
default:
panic("Don't know how this test behaves on this platform")
}
buf := make([]byte, 32)
if _, err := unix.Lgetxattr(state.MountPath("dir"), "user.foo", buf); err != wantErr {
t.Errorf("Invalid error from Lgetxattr: got %v, want %v", err, wantErr)
}
}