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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user