Skip to content

Commit

Permalink
Allow SetAttr and Allocate for deleted files
Browse files Browse the repository at this point in the history
It's safe to call SetAttr and Allocate on fsgofer because the
file path is not used to open the file, if needed.

Fixes #3654

PiperOrigin-RevId: 407149393
  • Loading branch information
fvoznika authored and gvisor-bot committed Nov 2, 2021
1 parent 42a08f0 commit 1e1d6b2
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 8 deletions.
35 changes: 32 additions & 3 deletions pkg/p9/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,37 @@ import (
"gvisor.dev/gvisor/pkg/fd"
)

// AttacherOptions contains Attacher configuration.
type AttacherOptions struct {
// SetAttrOnDeleted is set to true if it's safe to call File.SetAttr for
// deleted files.
SetAttrOnDeleted bool

// AllocateOnDeleted is set to true if it's safe to call File.Allocate for
// deleted files.
AllocateOnDeleted bool
}

// NoServerOptions partially implements Attacher with empty AttacherOptions.
type NoServerOptions struct{}

// ServerOptions implements Attacher.
func (*NoServerOptions) ServerOptions() AttacherOptions {
return AttacherOptions{}
}

// Attacher is provided by the server.
type Attacher interface {
// Attach returns a new File.
//
// The client-side attach will be translate to a series of walks from
// The client-side attach will be translated to a series of walks from
// the file returned by this Attach call.
Attach() (File, error)

// ServerOptions returns configuration options for this attach point.
//
// This is never caller in the client-side.
ServerOptions() AttacherOptions
}

