gvisor/pkg/sentry/fs/inode_overlay_test.go

470 lines
12 KiB
Go

// Copyright 2018 Google LLC
//
// 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 fs_test
import (
"testing"
"gvisor.googlesource.com/gvisor/pkg/sentry/context"
"gvisor.googlesource.com/gvisor/pkg/sentry/fs"
"gvisor.googlesource.com/gvisor/pkg/sentry/fs/fsutil"
"gvisor.googlesource.com/gvisor/pkg/sentry/fs/ramfs"
"gvisor.googlesource.com/gvisor/pkg/sentry/kernel/contexttest"
"gvisor.googlesource.com/gvisor/pkg/syserror"
)
func TestLookup(t *testing.T) {
ctx := contexttest.Context(t)
for _, test := range []struct {
// Test description.
desc string
// Lookup parameters.
dir *fs.Inode
name string
// Want from lookup.
found bool
hasUpper bool
hasLower bool
}{
{
desc: "no upper, lower has name",
dir: fs.NewTestOverlayDir(ctx,
nil, /* upper */
newTestRamfsDir(ctx, []dirContent{
{
name: "a",
dir: false,
},
}, nil), /* lower */
false /* revalidate */),
name: "a",
found: true,
hasUpper: false,
hasLower: true,
},
{
desc: "no lower, upper has name",
dir: fs.NewTestOverlayDir(ctx,
newTestRamfsDir(ctx, []dirContent{
{
name: "a",
dir: false,
},
}, nil), /* upper */
nil, /* lower */
false /* revalidate */),
name: "a",
found: true,
hasUpper: true,
hasLower: false,
},
{
desc: "upper and lower, only lower has name",
dir: fs.NewTestOverlayDir(ctx,
newTestRamfsDir(ctx, []dirContent{
{
name: "b",
dir: false,
},
}, nil), /* upper */
newTestRamfsDir(ctx, []dirContent{
{
name: "a",
dir: false,
},
}, nil), /* lower */
false /* revalidate */),
name: "a",
found: true,
hasUpper: false,
hasLower: true,
},
{
desc: "upper and lower, only upper has name",
dir: fs.NewTestOverlayDir(ctx,
newTestRamfsDir(ctx, []dirContent{
{
name: "a",
dir: false,
},
}, nil), /* upper */
newTestRamfsDir(ctx, []dirContent{
{
name: "b",
dir: false,
},
}, nil), /* lower */
false /* revalidate */),
name: "a",
found: true,
hasUpper: true,
hasLower: false,
},
{
desc: "upper and lower, both have file",
dir: fs.NewTestOverlayDir(ctx,
newTestRamfsDir(ctx, []dirContent{
{
name: "a",
dir: false,
},
}, nil), /* upper */
newTestRamfsDir(ctx, []dirContent{
{
name: "a",
dir: false,
},
}, nil), /* lower */
false /* revalidate */),
name: "a",
found: true,
hasUpper: true,
hasLower: false,
},
{
desc: "upper and lower, both have directory",
dir: fs.NewTestOverlayDir(ctx,
newTestRamfsDir(ctx, []dirContent{
{
name: "a",
dir: true,
},
}, nil), /* upper */
newTestRamfsDir(ctx, []dirContent{
{
name: "a",
dir: true,
},
}, nil), /* lower */
false /* revalidate */),
name: "a",
found: true,
hasUpper: true,
hasLower: true,
},
{
desc: "upper and lower, upper negative masks lower file",
dir: fs.NewTestOverlayDir(ctx,
newTestRamfsDir(ctx, nil, []string{"a"}), /* upper */
newTestRamfsDir(ctx, []dirContent{
{
name: "a",
dir: false,
},
}, nil), /* lower */
false /* revalidate */),
name: "a",
found: false,
hasUpper: false,
hasLower: false,
},
{
desc: "upper and lower, upper negative does not mask lower file",
dir: fs.NewTestOverlayDir(ctx,
newTestRamfsDir(ctx, nil, []string{"b"}), /* upper */
newTestRamfsDir(ctx, []dirContent{
{
name: "a",
dir: false,
},
}, nil), /* lower */
false /* revalidate */),
name: "a",
found: true,
hasUpper: false,
hasLower: true,
},
} {
t.Run(test.desc, func(t *testing.T) {
dirent, err := test.dir.Lookup(ctx, test.name)
if test.found && (err == syserror.ENOENT || dirent.IsNegative()) {
t.Fatalf("lookup %q expected to find positive dirent, got dirent %v err %v", test.name, dirent, err)
}
if !test.found {
if err != syserror.ENOENT && !dirent.IsNegative() {
t.Errorf("lookup %q expected to return ENOENT or negative dirent, got dirent %v err %v", test.name, dirent, err)
}
// Nothing more to check.
return
}
if hasUpper := dirent.Inode.TestHasUpperFS(); hasUpper != test.hasUpper {
t.Fatalf("lookup got upper filesystem %v, want %v", hasUpper, test.hasUpper)
}
if hasLower := dirent.Inode.TestHasLowerFS(); hasLower != test.hasLower {
t.Errorf("lookup got lower filesystem %v, want %v", hasLower, test.hasLower)
}
})
}
}
func TestLookupRevalidation(t *testing.T) {
// File name used in the tests.
fileName := "foofile"
ctx := contexttest.Context(t)
for _, tc := range []struct {
// Test description.
desc string
// Upper and lower fs for the overlay.
upper *fs.Inode
lower *fs.Inode
// Whether the upper requires revalidation.
revalidate bool
// Whether we should get the same dirent on second lookup.
wantSame bool
}{
{
desc: "file from upper with no revalidation",
upper: newTestRamfsDir(ctx, []dirContent{{name: fileName}}, nil),
lower: newTestRamfsDir(ctx, nil, nil),
revalidate: false,
wantSame: true,
},
{
desc: "file from upper with revalidation",
upper: newTestRamfsDir(ctx, []dirContent{{name: fileName}}, nil),
lower: newTestRamfsDir(ctx, nil, nil),
revalidate: true,
wantSame: false,
},
{
desc: "file from lower with no revalidation",
upper: newTestRamfsDir(ctx, nil, nil),
lower: newTestRamfsDir(ctx, []dirContent{{name: fileName}}, nil),
revalidate: false,
wantSame: true,
},
{
desc: "file from lower with revalidation",
upper: newTestRamfsDir(ctx, nil, nil),
lower: newTestRamfsDir(ctx, []dirContent{{name: fileName}}, nil),
revalidate: true,
// The file does not exist in the upper, so we do not
// need to revalidate it.
wantSame: true,
},
{
desc: "file from upper and lower with no revalidation",
upper: newTestRamfsDir(ctx, []dirContent{{name: fileName}}, nil),
lower: newTestRamfsDir(ctx, []dirContent{{name: fileName}}, nil),
revalidate: false,
wantSame: true,
},
{
desc: "file from upper and lower with revalidation",
upper: newTestRamfsDir(ctx, []dirContent{{name: fileName}}, nil),
lower: newTestRamfsDir(ctx, []dirContent{{name: fileName}}, nil),
revalidate: true,
wantSame: false,
},
} {
t.Run(tc.desc, func(t *testing.T) {
root := fs.NewDirent(newTestRamfsDir(ctx, nil, nil), "root")
ctx = &rootContext{
Context: ctx,
root: root,
}
overlay := fs.NewDirent(fs.NewTestOverlayDir(ctx, tc.upper, tc.lower, tc.revalidate), "overlay")
// Lookup the file twice through the overlay.
first, err := overlay.Walk(ctx, root, fileName)
if err != nil {
t.Fatalf("overlay.Walk(%q) failed: %v", fileName, err)
}
second, err := overlay.Walk(ctx, root, fileName)
if err != nil {
t.Fatalf("overlay.Walk(%q) failed: %v", fileName, err)
}
if tc.wantSame && first != second {
t.Errorf("dirent lookup got different dirents, wanted same\nfirst=%+v\nsecond=%+v", first, second)
} else if !tc.wantSame && first == second {
t.Errorf("dirent lookup got the same dirent, wanted different: %+v", first)
}
})
}
}
func TestCacheFlush(t *testing.T) {
ctx := contexttest.Context(t)
// Upper and lower each have a file.
upperFileName := "file-from-upper"
lowerFileName := "file-from-lower"
upper := newTestRamfsDir(ctx, []dirContent{{name: upperFileName}}, nil)
lower := newTestRamfsDir(ctx, []dirContent{{name: lowerFileName}}, nil)
overlay := fs.NewTestOverlayDir(ctx, upper, lower, true /* revalidate */)
mns, err := fs.NewMountNamespace(ctx, overlay)
if err != nil {
t.Fatalf("NewMountNamespace failed: %v", err)
}
root := mns.Root()
defer root.DecRef()
ctx = &rootContext{
Context: ctx,
root: root,
}
for _, fileName := range []string{upperFileName, lowerFileName} {
// Walk to the file.
maxTraversals := uint(0)
dirent, err := mns.FindInode(ctx, root, nil, fileName, &maxTraversals)
if err != nil {
t.Fatalf("FindInode(%q) failed: %v", fileName, err)
}
// Get a file from the dirent.
file, err := dirent.Inode.GetFile(ctx, dirent, fs.FileFlags{Read: true})
if err != nil {
t.Fatalf("GetFile() failed: %v", err)
}
// The dirent should have 3 refs, one from us, one from the
// file, and one from the dirent cache.
// dirent cache.
if got, want := dirent.ReadRefs(), 3; int(got) != want {
t.Errorf("dirent.ReadRefs() got %d want %d", got, want)
}
// Drop the file reference.
file.DecRef()
// Dirent should have 2 refs left.
if got, want := dirent.ReadRefs(), 2; int(got) != want {
t.Errorf("dirent.ReadRefs() got %d want %d", got, want)
}
// Flush the dirent cache.
mns.FlushMountSourceRefs()
// Dirent should have 1 ref left from the dirent cache.
if got, want := dirent.ReadRefs(), 1; int(got) != want {
t.Errorf("dirent.ReadRefs() got %d want %d", got, want)
}
// Drop our ref.
dirent.DecRef()
// We should be back to zero refs.
if got, want := dirent.ReadRefs(), 0; int(got) != want {
t.Errorf("dirent.ReadRefs() got %d want %d", got, want)
}
}
}
type dir struct {
fs.InodeOperations
// List of negative child names.
negative []string
// ReaddirCalled records whether Readdir was called on a file
// corresponding to this inode.
ReaddirCalled bool
}
// Getxattr implements InodeOperations.Getxattr.
func (d *dir) Getxattr(inode *fs.Inode, name string) (string, error) {
for _, n := range d.negative {
if name == fs.XattrOverlayWhiteout(n) {
return "y", nil
}
}
return "", syserror.ENOATTR
}
// GetFile implements InodeOperations.GetFile.
func (d *dir) GetFile(ctx context.Context, dirent *fs.Dirent, flags fs.FileFlags) (*fs.File, error) {
file, err := d.InodeOperations.GetFile(ctx, dirent, flags)
if err != nil {
return nil, err
}
defer file.DecRef()
// Wrap the file's FileOperations in a dirFile.
fops := &dirFile{
FileOperations: file.FileOperations,
inode: d,
}
return fs.NewFile(ctx, dirent, flags, fops), nil
}
type dirContent struct {
name string
dir bool
}
type dirFile struct {
fs.FileOperations
inode *dir
}
type inode struct {
fsutil.InodeGenericChecker `state:"nosave"`
fsutil.InodeNoExtendedAttributes `state:"nosave"`
fsutil.InodeNoopRelease `state:"nosave"`
fsutil.InodeNoopWriteOut `state:"nosave"`
fsutil.InodeNotDirectory `state:"nosave"`
fsutil.InodeNotMappable `state:"nosave"`
fsutil.InodeNotSocket `state:"nosave"`
fsutil.InodeNotSymlink `state:"nosave"`
fsutil.InodeNotTruncatable `state:"nosave"`
fsutil.InodeNotVirtual `state:"nosave"`
fsutil.InodeSimpleAttributes
fsutil.InodeStaticFileGetter
}
// Readdir implements fs.FileOperations.Readdir. It sets the ReaddirCalled
// field on the inode.
func (f *dirFile) Readdir(ctx context.Context, file *fs.File, ser fs.DentrySerializer) (int64, error) {
f.inode.ReaddirCalled = true
return f.FileOperations.Readdir(ctx, file, ser)
}
func newTestRamfsInode(ctx context.Context, msrc *fs.MountSource) *fs.Inode {
inode := fs.NewInode(&inode{
InodeStaticFileGetter: fsutil.InodeStaticFileGetter{
Contents: []byte("foobar"),
},
}, msrc, fs.StableAttr{Type: fs.RegularFile})
return inode
}
func newTestRamfsDir(ctx context.Context, contains []dirContent, negative []string) *fs.Inode {
msrc := fs.NewPseudoMountSource()
contents := make(map[string]*fs.Inode)
for _, c := range contains {
if c.dir {
contents[c.name] = newTestRamfsDir(ctx, nil, nil)
} else {
contents[c.name] = newTestRamfsInode(ctx, msrc)
}
}
dops := ramfs.NewDir(ctx, contents, fs.RootOwner, fs.FilePermissions{
User: fs.PermMask{Read: true, Execute: true},
})
return fs.NewInode(&dir{
InodeOperations: dops,
negative: negative,
}, msrc, fs.StableAttr{Type: fs.Directory})
}