diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD index 0435f61a2..85412f54b 100644 --- a/test/syscalls/BUILD +++ b/test/syscalls/BUILD @@ -312,6 +312,10 @@ syscall_test( test = "//test/syscalls/linux:mmap_test", ) +syscall_test( + test = "//test/syscalls/linux:verity_mmap_test", +) + syscall_test( add_overlay = True, test = "//test/syscalls/linux:mount_test", diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index efed4aeb0..729b4c63b 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -1024,6 +1024,7 @@ cc_binary( "//test/util:temp_path", "//test/util:test_main", "//test/util:test_util", + "//test/util:verity_util", ], ) @@ -1293,6 +1294,23 @@ cc_binary( ], ) +cc_binary( + name = "verity_mmap_test", + testonly = 1, + srcs = ["verity_mmap.cc"], + linkstatic = 1, + deps = [ + "//test/util:capability_util", + gtest, + "//test/util:fs_util", + "//test/util:memory_util", + "//test/util:temp_path", + "//test/util:test_main", + "//test/util:test_util", + "//test/util:verity_util", + ], +) + cc_binary( name = "mount_test", testonly = 1, diff --git a/test/syscalls/linux/verity_ioctl.cc b/test/syscalls/linux/verity_ioctl.cc index 822e16f3c..be91b23d0 100644 --- a/test/syscalls/linux/verity_ioctl.cc +++ b/test/syscalls/linux/verity_ioctl.cc @@ -28,40 +28,13 @@ #include "test/util/mount_util.h" #include "test/util/temp_path.h" #include "test/util/test_util.h" +#include "test/util/verity_util.h" namespace gvisor { namespace testing { namespace { -#ifndef FS_IOC_ENABLE_VERITY -#define FS_IOC_ENABLE_VERITY 1082156677 -#endif - -#ifndef FS_IOC_MEASURE_VERITY -#define FS_IOC_MEASURE_VERITY 3221513862 -#endif - -#ifndef FS_VERITY_FL -#define FS_VERITY_FL 1048576 -#endif - -#ifndef FS_IOC_GETFLAGS -#define FS_IOC_GETFLAGS 2148034049 -#endif - -struct fsverity_digest { - __u16 digest_algorithm; - __u16 digest_size; /* input/output */ - __u8 digest[]; -}; - -constexpr int kMaxDigestSize = 64; -constexpr int kDefaultDigestSize = 32; -constexpr char kContents[] = "foobarbaz"; -constexpr char kMerklePrefix[] = ".merkle.verity."; -constexpr char kMerkleRootPrefix[] = ".merkleroot.verity."; - class IoctlTest : public ::testing::Test { protected: void SetUp() override { @@ -85,80 +58,6 @@ class IoctlTest : public ::testing::Test { std::string filename_; }; -// Provide a function to convert bytes to hex string, since -// absl::BytesToHexString does not seem to be compatible with golang -// hex.DecodeString used in verity due to zero-padding. -std::string BytesToHexString(uint8_t bytes[], int size) { - std::stringstream ss; - ss << std::hex; - for (int i = 0; i < size; ++i) { - ss << std::setw(2) << std::setfill('0') << static_cast(bytes[i]); - } - return ss.str(); -} - -std::string MerklePath(absl::string_view path) { - return JoinPath(Dirname(path), - std::string(kMerklePrefix) + std::string(Basename(path))); -} - -std::string MerkleRootPath(absl::string_view path) { - return JoinPath(Dirname(path), - std::string(kMerkleRootPrefix) + std::string(Basename(path))); -} - -// Flip a random bit in the file represented by fd. -PosixError FlipRandomBit(int fd, int size) { - // Generate a random offset in the file. - srand(time(nullptr)); - unsigned int seed = 0; - int random_offset = rand_r(&seed) % size; - - // Read a random byte and flip a bit in it. - char buf[1]; - RETURN_ERROR_IF_SYSCALL_FAIL(PreadFd(fd, buf, 1, random_offset)); - buf[0] ^= 1; - RETURN_ERROR_IF_SYSCALL_FAIL(PwriteFd(fd, buf, 1, random_offset)); - return NoError(); -} - -// Mount a verity on the tmpfs and enable both the file and the direcotry. Then -// mount a new verity with measured root hash. -PosixErrorOr MountVerity(std::string tmpfs_dir, - std::string filename) { - // Mount a verity fs on the existing tmpfs mount. - std::string mount_opts = "lower_path=" + tmpfs_dir; - ASSIGN_OR_RETURN_ERRNO(TempPath verity_dir, TempPath::CreateDir()); - RETURN_ERROR_IF_SYSCALL_FAIL( - mount("", verity_dir.path().c_str(), "verity", 0, mount_opts.c_str())); - - // Enable both the file and the directory. - ASSIGN_OR_RETURN_ERRNO( - auto fd, Open(JoinPath(verity_dir.path(), filename), O_RDONLY, 0777)); - RETURN_ERROR_IF_SYSCALL_FAIL(ioctl(fd.get(), FS_IOC_ENABLE_VERITY)); - ASSIGN_OR_RETURN_ERRNO(auto dir_fd, Open(verity_dir.path(), O_RDONLY, 0777)); - RETURN_ERROR_IF_SYSCALL_FAIL(ioctl(dir_fd.get(), FS_IOC_ENABLE_VERITY)); - - // Measure the root hash. - uint8_t digest_array[sizeof(struct fsverity_digest) + kMaxDigestSize] = {0}; - struct fsverity_digest* digest = - reinterpret_cast(digest_array); - digest->digest_size = kMaxDigestSize; - RETURN_ERROR_IF_SYSCALL_FAIL( - ioctl(dir_fd.get(), FS_IOC_MEASURE_VERITY, digest)); - - // Mount a verity fs with specified root hash. - mount_opts += - ",root_hash=" + BytesToHexString(digest->digest, digest->digest_size); - ASSIGN_OR_RETURN_ERRNO(TempPath verity_with_hash_dir, TempPath::CreateDir()); - RETURN_ERROR_IF_SYSCALL_FAIL(mount("", verity_with_hash_dir.path().c_str(), - "verity", 0, mount_opts.c_str())); - // Verity directories should not be deleted. Release the TempPath objects to - // prevent those directories from being deleted by the destructor. - verity_dir.release(); - return verity_with_hash_dir.release(); -} - TEST_F(IoctlTest, Enable) { // Mount a verity fs on the existing tmpfs mount. std::string mount_opts = "lower_path=" + tmpfs_dir_.path(); diff --git a/test/syscalls/linux/verity_mmap.cc b/test/syscalls/linux/verity_mmap.cc new file mode 100644 index 000000000..dde74cc91 --- /dev/null +++ b/test/syscalls/linux/verity_mmap.cc @@ -0,0 +1,158 @@ +// 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 +#include +#include +#include + +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "test/util/capability_util.h" +#include "test/util/fs_util.h" +#include "test/util/memory_util.h" +#include "test/util/temp_path.h" +#include "test/util/test_util.h" +#include "test/util/verity_util.h" + +namespace gvisor { +namespace testing { + +namespace { + +class MmapTest : public ::testing::Test { + protected: + void SetUp() override { + // Verity is implemented in VFS2. + SKIP_IF(IsRunningWithVFS1()); + + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + // Mount a tmpfs file system, to be wrapped by a verity fs. + tmpfs_dir_ = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); + ASSERT_THAT(mount("", tmpfs_dir_.path().c_str(), "tmpfs", 0, ""), + SyscallSucceeds()); + + // Create a new file in the tmpfs mount. + file_ = ASSERT_NO_ERRNO_AND_VALUE( + TempPath::CreateFileWith(tmpfs_dir_.path(), kContents, 0777)); + filename_ = Basename(file_.path()); + } + + TempPath tmpfs_dir_; + TempPath file_; + std::string filename_; +}; + +TEST_F(MmapTest, MmapRead) { + std::string verity_dir = + ASSERT_NO_ERRNO_AND_VALUE(MountVerity(tmpfs_dir_.path(), filename_)); + + // Make sure the file can be open and mmapped in the mounted verity fs. + auto const verity_fd = ASSERT_NO_ERRNO_AND_VALUE( + Open(JoinPath(verity_dir, filename_), O_RDONLY, 0777)); + + Mapping const m = + ASSERT_NO_ERRNO_AND_VALUE(Mmap(nullptr, sizeof(kContents) - 1, PROT_READ, + MAP_SHARED, verity_fd.get(), 0)); + EXPECT_THAT(std::string(m.view()), ::testing::StrEq(kContents)); +} + +TEST_F(MmapTest, ModifiedBeforeMmap) { + std::string verity_dir = + ASSERT_NO_ERRNO_AND_VALUE(MountVerity(tmpfs_dir_.path(), filename_)); + + // Modify the file and check verification failure upon mmapping. + auto const fd = ASSERT_NO_ERRNO_AND_VALUE( + Open(JoinPath(tmpfs_dir_.path(), filename_), O_RDWR, 0777)); + ASSERT_NO_ERRNO(FlipRandomBit(fd.get(), sizeof(kContents) - 1)); + + auto const verity_fd = ASSERT_NO_ERRNO_AND_VALUE( + Open(JoinPath(verity_dir, filename_), O_RDONLY, 0777)); + Mapping const m = + ASSERT_NO_ERRNO_AND_VALUE(Mmap(nullptr, sizeof(kContents) - 1, PROT_READ, + MAP_SHARED, verity_fd.get(), 0)); + + // Memory fault is expected when Translate fails. + EXPECT_EXIT(std::string(m.view()), ::testing::KilledBySignal(SIGSEGV), ""); +} + +TEST_F(MmapTest, ModifiedAfterMmap) { + std::string verity_dir = + ASSERT_NO_ERRNO_AND_VALUE(MountVerity(tmpfs_dir_.path(), filename_)); + + auto const verity_fd = ASSERT_NO_ERRNO_AND_VALUE( + Open(JoinPath(verity_dir, filename_), O_RDONLY, 0777)); + Mapping const m = + ASSERT_NO_ERRNO_AND_VALUE(Mmap(nullptr, sizeof(kContents) - 1, PROT_READ, + MAP_SHARED, verity_fd.get(), 0)); + + // Modify the file after mapping and check verification failure upon mmapping. + auto const fd = ASSERT_NO_ERRNO_AND_VALUE( + Open(JoinPath(tmpfs_dir_.path(), filename_), O_RDWR, 0777)); + ASSERT_NO_ERRNO(FlipRandomBit(fd.get(), sizeof(kContents) - 1)); + + // Memory fault is expected when Translate fails. + EXPECT_EXIT(std::string(m.view()), ::testing::KilledBySignal(SIGSEGV), ""); +} + +class MmapParamTest + : public MmapTest, + public ::testing::WithParamInterface> { + protected: + int prot() const { return std::get<0>(GetParam()); } + int flags() const { return std::get<1>(GetParam()); } +}; + +INSTANTIATE_TEST_SUITE_P( + WriteExecNoneSharedPrivate, MmapParamTest, + ::testing::Combine(::testing::ValuesIn({ + PROT_WRITE, + PROT_EXEC, + PROT_NONE, + }), + ::testing::ValuesIn({MAP_SHARED, MAP_PRIVATE}))); + +TEST_P(MmapParamTest, Mmap) { + std::string verity_dir = + ASSERT_NO_ERRNO_AND_VALUE(MountVerity(tmpfs_dir_.path(), filename_)); + + // Make sure the file can be open and mmapped in the mounted verity fs. + auto const verity_fd = ASSERT_NO_ERRNO_AND_VALUE( + Open(JoinPath(verity_dir, filename_), O_RDONLY, 0777)); + + if (prot() == PROT_WRITE && flags() == MAP_SHARED) { + // Verity file system is read-only. + EXPECT_THAT( + reinterpret_cast(mmap(nullptr, sizeof(kContents) - 1, prot(), + flags(), verity_fd.get(), 0)), + SyscallFailsWithErrno(EACCES)); + } else { + Mapping const m = ASSERT_NO_ERRNO_AND_VALUE(Mmap( + nullptr, sizeof(kContents) - 1, prot(), flags(), verity_fd.get(), 0)); + if (prot() == PROT_NONE) { + // Memory mapped by MAP_NONE cannot be accessed. + EXPECT_EXIT(std::string(m.view()), ::testing::KilledBySignal(SIGSEGV), + ""); + } else { + EXPECT_THAT(std::string(m.view()), ::testing::StrEq(kContents)); + } + } +} + +} // namespace + +} // namespace testing +} // namespace gvisor diff --git a/test/util/BUILD b/test/util/BUILD index 8985b54af..cc83221ea 100644 --- a/test/util/BUILD +++ b/test/util/BUILD @@ -401,3 +401,16 @@ cc_library( "@com_google_absl//absl/strings", ], ) + +cc_library( + name = "verity_util", + testonly = 1, + srcs = ["verity_util.cc"], + hdrs = ["verity_util.h"], + deps = [ + ":fs_util", + ":mount_util", + ":posix_error", + ":temp_path", + ], +) diff --git a/test/util/verity_util.cc b/test/util/verity_util.cc new file mode 100644 index 000000000..f1b4c251b --- /dev/null +++ b/test/util/verity_util.cc @@ -0,0 +1,93 @@ +// 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 "test/util/verity_util.h" + +#include "test/util/fs_util.h" +#include "test/util/mount_util.h" +#include "test/util/temp_path.h" + +namespace gvisor { +namespace testing { + +std::string BytesToHexString(uint8_t bytes[], int size) { + std::stringstream ss; + ss << std::hex; + for (int i = 0; i < size; ++i) { + ss << std::setw(2) << std::setfill('0') << static_cast(bytes[i]); + } + return ss.str(); +} + +std::string MerklePath(absl::string_view path) { + return JoinPath(Dirname(path), + std::string(kMerklePrefix) + std::string(Basename(path))); +} + +std::string MerkleRootPath(absl::string_view path) { + return JoinPath(Dirname(path), + std::string(kMerkleRootPrefix) + std::string(Basename(path))); +} + +PosixError FlipRandomBit(int fd, int size) { + // Generate a random offset in the file. + srand(time(nullptr)); + unsigned int seed = 0; + int random_offset = rand_r(&seed) % size; + + // Read a random byte and flip a bit in it. + char buf[1]; + RETURN_ERROR_IF_SYSCALL_FAIL(PreadFd(fd, buf, 1, random_offset)); + buf[0] ^= 1; + RETURN_ERROR_IF_SYSCALL_FAIL(PwriteFd(fd, buf, 1, random_offset)); + return NoError(); +} + +PosixErrorOr MountVerity(std::string tmpfs_dir, + std::string filename) { + // Mount a verity fs on the existing tmpfs mount. + std::string mount_opts = "lower_path=" + tmpfs_dir; + ASSIGN_OR_RETURN_ERRNO(TempPath verity_dir, TempPath::CreateDir()); + RETURN_ERROR_IF_SYSCALL_FAIL( + mount("", verity_dir.path().c_str(), "verity", 0, mount_opts.c_str())); + + // Enable both the file and the directory. + ASSIGN_OR_RETURN_ERRNO( + auto fd, Open(JoinPath(verity_dir.path(), filename), O_RDONLY, 0777)); + RETURN_ERROR_IF_SYSCALL_FAIL(ioctl(fd.get(), FS_IOC_ENABLE_VERITY)); + ASSIGN_OR_RETURN_ERRNO(auto dir_fd, Open(verity_dir.path(), O_RDONLY, 0777)); + RETURN_ERROR_IF_SYSCALL_FAIL(ioctl(dir_fd.get(), FS_IOC_ENABLE_VERITY)); + + // Measure the root hash. + uint8_t digest_array[sizeof(struct fsverity_digest) + kMaxDigestSize] = {0}; + struct fsverity_digest* digest = + reinterpret_cast(digest_array); + digest->digest_size = kMaxDigestSize; + RETURN_ERROR_IF_SYSCALL_FAIL( + ioctl(dir_fd.get(), FS_IOC_MEASURE_VERITY, digest)); + + // Mount a verity fs with specified root hash. + mount_opts += + ",root_hash=" + BytesToHexString(digest->digest, digest->digest_size); + ASSIGN_OR_RETURN_ERRNO(TempPath verity_with_hash_dir, TempPath::CreateDir()); + RETURN_ERROR_IF_SYSCALL_FAIL(mount("", verity_with_hash_dir.path().c_str(), + "verity", 0, mount_opts.c_str())); + // Verity directories should not be deleted. Release the TempPath objects to + // prevent those directories from being deleted by the destructor. + verity_dir.release(); + return verity_with_hash_dir.release(); +} + +} // namespace testing +} // namespace gvisor diff --git a/test/util/verity_util.h b/test/util/verity_util.h new file mode 100644 index 000000000..18743ecd6 --- /dev/null +++ b/test/util/verity_util.h @@ -0,0 +1,75 @@ +// 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. + +#ifndef GVISOR_TEST_UTIL_VERITY_UTIL_H_ +#define GVISOR_TEST_UTIL_VERITY_UTIL_H_ + +#include + +#include "test/util/posix_error.h" + +namespace gvisor { +namespace testing { + +#ifndef FS_IOC_ENABLE_VERITY +#define FS_IOC_ENABLE_VERITY 1082156677 +#endif + +#ifndef FS_IOC_MEASURE_VERITY +#define FS_IOC_MEASURE_VERITY 3221513862 +#endif + +#ifndef FS_VERITY_FL +#define FS_VERITY_FL 1048576 +#endif + +#ifndef FS_IOC_GETFLAGS +#define FS_IOC_GETFLAGS 2148034049 +#endif + +struct fsverity_digest { + unsigned short digest_algorithm; + unsigned short digest_size; /* input/output */ + unsigned char digest[]; +}; + +constexpr int kMaxDigestSize = 64; +constexpr int kDefaultDigestSize = 32; +constexpr char kContents[] = "foobarbaz"; +constexpr char kMerklePrefix[] = ".merkle.verity."; +constexpr char kMerkleRootPrefix[] = ".merkleroot.verity."; + +// Get the Merkle tree file path for |path|. +std::string MerklePath(absl::string_view path); + +// Get the root Merkle tree file path for |path|. +std::string MerkleRootPath(absl::string_view path); + +// Provide a function to convert bytes to hex string, since +// absl::BytesToHexString does not seem to be compatible with golang +// hex.DecodeString used in verity due to zero-padding. +std::string BytesToHexString(uint8_t bytes[], int size); + +// Flip a random bit in the file represented by fd. +PosixError FlipRandomBit(int fd, int size); + +// Mount a verity on the tmpfs and enable both the file and the direcotry. Then +// mount a new verity with measured root hash. +PosixErrorOr MountVerity(std::string tmpfs_dir, + std::string filename); + +} // namespace testing +} // namespace gvisor + +#endif // GVISOR_TEST_UTIL_VERITY_UTIL_H_