gvisor/pkg/sentry/fs/gofer/gofer_test.go

311 lines
9.9 KiB
Go

// Copyright 2018 The gVisor Authors.
//
// 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 gofer
import (
"fmt"
"syscall"
"testing"
"time"
"gvisor.googlesource.com/gvisor/pkg/p9"
"gvisor.googlesource.com/gvisor/pkg/p9/p9test"
"gvisor.googlesource.com/gvisor/pkg/sentry/context"
"gvisor.googlesource.com/gvisor/pkg/sentry/context/contexttest"
"gvisor.googlesource.com/gvisor/pkg/sentry/fs"
)
// rootTest runs a test with a p9 mock and an fs.InodeOperations created from
// the attached root directory. The root file will be closed and client
// disconnected, but additional files must be closed manually.
func rootTest(t *testing.T, name string, cp cachePolicy, fn func(context.Context, *p9test.Harness, *p9test.Mock, *fs.Inode)) {
t.Run(name, func(t *testing.T) {
h, c := p9test.NewHarness(t)
defer h.Finish()
// Create a new root. Note that we pass an empty, but non-nil
// map here. This allows tests to extend the root children
// dynamically.
root := h.NewDirectory(map[string]p9test.Generator{})(nil)
// Return this as the root.
h.Attacher.EXPECT().Attach().Return(root, nil).Times(1)
// ... and open via the client.
rootFile, err := c.Attach("/")
if err != nil {
t.Fatalf("unable to attach: %v", err)
}
defer rootFile.Close()
// Wrap an a session.
s := &session{
mounter: fs.RootOwner,
cachePolicy: cp,
client: c,
}
// ... and an INode, with only the mode being explicitly valid for now.
ctx := contexttest.Context(t)
sattr, rootInodeOperations := newInodeOperations(ctx, s, contextFile{
file: rootFile,
}, root.QID, p9.AttrMaskAll(), root.Attr, false /* socket */)
m := fs.NewMountSource(s, &filesystem{}, fs.MountSourceFlags{})
rootInode := fs.NewInode(rootInodeOperations, m, sattr)
// Ensure that the cache is fully invalidated, so that any
// close actions actually take place before the full harness is
// torn down.
defer func() {
m.FlushDirentRefs()
// Wait for all resources to be released, otherwise the
// operations may fail after we close the rootFile.
fs.AsyncBarrier()
}()
// Execute the test.
fn(ctx, h, root, rootInode)
})
}
func TestLookup(t *testing.T) {
type lookupTest struct {
// Name of the test.
name string
// Expected return value.
want error
}
tests := []lookupTest{
{
name: "mock Walk passes (function succeeds)",
want: nil,
},
{
name: "mock Walk fails (function fails)",
want: syscall.ENOENT,
},
}
const file = "file" // The walked target file.
for _, test := range tests {
rootTest(t, test.name, cacheNone, func(ctx context.Context, h *p9test.Harness, rootFile *p9test.Mock, rootInode *fs.Inode) {
// Setup the appropriate result.
rootFile.WalkCallback = func() error {
return test.want
}
if test.want == nil {
// Set the contents of the root. We expect a
// normal file generator for ppp above. This is
// overriden by setting WalkErr in the mock.
rootFile.AddChild(file, h.NewFile())
}
// Call function.
dirent, err := rootInode.Lookup(ctx, file)
// Unwrap the InodeOperations.
var newInodeOperations fs.InodeOperations
if dirent != nil {
if dirent.IsNegative() {
err = syscall.ENOENT
} else {
newInodeOperations = dirent.Inode.InodeOperations
}
}
// Check return values.
if err != test.want {
t.Errorf("Lookup got err %v, want %v", err, test.want)
}
if err == nil && newInodeOperations == nil {
t.Errorf("Lookup got non-nil err and non-nil node, wanted at least one non-nil")
}
})
}
}
func TestRevalidation(t *testing.T) {
type revalidationTest struct {
cachePolicy cachePolicy
// Whether dirent should be reloaded before any modifications.
preModificationWantReload bool
// Whether dirent should be reloaded after updating an unstable
// attribute on the remote fs.
postModificationWantReload bool
// Whether dirent unstable attributes should be updated after
// updating an attribute on the remote fs.
postModificationWantUpdatedAttrs bool
// Whether dirent should be reloaded after the remote has
// removed the file.
postRemovalWantReload bool
}
tests := []revalidationTest{
{
// Policy cacheNone causes Revalidate to always return
// true.
cachePolicy: cacheNone,
preModificationWantReload: true,
postModificationWantReload: true,
postModificationWantUpdatedAttrs: true,
postRemovalWantReload: true,
},
{
// Policy cacheAll causes Revalidate to always return
// false.
cachePolicy: cacheAll,
preModificationWantReload: false,
postModificationWantReload: false,
postModificationWantUpdatedAttrs: false,
postRemovalWantReload: false,
},
{
// Policy cacheAllWritethrough causes Revalidate to
// always return false.
cachePolicy: cacheAllWritethrough,
preModificationWantReload: false,
postModificationWantReload: false,
postModificationWantUpdatedAttrs: false,
postRemovalWantReload: false,
},
{
// Policy cacheRemoteRevalidating causes Revalidate to
// return update cached unstable attrs, and returns
// true only when the remote inode itself has been
// removed or replaced.
cachePolicy: cacheRemoteRevalidating,
preModificationWantReload: false,
postModificationWantReload: false,
postModificationWantUpdatedAttrs: true,
postRemovalWantReload: true,
},
}
const file = "file" // The file walked below.
for _, test := range tests {
name := fmt.Sprintf("cachepolicy=%s", test.cachePolicy)
rootTest(t, name, test.cachePolicy, func(ctx context.Context, h *p9test.Harness, rootFile *p9test.Mock, rootInode *fs.Inode) {
// Wrap in a dirent object.
rootDir := fs.NewDirent(rootInode, "root")
// Create a mock file a child of the root. We save when
// this is generated, so that when the time changed, we
// can update the original entry.
var origMocks []*p9test.Mock
rootFile.AddChild(file, func(parent *p9test.Mock) *p9test.Mock {
// Regular a regular file that has a consistent
// path number. This might be used by
// validation so we don't change it.
m := h.NewMock(parent, 0, p9.Attr{
Mode: p9.ModeRegular,
})
origMocks = append(origMocks, m)
return m
})
// Do the walk.
dirent, err := rootDir.Walk(ctx, rootDir, file)
if err != nil {
t.Fatalf("Lookup failed: %v", err)
}
// We must release the dirent, of the test will fail
// with a reference leak. This is tracked by p9test.
defer dirent.DecRef()
// Walk again. Depending on the cache policy, we may
// get a new dirent.
newDirent, err := rootDir.Walk(ctx, rootDir, file)
if err != nil {
t.Fatalf("Lookup failed: %v", err)
}
if test.preModificationWantReload && dirent == newDirent {
t.Errorf("Lookup with cachePolicy=%s got old dirent %+v, wanted a new dirent", test.cachePolicy, dirent)
}
if !test.preModificationWantReload && dirent != newDirent {
t.Errorf("Lookup with cachePolicy=%s got new dirent %+v, wanted old dirent %+v", test.cachePolicy, newDirent, dirent)
}
newDirent.DecRef() // See above.
// Modify the underlying mocked file's modification
// time for the next walk that occurs.
nowSeconds := time.Now().Unix()
rootFile.AddChild(file, func(parent *p9test.Mock) *p9test.Mock {
// Ensure that the path is the same as above,
// but we change only the modification time of
// the file.
return h.NewMock(parent, 0, p9.Attr{
Mode: p9.ModeRegular,
MTimeSeconds: uint64(nowSeconds),
})
})
// We also modify the original time, so that GetAttr
// behaves as expected for the caching case.
for _, m := range origMocks {
m.Attr.MTimeSeconds = uint64(nowSeconds)
}
// Walk again. Depending on the cache policy, we may
// get a new dirent.
newDirent, err = rootDir.Walk(ctx, rootDir, file)
if err != nil {
t.Fatalf("Lookup failed: %v", err)
}
if test.postModificationWantReload && dirent == newDirent {
t.Errorf("Lookup with cachePolicy=%s got old dirent, wanted a new dirent", test.cachePolicy)
}
if !test.postModificationWantReload && dirent != newDirent {
t.Errorf("Lookup with cachePolicy=%s got new dirent, wanted old dirent", test.cachePolicy)
}
uattrs, err := newDirent.Inode.UnstableAttr(ctx)
if err != nil {
t.Fatalf("Error getting unstable attrs: %v", err)
}
gotModTimeSeconds := uattrs.ModificationTime.Seconds()
if test.postModificationWantUpdatedAttrs && gotModTimeSeconds != nowSeconds {
t.Fatalf("Lookup with cachePolicy=%s got new modification time %v, wanted %v", test.cachePolicy, gotModTimeSeconds, nowSeconds)
}
newDirent.DecRef() // See above.
// Remove the file from the remote fs, subsequent walks
// should now fail to find anything.
rootFile.RemoveChild(file)
// Walk again. Depending on the cache policy, we may
// get ENOENT.
newDirent, err = rootDir.Walk(ctx, rootDir, file)
if test.postRemovalWantReload && err == nil {
t.Errorf("Lookup with cachePolicy=%s got nil error, wanted ENOENT", test.cachePolicy)
}
if !test.postRemovalWantReload && (err != nil || dirent != newDirent) {
t.Errorf("Lookup with cachePolicy=%s got new dirent and error %v, wanted old dirent and nil error", test.cachePolicy, err)
}
if err == nil {
newDirent.DecRef() // See above.
}
})
}
}