// File is a set of operations corresponding to a single node.
Expand Down Expand Up @@ -301,15 +325,15 @@ type File interface {
type DefaultWalkGetAttr struct{}

// WalkGetAttr implements File.WalkGetAttr.
func (DefaultWalkGetAttr) WalkGetAttr([]string) ([]QID, File, AttrMask, Attr, error) {
func (*DefaultWalkGetAttr) WalkGetAttr([]string) ([]QID, File, AttrMask, Attr, error) {
return nil, nil, AttrMask{}, Attr{}, unix.ENOSYS
}

// DisallowClientCalls panics if a client-only function is called.
type DisallowClientCalls struct{}

// SetAttrClose implements File.SetAttrClose.
func (DisallowClientCalls) SetAttrClose(SetAttrMask, SetAttr) error {
func (*DisallowClientCalls) SetAttrClose(SetAttrMask, SetAttr) error {
panic("SetAttrClose should not be called on the server")
}

Expand All @@ -321,6 +345,11 @@ func (*DisallowServerCalls) Renamed(File, string) {
panic("Renamed should not be called on the client")
}

// ServerOptions implements Attacher.
func (*DisallowServerCalls) ServerOptions() AttacherOptions {
panic("ServerOptions should not be called on the client")
}

// DefaultMultiGetAttr implements File.MultiGetAttr() on top of File.
func DefaultMultiGetAttr(start File, names []string) ([]FullStat, error) {
stats := make([]FullStat, 0, len(names))
Expand Down
6 changes: 3 additions & 3 deletions pkg/p9/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func (t *Tsetattrclunk) handle(cs *connState) message {
// This might be technically incorrect, as it's possible that
// there were multiple links and you can still change the
// corresponding inode information.
if ref.isDeleted() {
if !cs.server.options.SetAttrOnDeleted && ref.isDeleted() {
return unix.EINVAL
}

Expand Down Expand Up @@ -913,7 +913,7 @@ func (t *Tsetattr) handle(cs *connState) message {
// This might be technically incorrect, as it's possible that
// there were multiple links and you can still change the
// corresponding inode information.
if ref.isDeleted() {
if !cs.server.options.SetAttrOnDeleted && ref.isDeleted() {
return unix.EINVAL
}

Expand Down Expand Up @@ -946,7 +946,7 @@ func (t *Tallocate) handle(cs *connState) message {
}

// We don't allow allocate on files that have been deleted.
if ref.isDeleted() {
if !cs.server.options.AllocateOnDeleted && ref.isDeleted() {
return unix.EINVAL
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/p9/p9test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ MOCK_SRC_PACKAGE = "gvisor.dev/gvisor/pkg/p9"
# mockgen_reflect is a source file that contains mock generation code that
# imports the p9 package and generates a specification via reflection. The
# usual generation path must be split into two distinct parts because the full
# source tree is not available to all build targets. Only declared depencies
# source tree is not available to all build targets. Only declared dependencies
# are available (and even then, not the Go source files).
genrule(
name = "mockgen_reflect",
Expand Down
1 change: 1 addition & 0 deletions pkg/p9/p9test/p9test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ func NewHarness(t *testing.T) (*Harness, *p9.Client) {
}

// Start the server, synchronized on exit.
h.Attacher.EXPECT().ServerOptions().Return(p9.AttacherOptions{}).Times(1)
server := p9.NewServer(h.Attacher)
h.wg.Add(1)
go func() {
Expand Down
9 changes: 8 additions & 1 deletion pkg/p9/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type Server struct {
// attacher provides the attach function.
attacher Attacher

options AttacherOptions

// pathTree is the full set of paths opened on this server.
//
// These may be across different connections, but rename operations
Expand All @@ -48,10 +50,15 @@ type Server struct {
renameMu sync.RWMutex
}

// NewServer returns a new server.
// NewServer returns a new server. attacher may be nil.
func NewServer(attacher Attacher) *Server {
opts := AttacherOptions{}
if attacher != nil {
opts = attacher.ServerOptions()
}
return &Server{
attacher: attacher,
options: opts,
pathTree: newPathNode(),
}
}
Expand Down
11 changes: 11 additions & 0 deletions runsc/fsgofer/fsgofer.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ func (a *attachPoint) Attach() (p9.File, error) {
return lf, nil
}

// ServerOptions implements p9.Attacher. It's safe to call SetAttr and Allocate
// on deleted files because fsgofer either uses an existing FD or opens a new
// one using the magic symlink in `/proc/[pid]/fd` and cannot mistakely open
// a file that was created in the same path as the delete file.
func (a *attachPoint) ServerOptions() p9.AttacherOptions {
return p9.AttacherOptions{
SetAttrOnDeleted: true,
AllocateOnDeleted: true,
}
}

// makeQID returns a unique QID for the given stat buffer.
func (a *attachPoint) makeQID(stat *unix.Stat_t) p9.QID {
a.deviceMu.Lock()
Expand Down
4 changes: 4 additions & 0 deletions test/syscalls/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -1053,3 +1053,7 @@ syscall_test(
syscall_test(
test = "//test/syscalls/linux:verity_mount_test",
)

syscall_test(
test = "//test/syscalls/linux:deleted_test",
)
15 changes: 15 additions & 0 deletions test/syscalls/linux/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -4432,3 +4432,18 @@ cc_binary(
"@com_google_absl//absl/container:flat_hash_set",
],
)

cc_binary(
name = "deleted_test",
testonly = 1,
srcs = ["deleted.cc"],
linkstatic = 1,
deps = [
"//test/util:file_descriptor",
"//test/util:fs_util",
gtest,
"//test/util:temp_path",
"//test/util:test_main",
"//test/util:test_util",
],
)
116 changes: 116 additions & 0 deletions test/syscalls/linux/deleted.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2021 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.

#include <errno.h>
#include <fcntl.h>
#include <time.h>
#include <unistd.h>

#include <string>

#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "test/util/file_descriptor.h"
#include "test/util/fs_util.h"
#include "test/util/temp_path.h"
#include "test/util/test_util.h"

constexpr mode_t mode = 1;

namespace gvisor {
namespace testing {

namespace {

PosixErrorOr<FileDescriptor> createdDeleted() {
auto path = NewTempAbsPath();
PosixErrorOr<FileDescriptor> fd = Open(path, O_RDWR | O_CREAT, mode);
if (!fd.ok()) {
return fd.error();
}

auto err = Unlink(path);
if (!err.ok()) {
return err;
}
return fd;
}

TEST(DeletedTest, Utime) {
auto fd = ASSERT_NO_ERRNO_AND_VALUE(createdDeleted());

const struct timespec times[2] = {{10, 0}, {20, 0}};
EXPECT_THAT(futimens(fd.get(), times), SyscallSucceeds());

struct stat stat;
ASSERT_THAT(fstat(fd.get(), &stat), SyscallSucceeds());
EXPECT_EQ(10, stat.st_atime);
EXPECT_EQ(20, stat.st_mtime);
}

TEST(DeletedTest, Chmod) {
auto fd = ASSERT_NO_ERRNO_AND_VALUE(createdDeleted());

ASSERT_THAT(fchmod(fd.get(), mode + 1), SyscallSucceeds());

struct stat stat;
ASSERT_THAT(fstat(fd.get(), &stat), SyscallSucceeds());
EXPECT_EQ(mode + 1, stat.st_mode & ~S_IFMT);
}

TEST(DeletedTest, Truncate) {
auto fd = ASSERT_NO_ERRNO_AND_VALUE(createdDeleted());
const std::string data = "foobar";
ASSERT_THAT(write(fd.get(), data.c_str(), data.size()), SyscallSucceeds());

ASSERT_THAT(ftruncate(fd.get(), 0), SyscallSucceeds());

struct stat stat;
ASSERT_THAT(fstat(fd.get(), &stat), SyscallSucceeds());
ASSERT_EQ(stat.st_size, 0);
}

TEST(DeletedTest, Fallocate) {
auto fd = ASSERT_NO_ERRNO_AND_VALUE(createdDeleted());

ASSERT_THAT(fallocate(fd.get(), 0, 0, 123), SyscallSucceeds());

struct stat stat;
ASSERT_THAT(fstat(fd.get(), &stat), SyscallSucceeds());
EXPECT_EQ(123, stat.st_size);
}

// Tests that a file can be created with the same path as a deleted file that
// still have an open FD to it.
TEST(DeletedTest, Replace) {
auto path = NewTempAbsPath();
auto fd = ASSERT_NO_ERRNO_AND_VALUE(Open(path, O_RDWR | O_CREAT, mode));
ASSERT_NO_ERRNO(Unlink(path));

auto other =
ASSERT_NO_ERRNO_AND_VALUE(Open(path, O_RDWR | O_CREAT | O_EXCL, mode));

auto stat = ASSERT_NO_ERRNO_AND_VALUE(Fstat(fd.get()));
auto stat_other = ASSERT_NO_ERRNO_AND_VALUE(Fstat(other.get()));
ASSERT_NE(stat.st_ino, stat_other.st_ino);

// Check that the path points to the new file.
stat = ASSERT_NO_ERRNO_AND_VALUE(Stat(path));
ASSERT_EQ(stat.st_ino, stat_other.st_ino);
}

} // namespace

} // namespace testing
} // namespace gvisor
8 changes: 8 additions & 0 deletions test/util/fs_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ PosixError MknodAt(const FileDescriptor& dfd, absl::string_view path, int mode,
return NoError();
}

PosixError Unlink(absl::string_view path) {
int res = unlink(std::string(path).c_str());
if (res < 0) {
return PosixError(errno, absl::StrCat("unlink ", path));
}
return NoError();
}

PosixError UnlinkAt(const FileDescriptor& dfd, absl::string_view path,
int flags) {
int res = unlinkat(dfd.get(), std::string(path).c_str(), flags);
Expand Down
1 change: 1 addition & 0 deletions test/util/fs_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ PosixError MknodAt(const FileDescriptor& dfd, absl::string_view path, int mode,
dev_t dev);

// Unlink the file.
PosixError Unlink(absl::string_view path);
PosixError UnlinkAt(const FileDescriptor& dfd, absl::string_view path,
int flags);

Expand Down

0 comments on commit 1e1d6b2

Please sign in to comment.