blob: f49894353990d075b92411ab5e295cd9a46e4b98 [file] [log] [blame]
// Copyright 2025 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 <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/utsname.h>
#include <sys/xattr.h>
#include <syscall.h>
#include <unistd.h>
#include <cerrno>
#include <csignal>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <functional>
#include <optional>
#include <linux/userfaultfd.h>
#include "gtest/gtest.h"
#include "src/lib/files/file.h"
#include "src/starnix/tests/syscalls/cpp/proc_test_base.h"
#include "src/starnix/tests/syscalls/cpp/test_helper.h"
#ifndef MREMAP_DONTUNMAP
#define MREMAP_DONTUNMAP 4
#endif
#ifndef UFFD_USER_MODE_ONLY
#define UFFD_USER_MODE_ONLY 1
#endif
class UffdWrapper {
public:
UffdWrapper() {
int flags = O_CLOEXEC | O_NONBLOCK;
// UFFD_USER_MODE_ONLY was introduced in 5.11 and restricts page fault handling to faults
// originated from user space only. Without this flag, additional privileges might be needed to
// run these tests.
if (test_helper::IsKernelVersionAtLeast(5, 11) || test_helper::IsStarnix()) {
flags |= UFFD_USER_MODE_ONLY;
}
uffd_ = SAFE_SYSCALL(static_cast<int>(syscall(__NR_userfaultfd, flags)));
}
~UffdWrapper() { SAFE_SYSCALL(close(uffd_)); }
void api() const { api(0); }
void api(uint64_t features) const {
uffdio_api api = {.api = UFFD_API, .features = features, .ioctls = 0};
SAFE_SYSCALL(ioctl(uffd_, UFFDIO_API, &api));
}
void register_range(uintptr_t start, size_t len, size_t mode) const {
uffdio_register uffdio_register;
uffdio_register.range.start = start;
uffdio_register.range.len = len;
uffdio_register.mode = mode;
ASSERT_EQ(ioctl(uffd_, UFFDIO_REGISTER, &uffdio_register), 0);
}
int unregister_range(uintptr_t start, size_t len) const {
uffdio_range uffdio_range;
uffdio_range.start = start;
uffdio_range.len = len;
return ioctl(uffd_, UFFDIO_UNREGISTER, &uffdio_range);
}
void copy_one_page(uintptr_t src, uintptr_t dst) const {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
uffdio_copy uffdio_copy{
.dst = dst & ~(page_size - 1), // Align to page size
.src = src,
.len = page_size,
.mode = 0,
.copy = 0,
};
ASSERT_EQ(ioctl(uffd_, UFFDIO_COPY, &uffdio_copy), 0);
}
int zero_one_page(uintptr_t dst) const {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
uffdio_range uffdio_range;
uffdio_range.start = dst;
uffdio_range.len = page_size;
uffdio_zeropage uffdio_zeropage;
uffdio_zeropage.range = uffdio_range;
uffdio_zeropage.mode = 0;
int res = ioctl(uffd_, UFFDIO_ZEROPAGE, &uffdio_zeropage);
return res;
}
int fd() const { return uffd_; }
private:
int uffd_;
};
TEST(UffdTest, InitReturnsSupportedFeatures) {
UffdWrapper uffd;
uffdio_api api = {.api = UFFD_API, .features = 0, .ioctls = 0};
ASSERT_EQ(ioctl(uffd.fd(), UFFDIO_API, &api), 0);
// Lists of supported features and supported ioctls are not empty
ASSERT_NE(api.features, 0ul);
ASSERT_NE(api.ioctls, 0ul);
}
TEST(UffdTest, NoOperationsBeforeInit) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
uffdio_range uffdio_range{.start = reinterpret_cast<uintptr_t>(full_range.mapping()),
.len = page_size};
UffdWrapper uffd;
// No other ioctl is permitted on this file descriptor until the UFFDIO_API is called for the
// first time
uffdio_register uffdio_register{
.range = uffdio_range, .mode = UFFDIO_REGISTER_MODE_MISSING, .ioctls = 0};
EXPECT_NE(ioctl(uffd.fd(), UFFDIO_REGISTER, &uffdio_register), 0);
EXPECT_EQ(errno, EINVAL);
EXPECT_NE(ioctl(uffd.fd(), UFFDIO_UNREGISTER, &uffdio_range), 0);
EXPECT_EQ(errno, EINVAL);
uffdio_api api = {.api = UFFD_API, .features = UFFD_FEATURE_SIGBUS, .ioctls = 0};
ASSERT_EQ(ioctl(uffd.fd(), UFFDIO_API, &api), 0);
// The same ioctls succeed now
EXPECT_EQ(ioctl(uffd.fd(), UFFDIO_REGISTER, &uffdio_register), 0);
EXPECT_EQ(ioctl(uffd.fd(), UFFDIO_UNREGISTER, &uffdio_range), 0);
// Reinitialization is not allowed
ASSERT_NE(ioctl(uffd.fd(), UFFDIO_API, &api), 0);
EXPECT_EQ(errno, EPERM);
}
class UffdProcTest : public ProcTestBase {
public:
std::optional<test_helper::MemoryMappingExt> FindMemoryMappingInSmaps(uintptr_t addr) const {
std::string smaps;
if (!files::ReadFileToString(proc_path() + "/self/smaps", &smaps)) {
return std::nullopt;
}
return test_helper::find_memory_mapping_ext(addr, smaps);
}
};
TEST_F(UffdProcTest, Register) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
uintptr_t start = reinterpret_cast<uintptr_t>(full_range.mapping());
uffd.register_range(start, len, UFFDIO_REGISTER_MODE_MISSING);
auto registered_mapping = FindMemoryMappingInSmaps(start);
ASSERT_TRUE(registered_mapping->ContainsFlag("um"));
}
TEST_F(UffdProcTest, RegisterPart) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = 3 * page_size;
UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
uintptr_t start = reinterpret_cast<uintptr_t>(full_range.mapping());
uffd.register_range(start + page_size, page_size, UFFDIO_REGISTER_MODE_MISSING);
auto page1 = FindMemoryMappingInSmaps(start);
ASSERT_FALSE(page1->ContainsFlag("um"));
auto page2 = FindMemoryMappingInSmaps(start + page_size);
ASSERT_TRUE(page2.has_value());
ASSERT_TRUE(page2->ContainsFlag("um"));
ASSERT_EQ(page2->start, start + page_size);
ASSERT_EQ(page2->end, start + (2 * page_size));
auto page3 = FindMemoryMappingInSmaps(start + (2 * page_size));
ASSERT_FALSE(page3->ContainsFlag("um"));
}
TEST(UffdTest, RegisterEmpty) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
// Identify an empty page by mapping and then unmapping
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
uintptr_t start = reinterpret_cast<uintptr_t>(full_range.mapping());
full_range.Unmap();
uffdio_register uffdio_register;
uffdio_register.range.start = start;
uffdio_register.range.len = page_size;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
ASSERT_NE(ioctl(uffd.fd(), UFFDIO_REGISTER, &uffdio_register), 0);
EXPECT_EQ(errno, EINVAL);
}
TEST(UffdTest, TwoUffdCantOverlap) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
uintptr_t start = reinterpret_cast<uintptr_t>(full_range.mapping());
uffdio_register uffdio_register;
// Register the mapping with uffd_a
UffdWrapper uffd_a;
uffd_a.api(UFFD_FEATURE_SIGBUS);
uffdio_register.range.start = start;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
ASSERT_EQ(ioctl(uffd_a.fd(), UFFDIO_REGISTER, &uffdio_register), 0);
// Can't register the same mapping with a different uffd
UffdWrapper uffd_b;
uffd_b.api(UFFD_FEATURE_SIGBUS);
uffdio_register.range.start = start;
uffdio_register.range.len = page_size;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
ASSERT_NE(ioctl(uffd_b.fd(), UFFDIO_REGISTER, &uffdio_register), 0);
EXPECT_EQ(errno, EBUSY);
}
TEST_F(UffdProcTest, SmapsOnFork) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
uintptr_t start = reinterpret_cast<uintptr_t>(full_range.mapping());
test_helper::ForkHelper helper;
helper.RunInForkedProcess([&] {
uffd.register_range(start, len, UFFDIO_REGISTER_MODE_MISSING);
// The registration isn't reflected in child's smaps
auto registered_mapping = FindMemoryMappingInSmaps(start);
ASSERT_FALSE(registered_mapping->ContainsFlag("um"));
});
ASSERT_TRUE(helper.WaitForChildren());
// Instead, it is reflected in the parent's smaps
auto registered_mapping = FindMemoryMappingInSmaps(start);
ASSERT_TRUE(registered_mapping->ContainsFlag("um"));
}
TEST_F(UffdProcTest, Unregister) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = 2 * page_size;
UffdWrapper uffd_a;
uffd_a.api(UFFD_FEATURE_SIGBUS);
UffdWrapper uffd_b;
uffd_b.api(UFFD_FEATURE_SIGBUS);
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
uintptr_t start = reinterpret_cast<uintptr_t>(full_range.mapping());
reinterpret_cast<char *>(full_range.mapping())[0] = 'a';
// Unregistering without prior registration is a noop
EXPECT_EQ(uffd_a.unregister_range(start, len), 0);
uffd_a.register_range(start, page_size, UFFDIO_REGISTER_MODE_MISSING);
EXPECT_EQ(uffd_a.unregister_range(start, len), 0);
uffd_b.register_range(start, page_size, UFFDIO_REGISTER_MODE_MISSING);
uffd_a.register_range(start + page_size, page_size, UFFDIO_REGISTER_MODE_MISSING);
// Unregistration affects all uffd registrations in the range, regardless of which uffd they
// belong to.
EXPECT_EQ(uffd_a.unregister_range(start, len), 0);
auto registered_mapping_a = FindMemoryMappingInSmaps(start);
ASSERT_FALSE(registered_mapping_a->ContainsFlag("um"));
auto registered_mapping_b = FindMemoryMappingInSmaps(start + page_size);
ASSERT_FALSE(registered_mapping_b->ContainsFlag("um"));
}
TEST_F(UffdProcTest, Close) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = 2 * page_size;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
uintptr_t start = reinterpret_cast<uintptr_t>(full_range.mapping());
memset(full_range.mapping(), 'a', len);
UffdWrapper uffd_a;
uffd_a.api(UFFD_FEATURE_SIGBUS);
{
UffdWrapper uffd_b;
uffd_b.api(UFFD_FEATURE_SIGBUS);
uffd_a.register_range(start, page_size, UFFDIO_REGISTER_MODE_MISSING);
uffd_b.register_range(start + page_size, page_size, UFFDIO_REGISTER_MODE_MISSING);
}
auto registered_mapping_a = FindMemoryMappingInSmaps(start);
auto registered_mapping_b = FindMemoryMappingInSmaps(start + page_size);
ASSERT_NE(registered_mapping_a->start, registered_mapping_b->start);
ASSERT_TRUE(registered_mapping_a->ContainsFlag("um"));
ASSERT_FALSE(registered_mapping_b->ContainsFlag("um"));
}
// This test intentionally triggers a deadly signal (SIGBUS) which can't pass on sanitizer runs.
#if (!__has_feature(address_sanitizer))
TEST(UffdTest, Sigbus) {
test_helper::ForkHelper helper;
helper.ExpectSignal(SIGBUS);
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([] {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
intptr_t start = reinterpret_cast<intptr_t>(full_range.mapping());
// Register the mapping with uffd_a
UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
uffd.register_range(start, len, UFFDIO_REGISTER_MODE_MISSING);
reinterpret_cast<char *>(full_range.mapping())[0] = 'a';
});
ASSERT_TRUE(helper.WaitForChildren());
}
#endif
TEST(UffdTest, NoSigbusAfterUnregister) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([] {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
intptr_t start = reinterpret_cast<intptr_t>(full_range.mapping());
// Register the mapping with uffd_a
UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
uffd.register_range(start, len, UFFDIO_REGISTER_MODE_MISSING);
uffd.unregister_range(start, len);
reinterpret_cast<char *>(full_range.mapping())[0] = 'a';
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(UffdTest, NoSigbusAfterClose) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([] {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
intptr_t start = reinterpret_cast<intptr_t>(full_range.mapping());
{
UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
uffd.register_range(start, len, UFFDIO_REGISTER_MODE_MISSING);
}
reinterpret_cast<char *>(full_range.mapping())[0] = 'a';
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(UffdTest, NoSigbusFromSyscall) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, 2 * page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
uintptr_t start = reinterpret_cast<uintptr_t>(full_range.mapping());
UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
// Register 2nd page only.
uffd.register_range(start + page_size, page_size, UFFDIO_REGISTER_MODE_MISSING);
test_helper::ScopedTempFD temp_file;
ASSERT_TRUE(temp_file);
// Complete bad write.
errno = 0;
ssize_t sresult = write(temp_file.fd(), reinterpret_cast<void *>(start + page_size), page_size);
EXPECT_EQ(-1, sresult);
EXPECT_EQ(EFAULT, errno);
}
// This test intentionally triggers a deadly signal (SIGBUS) which can't pass on sanitizer runs.
#if (!__has_feature(address_sanitizer))
TEST(UffdTest, RegisterThenFork) {
test_helper::ForkHelper helper;
helper.ExpectSignal(SIGBUS);
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([] {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
intptr_t start = reinterpret_cast<intptr_t>(full_range.mapping());
UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
uffd.register_range(start, len, UFFDIO_REGISTER_MODE_MISSING);
test_helper::ForkHelper helper2;
helper2.OnlyWaitForForkedChildren();
helper2.RunInForkedProcess([&] {
char c_from_fork = reinterpret_cast<char *>(full_range.mapping())[0];
// Accessing the registered range from child doesn't trigger uffd
EXPECT_EQ(c_from_fork, 0);
exit(EXIT_SUCCESS);
});
ASSERT_TRUE(helper2.WaitForChildren());
// Accessing the registered range from parent triggers uffd
char c = reinterpret_cast<char *>(full_range.mapping())[0];
EXPECT_EQ(c, 0);
});
ASSERT_TRUE(helper.WaitForChildren());
}
#endif
// This test intentionally triggers a deadly signal (SIGBUS) which can't pass on sanitizer runs.
#if (!__has_feature(address_sanitizer))
TEST(UffdTest, ForkThenRegister) {
test_helper::ForkHelper helper;
helper.ExpectSignal(SIGBUS);
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([] {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
intptr_t start = reinterpret_cast<intptr_t>(full_range.mapping());
UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
test_helper::ForkHelper helper2;
helper2.OnlyWaitForForkedChildren();
helper2.RunInForkedProcess([&] {
uffd.register_range(start, len, UFFDIO_REGISTER_MODE_MISSING);
// Accessing the registered range from child doesn't trigger uffd
char c_from_fork = reinterpret_cast<char *>(full_range.mapping())[0];
EXPECT_EQ(c_from_fork, 0);
exit(EXIT_SUCCESS);
});
ASSERT_TRUE(helper2.WaitForChildren());
// Accessing the registered range from parent triggers uffd
char c = reinterpret_cast<char *>(full_range.mapping())[0];
EXPECT_EQ(c, 0);
});
ASSERT_TRUE(helper.WaitForChildren());
}
#endif
// This test intentionally triggers a deadly signal (SIGBUS) which can't pass on sanitizer runs.
#if (!__has_feature(address_sanitizer))
TEST(UffdTest, ForkThenInit) {
test_helper::ForkHelper helper;
helper.ExpectSignal(SIGBUS);
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([] {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
UffdWrapper uffd;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
intptr_t start = reinterpret_cast<intptr_t>(full_range.mapping());
test_helper::ForkHelper helper2;
helper2.OnlyWaitForForkedChildren();
helper2.RunInForkedProcess([&] {
uffd.api(UFFD_FEATURE_SIGBUS);
uffd.register_range(start, len, UFFDIO_REGISTER_MODE_MISSING);
// Accessing the registered range from child doesn't trigger uffd
char c_from_fork = reinterpret_cast<char *>(full_range.mapping())[0];
EXPECT_EQ(c_from_fork, 0);
exit(EXIT_SUCCESS);
});
ASSERT_TRUE(helper2.WaitForChildren());
// Accessing the registered range from parent triggers uffd
char c = reinterpret_cast<char *>(full_range.mapping())[0];
EXPECT_EQ(c, 0);
});
ASSERT_TRUE(helper.WaitForChildren());
}
#endif
TEST(UffdTest, SigbusHandling) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([] {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
intptr_t start = reinterpret_cast<intptr_t>(full_range.mapping());
static UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
// SIGBUS handler
struct sigaction act;
std::memset(&act, '\0', sizeof(act));
act.sa_flags = SA_SIGINFO | SA_RESTART;
act.sa_sigaction = [](int sig, siginfo_t *info, void *context) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
auto src = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
auto dst = reinterpret_cast<uintptr_t>(info->si_addr) & ~(page_size - 1);
uffd.copy_one_page(reinterpret_cast<uintptr_t>(src.mapping()), dst);
};
SAFE_SYSCALL(sigaction(SIGBUS, &act, nullptr));
uffd.register_range(start, len, UFFDIO_REGISTER_MODE_MISSING);
reinterpret_cast<char *>(full_range.mapping())[5] = 'a';
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(UffdTest, PopulateFromAnotherUffd) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([] {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
intptr_t start = reinterpret_cast<intptr_t>(full_range.mapping());
static UffdWrapper uffd_a;
uffd_a.api(UFFD_FEATURE_SIGBUS);
static UffdWrapper uffd_b;
uffd_b.api(UFFD_FEATURE_SIGBUS);
uffd_a.register_range(start, page_size, UFFDIO_REGISTER_MODE_MISSING);
struct sigaction act;
std::memset(&act, '\0', sizeof(act));
act.sa_flags = SA_SIGINFO | SA_RESTART;
act.sa_sigaction = [](int sig, siginfo_t *info, void *context) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
auto dst = reinterpret_cast<uintptr_t>(info->si_addr) & ~(page_size - 1);
auto buffer = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
memset(buffer.mapping(), 'a', page_size);
// Populate from uffd_b a range that is registered with uffd_a
uffd_b.copy_one_page(reinterpret_cast<uintptr_t>(buffer.mapping()), dst);
};
SAFE_SYSCALL(sigaction(SIGBUS, &act, nullptr));
EXPECT_EQ(reinterpret_cast<char *>(start)[5], 'a');
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(UffdTest, IoctlZeroIncludingUnregistered) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([] {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = 2 * page_size;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
intptr_t start = reinterpret_cast<intptr_t>(full_range.mapping());
static UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
// Register only the first page
uffd.register_range(start, page_size, UFFDIO_REGISTER_MODE_MISSING);
struct sigaction act;
std::memset(&act, '\0', sizeof(act));
act.sa_flags = SA_SIGINFO | SA_RESTART;
act.sa_sigaction = [](int sig, siginfo_t *info, void *context) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
auto dst = reinterpret_cast<uintptr_t>(info->si_addr) & ~(page_size - 1);
uffdio_range uffdio_range;
uffdio_range.start = dst;
uffdio_range.len = 2 * page_size;
uffdio_zeropage uffdio_zeropage;
uffdio_zeropage.range = uffdio_range;
uffdio_zeropage.mode = 0;
// Try to fill both pages with zeroes. This should fail and the first page should remain
// unmodified.
EXPECT_NE(ioctl(uffd.fd(), UFFDIO_ZEROPAGE, &uffdio_zeropage), 0);
EXPECT_EQ(errno, ENOENT);
EXPECT_EQ(uffdio_zeropage.zeropage, -1 * ENOENT);
// Try filling just first page.
uffdio_range.len = page_size;
uffdio_zeropage.range = uffdio_range;
EXPECT_EQ(ioctl(uffd.fd(), UFFDIO_ZEROPAGE, &uffdio_zeropage), 0);
EXPECT_EQ(uffdio_zeropage.zeropage, static_cast<int64_t>(page_size));
};
SAFE_SYSCALL(sigaction(SIGBUS, &act, nullptr));
EXPECT_EQ(reinterpret_cast<char *>(start)[5], '\0');
});
ASSERT_TRUE(helper.WaitForChildren());
}
TEST(UffdTest, CannotPopulateTwice) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([] {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = 2 * page_size;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
static intptr_t start = reinterpret_cast<intptr_t>(full_range.mapping());
static UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
uffd.register_range(start, len, UFFDIO_REGISTER_MODE_MISSING);
struct sigaction act;
std::memset(&act, '\0', sizeof(act));
act.sa_flags = SA_SIGINFO | SA_RESTART;
act.sa_sigaction = [](int sig, siginfo_t *info, void *context) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
auto buffer = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, 2 * page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
memset(buffer.mapping(), 'a', 2 * page_size);
uintptr_t src = reinterpret_cast<uintptr_t>(buffer.mapping());
uintptr_t first_page = start;
uintptr_t second_page = start + page_size;
// Populate the second page (this should work)
uffd.copy_one_page(src, second_page);
// Filling the same page should fail as it is not empty.
uffdio_copy uffdio_copy_second{
.dst = second_page,
.src = src,
.len = page_size,
.mode = 0,
.copy = 0,
};
EXPECT_NE(ioctl(uffd.fd(), UFFDIO_COPY, &uffdio_copy_second), 0);
EXPECT_EQ(errno, EEXIST);
// The error is reported
EXPECT_EQ(uffdio_copy_second.copy, -1 * EEXIST);
// Filling both pages should fail as well, but first page should get filled. The error is
// EAGAIN in that case.
uffdio_copy uffdio_copy_both{
.dst = first_page,
.src = src,
.len = 2 * page_size,
.mode = 0,
.copy = 0,
};
EXPECT_NE(ioctl(uffd.fd(), UFFDIO_COPY, &uffdio_copy_both), 0);
EXPECT_EQ(errno, EAGAIN);
// The number of bytes copied is reported
EXPECT_EQ(uffdio_copy_both.copy, static_cast<int64_t>(page_size));
};
SAFE_SYSCALL(sigaction(SIGBUS, &act, nullptr));
// Accessing any page in the registered range will trigger the signal handler.
EXPECT_EQ(reinterpret_cast<char *>(start)[5], 'a');
// Second page is still populated
EXPECT_EQ(reinterpret_cast<char *>(start)[page_size + 5], 'a');
});
ASSERT_TRUE(helper.WaitForChildren());
}
// This doesn't work on Starnix as we don't have an easy way to check if a page is present in
// physical memory or not.
TEST(UffdTest, NoSigBusOnAccessedPage) {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
intptr_t start = reinterpret_cast<intptr_t>(full_range.mapping());
EXPECT_EQ(reinterpret_cast<char *>(start)[5], '\0');
static UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
uffd.register_range(start, page_size, UFFDIO_REGISTER_MODE_MISSING);
EXPECT_EQ(reinterpret_cast<char *>(start)[5], '\0');
}
// A weaker version of the previous test: sigbus is not triggered if the page was already populated
// from uffd.
TEST(UffdTest, NoSigbusAfterPopulating) {
test_helper::ForkHelper helper;
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([] {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
auto full_range = ASSERT_RESULT_SUCCESS_AND_RETURN(test_helper::ScopedMMap::MMap(
nullptr, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
intptr_t start = reinterpret_cast<intptr_t>(full_range.mapping());
UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
uffd.register_range(start, len, UFFDIO_REGISTER_MODE_MISSING);
uffd.zero_one_page(start);
ASSERT_EQ(reinterpret_cast<char *>(start)[5], '\0');
});
ASSERT_TRUE(helper.WaitForChildren());
}
// This test intentionally triggers a deadly signal (SIGBUS) which can't pass on sanitizer runs.
#if (!__has_feature(address_sanitizer))
TEST_F(UffdProcTest, RegisterThenMove) {
test_helper::ForkHelper helper;
helper.ExpectSignal(SIGBUS);
helper.OnlyWaitForForkedChildren();
helper.RunInForkedProcess([&] {
const size_t page_size = SAFE_SYSCALL(sysconf(_SC_PAGE_SIZE));
size_t len = page_size;
void *source =
mmap(nullptr, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// Add an extra page to prevent the source being merged with the remapped region
void *filler =
mmap(nullptr, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
ASSERT_NE(source, MAP_FAILED);
ASSERT_NE(filler, MAP_FAILED);
intptr_t start = reinterpret_cast<intptr_t>(source);
UffdWrapper uffd;
uffd.api(UFFD_FEATURE_SIGBUS);
uffd.register_range(start, len, UFFDIO_REGISTER_MODE_MISSING);
auto smaps_init = FindMemoryMappingInSmaps(start);
void *remapped = mremap(source, len, len, MREMAP_MAYMOVE | MREMAP_DONTUNMAP, 0);
ASSERT_NE(remapped, MAP_FAILED);
ASSERT_NE(remapped, source);
auto smaps_source = FindMemoryMappingInSmaps(start);
EXPECT_TRUE(smaps_source->ContainsFlag("um"));
auto smaps_remapped = FindMemoryMappingInSmaps(reinterpret_cast<intptr_t>(remapped));
EXPECT_FALSE(smaps_remapped->ContainsFlag("um"));
// No sigbus on remapped
EXPECT_EQ(reinterpret_cast<char *>(remapped)[5], '\0');
// SIGBUS on original
EXPECT_EQ(reinterpret_cast<char *>(start)[5], '\0');
});
ASSERT_TRUE(helper.WaitForChildren());
}
#endif