summaryrefslogtreecommitdiffhomepage
path: root/test/syscalls/linux/mount.cc
diff options
context:
space:
mode:
Diffstat (limited to 'test/syscalls/linux/mount.cc')
-rw-r--r--test/syscalls/linux/mount.cc327
1 files changed, 327 insertions, 0 deletions
diff --git a/test/syscalls/linux/mount.cc b/test/syscalls/linux/mount.cc
new file mode 100644
index 000000000..a3e9745cf
--- /dev/null
+++ b/test/syscalls/linux/mount.cc
@@ -0,0 +1,327 @@
+// 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.
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/strings/string_view.h"
+#include "absl/time/time.h"
+#include "test/util/capability_util.h"
+#include "test/util/file_descriptor.h"
+#include "test/util/fs_util.h"
+#include "test/util/mount_util.h"
+#include "test/util/multiprocess_util.h"
+#include "test/util/posix_error.h"
+#include "test/util/temp_path.h"
+#include "test/util/test_util.h"
+#include "test/util/thread_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+TEST(MountTest, MountBadFilesystem) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ // Linux expects a valid target before it checks the file system name.
+ auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ EXPECT_THAT(mount("", dir.path().c_str(), "foobar", 0, ""),
+ SyscallFailsWithErrno(ENODEV));
+}
+
+TEST(MountTest, MountInvalidTarget) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ auto const dir = NewTempAbsPath();
+ EXPECT_THAT(mount("", dir.c_str(), "tmpfs", 0, ""),
+ SyscallFailsWithErrno(ENOENT));
+}
+
+TEST(MountTest, MountPermDenied) {
+ // Clear CAP_SYS_ADMIN.
+ if (ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))) {
+ EXPECT_NO_ERRNO(SetCapability(CAP_SYS_ADMIN, false));
+ }
+
+ // Linux expects a valid target before checking capability.
+ auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ EXPECT_THAT(mount("", dir.path().c_str(), "", 0, ""),
+ SyscallFailsWithErrno(EPERM));
+}
+
+TEST(MountTest, UmountPermDenied) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto const mount =
+ ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir.path(), "tmpfs", 0, "", 0));
+
+ // Drop privileges in another thread, so we can still unmount the mounted
+ // directory.
+ ScopedThread([&]() {
+ EXPECT_NO_ERRNO(SetCapability(CAP_SYS_ADMIN, false));
+ EXPECT_THAT(umount(dir.path().c_str()), SyscallFailsWithErrno(EPERM));
+ });
+}
+
+TEST(MountTest, MountOverBusy) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto const fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(JoinPath(dir.path(), "foo"), O_CREAT | O_RDWR, 0777));
+
+ // Should be able to mount over a busy directory.
+ ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir.path(), "tmpfs", 0, "", 0));
+}
+
+TEST(MountTest, OpenFileBusy) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto const mount = ASSERT_NO_ERRNO_AND_VALUE(
+ Mount("", dir.path(), "tmpfs", 0, "mode=0700", 0));
+ auto const fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(JoinPath(dir.path(), "foo"), O_CREAT | O_RDWR, 0777));
+
+ // An open file should prevent unmounting.
+ EXPECT_THAT(umount(dir.path().c_str()), SyscallFailsWithErrno(EBUSY));
+}
+
+TEST(MountTest, UmountDetach) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ // structure:
+ //
+ // dir (mount point)
+ // subdir
+ // file
+ //
+ // We show that we can walk around in the mount after detach-unmount dir.
+ //
+ // We show that even though dir is unreachable from outside the mount, we can
+ // still reach dir's (former) parent!
+ auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+
+ const struct stat before = ASSERT_NO_ERRNO_AND_VALUE(Stat(dir.path()));
+ auto mount =
+ ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir.path(), "tmpfs", 0, "mode=0700",
+ /* umountflags= */ MNT_DETACH));
+ const struct stat after = ASSERT_NO_ERRNO_AND_VALUE(Stat(dir.path()));
+ EXPECT_NE(before.st_ino, after.st_ino);
+
+ // Create files in the new mount.
+ constexpr char kContents[] = "no no no";
+ auto const subdir =
+ ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(dir.path()));
+ auto const file = ASSERT_NO_ERRNO_AND_VALUE(
+ TempPath::CreateFileWith(dir.path(), kContents, 0777));
+
+ auto const dir_fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(subdir.path(), O_RDONLY | O_DIRECTORY));
+ auto const fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDONLY));
+
+ // Unmount the tmpfs.
+ mount.Release()();
+
+ const struct stat after2 = ASSERT_NO_ERRNO_AND_VALUE(Stat(dir.path()));
+ EXPECT_EQ(before.st_ino, after2.st_ino);
+
+ // Can still read file after unmounting.
+ std::vector<char> buf(sizeof(kContents));
+ EXPECT_THAT(ReadFd(fd.get(), buf.data(), buf.size()), SyscallSucceeds());
+
+ // Walk to dir.
+ auto const mounted_dir = ASSERT_NO_ERRNO_AND_VALUE(
+ OpenAt(dir_fd.get(), "..", O_DIRECTORY | O_RDONLY));
+ // Walk to dir/file.
+ auto const fd_again = ASSERT_NO_ERRNO_AND_VALUE(
+ OpenAt(mounted_dir.get(), std::string(Basename(file.path())), O_RDONLY));
+
+ std::vector<char> buf2(sizeof(kContents));
+ EXPECT_THAT(ReadFd(fd_again.get(), buf2.data(), buf2.size()),
+ SyscallSucceeds());
+ EXPECT_EQ(buf, buf2);
+
+ // Walking outside the unmounted realm should still work, too!
+ auto const dir_parent = ASSERT_NO_ERRNO_AND_VALUE(
+ OpenAt(mounted_dir.get(), "..", O_DIRECTORY | O_RDONLY));
+}
+
+TEST(MountTest, ActiveSubmountBusy) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto const mount1 = ASSERT_NO_ERRNO_AND_VALUE(
+ Mount("", dir.path(), "tmpfs", 0, "mode=0700", 0));
+
+ auto const dir2 =
+ ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(dir.path()));
+ auto const mount2 =
+ ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir2.path(), "tmpfs", 0, "", 0));
+
+ // Since dir now has an active submount, should not be able to unmount.
+ EXPECT_THAT(umount(dir.path().c_str()), SyscallFailsWithErrno(EBUSY));
+}
+
+TEST(MountTest, MountTmpfs) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+
+ // NOTE(b/129868551): Inode IDs are only stable across S/R if we have an open
+ // FD for that inode. Since we are going to compare inode IDs below, get a
+ // FileDescriptor for this directory here, which will be closed automatically
+ // at the end of the test.
+ auto const fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(dir.path(), O_DIRECTORY, O_RDONLY));
+
+ const struct stat before = ASSERT_NO_ERRNO_AND_VALUE(Stat(dir.path()));
+
+ {
+ auto const mount = ASSERT_NO_ERRNO_AND_VALUE(
+ Mount("", dir.path(), "tmpfs", 0, "mode=0700", 0));
+
+ const struct stat s = ASSERT_NO_ERRNO_AND_VALUE(Stat(dir.path()));
+ EXPECT_EQ(s.st_mode, S_IFDIR | 0700);
+ EXPECT_NE(s.st_ino, before.st_ino);
+
+ EXPECT_NO_ERRNO(Open(JoinPath(dir.path(), "foo"), O_CREAT | O_RDWR, 0777));
+ }
+
+ // Now that dir is unmounted again, we should have the old inode back.
+ const struct stat after = ASSERT_NO_ERRNO_AND_VALUE(Stat(dir.path()));
+ EXPECT_EQ(before.st_ino, after.st_ino);
+}
+
+TEST(MountTest, MountTmpfsMagicValIgnored) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+
+ auto const mount = ASSERT_NO_ERRNO_AND_VALUE(
+ Mount("", dir.path(), "tmpfs", MS_MGC_VAL, "mode=0700", 0));
+}
+
+// Passing nullptr to data is equivalent to "".
+TEST(MountTest, NullData) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+
+ EXPECT_THAT(mount("", dir.path().c_str(), "tmpfs", 0, nullptr),
+ SyscallSucceeds());
+ EXPECT_THAT(umount2(dir.path().c_str(), 0), SyscallSucceeds());
+}
+
+TEST(MountTest, MountReadonly) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto const mount = ASSERT_NO_ERRNO_AND_VALUE(
+ Mount("", dir.path(), "tmpfs", MS_RDONLY, "mode=0777", 0));
+
+ const struct stat s = ASSERT_NO_ERRNO_AND_VALUE(Stat(dir.path()));
+ EXPECT_EQ(s.st_mode, S_IFDIR | 0777);
+
+ std::string const filename = JoinPath(dir.path(), "foo");
+ EXPECT_THAT(open(filename.c_str(), O_RDWR | O_CREAT, 0777),
+ SyscallFailsWithErrno(EROFS));
+}
+
+PosixErrorOr<absl::Time> ATime(absl::string_view file) {
+ struct stat s = {};
+ if (stat(std::string(file).c_str(), &s) == -1) {
+ return PosixError(errno, "stat failed");
+ }
+ return absl::TimeFromTimespec(s.st_atim);
+}
+
+TEST(MountTest, MountNoAtime) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto const mount = ASSERT_NO_ERRNO_AND_VALUE(
+ Mount("", dir.path(), "tmpfs", MS_NOATIME, "mode=0777", 0));
+
+ std::string const contents = "No no no, don't follow the instructions!";
+ auto const file = ASSERT_NO_ERRNO_AND_VALUE(
+ TempPath::CreateFileWith(dir.path(), contents, 0777));
+
+ absl::Time const before = ASSERT_NO_ERRNO_AND_VALUE(ATime(file.path()));
+
+ // Reading from the file should change the atime, but the MS_NOATIME flag
+ // should prevent that.
+ auto const fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR));
+ char buf[100];
+ int read_n;
+ ASSERT_THAT(read_n = read(fd.get(), buf, sizeof(buf)), SyscallSucceeds());
+ EXPECT_EQ(std::string(buf, read_n), contents);
+
+ absl::Time const after = ASSERT_NO_ERRNO_AND_VALUE(ATime(file.path()));
+
+ // Expect that atime hasn't changed.
+ EXPECT_EQ(before, after);
+}
+
+TEST(MountTest, MountNoExec) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ auto const dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto const mount = ASSERT_NO_ERRNO_AND_VALUE(
+ Mount("", dir.path(), "tmpfs", MS_NOEXEC, "mode=0777", 0));
+
+ std::string const contents = "No no no, don't follow the instructions!";
+ auto const file = ASSERT_NO_ERRNO_AND_VALUE(
+ TempPath::CreateFileWith(dir.path(), contents, 0777));
+
+ int execve_errno;
+ ASSERT_NO_ERRNO_AND_VALUE(
+ ForkAndExec(file.path(), {}, {}, nullptr, &execve_errno));
+ EXPECT_EQ(execve_errno, EACCES);
+}
+
+TEST(MountTest, RenameRemoveMountPoint) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
+
+ auto const dir_parent = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto const dir =
+ ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(dir_parent.path()));
+ auto const new_dir = NewTempAbsPath();
+
+ auto const mount =
+ ASSERT_NO_ERRNO_AND_VALUE(Mount("", dir.path(), "tmpfs", 0, "", 0));
+
+ ASSERT_THAT(rename(dir.path().c_str(), new_dir.c_str()),
+ SyscallFailsWithErrno(EBUSY));
+
+ ASSERT_THAT(rmdir(dir.path().c_str()), SyscallFailsWithErrno(EBUSY));
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor