//===-- Unit tests for pkey functions -------------------------------------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #include "hdr/errno_macros.h" #include "hdr/signal_macros.h" #include "hdr/types/size_t.h" #include "src/sys/mman/mmap.h" #include "src/sys/mman/munmap.h" #include "src/sys/mman/pkey_alloc.h" #include "src/sys/mman/pkey_free.h" #include "src/sys/mman/pkey_get.h" #include "src/sys/mman/pkey_mprotect.h" #include "src/sys/mman/pkey_set.h" #include "test/UnitTest/ErrnoCheckingTest.h" #include "test/UnitTest/ErrnoSetterMatcher.h" #include "test/UnitTest/LibcTest.h" #include "test/UnitTest/TestLogger.h" #include // For EXEC_PAGESIZE. using LIBC_NAMESPACE::testing::tlog; using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Fails; using LIBC_NAMESPACE::testing::ErrnoSetterMatcher::Succeeds; using LlvmLibcProtectionKeyTest = LIBC_NAMESPACE::testing::ErrnoCheckingTest; constexpr size_t MMAP_SIZE = EXEC_PAGESIZE; // Wrapper around a pkey to ensure it is freed. class PKeyGuard { public: int key; PKeyGuard() : key(-1) {} PKeyGuard(int key) : key(key) {} ~PKeyGuard() { if (key != -1) { LIBC_NAMESPACE::pkey_free(key); } } }; // Wrapper around mmap to ensure munmap is called. class MMapPageGuard { public: void *addr = nullptr; size_t size = 0; static MMapPageGuard mmap(int prot) { void *addr = LIBC_NAMESPACE::mmap(nullptr, MMAP_SIZE, prot, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (addr == MAP_FAILED) { return MMapPageGuard(nullptr, 0); } return MMapPageGuard(addr, MMAP_SIZE); } MMapPageGuard(void *addr, size_t size) : addr(addr), size(size) {} ~MMapPageGuard() { if (addr != nullptr) { LIBC_NAMESPACE::munmap(addr, size); } } }; bool protection_keys_supported() { static bool supported = []() { PKeyGuard pkey(LIBC_NAMESPACE::pkey_alloc(0, 0)); int err = libc_errno; libc_errno = 0; if (pkey.key < 0 || (err == ENOSPC || err == ENOSYS || err == EINVAL)) { tlog << "pkey_alloc failed with errno=" << err << "\n"; return false; } int access_rights = LIBC_NAMESPACE::pkey_get(pkey.key); err = libc_errno; libc_errno = 0; if (access_rights < 0 || err == ENOSYS) { tlog << "pkey_get failed with errno=" << err << "\n"; return false; } return true; }(); return supported; } TEST_F(LlvmLibcProtectionKeyTest, MProtectWithPKeyDisablesWrite) { if (!protection_keys_supported()) { tlog << "Skipping test: pkey is not available\n"; return; } PKeyGuard pkey(LIBC_NAMESPACE::pkey_alloc(0, PKEY_DISABLE_WRITE)); ASSERT_NE(pkey.key, -1); MMapPageGuard page = MMapPageGuard::mmap(PROT_READ | PROT_WRITE); ASSERT_NE(page.addr, nullptr); volatile char *data = (char *)page.addr; data[0] = 'a'; EXPECT_THAT(LIBC_NAMESPACE::pkey_mprotect(page.addr, page.size, PROT_READ | PROT_WRITE, pkey.key), Succeeds()); // Read is still allowed. EXPECT_EQ(data[0], 'a'); // Write is not allowed. EXPECT_DEATH([&data]() { data[0] = 'b'; }, WITH_SIGNAL(SIGSEGV)); } TEST_F(LlvmLibcProtectionKeyTest, PKeySetChangesAccessRights) { if (!protection_keys_supported()) { tlog << "Skipping test: pkey is not available\n"; return; } PKeyGuard pkey(LIBC_NAMESPACE::pkey_alloc(0, 0)); ASSERT_NE(pkey.key, -1); MMapPageGuard page = MMapPageGuard::mmap(PROT_READ | PROT_WRITE); ASSERT_NE(page.addr, nullptr); EXPECT_THAT(LIBC_NAMESPACE::pkey_mprotect(page.addr, page.size, PROT_READ | PROT_WRITE, pkey.key), Succeeds()); // Write is allowed by default. volatile char *data = (char *)page.addr; data[0] = 'a'; EXPECT_THAT(LIBC_NAMESPACE::pkey_set(pkey.key, PKEY_DISABLE_WRITE), Succeeds()); // Now read is allowed but write is not. EXPECT_EQ(data[0], 'a'); EXPECT_DEATH([&data]() { data[0] = 'b'; }, WITH_SIGNAL(SIGSEGV)); // Now neither read nor write is allowed. EXPECT_THAT(LIBC_NAMESPACE::pkey_set(pkey.key, PKEY_DISABLE_ACCESS | PKEY_DISABLE_WRITE), Succeeds()); EXPECT_DEATH([&data]() { (void)data[0]; }, WITH_SIGNAL(SIGSEGV)); EXPECT_DEATH([&data]() { data[0] = 'b'; }, WITH_SIGNAL(SIGSEGV)); } TEST_F(LlvmLibcProtectionKeyTest, FallsBackToMProtectForInvalidPKey) { MMapPageGuard page = MMapPageGuard::mmap(PROT_READ | PROT_WRITE); ASSERT_NE(page.addr, nullptr); volatile char *data = (char *)page.addr; data[0] = 'a'; EXPECT_THAT( LIBC_NAMESPACE::pkey_mprotect(page.addr, page.size, PROT_READ, -1), Succeeds()); // Read is still allowed. EXPECT_EQ(data[0], 'a'); // Write is not allowed. EXPECT_DEATH([&data]() { data[0] = 'b'; }, WITH_SIGNAL(SIGSEGV)); } TEST_F(LlvmLibcProtectionKeyTest, ExhaustedKeysFailsWithENOSPC) { if (!protection_keys_supported()) { tlog << "Skipping test: pkey is not available\n"; return; } // Use an unreasonably large limit to ensure test is cross-platform. // This limit is intended to be much larger than the actual hardware limit. constexpr int MAX_PKEYS = 64; PKeyGuard pkeys[MAX_PKEYS]; for (int i = 0; i < MAX_PKEYS; ++i) { pkeys[i].key = LIBC_NAMESPACE::pkey_alloc(0, 0); } // pkey allocation should eventually fail with ENOSPC. PKeyGuard pkey(LIBC_NAMESPACE::pkey_alloc(0, 0)); EXPECT_THAT(pkey.key, Fails(ENOSPC)); libc_errno = 0; } TEST_F(LlvmLibcProtectionKeyTest, Accessors) { if (!protection_keys_supported()) { tlog << "Skipping test: pkey is not available\n"; return; } PKeyGuard pkey(LIBC_NAMESPACE::pkey_alloc(0, PKEY_DISABLE_WRITE)); ASSERT_NE(pkey.key, -1); // Check that pkey_alloc sets the access rights. EXPECT_EQ(LIBC_NAMESPACE::pkey_get(pkey.key), PKEY_DISABLE_WRITE); // Check that pkey_set changes the access rights. EXPECT_THAT(LIBC_NAMESPACE::pkey_set(pkey.key, PKEY_DISABLE_ACCESS), Succeeds()); EXPECT_EQ(LIBC_NAMESPACE::pkey_get(pkey.key), PKEY_DISABLE_ACCESS); } TEST_F(LlvmLibcProtectionKeyTest, AccessorsErrorForInvalidValues) { if (!protection_keys_supported()) { tlog << "Skipping test: pkey is not available\n"; return; } PKeyGuard pkey(LIBC_NAMESPACE::pkey_alloc(0, PKEY_DISABLE_WRITE)); ASSERT_NE(pkey.key, -1); // Pkey is out of bounds in pkey_get. EXPECT_THAT(LIBC_NAMESPACE::pkey_get(100), Fails(EINVAL)); EXPECT_THAT(LIBC_NAMESPACE::pkey_get(-1234), Fails(EINVAL)); // Pkey is out of bounds in pkey_set. EXPECT_THAT(LIBC_NAMESPACE::pkey_set(100, PKEY_DISABLE_ACCESS), Fails(EINVAL)); EXPECT_THAT(LIBC_NAMESPACE::pkey_set(-1234, PKEY_DISABLE_ACCESS), Fails(EINVAL)); // Non-zero flags are not supported in pkey_alloc. EXPECT_THAT(LIBC_NAMESPACE::pkey_alloc(123, PKEY_DISABLE_WRITE), Fails(EINVAL)); // Access rights are out of bounds. EXPECT_THAT(LIBC_NAMESPACE::pkey_alloc(0, 1000), Fails(EINVAL)); EXPECT_THAT(LIBC_NAMESPACE::pkey_set(pkey.key, 1000), Fails(EINVAL)); }