diff options
Diffstat (limited to 'pkg/sentry/fs/host/fs_test.go')
-rw-r--r-- | pkg/sentry/fs/host/fs_test.go | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/pkg/sentry/fs/host/fs_test.go b/pkg/sentry/fs/host/fs_test.go new file mode 100644 index 000000000..c000afc49 --- /dev/null +++ b/pkg/sentry/fs/host/fs_test.go @@ -0,0 +1,383 @@ +// Copyright 2018 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package host + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "reflect" + "sort" + "testing" + + "gvisor.googlesource.com/gvisor/pkg/sentry/context" + "gvisor.googlesource.com/gvisor/pkg/sentry/context/contexttest" + "gvisor.googlesource.com/gvisor/pkg/sentry/fs" +) + +// newTestMountNamespace creates a MountNamespace with a ramfs root. +// It returns the host folder created, which should be removed when done. +func newTestMountNamespace(t *testing.T) (*fs.MountNamespace, string, error) { + p, err := ioutil.TempDir("", "root") + if err != nil { + return nil, "", err + } + + fd, err := open(nil, p) + if err != nil { + os.RemoveAll(p) + return nil, "", err + } + ctx := contexttest.Context(t) + root, err := newInode(ctx, newMountSource(ctx, p, fs.RootOwner, &Filesystem{}, fs.MountSourceFlags{}, false), fd, false, false) + if err != nil { + os.RemoveAll(p) + return nil, "", err + } + mm, err := fs.NewMountNamespace(ctx, root) + if err != nil { + os.RemoveAll(p) + return nil, "", err + } + return mm, p, nil +} + +// createTestDirs populates the root with some test files and directories. +// /a/a1.txt +// /a/a2.txt +// /b/b1.txt +// /b/c/c1.txt +// /symlinks/normal.txt +// /symlinks/to_normal.txt -> /symlinks/normal.txt +// /symlinks/recursive -> /symlinks +func createTestDirs(ctx context.Context, t *testing.T, m *fs.MountNamespace) error { + r := m.Root() + defer r.DecRef() + + if err := r.CreateDirectory(ctx, r, "a", fs.FilePermsFromMode(0777)); err != nil { + return err + } + + a, err := r.Walk(ctx, r, "a") + if err != nil { + return err + } + defer a.DecRef() + + a1, err := a.Create(ctx, r, "a1.txt", fs.FileFlags{Read: true, Write: true}, fs.FilePermsFromMode(0666)) + if err != nil { + return err + } + a1.DecRef() + + a2, err := a.Create(ctx, r, "a2.txt", fs.FileFlags{Read: true, Write: true}, fs.FilePermsFromMode(0666)) + if err != nil { + return err + } + a2.DecRef() + + if err := r.CreateDirectory(ctx, r, "b", fs.FilePermsFromMode(0777)); err != nil { + return err + } + + b, err := r.Walk(ctx, r, "b") + if err != nil { + return err + } + defer b.DecRef() + + b1, err := b.Create(ctx, r, "b1.txt", fs.FileFlags{Read: true, Write: true}, fs.FilePermsFromMode(0666)) + if err != nil { + return err + } + b1.DecRef() + + if err := b.CreateDirectory(ctx, r, "c", fs.FilePermsFromMode(0777)); err != nil { + return err + } + + c, err := b.Walk(ctx, r, "c") + if err != nil { + return err + } + defer c.DecRef() + + c1, err := c.Create(ctx, r, "c1.txt", fs.FileFlags{Read: true, Write: true}, fs.FilePermsFromMode(0666)) + if err != nil { + return err + } + c1.DecRef() + + if err := r.CreateDirectory(ctx, r, "symlinks", fs.FilePermsFromMode(0777)); err != nil { + return err + } + + symlinks, err := r.Walk(ctx, r, "symlinks") + if err != nil { + return err + } + defer symlinks.DecRef() + + normal, err := symlinks.Create(ctx, r, "normal.txt", fs.FileFlags{Read: true, Write: true}, fs.FilePermsFromMode(0666)) + if err != nil { + return err + } + normal.DecRef() + + if err := symlinks.CreateLink(ctx, r, "/symlinks/normal.txt", "to_normal.txt"); err != nil { + return err + } + + if err := symlinks.CreateLink(ctx, r, "/symlinks", "recursive"); err != nil { + return err + } + + return nil +} + +// allPaths returns a slice of all paths of entries visible in the rootfs. +func allPaths(ctx context.Context, t *testing.T, m *fs.MountNamespace, base string) ([]string, error) { + var paths []string + root := m.Root() + defer root.DecRef() + + d, err := m.FindLink(ctx, root, nil, base, 1) + if err != nil { + t.Logf("FindLink failed for %q", base) + return paths, err + } + defer d.DecRef() + + if fs.IsDir(d.Inode.StableAttr) { + dir, err := d.Inode.GetFile(ctx, d, fs.FileFlags{Read: true}) + if err != nil { + return nil, fmt.Errorf("failed to open directory %q: %v", base, err) + } + iter, ok := dir.FileOperations.(fs.DirIterator) + if !ok { + return nil, fmt.Errorf("cannot directly iterate on host directory %q", base) + } + dirCtx := &fs.DirCtx{ + Serializer: noopDentrySerializer{}, + } + if _, err := fs.DirentReaddir(ctx, d, iter, root, dirCtx, 0); err != nil { + return nil, err + } + for name := range dirCtx.DentAttrs() { + if name == "." || name == ".." { + continue + } + + fullName := path.Join(base, name) + paths = append(paths, fullName) + + // Recurse. + subpaths, err := allPaths(ctx, t, m, fullName) + if err != nil { + return paths, err + } + paths = append(paths, subpaths...) + } + } + + return paths, nil +} + +type noopDentrySerializer struct{} + +func (noopDentrySerializer) CopyOut(string, fs.DentAttr) error { + return nil +} +func (noopDentrySerializer) Written() int { + return 4096 +} + +// pathsEqual returns true if the two string slices contain the same entries. +func pathsEqual(got, want []string) bool { + sort.Strings(got) + sort.Strings(want) + + if len(got) != len(want) { + return false + } + + for i := range got { + if got[i] != want[i] { + return false + } + } + + return true +} + +func TestWhitelist(t *testing.T) { + for _, test := range []struct { + // description of the test. + desc string + // paths are the paths to whitelist + paths []string + // want are all of the directory entries that should be + // visible (nothing beyond this set should be visible). + want []string + }{ + { + desc: "root", + paths: []string{"/"}, + want: []string{"/a", "/a/a1.txt", "/a/a2.txt", "/b", "/b/b1.txt", "/b/c", "/b/c/c1.txt", "/symlinks", "/symlinks/normal.txt", "/symlinks/to_normal.txt", "/symlinks/recursive"}, + }, + { + desc: "top-level directories", + paths: []string{"/a", "/b"}, + want: []string{"/a", "/a/a1.txt", "/a/a2.txt", "/b", "/b/b1.txt", "/b/c", "/b/c/c1.txt"}, + }, + { + desc: "nested directories (1/2)", + paths: []string{"/b", "/b/c"}, + want: []string{"/b", "/b/b1.txt", "/b/c", "/b/c/c1.txt"}, + }, + { + desc: "nested directories (2/2)", + paths: []string{"/b/c", "/b"}, + want: []string{"/b", "/b/b1.txt", "/b/c", "/b/c/c1.txt"}, + }, + { + desc: "single file", + paths: []string{"/b/c/c1.txt"}, + want: []string{"/b", "/b/c", "/b/c/c1.txt"}, + }, + { + desc: "single file and directory", + paths: []string{"/a/a1.txt", "/b/c"}, + want: []string{"/a", "/a/a1.txt", "/b", "/b/c", "/b/c/c1.txt"}, + }, + { + desc: "symlink", + paths: []string{"/symlinks/to_normal.txt"}, + want: []string{"/symlinks", "/symlinks/normal.txt", "/symlinks/to_normal.txt"}, + }, + { + desc: "recursive symlink", + paths: []string{"/symlinks/recursive/normal.txt"}, + want: []string{"/symlinks", "/symlinks/normal.txt", "/symlinks/recursive"}, + }, + } { + t.Run(test.desc, func(t *testing.T) { + m, p, err := newTestMountNamespace(t) + if err != nil { + t.Errorf("Failed to create MountNamespace: %v", err) + } + defer os.RemoveAll(p) + + ctx := withRoot(contexttest.RootContext(t), m.Root()) + if err := createTestDirs(ctx, t, m); err != nil { + t.Errorf("Failed to create test dirs: %v", err) + } + + if err := installWhitelist(ctx, m, test.paths); err != nil { + t.Errorf("installWhitelist(%v) err got %v want nil", test.paths, err) + } + + got, err := allPaths(ctx, t, m, "/") + if err != nil { + t.Fatalf("Failed to lookup paths (whitelisted: %v): %v", test.paths, err) + } + + if !pathsEqual(got, test.want) { + t.Errorf("For paths %v got %v want %v", test.paths, got, test.want) + } + }) + } +} + +func TestRootPath(t *testing.T) { + // Create a temp dir, which will be the root of our mounted fs. + rootPath, err := ioutil.TempDir(os.TempDir(), "root") + if err != nil { + t.Fatalf("TempDir failed: %v", err) + } + defer os.RemoveAll(rootPath) + + // Create two files inside the new root, one which will be whitelisted + // and one not. + whitelisted, err := ioutil.TempFile(rootPath, "white") + if err != nil { + t.Fatalf("TempFile failed: %v", err) + } + if _, err := ioutil.TempFile(rootPath, "black"); err != nil { + t.Fatalf("TempFile failed: %v", err) + } + + // Create a mount with a root path and single whitelisted file. + hostFS := &Filesystem{} + ctx := contexttest.Context(t) + data := fmt.Sprintf("%s=%s,%s=%s", rootPathKey, rootPath, whitelistKey, whitelisted.Name()) + inode, err := hostFS.Mount(ctx, "", fs.MountSourceFlags{}, data) + if err != nil { + t.Fatalf("Mount failed: %v", err) + } + mm, err := fs.NewMountNamespace(ctx, inode) + if err != nil { + t.Fatalf("NewMountNamespace failed: %v", err) + } + if err := hostFS.InstallWhitelist(ctx, mm); err != nil { + t.Fatalf("InstallWhitelist failed: %v", err) + } + + // Get the contents of the root directory. + rootDir := mm.Root() + rctx := withRoot(ctx, rootDir) + f, err := rootDir.Inode.GetFile(rctx, rootDir, fs.FileFlags{}) + if err != nil { + t.Fatalf("GetFile failed: %v", err) + } + c := &fs.CollectEntriesSerializer{} + if err := f.Readdir(rctx, c); err != nil { + t.Fatalf("Readdir failed: %v", err) + } + + // We should have only our whitelisted file, plus the dots. + want := []string{path.Base(whitelisted.Name()), ".", ".."} + got := c.Order + sort.Strings(want) + sort.Strings(got) + if !reflect.DeepEqual(got, want) { + t.Errorf("Readdir got %v, wanted %v", got, want) + } +} + +type rootContext struct { + context.Context + root *fs.Dirent +} + +// withRoot returns a copy of ctx with the given root. +func withRoot(ctx context.Context, root *fs.Dirent) context.Context { + return &rootContext{ + Context: ctx, + root: root, + } +} + +// Value implements Context.Value. +func (rc rootContext) Value(key interface{}) interface{} { + switch key { + case fs.CtxRoot: + rc.root.IncRef() + return rc.root + default: + return rc.Context.Value(key) + } +} |