| //===-- 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/sys_mman_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 <linux/param.h> // 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 checked = false; | |
| static bool supported; | |
| if (checked) | |
| return supported; | |
| checked = true; | |
| 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)); | |
| } |