Commit 7ff863b3 authored by Daniel Lehmann's avatar Daniel Lehmann Committed by V8 LUCI CQ

[wasm] Add PKU key permissions functions

This is the first CL in a line of two to finish PKU-based WebAssembly
code space write protection. This CL adds two low-level PKU functions,
which are essentially wrapping the functionality in glibc's
{pkey_mprotect()} and {pkey_set()}).

The added functionality is in
(1) {SetPermissionsAndMemoryProtectionKey()}: Associate a memory
protection key with a page (simultaneously with setting the page's
regular permssions). This is as costly as a regular {mprotect()}.
This call itself does not restrict permissions besides the regular page
permissions.
(2) {SetPermissionsForMemoryProtectionKey()}: Set permissions for the
key itself (now associated with a page). This can be either "all data
access disabled" (i.e., no read or write, but execution is allowed) or
"write access disabled" (which we use for code space write protection).
The permissions are added on top of the page's regular permissions. This
operation is cheap (in the order of 20 cycles) since it is roughly a
thread-local register read, some bit-masking, and register write.
See the second CL (based on this one) for how those two functions will
be used.

A note on compatability and security implications: Because the functions
which we use here were only added in glibc 2.27, and since glibc is
dynamically linked, we check at runtime (with {dlsym()}) whether
{pkey_*()} functions are available. However, calling functions via a
pointer coming from {dlsym()} is not supported by CFI so far, which is
why we disable indirect call checking for the added functions.
Potentially, the functions could hence be used as an indirect call
gadget in a ROP attack. On the other hand, they are only compiled in
currently only on Linux on x64, and disabling CFI indirect call checking
is also done in other places already.

R=clemensb@chromium.org

Bug: v8:11714
Change-Id: I0da00818f28cf1da195a5149bf11fccf87c5f8ea
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2882797
Commit-Queue: Daniel Lehmann <dlehmann@google.com>
Reviewed-by: 's avatarClemens Backes <clemensb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#74498}
parent cc06b8c7
......@@ -4,8 +4,15 @@
#include "src/wasm/memory-protection-key.h"
#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
#include <sys/mman.h> // For {mprotect()} protection macros.
#undef MAP_TYPE // Conflicts with MAP_TYPE in Torque-generated instance-types.h
#endif
#include "src/base/build_config.h"
#include "src/base/logging.h"
#include "src/base/macros.h"
#include "src/base/platform/platform.h"
// Runtime-detection of PKU support with {dlsym()}.
//
......@@ -36,12 +43,27 @@ namespace v8 {
namespace internal {
namespace wasm {
// TODO(dlehmann) Security: Are there alternatives to disabling CFI altogether
// for the functions below? Since they are essentially an arbitrary indirect
// call gadget, disabling CFI should be only a last resort. In Chromium, there
// was {base::ProtectedMemory} to protect the function pointer from being
// overwritten, but t seems it was removed to not begin used and AFAICT no such
// thing exists in V8 to begin with. See
// https://www.chromium.org/developers/testing/control-flow-integrity and
// https://crrev.com/c/1884819.
// What is the general solution for CFI + {dlsym()}?
// An alternative would be to not rely on glibc and instead implement PKEY
// directly on top of Linux syscalls + inline asm, but that is quite some low-
// level code (probably in the order of 100 lines).
DISABLE_CFI_ICALL
int AllocateMemoryProtectionKey() {
// See comment on the import on feature testing for PKEY support.
#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
// Try to to find {pkey_alloc()} support in glibc.
typedef int (*pkey_alloc_t)(unsigned int, unsigned int);
auto pkey_alloc = bit_cast<pkey_alloc_t>(dlsym(RTLD_DEFAULT, "pkey_alloc"));
// Cache the {dlsym()} lookup in a {static} variable.
static auto* pkey_alloc =
bit_cast<pkey_alloc_t>(dlsym(RTLD_DEFAULT, "pkey_alloc"));
if (pkey_alloc != nullptr) {
// If there is support in glibc, try to allocate a new key.
// This might still return -1, e.g., because the kernel does not support
......@@ -56,21 +78,109 @@ int AllocateMemoryProtectionKey() {
return kNoMemoryProtectionKey;
}
DISABLE_CFI_ICALL
void FreeMemoryProtectionKey(int key) {
#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
// Only free the key if one was allocated.
if (key != kNoMemoryProtectionKey) {
if (key == kNoMemoryProtectionKey) return;
#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
typedef int (*pkey_free_t)(int);
auto pkey_free = bit_cast<pkey_free_t>(dlsym(RTLD_DEFAULT, "pkey_free"));
// If a key was allocated with {pkey_alloc()}, {pkey_free()} must also be
// available.
CHECK_NOT_NULL(pkey_free);
CHECK_EQ(/* success */ 0, pkey_free(key));
static auto* pkey_free =
bit_cast<pkey_free_t>(dlsym(RTLD_DEFAULT, "pkey_free"));
// If a valid key was allocated, {pkey_free()} must also be available.
DCHECK_NOT_NULL(pkey_free);
int ret = pkey_free(key);
CHECK_EQ(/* success */ 0, ret);
#else
// On platforms without PKU support, we should have already returned because
// the key must be {kNoMemoryProtectionKey}.
UNREACHABLE();
#endif
}
#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
// TODO(dlehmann): Copied from base/platform/platform-posix.cc. Should be
// removed once this code is integrated in base/platform/platform-linux.cc.
int GetProtectionFromMemoryPermission(base::OS::MemoryPermission access) {
switch (access) {
case base::OS::MemoryPermission::kNoAccess:
case base::OS::MemoryPermission::kNoAccessWillJitLater:
return PROT_NONE;
case base::OS::MemoryPermission::kRead:
return PROT_READ;
case base::OS::MemoryPermission::kReadWrite:
return PROT_READ | PROT_WRITE;
case base::OS::MemoryPermission::kReadWriteExecute:
return PROT_READ | PROT_WRITE | PROT_EXEC;
case base::OS::MemoryPermission::kReadExecute:
return PROT_READ | PROT_EXEC;
}
UNREACHABLE();
}
#endif
DISABLE_CFI_ICALL
bool SetPermissionsAndMemoryProtectionKey(
PageAllocator* page_allocator, base::AddressRegion region,
PageAllocator::Permission page_permissions, int key) {
DCHECK_NOT_NULL(page_allocator);
void* address = reinterpret_cast<void*>(region.begin());
size_t size = region.size();
#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
typedef int (*pkey_mprotect_t)(void*, size_t, int, int);
static auto* pkey_mprotect =
bit_cast<pkey_mprotect_t>(dlsym(RTLD_DEFAULT, "pkey_mprotect"));
if (pkey_mprotect == nullptr) {
// If there is no runtime support for {pkey_mprotect()}, no key should have
// been allocated in the first place.
DCHECK_EQ(kNoMemoryProtectionKey, key);
// Without PKU support, fallback to regular {mprotect()}.
return page_allocator->SetPermissions(address, size, page_permissions);
}
// Copied with slight modifications from base/platform/platform-posix.cc
// {OS::SetPermissions()}.
// TODO(dlehmann): Move this block into its own function at the right
// abstraction boundary (likely some static method in platform.h {OS})
// once the whole PKU code is moved into base/platform/.
DCHECK_EQ(0, region.begin() % page_allocator->CommitPageSize());
DCHECK_EQ(0, size % page_allocator->CommitPageSize());
int protection = GetProtectionFromMemoryPermission(
static_cast<base::OS::MemoryPermission>(page_permissions));
int ret = pkey_mprotect(address, size, protection, key);
return ret == /* success */ 0;
#else
// Without PKU support, fallback to regular {mprotect()}.
return page_allocator->SetPermissions(address, size, page_permissions);
#endif
}
DISABLE_CFI_ICALL
bool SetPermissionsForMemoryProtectionKey(
int key, MemoryProtectionKeyPermission permissions) {
if (key == kNoMemoryProtectionKey) return false;
#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
typedef int (*pkey_set_t)(int, unsigned int);
static auto* pkey_set = bit_cast<pkey_set_t>(dlsym(RTLD_DEFAULT, "pkey_set"));
// If a valid key was allocated, {pkey_set()} must also be available.
DCHECK_NOT_NULL(pkey_set);
int ret = pkey_set(key, permissions);
return ret == /* success */ 0;
#else
// On platforms without support even compiled in, no key should have been
// allocated.
CHECK_EQ(kNoMemoryProtectionKey, key);
// On platforms without PKU support, we should have already returned because
// the key must be {kNoMemoryProtectionKey}.
UNREACHABLE();
#endif
}
......
......@@ -9,6 +9,14 @@
#ifndef V8_WASM_MEMORY_PROTECTION_KEY_H_
#define V8_WASM_MEMORY_PROTECTION_KEY_H_
#if defined(V8_OS_LINUX) && defined(V8_HOST_ARCH_X64)
#include <sys/mman.h> // For STATIC_ASSERT of permission values.
#undef MAP_TYPE // Conflicts with MAP_TYPE in Torque-generated instance-types.h
#endif
#include "include/v8-platform.h"
#include "src/base/address-region.h"
namespace v8 {
namespace internal {
namespace wasm {
......@@ -24,7 +32,7 @@ namespace wasm {
// mprotect().
constexpr int kNoMemoryProtectionKey = -1;
// Permissions for memory protection keys on top of the permissions by mprotect.
// Permissions for memory protection keys on top of the page's permissions.
// NOTE: Since there is no executable bit, the executable permission cannot be
// withdrawn by memory protection keys.
enum MemoryProtectionKeyPermission {
......@@ -33,6 +41,13 @@ enum MemoryProtectionKeyPermission {
kDisableWrite = 2,
};
// If sys/mman.h has PKEY support (on newer Linux distributions), ensure that
// our definitions of the permissions is consistent with the ones in glibc.
#if defined(PKEY_DISABLE_ACCESS)
STATIC_ASSERT(kDisableAccess == PKEY_DISABLE_ACCESS);
STATIC_ASSERT(kDisableWrite == PKEY_DISABLE_WRITE);
#endif
// Allocates a memory protection key on platforms with PKU support, returns
// {kNoMemoryProtectionKey} on platforms without support or when allocation
// failed at runtime.
......@@ -46,6 +61,28 @@ int AllocateMemoryProtectionKey();
// https://www.gnu.org/software/libc/manual/html_mono/libc.html#Memory-Protection-Keys
void FreeMemoryProtectionKey(int key);
// Associates a memory protection {key} with the given {region}.
// If {key} is {kNoMemoryProtectionKey} this behaves like "plain"
// {SetPermissions()} and associates the default key to the region. That is,
// explicitly calling with {kNoMemoryProtectionKey} can be used to disassociate
// any protection key from a region. This also means "plain" {SetPermissions()}
// disassociates the key from a region, making the key's access restrictions
// irrelevant/inactive for that region.
// Returns true if changing permissions and key was successful. (Returns a bool
// to be consistent with {SetPermissions()}).
// The {page_permissions} are the permissions of the page, not the key. For
// changing the permissions of the key, use
// {SetPermissionsForMemoryProtectionKey()} instead.
bool SetPermissionsAndMemoryProtectionKey(
PageAllocator* page_allocator, base::AddressRegion region,
PageAllocator::Permission page_permissions, int key);
// Set the key's permissions and return whether this was successful.
// Returns false on platforms without PKU support or when the operation failed,
// e.g., because the key was invalid.
bool SetPermissionsForMemoryProtectionKey(
int key, MemoryProtectionKeyPermission permissions);
} // namespace wasm
} // namespace internal
} // namespace v8
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment