Extend W^X memory protection mechanism for RestrictFileSystemAccess (#43511)

Currently `RestrictFileSystemAccess` isn't trying hard enough to enforce
W^X and thus there are still places where an executable and writable
mapping can be created:

* `/proc/<pid>/mem` `FOLL_FORCE` writes
* `PTRACE_POKE{TEXT,DATA}` `FOLL_FORCE` writes
* `MAP_PRIVATE | MAP_EXECUTABLE` memory mappings

Let's block all of these mechanisms. Do note that
`RestrictFileSystemAccess` is still in very active development and also
cannot meaningfully be used on general purpose distros and systems.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Christian Brauner
2026-09-01 07:12:13 +02:00
committed by GitHub
13 changed files with 784 additions and 137 deletions
+7 -5
View File
@@ -553,6 +553,10 @@
shared libraries (<literal>mmap_file</literal>), and write-to-execute transitions such as JIT
compilation (<literal>file_mprotect</literal>).</para>
<para>The policy also enforces W^X system-wide: no mapping may be writable and executable at the
same time, and a private file mapping that has been modified through copy-on-write may not be made
executable afterwards.</para>
<para>Since v262, files on overlayfs mounts are resolved to the layer that hosts their data, so
execution from an overlay whose data resides on a signed dm-verity device is permitted, while files
in a writable upper layer are denied. This requires kernel v7.2 or newer; on older kernels execution from
@@ -561,13 +565,11 @@
btrfs are unsupported.</para>
<para>Note that, without further measures to secure the system, kexec can be used to circumvent
this. Writable and executable memory mappings can also be used to circumvent this, so they need to
be blocked, which can be achieved using other LSMs, or on a per-service basis by using the
<varname>MemoryDenyWriteExecute=</varname> setting.</para>
this.</para>
<para>This requires the kernel to be booted with <literal>dm_verity.require_signatures=1</literal>
on the kernel command line and with BPF LSM enabled (<literal>lsm=...,bpf</literal>). If either
prerequisite is not met, PID 1 will refuse to complete startup.</para>
and <literal>proc_mem.force_override=never</literal> on the kernel command line, and with BPF LSM
enabled (<literal>lsm=...,bpf</literal>).</para>
<para>The value <literal>yes</literal> is equivalent to <literal>exec</literal>. Additional
modes may be added in the future.</para>
+22 -4
View File
@@ -16,8 +16,11 @@
* dm-verity signals signature validity
* - bdev_free_security hook: removes devices from the map on teardown
* - bprm_check_security: blocks execve() from untrusted sources
* - mmap_file: blocks PROT_EXEC mmap from untrusted sources
* - file_mprotect: blocks W->X transitions from untrusted sources
* - mmap_file: blocks PROT_EXEC mmap from untrusted sources, and
* any writable+executable (W^X-violating) mapping
* - file_mprotect: blocks writable+executable mprotect, and making
* executable a mapping from an untrusted source or
* one that was modified through copy-on-write
*
* On kernels providing the bpf_real_data_inode() kfunc (v7.2+), files on
* union filesystems (overlayfs) are resolved to the layer hosting their
@@ -39,6 +42,7 @@
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#define PROT_WRITE 0x2
#define PROT_EXEC 0x4
#define VM_EXEC 0x00000004
#define PTRACE_MODE_ATTACH 0x02
@@ -218,6 +222,10 @@ SEC("lsm.s/mmap_file")
int BPF_PROG(restrict_fsaccess_mmap_file, struct file *file, unsigned long reqprot,
unsigned long prot, unsigned long flags)
{
/* W^X: a mapping may never be writable and executable at once. */
if ((prot & PROT_EXEC) && (prot & PROT_WRITE))
return -EPERM;
/* Only enforce on executable mappings */
if (!(prot & PROT_EXEC))
return 0;
@@ -245,17 +253,27 @@ int BPF_PROG(restrict_fsaccess_file_mprotect, struct vm_area_struct *vma,
{
struct file *file;
unsigned long vm_flags;
void *anon_vma;
/* W^X: a mapping may never be writable and executable at once. */
if ((prot & PROT_EXEC) && (prot & PROT_WRITE))
return -EPERM;
/* Only enforce when adding PROT_EXEC */
if (!(prot & PROT_EXEC))
return 0;
/* If VM_EXEC is already set, the mapping is already executable — this
* mprotect isn't granting new executable capability, allow */
/* Already executable so this mprotect() grants no new capability. */
BPF_CORE_READ_INTO(&vm_flags, vma, vm_flags);
if (vm_flags & VM_EXEC)
return 0;
/* A private file mapping with an anon_vma has been written to
* (copy-on-write), so its pages may no longer match the trusted file. */
BPF_CORE_READ_INTO(&anon_vma, vma, anon_vma);
if (anon_vma)
return -EPERM;
/* Anonymous executable mapping — no file backing, deny. Direct
* dereference, see restrict_fsaccess_bprm_check(). vm_file is in the
* verifier's BTF_TYPE_SAFE_TRUSTED_OR_NULL list, so this check is live
+148 -20
View File
@@ -3,7 +3,9 @@
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include "alloc-util.h"
#include "bpf-restrict-fsaccess.h"
#include "devnum-util.h"
#include "fd-util.h"
@@ -13,8 +15,11 @@
#include "lsm-util.h"
#include "manager.h"
#include "memory-util.h"
#include "proc-cmdline.h"
#include "seccomp-util.h"
#include "serialize.h"
#include "string-table.h"
#include "string-util.h"
/* DMVERITY_DEVICES_MAX lives in bpf-restrict-fsaccess.h for sharing with tests. */
@@ -81,7 +86,7 @@ assert_cc(sizeof_field(typeof_field(struct restrict_fsaccess_bpf, bss[0]), initr
[RESTRICT_FILESYSTEM_ACCESS_LINK_BPF_GUARD] = (obj)->links.restrict_fsaccess_bpf_guard, \
}
bool dm_verity_require_signatures(void) {
static bool dm_verity_require_signatures(void) {
int r;
r = read_boolean_file("/sys/module/dm_verity/parameters/require_signatures");
@@ -94,6 +99,129 @@ bool dm_verity_require_signatures(void) {
return r > 0;
}
/* proc_mem.force_override= is __ro_after_init. We require "never" because "ptrace" still lets a tracer
* rewrite its tracee. */
static int proc_mem_force_override_restricted(void) {
_cleanup_free_ char *value = NULL;
int r;
r = proc_cmdline_get_key("proc_mem.force_override", /* flags= */ 0, &value);
if (r <= 0)
return r;
return streq(value, "never");
}
/* Unknown command line options are silently ignored, so verify the kernel really refuses FOLL_FORCE
* writes. Returns > 0 if a write through /proc/self/mem overrode a read-only mapping, 0 if it was refused. */
static int proc_mem_can_force_override(void) {
_cleanup_close_ int fd = -EBADF;
const uint8_t zero = 0;
ssize_t n;
void *p;
int r;
p = mmap(/* addr= */ NULL, page_size(), PROT_READ, MAP_PRIVATE|MAP_ANONYMOUS, /* fd= */ -1, /* offset= */ 0);
if (p == MAP_FAILED)
return -errno;
fd = open("/proc/self/mem", O_RDWR|O_CLOEXEC);
if (fd < 0) {
r = -errno;
goto finish;
}
/* Without FOLL_FORCE access_remote_vm() copies nothing and mem_rw() fails with EIO. */
n = pwrite(fd, &zero, sizeof(zero), (off_t) (uintptr_t) p);
if (n < 0)
r = errno == EIO ? 0 : -errno;
else
r = n > 0 ? 1 : -EIO; /* Nothing written and no error is not a refusal, fail closed. */
finish:
(void) munmap(p, page_size());
return r;
}
/* Both /proc/<pid>/mem gates: the command line says "never", and the kernel honours it. */
static int restrict_fsaccess_check_proc_mem(void) {
int r;
r = proc_mem_force_override_restricted();
if (r < 0)
return log_error_errno(r, "bpf-restrict-fsaccess: Failed to read the kernel command line: %m");
if (r == 0)
return log_error_errno(SYNTHETIC_ERRNO(EPERM),
"bpf-restrict-fsaccess: proc_mem.force_override is not set to \"never\", so "
"/proc/<pid>/mem can rewrite executable pages and bypass "
"RestrictFileSystemAccess=. Add proc_mem.force_override=never to the kernel "
"command line, or boot with systemd.restrict_filesystem_access=no to disable "
"the policy.");
r = proc_mem_can_force_override();
if (r < 0)
return log_error_errno(r, "bpf-restrict-fsaccess: Failed to probe whether /proc/self/mem overrides "
"memory protections: %m");
if (r > 0)
return log_error_errno(SYNTHETIC_ERRNO(EPERM),
"bpf-restrict-fsaccess: proc_mem.force_override=never is on the kernel command "
"line, but a write through /proc/self/mem still overrides memory protections. "
"The kernel apparently does not support the option (Linux 6.12 or newer is "
"required). Boot with systemd.restrict_filesystem_access=no to disable the "
"policy.");
return 0;
}
/* The kernel-side prerequisites of the policy, shared with test-bpf-restrict-fsaccess. */
int bpf_restrict_fsaccess_check_prerequisites(void) {
int r;
r = dlopen_bpf(LOG_ERR);
if (r < 0)
return r;
r = lsm_supported("bpf");
if (r == -ENOPKG)
return log_error_errno(r, "bpf-restrict-fsaccess: securityfs not mounted, BPF LSM not available.");
if (r < 0)
return log_error_errno(r, "bpf-restrict-fsaccess: Can't determine whether the BPF LSM module is "
"used: %m");
if (r == 0)
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"bpf-restrict-fsaccess: BPF LSM hook not enabled in the kernel, not supported.");
log_debug("bpf-restrict-fsaccess: BPF LSM: supported.");
if (!dm_verity_require_signatures())
return log_error_errno(SYNTHETIC_ERRNO(ENOKEY),
"bpf-restrict-fsaccess: dm-verity require_signatures is not enabled. "
"RestrictFileSystemAccess= requires the kernel to enforce dm-verity signatures. "
"Set dm_verity.require_signatures=1 on the kernel command line.");
log_debug("bpf-restrict-fsaccess: dm-verity require_signatures: enabled.");
return restrict_fsaccess_check_proc_mem();
}
/* The seccomp filter survives execve() so ensure that we're not installing another filter. */
static int restrict_fsaccess_install_ptrace_filter(Manager *m) {
int r;
assert(m);
if (m->restrict_fsaccess_ptrace_filter)
return 0;
r = seccomp_restrict_ptrace();
if (r == -EOPNOTSUPP)
return log_error_errno(r, "bpf-restrict-fsaccess: seccomp support is not available, cannot block "
"ptrace() code injection.");
if (r < 0)
return log_error_errno(r, "bpf-restrict-fsaccess: Failed to install the ptrace() seccomp filter: %m");
m->restrict_fsaccess_ptrace_filter = true;
return 0;
}
static int get_root_s_dev(uint32_t *ret) {
struct stat st;
@@ -413,29 +541,21 @@ int bpf_restrict_fsaccess_setup(Manager *m) {
return r;
}
/* A v261 PID 1 neither checked this nor installed the filter. Enforcement is active already,
* so only complain. */
(void) restrict_fsaccess_check_proc_mem();
(void) restrict_fsaccess_install_ptrace_filter(m);
return 0;
}
/* Fresh setup: verify BPF LSM is available */
r = dlopen_bpf(LOG_WARNING);
r = bpf_restrict_fsaccess_check_prerequisites();
if (r < 0)
return r;
r = lsm_supported("bpf");
if (r == -ENOPKG)
return log_warning_errno(r, "bpf-restrict-fsaccess: securityfs not mounted, BPF LSM not available.");
r = restrict_fsaccess_install_ptrace_filter(m);
if (r < 0)
return log_warning_errno(r, "bpf-restrict-fsaccess: Can't determine whether the BPF LSM module is used: %m");
if (r == 0)
return log_warning_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"bpf-restrict-fsaccess: BPF LSM hook not enabled in the kernel, not supported.");
/* Require dm-verity signature enforcement */
if (!dm_verity_require_signatures())
return log_error_errno(SYNTHETIC_ERRNO(ENOKEY),
"bpf-restrict-fsaccess: dm-verity require_signatures is not enabled. "
"RestrictFileSystemAccess= requires the kernel to enforce dm-verity signatures. "
"Set dm_verity.require_signatures=1 on the kernel command line.");
return r;
r = bpf_restrict_fsaccess_prepare(&obj);
if (r < 0)
@@ -536,7 +656,15 @@ int bpf_restrict_fsaccess_serialize(Manager *m, FILE *f, FDSet *fds) {
assert(f);
assert(fds);
if (!MANAGER_IS_SYSTEM(m) || m->restrict_filesystem_access <= RESTRICT_FILESYSTEM_ACCESS_NO)
if (!MANAGER_IS_SYSTEM(m))
return 0;
/* Process state, not policy state: the filter outlives the setting being turned off. */
r = serialize_bool_elide(f, "restrict-fsaccess-ptrace-filter", m->restrict_fsaccess_ptrace_filter);
if (r < 0)
return r;
if (m->restrict_filesystem_access <= RESTRICT_FILESYSTEM_ACCESS_NO)
return 0;
FOREACH_ELEMENT(fd, m->restrict_fsaccess_link_fds) {
@@ -554,8 +682,8 @@ int bpf_restrict_fsaccess_serialize(Manager *m, FILE *f, FDSet *fds) {
#else /* ! BPF_FRAMEWORK || ! HAVE_LSM_INTEGRITY_TYPE */
bool dm_verity_require_signatures(void) {
return false;
int bpf_restrict_fsaccess_check_prerequisites(void) {
return -EOPNOTSUPP;
}
int bpf_restrict_fsaccess_setup(Manager *m) {
+1 -1
View File
@@ -33,7 +33,7 @@ enum {
extern const char* const restrict_fsaccess_link_names[_RESTRICT_FILESYSTEM_ACCESS_LINK_MAX];
bool dm_verity_require_signatures(void);
int bpf_restrict_fsaccess_check_prerequisites(void);
int bpf_restrict_fsaccess_setup(Manager *m);
int bpf_restrict_fsaccess_prepare(struct restrict_fsaccess_bpf **ret);
int bpf_restrict_fsaccess_populate_guard(struct restrict_fsaccess_bpf *obj);
+12 -1
View File
@@ -519,7 +519,7 @@ static void manager_deserialize_gid_refs_one(Manager *m, const char *value) {
static void deserialize_restrict_fsaccess(Manager *m, const char *l, FDSet *fds) {
const char *val;
int fd;
int fd, r;
FOREACH_ELEMENT(name, restrict_fsaccess_link_names) {
val = startswith(l, *name);
@@ -537,6 +537,17 @@ static void deserialize_restrict_fsaccess(Manager *m, const char *l, FDSet *fds)
return;
}
val = startswith(l, "restrict-fsaccess-ptrace-filter=");
if (val) {
r = parse_boolean(val);
if (r < 0)
log_warning_errno(r, "bpf-restrict-fsaccess: Failed to parse ptrace filter state '%s', "
"ignoring: %m", val);
else
m->restrict_fsaccess_ptrace_filter = r;
return;
}
val = startswith(l, "restrict-fsaccess-bss-map=");
if (!val)
return;
+3
View File
@@ -514,6 +514,9 @@ typedef struct Manager {
int restrict_fsaccess_link_fds[_RESTRICT_FILESYSTEM_ACCESS_LINK_MAX];
int restrict_fsaccess_bss_map_fd;
/* Whether the ptrace seccomp filter is installed in this process; it survives execve() */
bool restrict_fsaccess_ptrace_filter;
/* Allow users to configure a rate limit for Reload()/Reexecute() operations */
RateLimit reload_reexec_ratelimit;
/* Dump*() are slow, so always rate limit them to 10 per 10 minutes */
+42
View File
@@ -9,6 +9,7 @@
#include <sched.h>
#include <sys/mman.h>
#include <sys/prctl.h> /* IWYU pragma: keep */
#include <sys/ptrace.h>
#include <sys/shm.h>
#include <sys/stat.h>
@@ -2152,6 +2153,47 @@ int seccomp_filter_set_add(Hashmap *filter, bool add, const SyscallFilterSet *se
return 0;
}
/* Block PTRACE_POKE{TEXT,DATA} FOLL_FORCE writes. A missing filter is a bypass, so unlike the other helpers
* this fails when the native architecture cannot be filtered. A secondary architecture the kernel refuses a
* filter for is skipped as everywhere else: it cannot run binaries either. */
int seccomp_restrict_ptrace(void) {
uint32_t arch;
int r;
r = dlopen_libseccomp(LOG_DEBUG);
if (r < 0)
return r;
SECCOMP_FOREACH_LOCAL_ARCH(arch) {
_cleanup_(seccomp_releasep) scmp_filter_ctx seccomp = NULL;
r = seccomp_init_for_arch(&seccomp, arch, SCMP_ACT_ALLOW);
if (r < 0)
return r;
r = add_seccomp_syscall_filter(seccomp, arch, SCMP_SYS(ptrace),
1, SCMP_A0(SCMP_CMP_EQ, PTRACE_POKETEXT));
if (r < 0)
return r;
r = add_seccomp_syscall_filter(seccomp, arch, SCMP_SYS(ptrace),
1, SCMP_A0(SCMP_CMP_EQ, PTRACE_POKEDATA));
if (r < 0)
return r;
r = sym_seccomp_load(seccomp);
if (r < 0) {
if (ERRNO_IS_NEG_SECCOMP_FATAL(r) || arch == sym_seccomp_arch_native())
return log_debug_errno(r, "Failed to apply ptrace restrictions for architecture %s: %m",
seccomp_arch_to_string(arch));
log_debug_errno(r, "Failed to apply ptrace restrictions for architecture %s, skipping: %m",
seccomp_arch_to_string(arch));
}
}
return 0;
}
int seccomp_lock_personality(unsigned long personality) {
uint32_t arch;
int r;
+5
View File
@@ -122,6 +122,7 @@ static inline int seccomp_restrict_realtime(void) {
return seccomp_restrict_realtime_full(EPERM);
}
int seccomp_memory_deny_write_execute(void);
int seccomp_restrict_ptrace(void);
int seccomp_lock_personality(unsigned long personality);
int seccomp_protect_hostname(void);
int seccomp_restrict_suid_sgid(void);
@@ -166,6 +167,10 @@ static inline bool is_seccomp_available(void) {
return false;
}
static inline int seccomp_restrict_ptrace(void) {
return -EOPNOTSUPP;
}
#endif
int dlopen_libseccomp(int log_level) _dlopen_loader_;
+326 -77
View File
@@ -7,10 +7,21 @@
* test-bpf-restrict-fsaccess attach — Load, attach, print IDs, then block.
* Kill the process to detach (synchronous
* via bpf_link_put_direct on last FD close).
* test-bpf-restrict-fsaccess check — Check BPF LSM + require_signatures preconditions
* test-bpf-restrict-fsaccess check — Run PID 1's prerequisite checks and install its
* ptrace seccomp filter
* test-bpf-restrict-fsaccess mmap-exec PATH — Attempt PROT_READ|PROT_EXEC mmap of PATH
* test-bpf-restrict-fsaccess anon-mmap-exec — Attempt anonymous PROT_READ|PROT_EXEC mmap
* test-bpf-restrict-fsaccess mprotect-exec PATH — mmap PATH PROT_READ, then mprotect to PROT_EXEC
* test-bpf-restrict-fsaccess mmap-wx PATH — PROT_WRITE|PROT_EXEC mmap of PATH (W^X variant A)
* test-bpf-restrict-fsaccess mprotect-cow-exec PATH — mmap PROT_WRITE, write, mprotect PROT_EXEC (variant B)
* test-bpf-restrict-fsaccess mprotect-wx PATH — mmap PROT_EXEC, then mprotect +PROT_WRITE (variant C)
* test-bpf-restrict-fsaccess procmem-cow PATH — mmap PROT_EXEC, then rewrite it via /proc/self/mem
* test-bpf-restrict-fsaccess poketext — install ptrace seccomp, verify PTRACE_POKE{TEXT,DATA}
* are denied
*
* The probes exit with 0 if the operation was allowed (the bypass works), 1 if
* it was refused with the errno the mechanism under test produces, 2 if it
* could not be attempted and 3 if it was refused with an unexpected errno.
*
* When "attach" is used, the BPF LSM program is loaded with initramfs_s_dev
* set to the current rootfs s_dev, so the calling test script (running from
@@ -19,79 +30,320 @@
*/
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/ptrace.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include "bpf-link.h"
#include "bpf-restrict-fsaccess.h"
#include "devnum-util.h"
#include "errno-util.h"
#include "fd-util.h"
#include "log.h"
#include "lsm-util.h"
#include "pidref.h"
#include "process-util.h"
#include "seccomp-util.h"
#include "string-util.h"
#include "tests.h"
/* ---- mmap/mprotect probe commands (no BPF dependency) ----
/* ---- Probe commands (no BPF dependency) ----
*
* These exercise the mmap_file, file_mprotect, and anonymous-mmap LSM hooks.
* The test script copies a file to tmpfs and passes its path here.
* Returns 0 if the operation was allowed, negative errno if denied. */
* Each attempts one operation the hooks are supposed to deny. The exit codes
* let the test script tell a denial by the mechanism under test from a probe
* that could not run at all. */
enum {
PROBE_ALLOWED = 0, /* the operation succeeded, i.e. the bypass works */
PROBE_DENIED = 1, /* refused with the errno the mechanism under test produces */
PROBE_ERROR = 2, /* the operation could not be attempted */
PROBE_DENIED_OTHER = 3, /* refused, but with an unexpected errno (another LSM?) */
};
static int do_mmap_exec(const char *path) {
_cleanup_close_ int fd = -EBADF;
void *addr;
/* Only the errno the mechanism under test produces counts as a denial. */
static int probe_denied(int error, int expected) {
assert(error < 0);
fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0)
return log_error_errno(errno, "Failed to open %s: %m", path);
if (error == expected)
return PROBE_DENIED;
addr = mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED)
return log_info_errno(errno, "PROT_EXEC mmap of %s denied: %m", path);
(void) munmap(addr, 4096);
log_info("PROT_EXEC mmap of %s succeeded", path);
return 0;
log_warning("Refused with unexpected error %s, expected %s.", STRERROR(error), STRERROR(expected));
return PROBE_DENIED_OTHER;
}
static int do_anon_mmap_exec(void) {
void *addr;
addr = mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
return log_info_errno(errno, "Anonymous PROT_EXEC mmap denied: %m");
(void) munmap(addr, 4096);
log_info("Anonymous PROT_EXEC mmap succeeded");
return 0;
}
static int do_mprotect_exec(const char *path) {
static int mmap_probe(const char *path, int prot, const char *what) {
_cleanup_close_ int fd = -EBADF;
void *addr;
int r;
fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0)
return log_error_errno(errno, "Failed to open %s: %m", path);
if (fd < 0) {
log_error_errno(errno, "Failed to open %s: %m", path);
return PROBE_ERROR;
}
addr = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED)
return log_error_errno(errno, "PROT_READ mmap of %s failed: %m", path);
r = mprotect(addr, 4096, PROT_READ | PROT_EXEC);
if (r < 0)
r = -errno;
addr = mmap(NULL, 4096, prot, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) {
r = log_info_errno(errno, "%s of %s denied: %m", what, path);
return probe_denied(r, -EPERM);
}
(void) munmap(addr, 4096);
log_info("%s of %s succeeded", what, path);
return PROBE_ALLOWED;
}
/* Maps PATH with 'prot', then changes the protection to 'new_prot'. With 'poke' the mapping is written to
* first, so that the page is a private copy. */
static int mprotect_probe(const char *path, int prot, bool poke, int new_prot, const char *what) {
_cleanup_close_ int fd = -EBADF;
void *addr;
int r;
fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
log_error_errno(errno, "Failed to open %s: %m", path);
return PROBE_ERROR;
}
addr = mmap(NULL, 4096, prot, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) {
log_error_errno(errno, "Initial mmap of %s failed, cannot probe: %m", path);
return PROBE_ERROR;
}
if (poke)
*(volatile uint8_t*) addr = 0x90; /* trigger copy-on-write */
r = RET_NERRNO(mprotect(addr, 4096, new_prot));
(void) munmap(addr, 4096);
if (r < 0) {
log_info_errno(r, "%s on %s denied: %m", what, path);
return probe_denied(r, -EPERM);
}
log_info("%s on %s succeeded", what, path);
return PROBE_ALLOWED;
}
static int do_mmap_exec(const char *path) {
return mmap_probe(path, PROT_READ | PROT_EXEC, "PROT_EXEC mmap");
}
static int do_anon_mmap_exec(void) {
void *addr;
int r;
addr = mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
r = log_info_errno(errno, "Anonymous PROT_EXEC mmap denied: %m");
return probe_denied(r, -EPERM);
}
(void) munmap(addr, 4096);
log_info("Anonymous PROT_EXEC mmap succeeded");
return PROBE_ALLOWED;
}
/* mmap PROT_READ, then add PROT_EXEC: denied for untrusted files, a positive control for trusted ones. */
static int do_mprotect_exec(const char *path) {
return mprotect_probe(path, PROT_READ, /* poke= */ false, PROT_READ | PROT_EXEC, "mprotect PROT_EXEC");
}
/* ---- W^X bypass probes (GHSA-q65q-ggpx-fjgp) ----
*
* Each tries to get attacker-controlled bytes into an executable page of a
* trusted file. With the W^X hooks in place they must all be denied; an exit
* of PROBE_ALLOWED means the bypass still works. */
/* Variant A: a private writable+executable mapping. */
static int do_mmap_wx(const char *path) {
return mmap_probe(path, PROT_READ | PROT_WRITE | PROT_EXEC, "W+X mmap");
}
/* Variant B: map writable, modify via copy-on-write, then turn executable. */
static int do_mprotect_cow_exec(const char *path) {
return mprotect_probe(path, PROT_READ | PROT_WRITE, /* poke= */ true, PROT_READ | PROT_EXEC,
"mprotect PROT_EXEC after COW write");
}
/* Variant C: map executable, then add write. */
static int do_mprotect_wx(const char *path) {
return mprotect_probe(path, PROT_READ | PROT_EXEC, /* poke= */ false, PROT_READ | PROT_WRITE | PROT_EXEC,
"mprotect PROT_WRITE|PROT_EXEC");
}
/* mmap PROT_EXEC read-only (allowed), then rewrite the page through /proc/self/mem, which the kernel only
* services with FOLL_FORCE, i.e. unless booted with proc_mem.force_override=never. */
static int do_procmem_cow(const char *path) {
_cleanup_close_ int fd = -EBADF, mem_fd = -EBADF;
static const uint8_t byte = 0x90;
void *addr;
ssize_t n;
int r;
fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
log_error_errno(errno, "Failed to open %s: %m", path);
return PROBE_ERROR;
}
addr = mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) {
log_error_errno(errno, "PROT_EXEC mmap of %s failed, cannot probe: %m", path);
return PROBE_ERROR;
}
mem_fd = open("/proc/self/mem", O_RDWR | O_CLOEXEC);
if (mem_fd < 0) {
r = -errno;
(void) munmap(addr, 4096);
log_error_errno(r, "Failed to open /proc/self/mem: %m");
return PROBE_ERROR;
}
n = pwrite(mem_fd, &byte, sizeof(byte), (off_t) (uintptr_t) addr);
r = n < 0 ? -errno : 0;
(void) munmap(addr, 4096);
if (r < 0) {
log_info_errno(r, "/proc/self/mem write to exec page of %s denied: %m", path);
/* Without FOLL_FORCE access_remote_vm() copies nothing and mem_rw() fails with EIO. */
return probe_denied(r, -EIO);
}
log_info("/proc/self/mem write to exec page of %s succeeded", path);
return PROBE_ALLOWED;
}
/* Writes one word into the stopped tracee with 'request' (PTRACE_POKETEXT or PTRACE_POKEDATA). */
static int poke(pid_t pid, int request, const char *name) {
errno = 0;
if (ptrace(request, pid, (void*) (uintptr_t) &poke, (void*) 0x42) < 0) {
if (errno == EPERM) {
log_info("ptrace(%s) denied by seccomp (EPERM) as expected", name);
return PROBE_DENIED;
}
log_error_errno(errno, "ptrace(%s) failed unexpectedly: %m", name);
return PROBE_ERROR;
}
log_info("ptrace(%s) succeeded, seccomp NOT enforcing", name);
return PROBE_ALLOWED;
}
/* Self-contained check of the ptrace() memory-write block (seccomp layer). The
* filter is installed by PID 1 in production, not by "attach", and seccomp is
* per-process-tree, so this probe installs it itself and then verifies that
* PTRACE_POKETEXT and PTRACE_POKEDATA are refused while attaching and reading
* (PTRACE_PEEKTEXT) still work. */
static int do_poketext_seccomp(void) {
_cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL;
int status = 0, r;
long peek;
r = pidref_safe_fork("(poketext)", FORK_LOG|FORK_DEATHSIG_SIGKILL, &pid);
if (r < 0)
return log_info_errno(r, "mprotect PROT_EXEC on %s denied: %m", path);
return PROBE_ERROR;
if (r == 0) {
/* Child: invite tracing (PTRACE_TRACEME is not blocked), then stop. */
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0)
_exit(EXIT_FAILURE);
raise(SIGSTOP);
_exit(EXIT_SUCCESS);
}
log_info("mprotect PROT_EXEC on %s succeeded", path);
return 0;
/* WUNTRACED: if PTRACE_TRACEME failed (e.g. Yama ptrace_scope=3) the child stops untraced, and
* without the flag we would wait forever. */
if (waitpid(pid.pid, &status, WUNTRACED) < 0) {
log_error_errno(errno, "waitpid failed: %m");
return PROBE_ERROR;
}
if (WIFEXITED(status)) {
log_error("PTRACE_TRACEME failed in the child, cannot ptrace in this environment.");
return PROBE_ERROR;
}
if (!WIFSTOPPED(status)) {
log_error("Child did not stop as expected (status=%i).", status);
return PROBE_ERROR;
}
/* Positive control: without the filter, writing the word back must work, so that a denial below
* really is the filter's. */
errno = 0;
peek = ptrace(PTRACE_PEEKTEXT, pid.pid, (void*) (uintptr_t) &poke, NULL);
if (peek == -1 && errno != 0) {
log_error_errno(errno, "ptrace(PTRACE_PEEKTEXT) failed, cannot ptrace in this environment: %m");
return PROBE_ERROR;
}
if (ptrace(PTRACE_POKETEXT, pid.pid, (void*) (uintptr_t) &poke, (void*) (uintptr_t) peek) < 0) {
log_error_errno(errno, "ptrace(PTRACE_POKETEXT) failed before the filter was installed: %m");
return PROBE_ERROR;
}
r = seccomp_restrict_ptrace();
if (r < 0) {
log_error_errno(r, "Failed to install ptrace seccomp filter: %m");
return PROBE_ERROR;
}
/* PEEKTEXT (read) must still work: attaching and inspection stay allowed. */
errno = 0;
peek = ptrace(PTRACE_PEEKTEXT, pid.pid, (void*) (uintptr_t) &poke, NULL);
if (peek == -1 && errno != 0) {
log_error_errno(errno, "ptrace(PTRACE_PEEKTEXT) failed, filter too broad?: %m");
return PROBE_ERROR;
}
/* Both writers must be refused. */
r = poke(pid.pid, PTRACE_POKETEXT, "PTRACE_POKETEXT");
if (r == PROBE_DENIED)
r = poke(pid.pid, PTRACE_POKEDATA, "PTRACE_POKEDATA");
return r;
}
static int usage(void) {
log_error("Usage: %s attach|check|mmap-exec PATH|anon-mmap-exec|mprotect-exec PATH|"
"mmap-wx PATH|mprotect-cow-exec PATH|mprotect-wx PATH|procmem-cow PATH|poketext",
program_invocation_short_name);
/* Not EXIT_FAILURE, which is PROBE_DENIED. */
return PROBE_ERROR;
}
/* The probes need no BPF support. Returns the probe's exit code, or -ENOENT if the verb is not a probe. */
static int dispatch_probe(int argc, char *argv[]) {
const char *verb = argv[1];
if (argc == 2) {
if (streq(verb, "anon-mmap-exec"))
return do_anon_mmap_exec();
if (streq(verb, "poketext"))
return do_poketext_seccomp();
return -ENOENT;
}
if (argc != 3)
return -ENOENT;
if (streq(verb, "mmap-exec"))
return do_mmap_exec(argv[2]);
if (streq(verb, "mprotect-exec"))
return do_mprotect_exec(argv[2]);
if (streq(verb, "mmap-wx"))
return do_mmap_wx(argv[2]);
if (streq(verb, "mprotect-cow-exec"))
return do_mprotect_cow_exec(argv[2]);
if (streq(verb, "mprotect-wx"))
return do_mprotect_wx(argv[2]);
if (streq(verb, "procmem-cow"))
return do_procmem_cow(argv[2]);
return -ENOENT;
}
#if BPF_FRAMEWORK && HAVE_LSM_INTEGRITY_TYPE
@@ -166,15 +418,18 @@ static int do_attach(void) {
static int do_check(void) {
int r;
r = dlopen_bpf(LOG_WARNING);
r = bpf_restrict_fsaccess_check_prerequisites();
if (r < 0)
return r;
r = lsm_supported("bpf");
if (r <= 0)
return log_error_errno(r < 0 ? r : EOPNOTSUPP, "BPF LSM is not available: %m");
log_info("BPF LSM: supported");
/* PID 1 refuses the policy without the ptrace filter, mirror that. */
r = seccomp_restrict_ptrace();
if (r < 0)
return log_error_errno(r, "Failed to install the ptrace seccomp filter: %m");
log_info("ptrace seccomp filter: installed");
/* Last: linking attaches bprm_check for an instant with an empty trust map, during which every
* execve() on the system is denied. Keep that window out of the cheap failure paths above. */
_cleanup_(restrict_fsaccess_bpf_freep) struct restrict_fsaccess_bpf *obj = NULL;
r = bpf_restrict_fsaccess_prepare(&obj);
if (r < 0)
@@ -184,55 +439,49 @@ static int do_check(void) {
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"bpf-restrict-fsaccess: Failed to link program.");
if (!dm_verity_require_signatures())
return log_error_errno(SYNTHETIC_ERRNO(ENOKEY), "dm-verity require_signatures is not enabled.");
log_info("dm-verity require_signatures: enabled");
return 0;
}
int main(int argc, char *argv[]) {
int r;
test_setup_logging(LOG_DEBUG);
if (argc < 2) {
log_error("Usage: %s attach|check|mmap-exec|anon-mmap-exec|mprotect-exec",
program_invocation_short_name);
return EXIT_FAILURE;
}
if (argc < 2)
return usage();
if (streq(argv[1], "attach"))
return do_attach() < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (streq(argv[1], "check"))
return do_check() < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (streq(argv[1], "mmap-exec") && argc == 3)
return do_mmap_exec(argv[2]) < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (streq(argv[1], "anon-mmap-exec"))
return do_anon_mmap_exec() < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (streq(argv[1], "mprotect-exec") && argc == 3)
return do_mprotect_exec(argv[2]) < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
log_error("Usage: %s attach|check|mmap-exec PATH|anon-mmap-exec|mprotect-exec PATH",
program_invocation_short_name);
return EXIT_FAILURE;
r = dispatch_probe(argc, argv);
if (r >= 0)
return r;
return usage();
}
#else /* ! BPF_FRAMEWORK || ! HAVE_LSM_INTEGRITY_TYPE */
int main(int argc, char *argv[]) {
int r;
test_setup_logging(LOG_DEBUG);
/* mmap/mprotect probes work without BPF */
if (argc >= 2) {
if (streq(argv[1], "mmap-exec") && argc == 3)
return do_mmap_exec(argv[2]) < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (streq(argv[1], "anon-mmap-exec"))
return do_anon_mmap_exec() < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (streq(argv[1], "mprotect-exec") && argc == 3)
return do_mprotect_exec(argv[2]) < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (argc < 2)
return usage();
if (streq(argv[1], "attach") || streq(argv[1], "check")) {
log_info("BPF framework not available, attach/check not supported");
return EXIT_TEST_SKIP;
}
log_info("BPF framework not available, attach/check not supported");
return 77; /* skip */
r = dispatch_probe(argc, argv);
if (r >= 0)
return r;
return usage();
}
#endif
+60
View File
@@ -2,13 +2,16 @@
#include <fcntl.h>
#include <poll.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/eventfd.h>
#include <sys/mman.h>
#include <sys/personality.h>
#include <sys/ptrace.h>
#include <sys/shm.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#if HAVE_VALGRIND_VALGRIND_H
#include <valgrind/valgrind.h>
@@ -643,6 +646,63 @@ TEST(memory_deny_write_execute_shmat) {
}
}
TEST(restrict_ptrace) {
int r;
CHECK_SECCOMP(/* refuse_container= */ false);
r = ASSERT_OK(pidref_safe_fork("(restrict-ptrace)", FORK_LOG|FORK_WAIT, NULL));
if (r == 0) {
void *addr = (void*) (uintptr_t) &ptrace;
int status;
pid_t pid;
long word;
/* A tracee that stops right away; PTRACE_TRACEME is not filtered. */
pid = fork();
ASSERT_OK_ERRNO(pid);
if (pid == 0) {
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0)
_exit(EXIT_FAILURE);
raise(SIGSTOP);
_exit(EXIT_SUCCESS);
}
ASSERT_OK_ERRNO(waitpid(pid, &status, WUNTRACED));
if (WIFEXITED(status)) {
log_tests_skipped("ptrace() not permitted");
_exit(EXIT_SUCCESS);
}
ASSERT_TRUE(WIFSTOPPED(status));
/* Positive control: without the filter, writing the word back works. */
errno = 0;
word = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL);
ASSERT_TRUE(word != -1 || errno == 0);
if (ptrace(PTRACE_POKETEXT, pid, addr, (void*) (uintptr_t) word) < 0) {
log_tests_skipped_errno(errno, "PTRACE_POKETEXT not permitted");
(void) kill(pid, SIGKILL);
_exit(EXIT_SUCCESS);
}
ASSERT_OK(seccomp_restrict_ptrace());
/* Reading stays allowed... */
errno = 0;
word = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL);
ASSERT_TRUE(word != -1 || errno == 0);
/* ...writing is refused by the filter. */
ASSERT_ERROR_ERRNO(ptrace(PTRACE_POKETEXT, pid, addr, (void*) (uintptr_t) word), EPERM);
ASSERT_ERROR_ERRNO(ptrace(PTRACE_POKEDATA, pid, addr, (void*) (uintptr_t) word), EPERM);
ASSERT_OK_ERRNO(kill(pid, SIGKILL));
ASSERT_OK_ERRNO(waitpid(pid, &status, 0));
_exit(EXIT_SUCCESS);
}
}
TEST(restrict_archs) {
int r;
@@ -6,6 +6,10 @@ integration_tests += [
'name' : fs.name(meson.current_source_dir()),
'vm' : true,
'firmware' : 'auto',
# Keep require_signatures the only gate that fails the helper's "check" here.
'cmdline' : integration_test_template['cmdline'] + [
'proc_mem.force_override=never',
],
'configuration' : integration_test_template['configuration'] + {
'env' : {'TEST_MATCH_SUBTEST' : 'config'},
},
@@ -14,7 +18,8 @@ integration_tests += [
# Enforce subtest: with require_signatures=1 — tests actual BPF enforcement
# via the SecureBoot DB → .platform keyring path (firmware: auto enables
# UEFI/SecureBoot in the test VM).
# UEFI/SecureBoot in the test VM). proc_mem.force_override=never is the
# other command line prerequisite of RestrictFileSystemAccess=.
integration_tests += [
integration_test_template + {
'name' : fs.name(meson.current_source_dir()) + '-enforce',
@@ -22,6 +27,7 @@ integration_tests += [
'firmware' : 'auto',
'cmdline' : integration_test_template['cmdline'] + [
'dm_verity.require_signatures=1',
'proc_mem.force_override=never',
],
'configuration' : integration_test_template['configuration'] + {
'command' : '/usr/lib/systemd/tests/testdata/units/' + fs.name(meson.current_source_dir()) + '.sh',
@@ -41,6 +47,7 @@ integration_tests += [
'cmdline' : integration_test_template['cmdline'] + [
'dm_verity.require_signatures=1',
'dm_verity.keyring_unsealed=1',
'proc_mem.force_override=never',
],
'configuration' : integration_test_template['configuration'] + {
'command' : '/usr/lib/systemd/tests/testdata/units/' + fs.name(meson.current_source_dir()) + '.sh',
+11 -4
View File
@@ -90,13 +90,20 @@ testcase_no_require_signatures_helper() {
fi
fi
# The helper's "check" command runs the same bpf_restrict_fsaccess_supported()
# and dm_verity_require_signatures() checks that PID1 uses. It must fail
# because require_signatures is not enabled.
if "$HELPER" check; then
# The helper's "check" command runs the same bpf_restrict_fsaccess_check_prerequisites()
# checks that PID1 uses. It must fail because require_signatures is not enabled.
local out
if out=$("$HELPER" check 2>&1); then
echo "ERROR: helper check succeeded but require_signatures is not enabled"
return 1
fi
# ... and for that reason, not some other broken gate (meson.build sets
# proc_mem.force_override=never for this subtest to keep the others green).
if ! grep -F 'dm-verity require_signatures is not enabled' <<<"$out" >/dev/null; then
echo "ERROR: helper check failed for a reason other than require_signatures:"
echo "$out"
return 1
fi
echo "Helper correctly rejected setup: require_signatures not enabled"
}
+139 -24
View File
@@ -8,8 +8,9 @@
# while execution from the rootfs continues to work. If dm-verity signing
# support is available, also tests execution from a signed verity device.
#
# Requires the VM to be booted with dm-verity.require_signatures=1 on the
# kernel command line (set in the test's meson.build).
# Requires the VM to be booted with dm_verity.require_signatures=1 and
# proc_mem.force_override=never on the kernel command line (set in the test's
# meson.build).
set -eux
set -o pipefail
@@ -47,14 +48,19 @@ if [[ ! -x "$HELPER" ]]; then
exit 1
fi
# Helper exits 77 when systemd was built with bpf-framework=enabled but no
# vmlinux.h (HAVE_LSM_INTEGRITY_TYPE=0), so the BPF program isn't compiled in.
rc=0
"$HELPER" check >/dev/null 2>&1 || rc=$?
if [[ "$rc" -eq 77 ]]; then
echo "test-bpf-restrict-fsaccess built without BPF attach support, skipping"
exit 77
fi
# The helper's probes exit with 0 if the operation was allowed, 1 if it was
# refused with the errno the mechanism under test produces, 2 if it could not
# be attempted and 3 if it was refused with an unexpected errno. Only the exact
# code counts, so a broken probe cannot pass as a denial.
expect_probe() {
local expected="$1" what="$2" rc=0
shift 2
"$HELPER" "$@" || rc=$?
if [[ "$rc" -ne "$expected" ]]; then
echo "ERROR: $what: helper '$*' exited with $rc, expected $expected" >&2
exit 1
fi
}
# require_signatures is read-only — must be set via kernel cmdline
if [[ ! -e /sys/module/dm_verity/parameters/require_signatures ]]; then
@@ -70,6 +76,39 @@ if [[ "$val" != "Y" && "$val" != "1" ]]; then
exit 77
fi
# proc_mem.force_override is __ro_after_init and has no readout: like
# require_signatures it must come from the kernel cmdline (set in meson.build).
if ! grep -E '(^|[[:space:]])proc_mem\.force_override=never([[:space:]]|$)' /proc/cmdline >/dev/null; then
echo "proc_mem.force_override=never not on the kernel cmdline (set in meson.build), skipping"
exit 77
fi
# The kernel silently ignores the option if it predates it (Linux 6.12), which "check" below would
# report as a hard failure: skip on such a kernel. On newer ones the /proc/self/mem test below
# asserts that the write really is refused.
if systemd-analyze compare-versions "$(uname -r)" lt 6.12; then
echo "Kernel $(uname -r) predates proc_mem.force_override= (Linux 6.12), skipping"
exit 77
fi
# PID1 refuses the policy without seccomp support, and so does the helper's "check".
if ! systemctl --version | grep -F -- "+SECCOMP" >/dev/null; then
echo "seccomp support not compiled in, skipping"
exit 77
fi
# Helper exits 77 when systemd was built with bpf-framework=enabled but no
# vmlinux.h (HAVE_LSM_INTEGRITY_TYPE=0), so the BPF program isn't compiled in.
# Run it only once, and only now that dm_verity is loaded so a pass is
# meaningful: a passing "check" briefly attaches the bprm_check program with
# an empty trust map, denying every execve() on the system meanwhile.
CHECK_RC=0
"$HELPER" check || CHECK_RC=$?
if [[ "$CHECK_RC" -eq 77 ]]; then
echo "test-bpf-restrict-fsaccess built without BPF attach support, skipping"
exit 77
fi
at_exit() {
set +e
# Kill the attach helper to detach BPF programs synchronously
@@ -90,6 +129,75 @@ at_exit() {
}
trap at_exit EXIT
# ------ Preconditions: helper "check" must agree with the cmdline ------
# "check" runs the same gates as PID1: BPF LSM, require_signatures,
# proc_mem.force_override=never on the cmdline plus a /proc/self/mem self-probe
# verifying the kernel honours it, and the ptrace seccomp filter.
# SYSTEMD_PROC_CMDLINE overrides what the helper considers the kernel command
# line; the gates run before any BPF program is loaded, so the rejections
# below are cheap and attach nothing.
if [[ "$CHECK_RC" -ne 0 ]]; then
echo "ERROR: helper check failed (rc=$CHECK_RC) although all prerequisites are met!" >&2
exit 1
fi
echo "Helper check with proc_mem.force_override=never: OK"
# "never ptrace" being rejected shows the last occurrence wins, like the
# kernel's early_param handling.
for bad in "" "proc_mem.force_override=always" "proc_mem.force_override=ptrace" \
"proc_mem.force_override=never proc_mem.force_override=ptrace"; do
if out=$(SYSTEMD_PROC_CMDLINE="$bad" "$HELPER" check 2>&1); then
echo "ERROR: helper check should have rejected cmdline '$bad'!" >&2
exit 1
fi
if ! grep -F 'proc_mem.force_override is not set to' <<<"$out" >/dev/null; then
echo "ERROR: helper check rejected cmdline '$bad' for the wrong reason:" >&2
echo "$out" >&2
exit 1
fi
done
echo "Helper check rejects always/ptrace/unset and honours the last occurrence: OK"
# ------ Test: /proc/self/mem cannot rewrite an executable page ------
# Independent of the BPF programs: with proc_mem.force_override=never the
# kernel drops FOLL_FORCE, so the copy-on-write through /proc/self/mem is
# refused with EIO.
expect_probe 1 "/proc/self/mem rewrite of an executable page" procmem-cow /usr/bin/true
echo "/proc/self/mem rewrite of executable page refused: OK"
# ------ Test: the ptrace seccomp filter refuses PTRACE_POKE{TEXT,DATA} ------
# The helper installs the same filter PID1 uses and pokes a traced child; the
# probe only counts as a denial if PTRACE_PEEKTEXT still worked.
expect_probe 1 "PTRACE_POKETEXT/PTRACE_POKEDATA" poketext
echo "PTRACE_POKETEXT/PTRACE_POKEDATA refused by seccomp filter: OK"
# ------ Baseline: W^X probes succeed WITHOUT our BPF ------
# Another LSM (e.g. SELinux execmem/execmod) may deny these on its own, which
# the helper reports as exit 3 (refused with an errno other than EPERM). A
# denial with BPF attached would then prove nothing, so skip them. Anything
# else is a broken probe or a stray policy and fails the test.
WX_BASELINE=1
for probe in mmap-wx mprotect-cow-exec mprotect-wx mprotect-exec; do
rc=0
"$HELPER" "$probe" /usr/bin/true || rc=$?
case "$rc" in
0) ;;
3)
echo "WARNING: $probe denied BEFORE BPF attach by another LSM, skipping W^X tests" >&2
WX_BASELINE=0
break
;;
*)
echo "ERROR: $probe failed BEFORE BPF attach (rc=$rc)!" >&2
exit 1
;;
esac
done
# ------ Baseline: verify tmpfs exec works WITHOUT our BPF ------
#
# Keep the destination basename as "true": on systems shipping uutils-coreutils
@@ -162,30 +270,37 @@ echo "Execution from tmpfs blocked: OK"
# Write a test file on the tmpfs mount for mmap/mprotect tests
dd if=/dev/zero of=/tmp/restrict-fsaccess-test/testfile bs=4096 count=1 2>/dev/null
# File-backed PROT_EXEC mmap should be denied.
# The helper exits 0 if mmap succeeds (bad), 1 if denied (good).
if "$HELPER" mmap-exec /tmp/restrict-fsaccess-test/testfile; then
echo "ERROR: PROT_EXEC mmap of tmpfs file should have been blocked!" >&2
exit 1
fi
# File-backed PROT_EXEC mmap should be denied (with EPERM, see expect_probe).
expect_probe 1 "PROT_EXEC mmap of tmpfs file" mmap-exec /tmp/restrict-fsaccess-test/testfile
echo "PROT_EXEC mmap from tmpfs blocked: OK"
# Anonymous PROT_EXEC mmap should be denied (NULL file — mmap_file hook)
if "$HELPER" anon-mmap-exec; then
echo "ERROR: Anonymous PROT_EXEC mmap should have been blocked!" >&2
exit 1
fi
expect_probe 1 "anonymous PROT_EXEC mmap" anon-mmap-exec
echo "Anonymous PROT_EXEC mmap blocked: OK"
# ------ Test: mprotect adding PROT_EXEC is blocked (file_mprotect hook) ------
# mmap PROT_READ then mprotect to PROT_EXEC — the file_mprotect hook should deny this.
if "$HELPER" mprotect-exec /tmp/restrict-fsaccess-test/testfile; then
echo "ERROR: mprotect PROT_EXEC on tmpfs file should have been blocked!" >&2
exit 1
fi
expect_probe 1 "mprotect PROT_EXEC on tmpfs file" mprotect-exec /tmp/restrict-fsaccess-test/testfile
echo "mprotect PROT_EXEC from tmpfs blocked: OK"
# ------ Test: W^X on a trusted file (mmap_file + file_mprotect hooks) ------
# /usr/bin/true lives on the trusted rootfs, so a denial here is due to the
# W^X rules, not provenance.
if [[ "$WX_BASELINE" == 1 ]]; then
for probe in mmap-wx mprotect-cow-exec mprotect-wx; do
expect_probe 1 "$probe on a trusted file" "$probe" /usr/bin/true
echo "W^X probe $probe blocked: OK"
done
# Positive control: an unmodified trusted file mapping may still be made
# executable — the copy-on-write heuristic must not over-deny.
expect_probe 0 "PROT_EXEC mmap of trusted file" mmap-exec /usr/bin/true
expect_probe 0 "mprotect PROT_EXEC on trusted file" mprotect-exec /usr/bin/true
echo "Executable mappings of unmodified trusted file allowed: OK"
fi
# ------ Test: Execution from signed dm-verity device ------
# Trust path: .platform keyring (SecureBoot DB auto-enrolled by mkosi, made
# available by 'firmware': 'auto' in the test's meson.build).