| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <fcntl.h> |
| #include <stdio.h> |
| #include <sys/stat.h> |
| #include <sys/syscall.h> |
| #include <sys/types.h> |
| |
| #include <gtest/gtest.h> |
| |
| #include "src/starnix/tests/syscalls/cpp/syscall_matchers.h" |
| #include "src/starnix/tests/syscalls/cpp/test_helper.h" |
| |
| namespace { |
| |
| class FcntlTest : public ::testing::Test { |
| protected: |
| void TearDown() override { |
| char *tmp = getenv("TEST_TMPDIR"); |
| std::string path = tmp == nullptr ? "/tmp/fcntltest" : std::string(tmp) + "/fcntltest"; |
| if (access(path.c_str(), F_OK) == 0) { |
| ASSERT_THAT(unlink(path.c_str()), SyscallSucceeds()); |
| } |
| } |
| }; |
| |
| class FcntlLockTest : public FcntlTest {}; |
| |
| bool CheckLock(int fd, short type, off_t start, off_t length, pid_t pid) { |
| test_helper::ForkHelper helper; |
| // Fork a process to be able to check the state of locks in fd. |
| helper.RunInForkedProcess([&] { |
| struct flock fl; |
| fl.l_type = F_WRLCK; |
| fl.l_whence = SEEK_SET; |
| fl.l_start = start; |
| fl.l_len = length; |
| SAFE_SYSCALL(fcntl(fd, F_GETLK, &fl)); |
| |
| ASSERT_EQ(fl.l_type, type); |
| if (type != F_UNLCK) { |
| ASSERT_EQ(fl.l_whence, SEEK_SET); |
| ASSERT_EQ(fl.l_start, start); |
| ASSERT_EQ(fl.l_len, length); |
| ASSERT_EQ(fl.l_pid, pid); |
| } |
| }); |
| return helper.WaitForChildren(); |
| } |
| |
| // Open a file to test. It will be of size 3000, and the position will be at |
| // 2000. |
| int OpenTestFile() { |
| char *tmp = getenv("TEST_TMPDIR"); |
| std::string path = tmp == nullptr ? "/tmp/fcntltest" : std::string(tmp) + "/fcntltest"; |
| int fd = open(path.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0777); |
| SAFE_SYSCALL(lseek(fd, 2999, SEEK_SET)); |
| // Make the file 3000 bytes longs |
| SAFE_SYSCALL(write(fd, &fd, 1)); |
| // Move to 2000 |
| SAFE_SYSCALL(lseek(fd, 2000, SEEK_SET)); |
| return fd; |
| } |
| |
| // Test that exiting a processes releases locks on a file. |
| TEST_F(FcntlLockTest, ChildProcessReleaseLock) { |
| for (int i = 0; i < 10; ++i) { |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([] { |
| int fd = OpenTestFile(); |
| |
| struct flock fl; |
| fl.l_type = F_WRLCK; |
| fl.l_whence = SEEK_SET; |
| fl.l_start = 0; |
| fl.l_len = 3000; |
| // This should succeed since the previous process that held the lock exited (as reported by |
| // wait(2)) and thus should no longer be holding a lock on the file. |
| SAFE_SYSCALL(fcntl(fd, F_SETLK, &fl)); |
| }); |
| } |
| } |
| |
| TEST_F(FcntlLockTest, ReleaseLockInMiddleOfAnotherLock) { |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([&] { |
| int fd = OpenTestFile(); |
| |
| struct flock fl; |
| fl.l_type = F_WRLCK; |
| fl.l_whence = SEEK_CUR; |
| fl.l_start = -2000; |
| fl.l_len = 3000; |
| SAFE_SYSCALL(fcntl(fd, F_SETLK, &fl)); |
| |
| fl.l_type = F_UNLCK; |
| fl.l_whence = SEEK_END; |
| fl.l_start = -2000; |
| fl.l_len = 1000; |
| SAFE_SYSCALL(fcntl(fd, F_SETLK, &fl)); |
| |
| // Check that we have a lock between [0, 1000[ and [2000, 3000[. |
| ASSERT_TRUE(CheckLock(fd, F_WRLCK, 0, 1000, getpid())); |
| ASSERT_TRUE(CheckLock(fd, F_UNLCK, 1000, 1000, 0)); |
| ASSERT_TRUE(CheckLock(fd, F_WRLCK, 2000, 1000, getpid())); |
| }); |
| } |
| |
| TEST_F(FcntlLockTest, ChangeLockTypeInMiddleOfAnotherLock) { |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([&] { |
| int fd = OpenTestFile(); |
| |
| struct flock fl; |
| fl.l_type = F_WRLCK; |
| fl.l_whence = SEEK_SET; |
| fl.l_start = 0; |
| fl.l_len = 3000; |
| SAFE_SYSCALL(fcntl(fd, F_SETLK, &fl)); |
| |
| fl.l_type = F_RDLCK; |
| fl.l_whence = SEEK_END; |
| fl.l_start = -2000; |
| fl.l_len = 1000; |
| SAFE_SYSCALL(fcntl(fd, F_SETLK, &fl)); |
| |
| // Check that we have a write lock between [0, 1000[ and [2000, 3000[ and a |
| // read lock between [1000, 2000[. |
| ASSERT_TRUE(CheckLock(fd, F_WRLCK, 0, 1000, getpid())); |
| ASSERT_TRUE(CheckLock(fd, F_RDLCK, 1000, 1000, getpid())); |
| ASSERT_TRUE(CheckLock(fd, F_WRLCK, 2000, 1000, getpid())); |
| }); |
| } |
| |
| TEST_F(FcntlLockTest, CloneFiles) { |
| // TODO(https://fxbug.dev/42080141): Find out why this test does not work on host in CQ |
| if (!test_helper::IsStarnix()) { |
| GTEST_SKIP() << "This test does not work on Linux in CQ"; |
| } |
| |
| // Do all the test in another process, as it will requires closing the parent |
| // process before the child one. |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([&] { |
| int fd = OpenTestFile(); |
| pid_t pid = getpid(); |
| |
| // Lock the file. |
| struct flock fl; |
| fl.l_type = F_WRLCK; |
| fl.l_whence = SEEK_SET; |
| fl.l_start = 0; |
| fl.l_len = 0; |
| SAFE_SYSCALL(fcntl(fd, F_SETLK, &fl)); |
| |
| // Clone the process, with CLONE_FILES |
| int flags = CLONE_FILES | SIGCHLD; |
| if (SAFE_SYSCALL(syscall(SYS_clone, flags, nullptr, nullptr, nullptr, nullptr)) > 0) { |
| // Parent immediately exit. |
| _exit(testing::Test::HasFailure()); |
| } |
| |
| // The child is a new process but with the exact same file table as its |
| // parent. |
| ASSERT_NE(getpid(), pid); |
| // Wait for our parent to finish. |
| while (getppid() == pid) { |
| usleep(1000); |
| } |
| |
| // Fork a process to be able to check the state of locks in fd. The returned |
| // pid is expected to be the one of the now dead process. |
| ASSERT_TRUE(CheckLock(fd, F_WRLCK, 0, 0, pid)); |
| |
| int new_fd = dup(fd); |
| // Closing fd should release the lock. |
| SAFE_SYSCALL(close(fd)); |
| ASSERT_TRUE(CheckLock(new_fd, F_UNLCK, 0, 0, 0)); |
| }); |
| } |
| |
| TEST_F(FcntlLockTest, CheckErrors) { |
| int fd = OpenTestFile(); |
| |
| struct flock fl; |
| fl.l_type = 42; |
| fl.l_whence = SEEK_SET; |
| fl.l_start = 0; |
| fl.l_len = 0; |
| |
| ASSERT_EQ(fcntl(fd, F_SETLK, &fl), -1); |
| ASSERT_EQ(errno, EINVAL); |
| |
| fl.l_type = F_WRLCK; |
| fl.l_whence = 42; |
| |
| ASSERT_EQ(fcntl(fd, F_SETLK, &fl), -1); |
| ASSERT_EQ(errno, EINVAL); |
| |
| fl.l_type = F_WRLCK; |
| fl.l_whence = SEEK_END; |
| fl.l_start = std::numeric_limits<decltype(fl.l_start)>::max(); |
| fl.l_len = 0; |
| |
| ASSERT_EQ(fcntl(fd, F_SETLK, &fl), -1); |
| ASSERT_EQ(errno, EOVERFLOW); |
| |
| fl.l_type = F_WRLCK; |
| fl.l_whence = SEEK_END; |
| fl.l_start = std::numeric_limits<decltype(fl.l_len)>::min(); |
| fl.l_len = std::numeric_limits<decltype(fl.l_len)>::min(); |
| |
| ASSERT_EQ(fcntl(fd, F_SETLK, &fl), -1); |
| ASSERT_EQ(errno, EINVAL); |
| |
| fl.l_type = F_WRLCK; |
| fl.l_whence = SEEK_SET; |
| fl.l_start = 0; |
| fl.l_len = -1; |
| |
| ASSERT_EQ(fcntl(fd, F_SETLK, &fl), -1); |
| ASSERT_EQ(errno, EINVAL); |
| } |
| |
| TEST_F(FcntlTest, FdDup) { |
| int fd = OpenTestFile(); |
| |
| int new_fd = SAFE_SYSCALL(fcntl(fd, F_DUPFD, 1000)); |
| ASSERT_GE(new_fd, 1000); |
| new_fd = SAFE_SYSCALL(fcntl(fd, F_DUPFD, 0)); |
| ASSERT_LT(new_fd, 1000); |
| } |
| |
| TEST_F(FcntlTest, SetFdAfterFdDup) { |
| int fd = OpenTestFile(); |
| int new_fd = SAFE_SYSCALL(dup(fd)); |
| |
| int new_fd_flags_before = SAFE_SYSCALL(fcntl(new_fd, F_GETFD)); |
| int flags = SAFE_SYSCALL(fcntl(fd, F_GETFD)); |
| ASSERT_EQ(SAFE_SYSCALL(fcntl(fd, F_SETFD, flags ^ FD_CLOEXEC)), 0); |
| |
| // `F_SETFD` sets per-FD flags, in contrast to `F_SETFL`, which modifies file "status" flags which |
| // are per file-description, and therefore shared by duplicated files. Changing `FD_CLOEXEC` on |
| // one of the duplicated descriptors has no effect on the other. |
| int new_fd_flags = SAFE_SYSCALL(fcntl(new_fd, F_GETFD)); |
| EXPECT_EQ(new_fd_flags, new_fd_flags_before); |
| } |
| |
| TEST_F(FcntlTest, SetFlAfterFdDup) { |
| int fd = OpenTestFile(); |
| int new_fd = SAFE_SYSCALL(dup(fd)); |
| |
| int new_fd_flags_before = SAFE_SYSCALL(fcntl(new_fd, F_GETFL)); |
| int flags = SAFE_SYSCALL(fcntl(fd, F_GETFL)); |
| EXPECT_EQ(new_fd_flags_before, flags); |
| ASSERT_EQ(SAFE_SYSCALL(fcntl(fd, F_SETFL, flags ^ O_NONBLOCK)), 0); |
| |
| // `F_SETFL` sets per-description file "status" flags, which are common to all FDs sharing the |
| // same file description. Changing `O_NONBLOCK` for an FD affects any duplicates. |
| int new_fd_flags = SAFE_SYSCALL(fcntl(new_fd, F_GETFL)); |
| EXPECT_EQ(new_fd_flags, new_fd_flags_before ^ O_NONBLOCK); |
| } |
| |
| TEST_F(FcntlTest, Noatime) { |
| int fd = OpenTestFile(); |
| |
| EXPECT_EQ(fcntl(fd, F_SETFL, O_NOATIME), 0); |
| } |
| |
| TEST_F(FcntlTest, NoatimePermission) { |
| if (getuid() != 0) { |
| GTEST_SKIP() << "Can only be run as root."; |
| } |
| |
| int fd = OpenTestFile(); |
| |
| // Fork to change UID. |
| test_helper::ForkHelper helper; |
| helper.RunInForkedProcess([&] { |
| ASSERT_EQ(setuid(1), 0); |
| |
| ASSERT_LT(fcntl(fd, F_SETFL, O_NOATIME), 0); |
| ASSERT_EQ(errno, EPERM); |
| }); |
| } |
| |
| TEST_F(FcntlTest, RenameExchangeLockOrdering) { |
| char *tmp = getenv("TEST_TMPDIR"); |
| std::string root_dir = tmp == nullptr ? "/tmp" : std::string(tmp); |
| |
| // This test exercises a niche lock ordering bug. In essence, the rename_exchange |
| // operation can muddle with the lock orderning in DirEntry due to the reparenting |
| // of nodes. See the following bug for a more detailed description: |
| // https://buganizer.corp.google.com/issues/387576826 |
| std::string first_parent_dir = root_dir + "/first_parent_dir"; |
| std::string second_parent_dir = root_dir + "/second_parent_dir"; |
| std::string file = second_parent_dir + "/file"; |
| |
| // Set up the initial folder and file structure. |
| ASSERT_THAT(mkdir(first_parent_dir.c_str(), 0700), SyscallSucceeds()); |
| ASSERT_THAT(mkdir(second_parent_dir.c_str(), 0700), SyscallSucceeds()); |
| ASSERT_THAT(open(file.c_str(), O_CREAT | O_WRONLY, 0600), SyscallSucceeds()); |
| |
| // The rename operation here is irrelevant, except in that it establishes |
| // the lock ordering for the parent directories. In other words, the lock |
| // ordering is set as first locking the children of "first_parent_dir," |
| // followed by locking the children of "second_parent_dir." |
| std::string dummy_first_parent_child = first_parent_dir + "/dummy_file.txt"; |
| std::string dummy_second_parent_child = second_parent_dir + "/dummy_file.txt"; |
| // Since these files don't exist, we expect the rename to fail. Once again, |
| // we are only doing this to establish the lock ordering for the directories. |
| ASSERT_THAT(rename(dummy_first_parent_child.c_str(), dummy_second_parent_child.c_str()), |
| SyscallFails()); |
| |
| // Next, we'll do the rename_exchange operation. This will exchange the nested |
| // file with a higher-level directory, which can potentially pollute the |
| // lock tracing state of the directory hierarchy. |
| ASSERT_THAT(renameat2(0, file.c_str(), 0, first_parent_dir.c_str(), RENAME_EXCHANGE), |
| SyscallSucceeds()); |
| |
| // Lastly, we'll attempt to touch the "first_parent_dir," which we've just |
| // exchanged to be nested under "second_parent_dir." This will cause the |
| // "second_parent_dir" children to be locked, followed by the "first_parent_dir" |
| // that's now nested under it. This could potentially trip our lock ordering |
| // which we established in the first rename operation of this test. |
| std::string newly_exchanged_node = second_parent_dir + "/file"; |
| ASSERT_THAT(rmdir(newly_exchanged_node.c_str()), SyscallSucceeds()); |
| |
| // Clean up, unlinking the `first_parent_dir` which is now a file per the RENAME_EXCHANGE. |
| ASSERT_THAT(unlink(first_parent_dir.c_str()), SyscallSucceeds()); |
| ASSERT_THAT(rmdir(second_parent_dir.c_str()), SyscallSucceeds()); |
| } |
| |
| } // namespace |