Make gofer.dentry.destroyLocked idempotent

gofer operations accumulate dentries touched in a slice to call
checkCachingLocked on them when the operation is over. In case
the same dentry is touched multiple times during the operation,
checkCachingLocked, and consequently destroyLocked, may be called
more than once for the same dentry.

Updates #1198

PiperOrigin-RevId: 305276819
This commit is contained in:
Fabricio Voznika 2020-04-07 09:40:38 -07:00 committed by gVisor bot
parent 51e461cf9c
commit 94319a8241
4 changed files with 129 additions and 5 deletions

View File

@ -1,4 +1,4 @@
load("//tools:defs.bzl", "go_library")
load("//tools:defs.bzl", "go_library", "go_test")
load("//tools/go_generics:defs.bzl", "go_template_instance")
licenses(["notice"])
@ -54,3 +54,13 @@ go_library(
"//pkg/usermem",
],
)
go_test(
name = "gofer_test",
srcs = ["gofer_test.go"],
library = ":gofer",
deps = [
"//pkg/p9",
"//pkg/sentry/contexttest",
],
)

View File

@ -444,7 +444,8 @@ type dentry struct {
// refs is the reference count. Each dentry holds a reference on its
// parent, even if disowned. refs is accessed using atomic memory
// operations.
// operations. When refs reaches 0, the dentry may be added to the cache or
// destroyed. If refs==-1 the dentry has already been destroyed.
refs int64
// fs is the owning filesystem. fs is immutable.
@ -860,7 +861,7 @@ func (d *dentry) IncRef() {
func (d *dentry) TryIncRef() bool {
for {
refs := atomic.LoadInt64(&d.refs)
if refs == 0 {
if refs <= 0 {
return false
}
if atomic.CompareAndSwapInt64(&d.refs, refs, refs+1) {
@ -883,13 +884,20 @@ func (d *dentry) DecRef() {
// checkCachingLocked should be called after d's reference count becomes 0 or it
// becomes disowned.
//
// It may be called on a destroyed dentry. For example,
// renameMu[R]UnlockAndCheckCaching may call checkCachingLocked multiple times
// for the same dentry when the dentry is visited more than once in the same
// operation. One of the calls may destroy the dentry, so subsequent calls will
// do nothing.
//
// Preconditions: d.fs.renameMu must be locked for writing.
func (d *dentry) checkCachingLocked() {
// Dentries with a non-zero reference count must be retained. (The only way
// to obtain a reference on a dentry with zero references is via path
// resolution, which requires renameMu, so if d.refs is zero then it will
// remain zero while we hold renameMu for writing.)
if atomic.LoadInt64(&d.refs) != 0 {
refs := atomic.LoadInt64(&d.refs)
if refs > 0 {
if d.cached {
d.fs.cachedDentries.Remove(d)
d.fs.cachedDentriesLen--
@ -897,6 +905,10 @@ func (d *dentry) checkCachingLocked() {
}
return
}
if refs == -1 {
// Dentry has already been destroyed.
return
}
// Non-child dentries with zero references are no longer reachable by path
// resolution and should be dropped immediately.
if d.vfsd.Parent() == nil || d.vfsd.IsDisowned() {
@ -949,9 +961,22 @@ func (d *dentry) checkCachingLocked() {
}
}
// destroyLocked destroys the dentry. It may flushes dirty pages from cache,
// close p9 file and remove reference on parent dentry.
//
// Preconditions: d.fs.renameMu must be locked for writing. d.refs == 0. d is
// not a child dentry.
func (d *dentry) destroyLocked() {
switch atomic.LoadInt64(&d.refs) {
case 0:
// Mark the dentry destroyed.
atomic.StoreInt64(&d.refs, -1)
case -1:
panic("dentry.destroyLocked() called on already destroyed dentry")
default:
panic("dentry.destroyLocked() called with references on the dentry")
}
ctx := context.Background()
d.handleMu.Lock()
if !d.handle.file.isNil() {
@ -971,7 +996,10 @@ func (d *dentry) destroyLocked() {
d.handle.close(ctx)
}
d.handleMu.Unlock()
d.file.close(ctx)
if !d.file.isNil() {
d.file.close(ctx)
d.file = p9file{}
}
// Remove d from the set of all dentries.
d.fs.syncMu.Lock()
delete(d.fs.dentries, d)

View File

@ -0,0 +1,64 @@
// Copyright 2020 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 (
"sync/atomic"
"testing"
"gvisor.dev/gvisor/pkg/p9"
"gvisor.dev/gvisor/pkg/sentry/contexttest"
)
func TestDestroyIdempotent(t *testing.T) {
fs := filesystem{
dentries: make(map[*dentry]struct{}),
opts: filesystemOptions{
// Test relies on no dentry being held in the cache.
maxCachedDentries: 0,
},
}
ctx := contexttest.Context(t)
attr := &p9.Attr{
Mode: p9.ModeRegular,
}
mask := p9.AttrMask{
Mode: true,
Size: true,
}
parent, err := fs.newDentry(ctx, p9file{}, p9.QID{}, mask, attr)
if err != nil {
t.Fatalf("fs.newDentry(): %v", err)
}
child, err := fs.newDentry(ctx, p9file{}, p9.QID{}, mask, attr)
if err != nil {
t.Fatalf("fs.newDentry(): %v", err)
}
parent.IncRef() // reference held by child on its parent.
parent.vfsd.InsertChild(&child.vfsd, "child")
child.checkCachingLocked()
if got := atomic.LoadInt64(&child.refs); got != -1 {
t.Fatalf("child.refs=%d, want: -1", got)
}
// Parent will also be destroyed when child reference is removed.
if got := atomic.LoadInt64(&parent.refs); got != -1 {
t.Fatalf("parent.refs=%d, want: -1", got)
}
child.checkCachingLocked()
child.checkCachingLocked()
}

View File

@ -186,6 +186,28 @@ TEST_F(OpenTest, OpenNoFollowStillFollowsLinksInPath) {
ASSERT_NO_ERRNO_AND_VALUE(Open(path_via_symlink, O_RDONLY | O_NOFOLLOW));
}
// Test that open(2) can follow symlinks that point back to the same tree.
// Test sets up files as follows:
// root/child/symlink => redirects to ../..
// root/child/target => regular file
//
// open("root/child/symlink/root/child/file")
TEST_F(OpenTest, SymlinkRecurse) {
auto root =
ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(GetAbsoluteTestTmpdir()));
auto child = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(root.path()));
auto symlink = ASSERT_NO_ERRNO_AND_VALUE(
TempPath::CreateSymlinkTo(child.path(), "../.."));
auto target = ASSERT_NO_ERRNO_AND_VALUE(
TempPath::CreateFileWith(child.path(), "abc", 0644));
auto path_via_symlink =
JoinPath(symlink.path(), Basename(root.path()), Basename(child.path()),
Basename(target.path()));
const auto contents =
ASSERT_NO_ERRNO_AND_VALUE(GetContents(path_via_symlink));
ASSERT_EQ(contents, "abc");
}
TEST_F(OpenTest, Fault) {
char* totally_not_null = nullptr;
ASSERT_THAT(open(totally_not_null, O_RDONLY), SyscallFailsWithErrno(EFAULT));