Skip to content

Commit

Permalink
[Support][Memory] Add memfd based fallback for strict W^X Linux systems
Browse files Browse the repository at this point in the history
PaX's MPROTECT feature as well as SELinux's deny_execmem policy prevent
a transition from a once writable memory mapping to an executable one.
This obviously breaks JIT code generation.

As both of these are Linux specific and the Linux kernel gained memfd
support almost 10 years ago in v3.17, making use of it to implement a
fallback seems like a viable option to get JIT code generation fixed
for such systems.

Implement a detour through a memfd for systems that are detected to deny
the W<->X transition of memory mappings. For PaX this can be easily
achieved by evaluating the per-process PaX flags, if access to
/proc/self/status is available. For others, and as a fallback for PaX, a
runtime test is done once.

This enables such systems to make use of JIT code generation without
completely abandoning their W^X policy.

Signed-off-by: Mathias Krause <[email protected]>
  • Loading branch information
minipli-oss committed Jul 23, 2024
1 parent e4163c0 commit 3a22789
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 3 deletions.
62 changes: 59 additions & 3 deletions llvm/lib/Support/Unix/Memory.inc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
#include "llvm/Support/Process.h"
#include "llvm/Support/Valgrind.h"

#ifdef __linux__
#include "MemoryLinux.h"
#endif

#ifdef HAVE_SYS_MMAN_H
#include <sys/mman.h>
#endif
Expand Down Expand Up @@ -177,6 +181,55 @@ std::error_code Memory::protectMappedMemory(const MemoryBlock &M,
alignAddr((const uint8_t *)M.Address + M.AllocatedSize, PageSize);

bool InvalidateCache = (Flags & MF_EXEC);
bool SkipMprotect = false;

#if defined(__linux__)
// Check for cases where the EXEC protection flag changes and a possible
// strict W^X policy cannot be bypassed via mprotect() alone, e.g. under
// PaX's MPROTECT or SELinux's deny_execmem.
//
// To support such systems, we need to create a fresh mapping with the
// target protection flags.
if ((M.Flags ^ Flags) & MF_EXEC && execProtChangeNeedsNewMapping()) {
class FDWrapper {
public:
FDWrapper(int fd) : fd(fd) {}
~FDWrapper() { ::close(fd); }
operator int() const { return fd; }
private:
int fd;
} fd(memfd_create("llvm", MFD_CLOEXEC));

if (fd < 0)
return errnoAsErrorCode();

const char *data = reinterpret_cast<char *>(Start);
uintptr_t len = End - Start;
uintptr_t left = len;

while (left) {
ssize_t cnt = ::write(fd, data, left);
if (cnt < 0) {
if (errno == EINTR)
continue;

return errnoAsErrorCode();
}
left -= cnt;
data += cnt;
}

void *addr = ::mmap(reinterpret_cast<void *>(Start), len, Protect,
MAP_PRIVATE | MAP_FIXED, fd, 0);
if (addr == MAP_FAILED)
return errnoAsErrorCode();

// We created a new mapping with the final protection bits, therefore
// don't need to call mprotect() with the very same flags again -- unless
// we have to toggle PROT_READ for ARM.
SkipMprotect = true;
}
#endif

#if defined(__arm__) || defined(__aarch64__)
// Certain ARM implementations treat icache clear instruction as a memory
Expand All @@ -190,13 +243,16 @@ std::error_code Memory::protectMappedMemory(const MemoryBlock &M,

Memory::InvalidateInstructionCache(M.Address, M.AllocatedSize);
InvalidateCache = false;
SkipMprotect = false;
}
#endif

int Result = ::mprotect((void *)Start, End - Start, Protect);
if (!SkipMprotect) {
int Result = ::mprotect((void *)Start, End - Start, Protect);

if (Result != 0)
return errnoAsErrorCode();
if (Result != 0)
return errnoAsErrorCode();
}

if (InvalidateCache)
Memory::InvalidateInstructionCache(M.Address, M.AllocatedSize);
Expand Down
105 changes: 105 additions & 0 deletions llvm/lib/Support/Unix/MemoryLinux.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//===- Unix/MemoryLinux.h - Linux specific Helper Fuctions ------*- C++ -*-===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
// This file defines Linux specific helper functions for memory management.
//
//===----------------------------------------------------------------------===//

#ifndef LLVM_LIB_SUPPORT_UNIX_MEMORYLINUX_H
#define LLVM_LIB_SUPPORT_UNIX_MEMORYLINUX_H

#ifndef __linux__
#error Linux only support header!
#endif

#include "llvm/Support/Process.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <sys/mman.h>
#include <sys/syscall.h>

#ifndef MFD_CLOEXEC
#define MFD_CLOEXEC 1U
#endif

namespace llvm {
namespace sys {
namespace {

static inline bool isPermissionError(int err) {
// PaX uses EPERM, SELinux uses EACCES
return err == EPERM || err == EACCES;
}

// FIXME: Make this either more low-level C'ish or C++'ish
static inline bool execProtChangeNeedsNewMapping() {
static int status = -1;

if (status != -1)
return status;

// Try to get the status from /proc/self/status, looking for PaX flags.
FILE *f = fopen("/proc/self/status", "re");
if (f) {
char *buf = NULL;
size_t len;

while (getline(&buf, &len, f) != -1) {
if (strncmp(buf, "PaX:", 4))
continue;

// Look for 'm', indicating PaX MPROTECT is disabled.
status = !strchr(buf + 4, 'm');
break;
}

fclose(f);
free(buf);

if (status != -1)
return status;
}

// Create a temporary writable mapping and try to make it executable. If
// this fails, test 'errno' to ensure it failed because we were not allowed
// to create such a mapping and not because of some transient error.
size_t size = Process::getPageSizeEstimate();
void *addr = ::mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
// Must be low on memory or have too many mappings already, not much we can
// do here.
status = 0;
} else {
if (::mprotect(addr, size, PROT_READ | PROT_EXEC) < 0)
status = isPermissionError(errno);
else
status = 0;
::munmap(addr, size);
}

return status;
}

static inline int memfd_create(const char *name, int flags) {
#ifdef SYS_memfd_create
return syscall(SYS_memfd_create, name, flags);
#else
return -1;
#endif
}

} // anonymous namespace
} // namespace sys
} // namespace llvm

#endif
6 changes: 6 additions & 0 deletions llvm/unittests/Support/MemoryTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
#include "gtest/gtest.h"
#include <cstdlib>

#if defined(__linux__)
#include "../lib/Support/Unix/MemoryLinux.h"
#endif

#if defined(__NetBSD__)
// clang-format off
#include <sys/param.h>
Expand Down Expand Up @@ -40,6 +44,8 @@ bool IsMPROTECT() {
err(EXIT_FAILURE, "sysctl");

return !!(paxflags & CTL_PROC_PAXFLAGS_MPROTECT);
#elif defined(__linux__)
return execProtChangeNeedsNewMapping();
#elif (defined(__APPLE__) && defined(__aarch64__)) || defined(__OpenBSD__)
return true;
#else
Expand Down

0 comments on commit 3a22789

Please sign in to comment.