diff --git a/.github/workflows/build-test.sh b/.github/workflows/build-test.sh index 506479a558..68242ac922 100755 --- a/.github/workflows/build-test.sh +++ b/.github/workflows/build-test.sh @@ -65,6 +65,7 @@ PACKAGES=( util-linux zstd ) +FEATURES=() COMPILER="${COMPILER:?}" COMPILER_VERSION="${COMPILER_VERSION:?}" LINKER="${LINKER:?}" @@ -133,6 +134,8 @@ sudo rm -f /etc/apt/sources.list.d/microsoft-prod.{list,sources} if grep -q 'VERSION_CODENAME=jammy' /usr/lib/os-release; then sudo add-apt-repository -y --no-update ppa:upstream-systemd-ci/systemd-ci sudo add-apt-repository -y --no-update --enable-source + # Jammy's kernel is too old and there's no vmlinux.h + FEATURES+=("-Dbpf-framework=disabled") else # add-apt-repository --enable-source does not work on deb822 style sources. for f in /etc/apt/sources.list.d/*.sources; do @@ -175,6 +178,7 @@ for args in "${ARGS[@]}"; do meson setup \ -Dtests=unsafe -Dslow-tests=true -Dfuzz-tests=true --werror \ -Dnobody-group=nogroup -Ddebug=false \ + "${FEATURES[@]}" \ $args build; then cat build/meson-logs/meson-log.txt diff --git a/man/kernel-command-line.xml b/man/kernel-command-line.xml index 83544b3606..03765010f0 100644 --- a/man/kernel-command-line.xml +++ b/man/kernel-command-line.xml @@ -79,6 +79,17 @@ + + systemd.restrict_filesystem_access= + + Controls the RestrictFileSystemAccess= execution enforcement policy. For + details, see + systemd-system.conf5. + + + + + systemd.mask= systemd.wants= diff --git a/man/systemd-system.conf.xml b/man/systemd-system.conf.xml index eb14cb7f30..fb565b0350 100644 --- a/man/systemd-system.conf.xml +++ b/man/systemd-system.conf.xml @@ -532,6 +532,49 @@ + + RestrictFileSystemAccess= + + Takes a boolean argument or the special value exec. Defaults to + no. When enabled, PID 1 loads a BPF LSM program that enforces a deny-default + execution policy: only binaries residing on signed dm-verity block devices (and the initramfs during + early boot) are permitted to execute. Execution from tmpfs, procfs, sysfs, unsigned dm-verity devices, + and anonymous executable memory mappings is denied. + + This setting is intended as one component of an image-based, fully verified system, where the + whole boot chain (firmware, kernel image, kernel command line, initramfs) is measured and attested. + On a general-purpose system without such guarantees it does not provide a meaningful security + boundary on its own: an attacker with sufficient privilege to edit + system.conf, modify the kernel command line, or kexec into an unsigned initrd + can disable or bypass the policy. + + The enforcement hooks block execve() of untrusted binaries + (bprm_check_security), PROT_EXEC memory mappings including + shared libraries (mmap_file), and write-to-execute transitions such as JIT + compilation (file_mprotect). + + Note that execution from overlayfs mounts is blocked even if the underlying layers reside on + signed dm-verity devices, because the BPF program sees the overlay filesystem's anonymous device + number rather than the underlying block device. Multi-device filesystems such as btrfs are similarly + unsupported. + + Note that, without further measures to secure the system, kexec can be used to circumvent this. + + This requires the kernel to be booted with dm_verity.require_signatures=1 + on the kernel command line and with BPF LSM enabled (lsm=...,bpf). If either + prerequisite is not met, PID 1 will refuse to complete startup. + + The value yes is equivalent to exec. Additional + modes may be added in the future. + + This option may also be set via the systemd.restrict_filesystem_access= kernel command + line option, see + kernel-command-line7. + + + + + SystemCallArchitectures= diff --git a/mkosi/mkosi.conf b/mkosi/mkosi.conf index 0fbb81eeed..ca5c061079 100644 --- a/mkosi/mkosi.conf +++ b/mkosi/mkosi.conf @@ -101,6 +101,7 @@ Packages= gzip jq kbd + keyutils kmod less lsof diff --git a/src/bpf/meson.build b/src/bpf/meson.build index 32ab32e93d..13af4c968e 100644 --- a/src/bpf/meson.build +++ b/src/bpf/meson.build @@ -311,6 +311,19 @@ endif conf.set10('HAVE_VMLINUX_H', use_provided_vmlinux_h or use_generated_vmlinux_h) +# 'enum lsm_integrity_type' was added together with the bdev_setintegrity LSM +# hook in kernel commit 2deeb6c333e5 (v6.5). The generated vmlinux.h reflects +# the running kernel's BTF; a provided vmlinux.h can be older, so probe. +have_lsm_integrity_type = false +if use_generated_vmlinux_h + have_lsm_integrity_type = true +elif use_provided_vmlinux_h + have_lsm_integrity_type = cc.compiles( + '#include "@0@"\nenum lsm_integrity_type _t;\n'.format(provided_vmlinux_h_path), + name : 'enum lsm_integrity_type in vmlinux.h') +endif +conf.set10('HAVE_LSM_INTEGRITY_TYPE', have_lsm_integrity_type) + conf.set10('ENABLE_SYSCTL_BPF', conf.get('HAVE_VMLINUX_H') == 1 and libbpf.version().version_compare('>= 0.7')) bpf_programs = [ @@ -318,6 +331,11 @@ bpf_programs = [ 'source' : files('bind-iface.bpf.c'), 'condition' : 'BPF_FRAMEWORK', }, + { + 'source' : files('restrict-fsaccess.bpf.c'), + 'condition' : 'HAVE_LSM_INTEGRITY_TYPE', + 'depends' : vmlinux_h_dependency, + }, { 'source' : files('restrict-fs.bpf.c'), 'condition' : 'BPF_FRAMEWORK', diff --git a/src/bpf/restrict-fsaccess.bpf.c b/src/bpf/restrict-fsaccess.bpf.c new file mode 100644 index 0000000000..13e11aff50 --- /dev/null +++ b/src/bpf/restrict-fsaccess.bpf.c @@ -0,0 +1,300 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* The SPDX header above is actually correct in claiming this was + * LGPL-2.1-or-later, because it is. Since the kernel doesn't consider that + * compatible with GPL we will claim this to be GPL however, which should be + * fine given that LGPL-2.1-or-later downgrades to GPL if needed. + */ + +/* Trusted Execution BPF LSM program. + * + * Enforces that only binaries from signed dm-verity block devices (or the + * initramfs during early boot) can be executed. + * + * Architecture: + * - bdev_setintegrity hook: self-populates a map of trusted devices when + * 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 + */ + +/* If offsetof() is implemented via __builtin_offset() then it doesn't work on current compilers, since the + * built-ins do not understand CO-RE. Let's undefine any such macros here, to force bpf_helpers.h to define + * its own definitions for this. (In new versions it will do so automatically, but at least in libbpf 1.1.0 + * it does not.) */ +#undef offsetof +#undef container_of + +#include "vmlinux.h" + +#include /* IWYU pragma: keep */ +#include +#include +#include + +#define PROT_EXEC 0x4 +#define VM_EXEC 0x00000004 +#define PTRACE_MODE_ATTACH 0x02 + +/* ---- Maps ---- */ + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 0); /* placeholder */ + __type(key, __u32); /* dev_t from bdev->bd_dev */ + __type(value, __u8); /* 1 = signature valid */ +} verity_devices SEC(".maps"); + +/* ---- Globals (set by PID1 via skeleton) ---- */ + +/* Device number of the initramfs superblock. PID1 sets this at load time and + * clears it (to 0) after switch_root. A value of 0 means "no initramfs trust + * — the window is closed." */ +volatile __u32 initramfs_s_dev; + +/* ---- Self-protection guard globals (set by PID1 after attach) ---- + * + * While all IDs are 0 (the .bss default), the guard is inactive — no real BPF + * object has ID 0, so no comparisons match. PID1 populates these after + * attaching all programs. */ +volatile __u32 protected_map_id_verity; +volatile __u32 protected_map_id_bss; + +/* Must equal _RESTRICT_FILESYSTEM_ACCESS_LINK_MAX in bpf-restrict-fsaccess.h — update when adding programs */ +#define NUM_PROTECTED_OBJS 9 /* 5 enforcement + 4 guard (bpf, bpf_map, bpf_prog, ptrace) */ +volatile __u32 protected_prog_ids[NUM_PROTECTED_OBJS]; +volatile __u32 protected_link_ids[NUM_PROTECTED_OBJS]; + +/* ---- Integrity tracking hooks ---- */ + +/* Preferred version: reads both value and size for defense-in-depth. + * Requires kernel v6.16+ or the backport of 1271a40eeafa ("bpf: Allow + * access to const void pointer arguments in tracing programs"). + * On older kernels btf_ctx_access() rejects loads from const void * + * arguments because it fails to skip the CONST modifier when checking + * for void pointers. prepare_restrict_fsaccess_bpf() tries this version + * first and falls back to the _compat variant below if loading fails. */ +SEC("lsm/bdev_setintegrity") +int BPF_PROG(restrict_fsaccess_bdev_setintegrity, struct block_device *bdev, + enum lsm_integrity_type type, const void *value, __u64 size) +{ + if (type == LSM_INT_DMVERITY_SIG_VALID) { + __u32 dev = bdev->bd_dev; + __u8 valid = value && size > 0; + bpf_map_update_elem(&verity_devices, &dev, &valid, BPF_ANY); + } + + return 0; +} + +/* Compatibility version for kernels without 1271a40eeafa: does not + * read the const void *value argument (ctx[2]) to avoid the verifier + * rejection. Reads size (ctx[3]) directly from the raw context instead. + * This is safe because dm-verity guarantees value!=NULL iff size>0. */ +#define BDEV_SETINTEGRITY_SIZE_CTX_IDX 3 /* bdev_setintegrity(bdev, type, value, size) */ +SEC("lsm/bdev_setintegrity") +int BPF_PROG(restrict_fsaccess_bdev_setintegrity_compat, struct block_device *bdev, + enum lsm_integrity_type type) +{ + if (type == LSM_INT_DMVERITY_SIG_VALID) { + __u32 dev = bdev->bd_dev; + __u8 valid = ctx[BDEV_SETINTEGRITY_SIZE_CTX_IDX] > 0; + bpf_map_update_elem(&verity_devices, &dev, &valid, BPF_ANY); + } + + return 0; +} + +SEC("lsm/bdev_free_security") +void BPF_PROG(restrict_fsaccess_bdev_free, struct block_device *bdev) +{ + __u32 dev = bdev->bd_dev; + bpf_map_delete_elem(&verity_devices, &dev); +} + +/* ---- Enforcement helpers ---- */ + +/* Check whether a file is from a trusted source. + * Returns 0 (allow) or -EPERM (deny). */ +static __always_inline int check_trusted_file(struct file *file) +{ + __u32 s_dev; + __u8 *sig_valid; + + BPF_CORE_READ_INTO(&s_dev, file, f_inode, i_sb, s_dev); + + /* Check initramfs trust (active only during early boot) */ + if (initramfs_s_dev != 0 && s_dev == initramfs_s_dev) + return 0; + + /* Check verity device map */ + sig_valid = bpf_map_lookup_elem(&verity_devices, &s_dev); + if (sig_valid && *sig_valid) + return 0; + + return -EPERM; +} + +/* ---- Enforcement hooks ---- */ + +SEC("lsm/bprm_check_security") +int BPF_PROG(restrict_fsaccess_bprm_check, struct linux_binprm *bprm) +{ + struct file *file; + + BPF_CORE_READ_INTO(&file, bprm, file); + return check_trusted_file(file); +} + +SEC("lsm/mmap_file") +int BPF_PROG(restrict_fsaccess_mmap_file, struct file *file, unsigned long reqprot, + unsigned long prot, unsigned long flags) +{ + /* Only enforce on executable mappings */ + if (!(prot & PROT_EXEC)) + return 0; + + /* Anonymous executable mapping — no file backing, deny */ + if (!file) + return -EPERM; + + return check_trusted_file(file); +} + +SEC("lsm/file_mprotect") +int BPF_PROG(restrict_fsaccess_file_mprotect, struct vm_area_struct *vma, + unsigned long reqprot, unsigned long prot) +{ + struct file *file; + unsigned long vm_flags; + + /* 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 */ + BPF_CORE_READ_INTO(&vm_flags, vma, vm_flags); + if (vm_flags & VM_EXEC) + return 0; + + /* Anonymous executable mapping — no file backing, deny */ + BPF_CORE_READ_INTO(&file, vma, vm_file); + if (!file) + return -EPERM; + + return check_trusted_file(file); +} + +/* ---- PID1 ptrace protection ---- + * + * Blocks PTRACE_MODE_ATTACH access to PID1 from any other process. This + * prevents ptrace(PTRACE_ATTACH), /proc/1/mem, process_vm_readv(), and + * pidfd_getfd() from extracting sensitive state from PID1's address space. + * + * PTRACE_MODE_READ is allowed — monitoring tools and systemctl need + * /proc/1/status, /proc/1/fd/, /proc/1/ns/ *, etc. + * + * PID1 accessing itself is allowed. */ + +SEC("lsm/ptrace_access_check") +int BPF_PROG(restrict_fsaccess_ptrace_guard, struct task_struct *child, + unsigned int mode) +{ + /* We only care about PID 1 and its threads (There are none but still.). */ + if (child->tgid != 1) + return 0; + + /* We only care about dangerous operations. */ + if (!(mode & PTRACE_MODE_ATTACH)) + return 0; + + /* PID1 (any thread) accessing itself is allowed. */ + if ((bpf_get_current_pid_tgid() >> 32) == 1) + return 0; + + return -EPERM; +} + +/* ---- Self-protection guard ---- + * + * Three hooks protect our BPF objects from non-PID1 processes: + * + * lsm/bpf_map — fires inside bpf_map_new_fd(), the chokepoint for ALL + * code paths that produce a map FD (BPF_MAP_GET_FD_BY_ID, + * BPF_OBJ_GET, BPF_MAP_CREATE). Blocks the primary attack: + * obtaining an FD to verity_devices to inject fake trusted + * devices via BPF_MAP_UPDATE_ELEM. + * + * lsm/bpf_prog — fires inside bpf_prog_new_fd(), same chokepoint coverage + * for programs. Defense-in-depth. + * + * lsm/bpf — handles BPF_LINK_GET_FD_BY_ID only. There is no + * security_bpf_link() hook in the kernel, so link + * protection uses the command-level bpf() hook. This is + * sufficient: we don't pin links in production, so + * BPF_OBJ_GET is not an attack vector for links. */ + +SEC("lsm/bpf_map") +int BPF_PROG(restrict_fsaccess_bpf_map_guard, struct bpf_map *map, + unsigned int fmode) +{ + __u32 id; + + if ((bpf_get_current_pid_tgid() >> 32) == 1) + return 0; + + id = map->id; + if (id != 0 && (id == protected_map_id_verity || + id == protected_map_id_bss)) + return -EPERM; + + return 0; +} + +SEC("lsm/bpf_prog") +int BPF_PROG(restrict_fsaccess_bpf_prog_guard, struct bpf_prog *prog) +{ + __u32 id; + + if ((bpf_get_current_pid_tgid() >> 32) == 1) + return 0; + + id = BPF_CORE_READ(prog, aux, id); + if (id == 0) + return 0; + + for (int i = 0; i < NUM_PROTECTED_OBJS; i++) + if (id == protected_prog_ids[i]) + return -EPERM; + + return 0; +} + +SEC("lsm/bpf") +int BPF_PROG(restrict_fsaccess_bpf_guard, int cmd, union bpf_attr *attr, + unsigned int size) +{ + __u32 id; + + if ((bpf_get_current_pid_tgid() >> 32) == 1) + return 0; + + if (cmd != BPF_LINK_GET_FD_BY_ID) + return 0; + + /* link_id/map_id/prog_id share the same offset in the bpf_attr union */ + id = attr->link_id; + if (id == 0) + return 0; + + for (int i = 0; i < NUM_PROTECTED_OBJS; i++) + if (id == protected_link_ids[i]) + return -EPERM; + + return 0; +} + +static const char _license[] SEC("license") = "GPL"; diff --git a/src/core/bpf-restrict-fs.c b/src/core/bpf-restrict-fs.c index e60e6ca7ef..d3c6adc145 100644 --- a/src/core/bpf-restrict-fs.c +++ b/src/core/bpf-restrict-fs.c @@ -30,19 +30,6 @@ static struct restrict_fs_bpf *restrict_fs_bpf_free(struct restrict_fs_bpf *obj) DEFINE_TRIVIAL_CLEANUP_FUNC(struct restrict_fs_bpf *, restrict_fs_bpf_free); -static bool bpf_can_link_lsm_program(struct bpf_program *prog) { - _cleanup_(bpf_link_freep) struct bpf_link *link = NULL; - - assert(prog); - - link = sym_bpf_program__attach_lsm(prog); - - /* If bpf_program__attach_lsm fails the resulting value stores libbpf error code instead of memory - * pointer. That is the case when the helper is called on architectures where BPF trampoline (hence - * BPF_LSM_MAC attach type) is not supported. */ - return bpf_get_error_translated(link) == 0; -} - static int prepare_restrict_fs_bpf(struct restrict_fs_bpf **ret_obj) { _cleanup_(restrict_fs_bpf_freep) struct restrict_fs_bpf *obj = NULL; _cleanup_close_ int inner_map_fd = -EBADF; diff --git a/src/core/bpf-restrict-fsaccess.c b/src/core/bpf-restrict-fsaccess.c new file mode 100644 index 0000000000..be66f7da01 --- /dev/null +++ b/src/core/bpf-restrict-fsaccess.c @@ -0,0 +1,562 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include + +#include "bpf-restrict-fsaccess.h" +#include "fd-util.h" +#include "fileio.h" +#include "initrd-util.h" +#include "log.h" +#include "lsm-util.h" +#include "manager.h" +#include "memory-util.h" +#include "serialize.h" +#include "string-table.h" + +/* DMVERITY_DEVICES_MAX lives in bpf-restrict-fsaccess.h for sharing with tests. */ + +static const char* const restrict_filesystem_access_table[_RESTRICT_FILESYSTEM_ACCESS_MAX] = { + [RESTRICT_FILESYSTEM_ACCESS_NO] = "no", + [RESTRICT_FILESYSTEM_ACCESS_EXEC] = "exec", +}; + +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(restrict_filesystem_access, RestrictFileSystemAccess, RESTRICT_FILESYSTEM_ACCESS_EXEC); + +const char* const restrict_fsaccess_link_names[_RESTRICT_FILESYSTEM_ACCESS_LINK_MAX] = { + [RESTRICT_FILESYSTEM_ACCESS_LINK_BDEV_SETINTEGRITY] = "restrict-fsaccess-bdev-setintegrity-link", + [RESTRICT_FILESYSTEM_ACCESS_LINK_BDEV_FREE] = "restrict-fsaccess-bdev-free-link", + [RESTRICT_FILESYSTEM_ACCESS_LINK_BPRM_CHECK] = "restrict-fsaccess-bprm-check-link", + [RESTRICT_FILESYSTEM_ACCESS_LINK_MMAP_FILE] = "restrict-fsaccess-mmap-file-link", + [RESTRICT_FILESYSTEM_ACCESS_LINK_FILE_MPROTECT] = "restrict-fsaccess-file-mprotect-link", + [RESTRICT_FILESYSTEM_ACCESS_LINK_PTRACE_GUARD] = "restrict-fsaccess-ptrace-guard-link", + [RESTRICT_FILESYSTEM_ACCESS_LINK_BPF_MAP_GUARD] = "restrict-fsaccess-bpf-map-guard-link", + [RESTRICT_FILESYSTEM_ACCESS_LINK_BPF_PROG_GUARD] = "restrict-fsaccess-bpf-prog-guard-link", + [RESTRICT_FILESYSTEM_ACCESS_LINK_BPF_GUARD] = "restrict-fsaccess-bpf-guard-link", +}; + +#if BPF_FRAMEWORK && HAVE_LSM_INTEGRITY_TYPE +#include "bpf-dlopen.h" +#include "bpf-link.h" +#include "restrict-fsaccess-skel.h" + +static struct restrict_fsaccess_bpf *restrict_fsaccess_bpf_free(struct restrict_fsaccess_bpf *obj) { + restrict_fsaccess_bpf__destroy(obj); + return NULL; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct restrict_fsaccess_bpf *, restrict_fsaccess_bpf_free); + +/* Verify that restrict_fsaccess_bss matches the skeleton's .bss layout. The sizeof + * check catches field additions/removals; the offsetof checks catch field + * reordering. Field order in restrict_fsaccess_bss must match the BPF global + * declaration order in restrict-fsaccess.bpf.c — this is what bpftool uses for the + * generated struct. The read-modify-write in restrict_fsaccess_clear_initramfs_trust() + * depends on this layout. */ +assert_cc(sizeof(struct restrict_fsaccess_bss) == sizeof_field(struct restrict_fsaccess_bpf, bss[0])); +assert_cc(offsetof(struct restrict_fsaccess_bss, initramfs_s_dev) == + offsetof(typeof_field(struct restrict_fsaccess_bpf, bss[0]), initramfs_s_dev)); +assert_cc(offsetof(struct restrict_fsaccess_bss, protected_map_id_verity) == + offsetof(typeof_field(struct restrict_fsaccess_bpf, bss[0]), protected_map_id_verity)); +assert_cc(offsetof(struct restrict_fsaccess_bss, protected_map_id_bss) == + offsetof(typeof_field(struct restrict_fsaccess_bpf, bss[0]), protected_map_id_bss)); + +/* Build the skeleton links array indexed by the link enum. + * For BDEV_SETINTEGRITY, use whichever variant was loaded (full or compat). + * This compat logic can be removed once the kernel baseline includes + * 1271a40eeafa ("bpf: Allow access to const void pointer arguments"). */ +#define RESTRICT_FSACCESS_LINKS(obj) { \ + [RESTRICT_FILESYSTEM_ACCESS_LINK_BDEV_SETINTEGRITY] = (obj)->links.restrict_fsaccess_bdev_setintegrity ?: \ + (obj)->links.restrict_fsaccess_bdev_setintegrity_compat, \ + [RESTRICT_FILESYSTEM_ACCESS_LINK_BDEV_FREE] = (obj)->links.restrict_fsaccess_bdev_free, \ + [RESTRICT_FILESYSTEM_ACCESS_LINK_BPRM_CHECK] = (obj)->links.restrict_fsaccess_bprm_check, \ + [RESTRICT_FILESYSTEM_ACCESS_LINK_MMAP_FILE] = (obj)->links.restrict_fsaccess_mmap_file, \ + [RESTRICT_FILESYSTEM_ACCESS_LINK_FILE_MPROTECT] = (obj)->links.restrict_fsaccess_file_mprotect, \ + [RESTRICT_FILESYSTEM_ACCESS_LINK_PTRACE_GUARD] = (obj)->links.restrict_fsaccess_ptrace_guard, \ + [RESTRICT_FILESYSTEM_ACCESS_LINK_BPF_MAP_GUARD] = (obj)->links.restrict_fsaccess_bpf_map_guard, \ + [RESTRICT_FILESYSTEM_ACCESS_LINK_BPF_PROG_GUARD] = (obj)->links.restrict_fsaccess_bpf_prog_guard, \ + [RESTRICT_FILESYSTEM_ACCESS_LINK_BPF_GUARD] = (obj)->links.restrict_fsaccess_bpf_guard, \ +} + +bool dm_verity_require_signatures(void) { + int r; + + r = read_boolean_file("/sys/module/dm_verity/parameters/require_signatures"); + if (r < 0) { + if (r != -ENOENT) + log_warning_errno(r, "bpf-restrict-fsaccess: Failed to read dm-verity require_signatures: %m"); + return false; + } + + return r > 0; +} + +static int get_root_s_dev(uint32_t *ret) { + struct stat st; + + assert(ret); + + /* Stat /usr/ rather than / — executable code lives in /usr/ and we push toward + * a writable non-executable /. On systems with a separate /usr partition this + * means / is intentionally not trusted. */ + if (stat("/usr/", &st) < 0) + return log_error_errno(errno, "bpf-restrict-fsaccess: Failed to stat /usr/ filesystem: %m"); + + *ret = STAT_DEV_TO_KERNEL(st.st_dev); + return 0; +} + +int bpf_restrict_fsaccess_prepare(struct restrict_fsaccess_bpf **ret) { + _cleanup_(restrict_fsaccess_bpf_freep) struct restrict_fsaccess_bpf *obj = NULL; + int r; + + assert(ret); + + /* Try the preferred version first — it reads the const void *value + * argument for defense-in-depth. On kernels before v6.16 (missing + * 1271a40eeafa) the verifier rejects loads from const void * context + * arguments, so we fall back to the _compat variant that only reads + * the size argument via raw ctx access. */ + obj = restrict_fsaccess_bpf__open(); + if (!obj) + return log_error_errno(errno, "bpf-restrict-fsaccess: Failed to open BPF object: %m"); + + r = sym_bpf_map__set_max_entries(obj->maps.verity_devices, DMVERITY_DEVICES_MAX); + if (r < 0) + return log_error_errno(r, "bpf-restrict-fsaccess: Failed to size hash table: %m"); + + r = sym_bpf_program__set_autoload(obj->progs.restrict_fsaccess_bdev_setintegrity_compat, false); + if (r < 0) + return log_error_errno(r, "bpf-restrict-fsaccess: Failed to disable compat program: %m"); + + r = restrict_fsaccess_bpf__load(obj); + if (r >= 0) { + log_debug("bpf-restrict-fsaccess: Loaded with full const void * access."); + *ret = TAKE_PTR(obj); + return 0; + } + + log_debug_errno(r, "bpf-restrict-fsaccess: Full version failed to load (%m), trying compat variant."); + obj = restrict_fsaccess_bpf_free(obj); + + obj = restrict_fsaccess_bpf__open(); + if (!obj) + return log_error_errno(errno, "bpf-restrict-fsaccess: Failed to reopen BPF object: %m"); + + r = sym_bpf_map__set_max_entries(obj->maps.verity_devices, DMVERITY_DEVICES_MAX); + if (r < 0) + return log_error_errno(r, "bpf-restrict-fsaccess: Failed to size hash table: %m"); + + r = sym_bpf_program__set_autoload(obj->progs.restrict_fsaccess_bdev_setintegrity, false); + if (r < 0) + return log_error_errno(r, "bpf-restrict-fsaccess: Failed to disable full program: %m"); + + r = restrict_fsaccess_bpf__load(obj); + if (r < 0) + return log_error_errno(r, "bpf-restrict-fsaccess: Failed to load BPF object (compat): %m"); + + log_debug("bpf-restrict-fsaccess: Loaded with compat bdev_setintegrity."); + *ret = TAKE_PTR(obj); + return 0; +} + +bool bpf_restrict_fsaccess_supported(void) { + _cleanup_(restrict_fsaccess_bpf_freep) struct restrict_fsaccess_bpf *obj = NULL; + static int supported = -1; + int r; + + if (supported >= 0) + return supported; + if (dlopen_bpf(LOG_WARNING) < 0) + return (supported = false); + + r = lsm_supported("bpf"); + if (r == -ENOPKG) { + log_debug_errno(r, "bpf-restrict-fsaccess: securityfs not mounted, BPF LSM not available."); + return (supported = false); + } + if (r < 0) { + log_warning_errno(r, "bpf-restrict-fsaccess: Can't determine whether the BPF LSM module is used: %m"); + return (supported = false); + } + if (r == 0) { + log_info("bpf-restrict-fsaccess: BPF LSM hook not enabled in the kernel, not supported."); + return (supported = false); + } + + r = bpf_restrict_fsaccess_prepare(&obj); + if (r < 0) + return (supported = false); + + if (!bpf_can_link_lsm_program(obj->progs.restrict_fsaccess_bprm_check)) { + log_warning("bpf-restrict-fsaccess: Failed to link program; assuming BPF LSM is not available."); + return (supported = false); + } + + return (supported = true); +} + +/* Partial deserialization (some FDs but not all) is fatal: continuing + * would leave enforcement incomplete. */ +static int restrict_fsaccess_have_deserialized_fds(Manager *m) { + size_t count = 0; + + assert(m); + + FOREACH_ELEMENT(fd, m->restrict_fsaccess_link_fds) + if (*fd >= 0) + count++; + + if (count == 0) + return 0; + if (count == ELEMENTSOF(m->restrict_fsaccess_link_fds)) + return 1; + + return log_error_errno(SYNTHETIC_ERRNO(EBADFD), + "bpf-restrict-fsaccess: Only %zu of %zu link FDs deserialized, refusing to continue with partial enforcement.", + count, ELEMENTSOF(m->restrict_fsaccess_link_fds)); +} + +/* Close the initramfs trust window after switch_root by clearing initramfs_s_dev + * in the BPF .bss map. The .bss is a BPF_F_MMAPABLE array map — mmap it and do + * a single aligned 4-byte store instead of a full-value read-modify-write via + * bpf_map_update_elem, which would needlessly rewrite the guard globals too. */ +static int restrict_fsaccess_clear_initramfs_trust(int bss_map_fd) { + void *p; + + assert(bss_map_fd >= 0); + assert_cc(offsetof(struct restrict_fsaccess_bss, initramfs_s_dev) == 0); + + p = mmap(NULL, page_size(), PROT_READ | PROT_WRITE, MAP_SHARED, bss_map_fd, 0); + if (p == MAP_FAILED) + return log_error_errno(errno, "bpf-restrict-fsaccess: Failed to mmap .bss map: %m"); + + /* initramfs_s_dev is at offset 0 in the .bss layout. Single aligned + * 32-bit store is atomic — BPF programs see either the old or new value, + * no torn reads possible. Guard globals are untouched. */ + *(uint32_t *) p = 0; + + /* munmap failure here is harmless: the clear above already landed in + * the kernel, and the mapping is discarded by exec anyway. */ + if (munmap(p, page_size()) < 0) + log_warning_errno(errno, "bpf-restrict-fsaccess: Failed to munmap .bss map, ignoring: %m"); + + log_info("bpf-restrict-fsaccess: Cleared initramfs trust window after switch_root."); + return 0; +} + +static int bpf_get_map_id(int fd, uint32_t *ret_id) { + struct bpf_map_info info = {}; + uint32_t len = sizeof(info); + int r; + + if (fd < 0) + return -EBADF; + + assert(ret_id); + + r = sym_bpf_obj_get_info_by_fd(fd, &info, &len); + if (r < 0) + return r; + + *ret_id = info.id; + return 0; +} + +static int bpf_get_link_ids(int fd, uint32_t *ret_link_id, uint32_t *ret_prog_id) { + struct bpf_link_info info = {}; + uint32_t len = sizeof(info); + int r; + + if (fd < 0) + return -EBADF; + + r = sym_bpf_obj_get_info_by_fd(fd, &info, &len); + if (r < 0) + return r; + + if (ret_link_id) + *ret_link_id = info.id; + if (ret_prog_id) + *ret_prog_id = info.prog_id; + + return 0; +} + +/* Populate guard globals with kernel-assigned IDs so the guard hooks block + * non-PID1 access to our maps/progs/links via the bpf() syscall. */ +int bpf_restrict_fsaccess_populate_guard(struct restrict_fsaccess_bpf *obj) { + int r; + + assert(obj); + + struct bpf_link *links[] = RESTRICT_FSACCESS_LINKS(obj); + assert_cc(ELEMENTSOF(links) == _RESTRICT_FILESYSTEM_ACCESS_LINK_MAX); + + /* Map IDs */ + r = bpf_get_map_id(sym_bpf_map__fd(obj->maps.verity_devices), &obj->bss->protected_map_id_verity); + if (r < 0) + return log_error_errno(r, "bpf-restrict-fsaccess: Failed to get verity_devices map ID: %m"); + + r = bpf_get_map_id(sym_bpf_map__fd(obj->maps.bss), &obj->bss->protected_map_id_bss); + if (r < 0) + return log_error_errno(r, "bpf-restrict-fsaccess: Failed to get .bss map ID: %m"); + + /* Link and program IDs (each link knows its associated program) */ + FOREACH_ELEMENT(link, links) { + size_t idx = link - links; + + /* BDEV_SETINTEGRITY slot resolves via ?: between full and compat + * variants; assert at least one was attached. */ + if (!*link) + return log_error_errno(SYNTHETIC_ERRNO(ENODATA), + "bpf-restrict-fsaccess: %s link missing after attach.", + restrict_fsaccess_link_names[idx]); + + r = bpf_get_link_ids(sym_bpf_link__fd(*link), + &obj->bss->protected_link_ids[idx], + &obj->bss->protected_prog_ids[idx]); + if (r < 0) + return log_error_errno(r, "bpf-restrict-fsaccess: Failed to get link/prog IDs for %s: %m", + restrict_fsaccess_link_names[idx]); + } + + log_info("bpf-restrict-fsaccess: Guard globals populated (verity_map=%u, bss_map=%u)", + (unsigned) obj->bss->protected_map_id_verity, + (unsigned) obj->bss->protected_map_id_bss); + return 0; +} + +/* Validate that deserialized FDs actually reference our LSM BPF links. A + * corrupted serialization file could leave FDs pointing at arbitrary kernel + * objects; a stale FD could point at a BPF link of an entirely different type + * (e.g. kprobe-multi). Verify both link type and attach type so a substituted + * FD that happens to be a BPF link still fails the check. */ +static int restrict_fsaccess_validate_deserialized_fds(Manager *m) { + int r; + + assert(m); + + r = dlopen_bpf(LOG_WARNING); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "bpf-restrict-fsaccess: Failed to load libbpf for FD validation, aborting."); + + FOREACH_ELEMENT(fd, m->restrict_fsaccess_link_fds) { + struct bpf_link_info info = {}; + uint32_t len = sizeof(info); + const char *name = restrict_fsaccess_link_names[fd - m->restrict_fsaccess_link_fds]; + + r = sym_bpf_obj_get_info_by_fd(*fd, &info, &len); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "bpf-restrict-fsaccess: Deserialized FD for %s is not a valid BPF object, aborting.", + name); + + if (info.type != BPF_LINK_TYPE_TRACING || info.tracing.attach_type != BPF_LSM_MAC) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "bpf-restrict-fsaccess: Deserialized FD for %s is not an LSM tracing link (type=%u attach=%u), aborting.", + name, info.type, info.tracing.attach_type); + } + + if (m->restrict_fsaccess_bss_map_fd >= 0) { + uint32_t id; + + r = bpf_get_map_id(m->restrict_fsaccess_bss_map_fd, &id); + if (r < 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "bpf-restrict-fsaccess: Deserialized FD for .bss map is not a valid BPF map, aborting."); + } + + return 0; +} + +int bpf_restrict_fsaccess_setup(Manager *m) { + _cleanup_(restrict_fsaccess_bpf_freep) struct restrict_fsaccess_bpf *obj = NULL; + int r; + + assert(m); + + if (!MANAGER_IS_SYSTEM(m) || m->restrict_filesystem_access <= RESTRICT_FILESYSTEM_ACCESS_NO) + return 0; + + r = restrict_fsaccess_have_deserialized_fds(m); + if (r < 0) + return r; + if (r > 0) { + log_info("bpf-restrict-fsaccess: Recovered link FDs from previous exec, programs still attached."); + + r = restrict_fsaccess_validate_deserialized_fds(m); + if (r < 0) + return r; + if (m->switching_root) { + if (m->restrict_fsaccess_bss_map_fd < 0) + return log_error_errno(SYNTHETIC_ERRNO(EBADF), + "bpf-restrict-fsaccess: Cannot clear initramfs trust after switch_root."); + r = restrict_fsaccess_clear_initramfs_trust(m->restrict_fsaccess_bss_map_fd); + if (r < 0) + return r; + } + + return 0; + } + + /* Fresh setup: verify BPF LSM is available */ + if (!bpf_restrict_fsaccess_supported()) + return log_warning_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "bpf-restrict-fsaccess: BPF LSM is not available."); + + /* 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."); + + r = bpf_restrict_fsaccess_prepare(&obj); + if (r < 0) + return r; + + /* If we're still in the initramfs, allow execution from it by recording + * its s_dev. After switch_root, PID1 re-execs and in_initrd() returns + * false — initramfs_s_dev stays at 0 (its default), closing the trust + * window. */ + if (in_initrd()) { + uint32_t root_dev; + + r = get_root_s_dev(&root_dev); + if (r < 0) + return r; + + obj->bss->initramfs_s_dev = root_dev; + log_info("bpf-restrict-fsaccess: Initramfs trusted (s_dev=%" PRIu32 ":%" PRIu32 ")", + root_dev >> 20, root_dev & 0xFFFFF); + } + + r = restrict_fsaccess_bpf__attach(obj); + if (r < 0) + return log_error_errno(r, "bpf-restrict-fsaccess: Failed to attach BPF programs: %m"); + + log_info("bpf-restrict-fsaccess: LSM BPF programs attached"); + + /* Now that all programs are attached, populate the guard's globals with + * the kernel-assigned IDs of our maps, programs, and links. From this + * point on, non-PID1 processes cannot obtain FDs to our BPF objects. */ + r = bpf_restrict_fsaccess_populate_guard(obj); + if (r < 0) + return r; + + /* Extract owned FDs from the skeleton. These keep the kernel BPF objects + * alive after the skeleton is destroyed. Destroying the skeleton unmaps + * the .bss page from our address space so no BPF state (guard globals, + * map IDs, initramfs_s_dev) is reachable via /proc/1/mem. */ + struct bpf_link *links[] = RESTRICT_FSACCESS_LINKS(obj); + FOREACH_ELEMENT(link, links) { + size_t idx = link - links; + + if (!*link) { + r = log_error_errno(SYNTHETIC_ERRNO(ENODATA), + "bpf-restrict-fsaccess: %s link missing after attach.", + restrict_fsaccess_link_names[idx]); + goto fail; + } + + m->restrict_fsaccess_link_fds[idx] = fcntl(sym_bpf_link__fd(*link), F_DUPFD_CLOEXEC, 3); + if (m->restrict_fsaccess_link_fds[idx] < 0) { + r = log_error_errno(errno, "bpf-restrict-fsaccess: Failed to dup link FD for %s: %m", + restrict_fsaccess_link_names[idx]); + goto fail; + } + } + + m->restrict_fsaccess_bss_map_fd = fcntl(sym_bpf_map__fd(obj->maps.bss), F_DUPFD_CLOEXEC, 3); + if (m->restrict_fsaccess_bss_map_fd < 0) { + r = log_error_errno(errno, "bpf-restrict-fsaccess: Failed to dup .bss map FD: %m"); + goto fail; + } + + return 0; + +fail: + /* Close partial FDs so we don't leave a half-baked policy attached + * once the skeleton is destroyed by _cleanup_. */ + FOREACH_ELEMENT(fd, m->restrict_fsaccess_link_fds) + *fd = safe_close(*fd); + m->restrict_fsaccess_bss_map_fd = safe_close(m->restrict_fsaccess_bss_map_fd); + return r; +} + +int bpf_restrict_fsaccess_close_initramfs_trust(Manager *m) { + assert(m); + + /* Clear initramfs_s_dev in the BPF .bss map BEFORE switch_root unmounts + * the initramfs. This eliminates the dev_t recycling window: the anonymous + * dev_t is still held by the mounted initramfs superblock, so no other + * filesystem can recycle it yet. Anonymous dev_t recycling is immediate + * and lowest-first, so a stale initramfs_s_dev is a near-certain trust + * bypass — fail closed. */ + if (!in_initrd() || m->restrict_fsaccess_bss_map_fd < 0) + return 0; + + return restrict_fsaccess_clear_initramfs_trust(m->restrict_fsaccess_bss_map_fd); +} + +int bpf_restrict_fsaccess_serialize(Manager *m, FILE *f, FDSet *fds) { + int r; + + assert(m); + assert(f); + assert(fds); + + if (!MANAGER_IS_SYSTEM(m) || m->restrict_filesystem_access <= RESTRICT_FILESYSTEM_ACCESS_NO) + return 0; + + FOREACH_ELEMENT(fd, m->restrict_fsaccess_link_fds) { + r = serialize_fd(f, fds, restrict_fsaccess_link_names[fd - m->restrict_fsaccess_link_fds], *fd); + if (r < 0) + return r; + } + + r = serialize_fd(f, fds, "restrict-fsaccess-bss-map", m->restrict_fsaccess_bss_map_fd); + if (r < 0) + return r; + + return 0; +} + +#else /* ! BPF_FRAMEWORK || ! HAVE_LSM_INTEGRITY_TYPE */ + +bool dm_verity_require_signatures(void) { + return false; +} + +bool bpf_restrict_fsaccess_supported(void) { + return false; +} + +int bpf_restrict_fsaccess_setup(Manager *m) { + if (!MANAGER_IS_SYSTEM(m) || m->restrict_filesystem_access <= RESTRICT_FILESYSTEM_ACCESS_NO) + return 0; + + return log_warning_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "bpf-restrict-fsaccess: RestrictFileSystemAccess= requested but BPF framework is not compiled in."); +} + +int bpf_restrict_fsaccess_prepare(struct restrict_fsaccess_bpf **ret) { + return -EOPNOTSUPP; +} + +int bpf_restrict_fsaccess_populate_guard(struct restrict_fsaccess_bpf *obj) { + return 0; +} + +int bpf_restrict_fsaccess_close_initramfs_trust(Manager *m) { + return 0; +} + +int bpf_restrict_fsaccess_serialize(Manager *m, FILE *f, FDSet *fds) { + return 0; +} + +#endif diff --git a/src/core/bpf-restrict-fsaccess.h b/src/core/bpf-restrict-fsaccess.h new file mode 100644 index 0000000000..a39f602539 --- /dev/null +++ b/src/core/bpf-restrict-fsaccess.h @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "core-forward.h" +#include "macro.h" +#include "shared-forward.h" + +typedef enum RestrictFileSystemAccess { + RESTRICT_FILESYSTEM_ACCESS_NO, + RESTRICT_FILESYSTEM_ACCESS_EXEC, + _RESTRICT_FILESYSTEM_ACCESS_MAX, + _RESTRICT_FILESYSTEM_ACCESS_INVALID = -EINVAL, +} RestrictFileSystemAccess; + +const char* restrict_filesystem_access_to_string(RestrictFileSystemAccess i) _const_; +RestrictFileSystemAccess restrict_filesystem_access_from_string(const char *s) _pure_; + +enum { + RESTRICT_FILESYSTEM_ACCESS_LINK_BDEV_SETINTEGRITY, + RESTRICT_FILESYSTEM_ACCESS_LINK_BDEV_FREE, + RESTRICT_FILESYSTEM_ACCESS_LINK_BPRM_CHECK, + RESTRICT_FILESYSTEM_ACCESS_LINK_MMAP_FILE, + RESTRICT_FILESYSTEM_ACCESS_LINK_FILE_MPROTECT, + RESTRICT_FILESYSTEM_ACCESS_LINK_PTRACE_GUARD, + RESTRICT_FILESYSTEM_ACCESS_LINK_BPF_MAP_GUARD, + RESTRICT_FILESYSTEM_ACCESS_LINK_BPF_PROG_GUARD, + RESTRICT_FILESYSTEM_ACCESS_LINK_BPF_GUARD, + _RESTRICT_FILESYSTEM_ACCESS_LINK_MAX, +}; + +/* Maximum number of dm-verity devices tracked in the BPF hash map. */ +#define DMVERITY_DEVICES_MAX (16U*1024U) + +/* Convert userspace dev_t (from stat()) to kernel dev_t encoding (MKDEV). + * stat() returns new_encode_dev(s_dev); the BPF program reads s_dev directly + * which uses MKDEV(major, minor) = (major << 20) | minor. */ +#define STAT_DEV_TO_KERNEL(dev) \ + ((uint32_t)major(dev) << 20 | (uint32_t)minor(dev)) + +/* Mirrors the BPF program's .bss section layout for read-modify-write via + * bpf_map_lookup_elem/bpf_map_update_elem on the serialized .bss map FD. */ +struct restrict_fsaccess_bss { + uint32_t initramfs_s_dev; /* kernel dev_t encoding: (major << 20) | minor */ + uint32_t protected_map_id_verity; + uint32_t protected_map_id_bss; + uint32_t protected_prog_ids[_RESTRICT_FILESYSTEM_ACCESS_LINK_MAX]; + uint32_t protected_link_ids[_RESTRICT_FILESYSTEM_ACCESS_LINK_MAX]; +}; + +extern const char* const restrict_fsaccess_link_names[_RESTRICT_FILESYSTEM_ACCESS_LINK_MAX]; + +bool dm_verity_require_signatures(void); +bool bpf_restrict_fsaccess_supported(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); + +int bpf_restrict_fsaccess_close_initramfs_trust(Manager *m); +int bpf_restrict_fsaccess_serialize(Manager *m, FILE *f, FDSet *fds); diff --git a/src/core/core-forward.h b/src/core/core-forward.h index 446cf501e4..14bcc142a2 100644 --- a/src/core/core-forward.h +++ b/src/core/core-forward.h @@ -52,3 +52,4 @@ typedef struct Unit Unit; typedef struct UnitRef UnitRef; struct restrict_fs_bpf; +struct restrict_fsaccess_bpf; diff --git a/src/core/main.c b/src/core/main.c index 3bdce441a8..c10df7d87a 100644 --- a/src/core/main.c +++ b/src/core/main.c @@ -22,6 +22,7 @@ #include "apparmor-setup.h" #include "architecture.h" #include "argv-util.h" +#include "bpf-restrict-fsaccess.h" #include "build.h" #include "bus-error.h" #include "capability-util.h" @@ -150,6 +151,7 @@ static char **arg_manager_environment; static uint64_t arg_capability_bounding_set; static bool arg_no_new_privs; static int arg_protect_system; +static RestrictFileSystemAccess arg_restrict_filesystem_access; static nsec_t arg_timer_slack_nsec; static Set* arg_syscall_archs; static FILE* arg_serialization; @@ -566,6 +568,17 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat return 0; } + } else if (proc_cmdline_key_streq(key, "systemd.restrict_filesystem_access")) { + + if (value) { + r = restrict_filesystem_access_from_string(value); + if (r < 0) + log_warning_errno(r, "Failed to parse systemd.restrict_filesystem_access= argument '%s', ignoring: %m", value); + else + arg_restrict_filesystem_access = r; + } else + arg_restrict_filesystem_access = RESTRICT_FILESYSTEM_ACCESS_EXEC; + } else if (streq(key, "quiet") && !value) { if (arg_show_status == _SHOW_STATUS_INVALID) @@ -717,6 +730,29 @@ static int config_parse_protect_system_pid1( return 0; } +static int config_parse_restrict_filesystem_access( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + RestrictFileSystemAccess *v = ASSERT_PTR(data); + RestrictFileSystemAccess re; + + re = restrict_filesystem_access_from_string(rvalue); + if (re < 0) + return log_syntax_parse_error(unit, filename, line, re, lvalue, rvalue); + + *v = re; + return 0; +} + static int config_parse_crash_reboot( const char *unit, const char *filename, @@ -774,6 +810,7 @@ static int parse_config_file(void) { { "Manager", "CapabilityBoundingSet", config_parse_capability_set, 0, &arg_capability_bounding_set }, { "Manager", "NoNewPrivileges", config_parse_bool, 0, &arg_no_new_privs }, { "Manager", "ProtectSystem", config_parse_protect_system_pid1, 0, &arg_protect_system }, + { "Manager", "RestrictFileSystemAccess", config_parse_restrict_filesystem_access, 0, &arg_restrict_filesystem_access }, #if HAVE_SECCOMP { "Manager", "SystemCallArchitectures", config_parse_syscall_archs, 0, &arg_syscall_archs }, #else @@ -925,6 +962,7 @@ static void set_manager_settings(Manager *m) { manager_set_show_status(m, arg_show_status, "command line"); m->status_unit_format = arg_status_unit_format; + m->restrict_filesystem_access = arg_restrict_filesystem_access; } static int parse_argv(int argc, char *argv[]) { @@ -1247,6 +1285,16 @@ static int prepare_reexecute( m->n_reloading++; bus_manager_send_reloading(m, true); + /* Only close the initramfs trust window when actually switching root. + * During a plain daemon-reexec in the initrd, PID1 still needs to + * execv() itself from the initramfs — clearing trust here would cause + * the BPF bprm_check_security hook to deny the exec. */ + if (switching_root) { + r = bpf_restrict_fsaccess_close_initramfs_trust(m); + if (r < 0) + return r; + } + r = manager_open_serialization(m, &f); if (r < 0) return log_error_errno(r, "Failed to create serialization file: %m"); @@ -2834,6 +2882,7 @@ static void reset_arguments(void) { arg_capability_bounding_set = CAP_MASK_ALL; arg_no_new_privs = false; arg_protect_system = -1; + arg_restrict_filesystem_access = RESTRICT_FILESYSTEM_ACCESS_NO; arg_timer_slack_nsec = NSEC_INFINITY; arg_syscall_archs = set_free(arg_syscall_archs); diff --git a/src/core/manager-serialize.c b/src/core/manager-serialize.c index c794888164..528540e57e 100644 --- a/src/core/manager-serialize.c +++ b/src/core/manager-serialize.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "alloc-util.h" +#include "bpf-restrict-fsaccess.h" #include "dbus.h" #include "dynamic-user.h" #include "fd-util.h" @@ -180,6 +181,10 @@ int manager_serialize( if (r < 0) return r; + r = bpf_restrict_fsaccess_serialize(m, f, fds); + if (r < 0) + return r; + (void) fputc('\n', f); HASHMAP_FOREACH_KEY(u, t, m->units) { @@ -386,6 +391,38 @@ static void manager_deserialize_gid_refs_one(Manager *m, const char *value) { manager_deserialize_uid_refs_one_internal(&m->gid_refs, value); } +static void deserialize_restrict_fsaccess(Manager *m, const char *l, FDSet *fds) { + const char *val; + int fd; + + FOREACH_ELEMENT(name, restrict_fsaccess_link_names) { + val = startswith(l, *name); + if (!val) + continue; + val = startswith(val, "="); + if (!val) + continue; + fd = deserialize_fd(fds, val); + if (fd < 0) { + log_warning_errno(fd, "bpf-restrict-fsaccess: Failed to deserialize FD for %s: %m", *name); + return; + } + close_and_replace(m->restrict_fsaccess_link_fds[name - restrict_fsaccess_link_names], fd); + return; + } + + val = startswith(l, "restrict-fsaccess-bss-map="); + if (!val) + return; + + fd = deserialize_fd(fds, val); + if (fd < 0) { + log_warning_errno(fd, "bpf-restrict-fsaccess: Failed to deserialize FD for .bss map: %m"); + return; + } + close_and_replace(m->restrict_fsaccess_bss_map_fd, fd); +} + int manager_deserialize(Manager *m, FILE *f, FDSet *fds) { int r; @@ -616,7 +653,9 @@ int manager_deserialize(Manager *m, FILE *f, FDSet *fds) { else (void) varlink_server_deserialize_one(m->varlink_server, val, fds); - } else if ((val = startswith(l, "dump-ratelimit="))) + } else if (startswith(l, "restrict-fsaccess-")) + deserialize_restrict_fsaccess(m, l, fds); + else if ((val = startswith(l, "dump-ratelimit="))) deserialize_ratelimit(&m->dump_ratelimit, "dump-ratelimit", val); else if ((val = startswith(l, "reload-reexec-ratelimit="))) deserialize_ratelimit(&m->reload_reexec_ratelimit, "reload-reexec-ratelimit", val); diff --git a/src/core/manager.c b/src/core/manager.c index da4e9ca408..0e33273c3d 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -21,6 +21,7 @@ #include "audit-fd.h" #include "boot-timestamps.h" #include "bpf-restrict-fs.h" +#include "bpf-restrict-fsaccess.h" #include "build-path.h" #include "bus-common-errors.h" #include "bus-error.h" @@ -941,8 +942,13 @@ int manager_new(RuntimeScope runtime_scope, ManagerTestRunFlags test_run_flags, .dump_ratelimit = (const RateLimit) { .interval = 10 * USEC_PER_MINUTE, .burst = 10 }, .executor_fd = -EBADF, + + .restrict_fsaccess_bss_map_fd = -EBADF, }; + FOREACH_ELEMENT(fd, m->restrict_fsaccess_link_fds) + *fd = -EBADF; + unit_defaults_init(&m->defaults, runtime_scope); #if ENABLE_EFI @@ -1784,6 +1790,8 @@ Manager* manager_free(Manager *m) { #if BPF_FRAMEWORK bpf_restrict_fs_destroy(m->restrict_fs); #endif + close_many(m->restrict_fsaccess_link_fds, ELEMENTSOF(m->restrict_fsaccess_link_fds)); + safe_close(m->restrict_fsaccess_bss_map_fd); safe_close(m->executor_fd); free(m->executor_path); @@ -2140,6 +2148,13 @@ int manager_startup(Manager *m, FILE *serialization, FDSet *fds, const char *roo m->send_reloading_done = true; } + /* Set up RestrictFileSystemAccess= BPF LSM after deserialization (so we can detect deserialized link FDs) + * and before clearing switching_root (so we can close the initramfs trust window). This must + * run after set_manager_settings() has set m->restrict_filesystem_access. */ + r = bpf_restrict_fsaccess_setup(m); + if (r < 0) + return r; + manager_ready(m); manager_set_switching_root(m, false); diff --git a/src/core/manager.h b/src/core/manager.h index 9afc70b39a..fb65705d32 100644 --- a/src/core/manager.h +++ b/src/core/manager.h @@ -3,6 +3,7 @@ #include "sd-event.h" +#include "bpf-restrict-fsaccess.h" #include "cgroup.h" #include "common-signal.h" #include "execute.h" @@ -479,6 +480,16 @@ typedef struct Manager { /* Reference to RestrictFileSystems= BPF program */ struct restrict_fs_bpf *restrict_fs; + /* Reference to RestrictFileSystemAccess= BPF LSM program */ + RestrictFileSystemAccess restrict_filesystem_access; + + /* Raw BPF FDs extracted from the skeleton after attach. The kernel + * reference chain (link FD -> bpf_link -> bpf_prog -> bpf_map) keeps + * programs attached and map data alive. The .bss map FD is used for + * targeted writes (clearing initramfs_s_dev after switch_root). */ + int restrict_fsaccess_link_fds[_RESTRICT_FILESYSTEM_ACCESS_LINK_MAX]; + int restrict_fsaccess_bss_map_fd; + /* 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 */ diff --git a/src/core/meson.build b/src/core/meson.build index eef53be94b..7f5845244e 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -7,6 +7,7 @@ libcore_sources = files( 'bpf-firewall.c', 'bpf-foreign.c', 'bpf-restrict-fs.c', + 'bpf-restrict-fsaccess.c', 'bpf-restrict-ifaces.c', 'bpf-socket-bind.c', 'bpf-bind-iface.c', @@ -86,6 +87,10 @@ if conf.get('BPF_FRAMEWORK') == 1 endforeach endif +if conf.get('HAVE_LSM_INTEGRITY_TYPE') == 1 + libcore_sources += bpf_programs_by_name['restrict-fsaccess'] +endif + sources += libcore_sources load_fragment_gperf_gperf = custom_target( diff --git a/src/core/system.conf.in b/src/core/system.conf.in index 63d2805930..35c7cec6ef 100644 --- a/src/core/system.conf.in +++ b/src/core/system.conf.in @@ -41,6 +41,7 @@ #CapabilityBoundingSet= #NoNewPrivileges=no #ProtectSystem=auto +#RestrictFileSystemAccess=no #SystemCallArchitectures= #TimerSlackNSec= #StatusUnitFormat={{STATUS_UNIT_FORMAT_DEFAULT_STR}} diff --git a/src/shared/bpf-dlopen.c b/src/shared/bpf-dlopen.c index 1d2fdef781..ca74682823 100644 --- a/src/shared/bpf-dlopen.c +++ b/src/shared/bpf-dlopen.c @@ -38,6 +38,7 @@ DLSYM_PROTOTYPE(bpf_map_delete_elem) = NULL; DLSYM_PROTOTYPE(bpf_map_get_fd_by_id) = NULL; DLSYM_PROTOTYPE(bpf_map_lookup_elem) = NULL; DLSYM_PROTOTYPE(bpf_map_update_elem) = NULL; +DLSYM_PROTOTYPE(bpf_obj_get_info_by_fd) = NULL; DLSYM_PROTOTYPE(bpf_object__attach_skeleton) = NULL; DLSYM_PROTOTYPE(bpf_object__destroy_skeleton) = NULL; DLSYM_PROTOTYPE(bpf_object__detach_skeleton) = NULL; @@ -49,6 +50,7 @@ DLSYM_PROTOTYPE(bpf_program__attach) = NULL; DLSYM_PROTOTYPE(bpf_program__attach_cgroup) = NULL; DLSYM_PROTOTYPE(bpf_program__attach_lsm) = NULL; DLSYM_PROTOTYPE(bpf_program__name) = NULL; +DLSYM_PROTOTYPE(bpf_program__set_autoload) = NULL; DLSYM_PROTOTYPE(libbpf_set_print) = NULL; DLSYM_PROTOTYPE(ring_buffer__epoll_fd) = NULL; DLSYM_PROTOTYPE(ring_buffer__free) = NULL; @@ -154,6 +156,7 @@ int dlopen_bpf(int log_level) { DLSYM_ARG(bpf_map_get_fd_by_id), DLSYM_ARG(bpf_map_lookup_elem), DLSYM_ARG(bpf_map_update_elem), + DLSYM_ARG(bpf_obj_get_info_by_fd), DLSYM_ARG(bpf_object__attach_skeleton), DLSYM_ARG(bpf_object__destroy_skeleton), DLSYM_ARG(bpf_object__detach_skeleton), @@ -172,6 +175,7 @@ int dlopen_bpf(int log_level) { DLSYM_ARG_FORCE(bpf_program__attach_lsm), #endif DLSYM_ARG(bpf_program__name), + DLSYM_ARG(bpf_program__set_autoload), DLSYM_ARG(libbpf_get_error), DLSYM_ARG(libbpf_set_print), DLSYM_ARG(ring_buffer__epoll_fd), diff --git a/src/shared/bpf-dlopen.h b/src/shared/bpf-dlopen.h index b3d14f9b5f..62ea9cf707 100644 --- a/src/shared/bpf-dlopen.h +++ b/src/shared/bpf-dlopen.h @@ -25,6 +25,7 @@ extern DLSYM_PROTOTYPE(bpf_map_delete_elem); extern DLSYM_PROTOTYPE(bpf_map_get_fd_by_id); extern DLSYM_PROTOTYPE(bpf_map_lookup_elem); extern DLSYM_PROTOTYPE(bpf_map_update_elem); +extern DLSYM_PROTOTYPE(bpf_obj_get_info_by_fd); /* The *_skeleton APIs are autogenerated by bpftool, the targets can be found * in ./build/src/core/bpf/socket-bind/socket-bind.skel.h */ extern DLSYM_PROTOTYPE(bpf_object__attach_skeleton); @@ -38,6 +39,7 @@ extern DLSYM_PROTOTYPE(bpf_program__attach); extern DLSYM_PROTOTYPE(bpf_program__attach_cgroup); extern DLSYM_PROTOTYPE(bpf_program__attach_lsm); extern DLSYM_PROTOTYPE(bpf_program__name); +extern DLSYM_PROTOTYPE(bpf_program__set_autoload); extern DLSYM_PROTOTYPE(libbpf_set_print); extern DLSYM_PROTOTYPE(ring_buffer__epoll_fd); extern DLSYM_PROTOTYPE(ring_buffer__free); diff --git a/src/shared/bpf-link.c b/src/shared/bpf-link.c index 95f7256a56..80a6db47de 100644 --- a/src/shared/bpf-link.c +++ b/src/shared/bpf-link.c @@ -19,6 +19,22 @@ bool bpf_can_link_program(struct bpf_program *prog) { return bpf_get_error_translated(link) == -EBADF; } +bool bpf_can_link_lsm_program(struct bpf_program *prog) { + _cleanup_(bpf_link_freep) struct bpf_link *link = NULL; + + assert(prog); + + if (dlopen_bpf(LOG_DEBUG) < 0) + return false; + + link = sym_bpf_program__attach_lsm(prog); + + /* If bpf_program__attach_lsm fails the resulting value stores libbpf error code instead of memory + * pointer. That is the case when the helper is called on architectures where BPF trampoline (hence + * BPF_LSM_MAC attach type) is not supported. */ + return bpf_get_error_translated(link) == 0; +} + int bpf_serialize_link(FILE *f, FDSet *fds, const char *key, struct bpf_link *link) { assert(key); diff --git a/src/shared/bpf-link.h b/src/shared/bpf-link.h index 79da1c2fea..4de95eb2e1 100644 --- a/src/shared/bpf-link.h +++ b/src/shared/bpf-link.h @@ -8,6 +8,7 @@ #include "shared-forward.h" bool bpf_can_link_program(struct bpf_program *prog); +bool bpf_can_link_lsm_program(struct bpf_program *prog); int bpf_serialize_link(FILE *f, FDSet *fds, const char *key, struct bpf_link *link); diff --git a/src/test/meson.build b/src/test/meson.build index f13868d4e1..faf075c103 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -539,6 +539,11 @@ executables += [ 'sources' : files('test-bpf-restrict-fs.c'), 'dependencies' : common_test_dependencies, }, + core_test_template + { + 'sources' : files('test-bpf-restrict-fsaccess.c'), + 'dependencies' : common_test_dependencies, + 'type' : 'manual', + }, core_test_template + { 'sources' : files('test-bpf-token.c'), 'dependencies' : common_test_dependencies + libbpf, diff --git a/src/test/test-bpf-restrict-fsaccess.c b/src/test/test-bpf-restrict-fsaccess.c new file mode 100644 index 0000000000..e95e706c27 --- /dev/null +++ b/src/test/test-bpf-restrict-fsaccess.c @@ -0,0 +1,222 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +/* + * Test helper for RestrictFileSystemAccess= BPF enforcement tests. + * + * Usage: + * 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 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 + * + * 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 + * the rootfs) continues to work. The process holds all link FDs and blocks; + * when killed, close() drops the last reference synchronously. + */ + +#include +#include +#include +#include +#include + +#include "bpf-restrict-fsaccess.h" +#include "fd-util.h" +#include "log.h" +#include "string-util.h" +#include "tests.h" + +/* ---- mmap/mprotect 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. */ + +static int do_mmap_exec(const char *path) { + _cleanup_close_ int fd = -EBADF; + void *addr; + + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open %s: %m", path); + + 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; +} + +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) { + _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); + + 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; + + (void) munmap(addr, 4096); + + if (r < 0) + return log_info_errno(r, "mprotect PROT_EXEC on %s denied: %m", path); + + log_info("mprotect PROT_EXEC on %s succeeded", path); + return 0; +} + +#if BPF_FRAMEWORK && HAVE_LSM_INTEGRITY_TYPE +#include "bpf-dlopen.h" +#include "restrict-fsaccess-skel.h" + +static struct restrict_fsaccess_bpf *restrict_fsaccess_bpf_free(struct restrict_fsaccess_bpf *obj) { + restrict_fsaccess_bpf__destroy(obj); + return NULL; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(struct restrict_fsaccess_bpf *, restrict_fsaccess_bpf_free); + +static int do_attach(void) { + _cleanup_(restrict_fsaccess_bpf_freep) struct restrict_fsaccess_bpf *obj = NULL; + struct stat st; + int r; + + r = dlopen_bpf(LOG_ERR); + if (r < 0) + return log_error_errno(r, "Failed to dlopen libbpf: %m"); + + r = bpf_restrict_fsaccess_prepare(&obj); + if (r < 0) + return r; + + /* Set initramfs_s_dev to rootfs s_dev so the test script keeps running */ + if (stat("/", &st) < 0) + return log_error_errno(errno, "Failed to stat /: %m"); + + obj->bss->initramfs_s_dev = STAT_DEV_TO_KERNEL(st.st_dev); + log_info("Set initramfs_s_dev to %u:%u (kernel dev_t=0x%x)", + major(st.st_dev), minor(st.st_dev), obj->bss->initramfs_s_dev); + + r = restrict_fsaccess_bpf__attach(obj); + if (r < 0) + return log_error_errno(r, "Failed to attach BPF programs: %m"); + + /* Populate guard globals so the guard protects our BPF objects */ + r = bpf_restrict_fsaccess_populate_guard(obj); + if (r < 0) + return log_error_errno(r, "Failed to populate guard globals: %m"); + + printf("VERITY_MAP_ID=%u\n", (unsigned) obj->bss->protected_map_id_verity); + printf("BSS_MAP_ID=%u\n", (unsigned) obj->bss->protected_map_id_bss); + + /* Print comma-separated prog and link IDs for guard tests */ + printf("PROG_IDS=\""); + for (size_t i = 0; i < _RESTRICT_FILESYSTEM_ACCESS_LINK_MAX; i++) + printf("%s%u", i > 0 ? "," : "", (unsigned) obj->bss->protected_prog_ids[i]); + printf("\"\n"); + + printf("LINK_IDS=\""); + for (size_t i = 0; i < _RESTRICT_FILESYSTEM_ACCESS_LINK_MAX; i++) + printf("%s%u", i > 0 ? "," : "", (unsigned) obj->bss->protected_link_ids[i]); + printf("\"\n"); + + fflush(stdout); + + /* Block until killed. The _cleanup_ destructor holds all link FDs via + * the skeleton. When this process is killed, close() on the FDs goes + * through bpf_link_put_direct() which synchronously detaches the + * trampoline before the process exits. No bpffs pins needed. */ + log_info("BPF programs attached, waiting for signal to detach..."); + for (;;) + pause(); + + /* unreachable — cleanup happens via signal/exit */ +} + +static int do_check(void) { + if (!bpf_restrict_fsaccess_supported()) { + log_error("BPF LSM is not available"); + return -EOPNOTSUPP; + } + log_info("BPF LSM: supported"); + + if (!dm_verity_require_signatures()) { + log_error("dm-verity require_signatures is not enabled"); + return -ENOKEY; + } + log_info("dm-verity require_signatures: enabled"); + + return 0; +} + +int main(int argc, char *argv[]) { + 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 (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; +} + +#else /* ! BPF_FRAMEWORK || ! HAVE_LSM_INTEGRITY_TYPE */ + +int main(int argc, char *argv[]) { + 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; + } + + log_info("BPF framework not available, attach/check not supported"); + return 77; /* skip */ +} + +#endif diff --git a/test/integration-tests/TEST-90-RESTRICT-FSACCESS/meson.build b/test/integration-tests/TEST-90-RESTRICT-FSACCESS/meson.build new file mode 100644 index 0000000000..2f3228f010 --- /dev/null +++ b/test/integration-tests/TEST-90-RESTRICT-FSACCESS/meson.build @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +# Config subtest: no require_signatures — tests error paths and config parsing. +integration_tests += [ + integration_test_template + { + 'name' : fs.name(meson.current_source_dir()), + 'vm' : true, + 'firmware' : 'auto', + 'configuration' : integration_test_template['configuration'] + { + 'env' : {'TEST_MATCH_SUBTEST' : 'config'}, + }, + }, +] + +# 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). +integration_tests += [ + integration_test_template + { + 'name' : fs.name(meson.current_source_dir()) + '-enforce', + 'vm' : true, + 'firmware' : 'auto', + 'cmdline' : integration_test_template['cmdline'] + [ + 'dm_verity.require_signatures=1', + ], + 'configuration' : integration_test_template['configuration'] + { + 'command' : '/usr/lib/systemd/tests/testdata/units/' + fs.name(meson.current_source_dir()) + '.sh', + 'env' : {'TEST_MATCH_SUBTEST' : 'enforce'}, + }, + }, +] + +# dm-verity keyring subtest: exercise the .dm-verity keyring provisioning path +# (kernel commit 033724b1c627, v7.0+) without UEFI/SecureBoot — boots with +# linux-noinitrd so the .platform keyring stays empty and the only way to +# trust the signed verity image is via the dedicated .dm-verity keyring. +integration_tests += [ + integration_test_template + { + 'name' : fs.name(meson.current_source_dir()) + '-dm-verity-keyring', + 'vm' : true, + 'cmdline' : integration_test_template['cmdline'] + [ + 'dm_verity.require_signatures=1', + 'dm_verity.keyring_unsealed=1', + ], + 'configuration' : integration_test_template['configuration'] + { + 'command' : '/usr/lib/systemd/tests/testdata/units/' + fs.name(meson.current_source_dir()) + '.sh', + 'env' : {'TEST_MATCH_SUBTEST' : 'dm-verity-keyring'}, + }, + }, +] diff --git a/test/integration-tests/meson.build b/test/integration-tests/meson.build index 37ce391598..dd9eba9b67 100644 --- a/test/integration-tests/meson.build +++ b/test/integration-tests/meson.build @@ -102,6 +102,7 @@ foreach dirname : [ 'TEST-87-AUX-UTILS-VM', 'TEST-88-UPGRADE', 'TEST-89-RESOLVED-MDNS', + 'TEST-90-RESTRICT-FSACCESS', ] subdir(dirname) endforeach diff --git a/test/units/TEST-90-RESTRICT-FSACCESS.config.sh b/test/units/TEST-90-RESTRICT-FSACCESS.config.sh new file mode 100755 index 0000000000..81bd0ff414 --- /dev/null +++ b/test/units/TEST-90-RESTRICT-FSACCESS.config.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Test RestrictFileSystemAccess= configuration parsing and graceful failure modes. +# +# Runs in a VM WITHOUT dm_verity.require_signatures=1, so enabling RestrictFileSystemAccess +# triggers the require_signatures error path without activating enforcement. +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +# RestrictFileSystemAccess= requires +BPF_FRAMEWORK at compile time +if systemctl --version | grep -F -- "-BPF_FRAMEWORK" >/dev/null; then + echo "BPF framework not compiled in, skipping" + exit 0 +fi + +HELPER=/usr/lib/systemd/tests/unit-tests/manual/test-bpf-restrict-fsaccess +CURSOR_FILE=/tmp/restrict-fsaccess-config.cursor + +cleanup() { + rm -f "$CURSOR_FILE" + rm -f /run/systemd/system.conf.d/50-restrict-fsaccess.conf +} +trap cleanup EXIT + +disable_restrict_fsaccess() { + rm -f /run/systemd/system.conf.d/50-restrict-fsaccess.conf +} + +# ------ Test case 1: Default (RestrictFileSystemAccess=no) — no log messages ------ + +testcase_default_no_messages() { + disable_restrict_fsaccess + + # Save a journal cursor so we only check messages from after this point. + journalctl -q -n 0 --cursor-file="$CURSOR_FILE" + + systemctl daemon-reexec + # daemon-reexec is synchronous: PID1 has completed startup (including any + # RestrictFileSystemAccess= setup) and is back on D-Bus by the time it returns. PID1 + # logs to kmsg synchronously, so messages are already in the journal. + + # No RestrictFileSystemAccess-related messages should appear + if journalctl --cursor-file="$CURSOR_FILE" -o cat _PID=1 | grep "bpf-restrict-fsaccess" >/dev/null 2>&1; then + echo "Unexpected RestrictFileSystemAccess log messages with RestrictFileSystemAccess=no" + return 1 + fi +} + +# ------ Test case 2: require_signatures check via helper binary ------ +# +# The helper binary runs the same precondition checks as PID1 (BPF LSM +# availability, dm-verity require_signatures). When require_signatures is +# off the check must fail — this verifies the C code gate without going +# through daemon-reexec (which would kill PID1). + +testcase_no_require_signatures_helper() { + if ! kernel_supports_lsm bpf; then + echo "BPF LSM not available, skipping require_signatures test" + return 0 + fi + + # Check that the kernel has the bdev_setintegrity LSM hook in BTF. + # Without it the skeleton fails to load and the check reports "BPF LSM + # is not available" which masks the real reason. + if command -v bpftool >/dev/null 2>&1; then + if ! bpftool btf dump file /sys/kernel/btf/vmlinux 2>/dev/null | grep 'bpf_lsm_bdev_setintegrity' >/dev/null; then + echo "Kernel lacks bdev_setintegrity LSM hook, skipping require_signatures test" + return 0 + fi + fi + + if [[ ! -x "$HELPER" ]]; then + echo "Helper binary not found, skipping" + return 0 + fi + + # This VM boots WITHOUT require_signatures. + if [[ -e /sys/module/dm_verity/parameters/require_signatures ]]; then + local val + val="$(cat /sys/module/dm_verity/parameters/require_signatures)" + if [[ "$val" == "Y" || "$val" == "1" ]]; then + echo "require_signatures already enabled, skipping (enforce VM covers this)" + return 0 + 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 + echo "ERROR: helper check succeeded but require_signatures is not enabled" + return 1 + fi + echo "Helper correctly rejected setup: require_signatures not enabled" +} + +run_testcases diff --git a/test/units/TEST-90-RESTRICT-FSACCESS.dm-verity-keyring.sh b/test/units/TEST-90-RESTRICT-FSACCESS.dm-verity-keyring.sh new file mode 100755 index 0000000000..6237483a45 --- /dev/null +++ b/test/units/TEST-90-RESTRICT-FSACCESS.dm-verity-keyring.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Exercise the dedicated .dm-verity keyring trust path (kernel commit +# 033724b1c627, v7.0+): boot with linux-noinitrd so .platform stays empty, +# provision the mkosi cert into .dm-verity via keyctl, then verify a signed +# verity image still loads and execs under the BPF policy. +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +if systemctl --version | grep -F -- "-BPF_FRAMEWORK" >/dev/null; then + echo "BPF framework not compiled in, skipping" + exit 0 +fi + +if ! kernel_supports_lsm bpf; then + echo "BPF LSM not available in kernel, skipping" + exit 0 +fi + +if command -v bpftool >/dev/null 2>&1; then + if ! bpftool btf dump file /sys/kernel/btf/vmlinux 2>/dev/null | grep 'bpf_lsm_bdev_setintegrity' >/dev/null; then + echo "Kernel lacks bdev_setintegrity LSM hook, skipping" + exit 0 + fi +fi + +if [[ -v ASAN_OPTIONS ]]; then + echo "Skipping under sanitizers" + exit 0 +fi + +HELPER="/usr/lib/systemd/tests/unit-tests/manual/test-bpf-restrict-fsaccess" +if [[ ! -x "$HELPER" ]]; then + echo "ERROR: test-bpf-restrict-fsaccess helper not found at $HELPER" >&2 + 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 0 +fi + +if [[ ! -e /sys/module/dm_verity/parameters/require_signatures ]]; then + modprobe dm_verity 2>/dev/null || true +fi +val="$(cat /sys/module/dm_verity/parameters/require_signatures 2>/dev/null || echo)" +if [[ "$val" != "Y" && "$val" != "1" ]]; then + echo "require_signatures not enabled, skipping" + exit 0 +fi + +# Provision the .dm-verity keyring. Empty description lets the kernel derive +# one from the X.509 subject so machine_supports_verity_keyring finds the CN. +keyid=$(openssl x509 -in /usr/share/mkosi.crt -outform DER | + keyctl padd asymmetric '' %:.dm-verity 2>/dev/null) || keyid="" +if [[ -z "$keyid" ]]; then + echo ".dm-verity keyring not provisionable (kernel < v7.0?), skipping" + exit 0 +fi +if ! keyctl restrict_keyring %:.dm-verity; then + keyctl unlink "$keyid" %:.dm-verity 2>/dev/null || true + echo "ERROR: keyctl restrict_keyring failed" >&2 + exit 1 +fi +echo "Provisioned .dm-verity keyring with mkosi.crt" + +at_exit() { + set +e + [[ -n "${HELPER_PID:-}" ]] && kill "$HELPER_PID" 2>/dev/null && wait "$HELPER_PID" 2>/dev/null || true + rm -rf /tmp/restrict-fsaccess-dvk-attach.out +} +trap at_exit EXIT + +HELPER_PID= +exec 3< <(exec "$HELPER" attach) +HELPER_PID=$! +while IFS= read -r -t 60 line <&3; do + echo "$line" + [[ "$line" == LINK_IDS=* ]] && break +done > /tmp/restrict-fsaccess-dvk-attach.out + +# Fail closed if helper died before printing the full handshake: an unattached +# program would let the subsequent verity exec test pass trivially. +if ! kill -0 "$HELPER_PID" 2>/dev/null; then + echo "ERROR: helper exited before BPF programs were attached" >&2 + exit 1 +fi +grep -E '^LINK_IDS="[^"]+"' /tmp/restrict-fsaccess-dvk-attach.out >/dev/null || { + echo "ERROR: helper did not report LINK_IDS, BPF programs not attached" >&2 + exit 1 +} + +# Run a binary off the signed minimal_0 verity image. Trust path is exclusively +# the .dm-verity keyring we just provisioned; .platform is empty under +# linux-noinitrd. +systemd-run --pipe --wait \ + --property RootImage=/usr/share/minimal_0.raw \ + bash --version >/dev/null +echo "Execution from signed dm-verity device (via .dm-verity keyring): OK" diff --git a/test/units/TEST-90-RESTRICT-FSACCESS.enforce.sh b/test/units/TEST-90-RESTRICT-FSACCESS.enforce.sh new file mode 100755 index 0000000000..a9db974803 --- /dev/null +++ b/test/units/TEST-90-RESTRICT-FSACCESS.enforce.sh @@ -0,0 +1,311 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Test RestrictFileSystemAccess= BPF enforcement. +# +# Uses a C test helper to load the BPF program with initramfs_s_dev set to the +# current rootfs s_dev, then verifies that execution from tmpfs is blocked +# 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). +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# Skip if prerequisites not met +if systemctl --version | grep -F -- "-BPF_FRAMEWORK" >/dev/null; then + echo "BPF framework not compiled in, skipping" + exit 0 +fi + +if ! kernel_supports_lsm bpf; then + echo "BPF LSM not available in kernel, skipping" + exit 0 +fi + +# Check that the kernel has the bdev_setintegrity LSM hook in BTF. +# Older kernels (e.g., CentOS 9 with 5.14) lack this hook entirely. +if command -v bpftool >/dev/null 2>&1; then + if ! bpftool btf dump file /sys/kernel/btf/vmlinux 2>/dev/null | grep 'bpf_lsm_bdev_setintegrity' >/dev/null; then + echo "Kernel lacks bdev_setintegrity LSM hook (required for RestrictFileSystemAccess=), skipping" + exit 0 + fi +fi + +if [[ -v ASAN_OPTIONS ]]; then + echo "Skipping enforcement test under sanitizers" + exit 0 +fi + +HELPER="/usr/lib/systemd/tests/unit-tests/manual/test-bpf-restrict-fsaccess" +if [[ ! -x "$HELPER" ]]; then + echo "ERROR: test-bpf-restrict-fsaccess helper not found at $HELPER" >&2 + 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 0 +fi + +# require_signatures is read-only — must be set via kernel cmdline +if [[ ! -e /sys/module/dm_verity/parameters/require_signatures ]]; then + modprobe dm_verity 2>/dev/null || true +fi +if [[ ! -e /sys/module/dm_verity/parameters/require_signatures ]]; then + echo "dm_verity module not available, skipping enforcement test" + exit 0 +fi +val="$(cat /sys/module/dm_verity/parameters/require_signatures)" +if [[ "$val" != "Y" && "$val" != "1" ]]; then + echo "require_signatures not enabled (need dm-verity.require_signatures=1 on cmdline), skipping" + exit 0 +fi + +at_exit() { + set +e + # Kill the attach helper to detach BPF programs synchronously + [[ -n "${HELPER_PID:-}" ]] && kill "$HELPER_PID" 2>/dev/null && wait "$HELPER_PID" 2>/dev/null || true + # Clean up tmpfs test directories + umount /tmp/restrict-fsaccess-test 2>/dev/null || true + rm -rf /tmp/restrict-fsaccess-test + umount /tmp/restrict-fsaccess-baseline 2>/dev/null || true + rm -rf /tmp/restrict-fsaccess-baseline + # Clean up background processes + [[ -n "${SLEEP_PID:-}" ]] && kill "$SLEEP_PID" 2>/dev/null || true + rm -rf /tmp/restrict-fsaccess-attach.out +} +trap at_exit EXIT + +# ------ Baseline: verify tmpfs exec works WITHOUT our BPF ------ +# +# Keep the destination basename as "true": on systems shipping uutils-coreutils +# (or busybox) as a multicall binary, /usr/bin/true is a symlink and cp +# dereferences it, copying the multicall binary. The dispatcher selects the +# subcommand from basename(argv[0]), so the copy only behaves as true when +# invoked under that name. + +mkdir -p /tmp/restrict-fsaccess-baseline +mount -t tmpfs tmpfs /tmp/restrict-fsaccess-baseline +cp /usr/bin/true /tmp/restrict-fsaccess-baseline/true +chmod +x /tmp/restrict-fsaccess-baseline/true +if ! /tmp/restrict-fsaccess-baseline/true 2>/dev/null; then + echo "WARNING: tmpfs exec blocked BEFORE BPF attach (another LSM?)" >&2 + echo "Skipping enforcement test, baseline tmpfs exec fails" + umount /tmp/restrict-fsaccess-baseline; rm -rf /tmp/restrict-fsaccess-baseline + exit 0 +fi +echo "Baseline: tmpfs exec works without BPF" +umount /tmp/restrict-fsaccess-baseline; rm -rf /tmp/restrict-fsaccess-baseline + +# ------ Attach BPF with rootfs trusted ------ +# The helper attaches, prints map/prog/link IDs, then blocks holding FDs. +# Kill it to detach synchronously (close() drops last ref via bpf_link_put_direct). + +HELPER_PID= +exec 3< <(exec "$HELPER" attach) +HELPER_PID=$! + +# Read helper output line by line until LINK_IDS= (the last line before pause()). +# read -t 60 handles both timeout and helper crash (EOF on death). +while IFS= read -r -t 60 line <&3; do + echo "$line" + [[ "$line" == LINK_IDS=* ]] && break +done > /tmp/restrict-fsaccess-attach.out + +VERITY_MAP_ID=$(sed -n 's/^VERITY_MAP_ID=//p' /tmp/restrict-fsaccess-attach.out) +BSS_MAP_ID=$(sed -n 's/^BSS_MAP_ID=//p' /tmp/restrict-fsaccess-attach.out) +PROG_IDS=$(sed -n 's/^PROG_IDS="\(.*\)"$/\1/p' /tmp/restrict-fsaccess-attach.out) +LINK_IDS=$(sed -n 's/^LINK_IDS="\(.*\)"$/\1/p' /tmp/restrict-fsaccess-attach.out) +[[ -n "$VERITY_MAP_ID" ]] || { echo "ERROR: Failed to capture VERITY_MAP_ID from helper output" >&2; exit 1; } +[[ -n "$BSS_MAP_ID" ]] || { echo "ERROR: Failed to capture BSS_MAP_ID from helper output" >&2; exit 1; } +[[ -n "$PROG_IDS" ]] || { echo "ERROR: Failed to capture PROG_IDS from helper output" >&2; exit 1; } +[[ -n "$LINK_IDS" ]] || { echo "ERROR: Failed to capture LINK_IDS from helper output" >&2; exit 1; } + +# ------ Test: Rootfs execution still works ------ + +/usr/bin/true +echo "Rootfs execution: OK" + +# ------ Test: Execution from tmpfs is blocked ------ + +mkdir -p /tmp/restrict-fsaccess-test +mount -t tmpfs tmpfs /tmp/restrict-fsaccess-test + +# Copy a binary to tmpfs. Basename must stay "true" for multicall coreutils +# binaries (uutils, busybox) — see the baseline comment above. +cp /usr/bin/true /tmp/restrict-fsaccess-test/true +chmod +x /tmp/restrict-fsaccess-test/true + +# This should fail with EPERM +if /tmp/restrict-fsaccess-test/true 2>/dev/null; then + echo "ERROR: Execution from tmpfs should have been blocked!" >&2 + exit 1 +fi +echo "Execution from tmpfs blocked: OK" + +# ------ Test: PROT_EXEC mmap from tmpfs is blocked (mmap_file hook) ------ + +# 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 +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 +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 +echo "mprotect PROT_EXEC from tmpfs blocked: OK" + +# ------ 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). + +MINIMAL=/usr/share/minimal_0 +if machine_supports_verity_keyring; then + systemd-run --pipe --wait \ + --property RootImage="$MINIMAL.raw" \ + bash --version >/dev/null + echo "Execution from signed dm-verity device: OK" +else + echo "Verity keyring trust not available, skipping positive verity test" +fi + +# ------ Test: Guard blocks non-PID1 from obtaining BPF object FDs by ID ------ + +if command -v bpftool >/dev/null 2>&1 && [[ -n "${VERITY_MAP_ID:-}" ]]; then + # bpftool uses BPF_MAP_GET_FD_BY_ID / BPF_PROG_GET_FD_BY_ID / + # BPF_LINK_GET_FD_BY_ID internally. The guard should block these for + # our protected IDs since we're not PID1. + + # -- Map ID guard -- + if bpftool map show id "$VERITY_MAP_ID" 2>/dev/null; then + echo "ERROR: bpftool should not be able to access verity_devices map (ID $VERITY_MAP_ID)!" >&2 + exit 1 + fi + echo "Guard blocked verity_devices map access: OK (ID $VERITY_MAP_ID)" + + if [[ -n "${BSS_MAP_ID:-}" ]]; then + if bpftool map show id "$BSS_MAP_ID" 2>/dev/null; then + echo "ERROR: bpftool should not be able to access .bss map (ID $BSS_MAP_ID)!" >&2 + exit 1 + fi + echo "Guard blocked .bss map access: OK (ID $BSS_MAP_ID)" + fi + + # -- Prog ID guard (defense-in-depth) -- + if [[ -n "${PROG_IDS:-}" ]]; then + IFS=',' read -ra prog_ids <<< "$PROG_IDS" + for prog_id in "${prog_ids[@]}"; do + if bpftool prog show id "$prog_id" 2>/dev/null; then + echo "ERROR: bpftool should not be able to access protected prog (ID $prog_id)!" >&2 + exit 1 + fi + done + echo "Guard blocked prog access: OK (${#prog_ids[@]} IDs)" + fi + + # -- Link ID guard (defense-in-depth) -- + if [[ -n "${LINK_IDS:-}" ]]; then + IFS=',' read -ra link_ids <<< "$LINK_IDS" + for lid in "${link_ids[@]}"; do + if bpftool link show id "$lid" 2>/dev/null; then + echo "ERROR: bpftool should not be able to access protected link (ID $lid)!" >&2 + exit 1 + fi + done + echo "Guard blocked link access: OK (${#link_ids[@]} IDs)" + fi + + # Verify the guard doesn't block unrelated BPF operations. + # bpftool prog list uses BPF_PROG_GET_NEXT_ID which the guard doesn't + # intercept (it only blocks *_GET_FD_BY_ID for specific IDs). + bpftool prog list >/dev/null 2>&1 || true + echo "Unrelated BPF operations still work: OK" +else + echo "bpftool not available or map IDs not captured, skipping guard test" +fi + +# ------ Test: ptrace attach to PID1 is blocked ------ + +# dd from /proc/1/mem uses PTRACE_MODE_ATTACH_FSCREDS via mm_access(). +# Read from a valid mapped address (not offset 0 which is the unmapped NULL +# page and would fail with -EIO even without the guard). +PID1_ADDR=$(awk '/r-xp/ { split($1, a, "-"); print a[1]; exit }' /proc/1/maps) +if [[ -n "$PID1_ADDR" ]]; then + PID1_OFFSET=$((16#$PID1_ADDR)) + if ! dd if=/proc/1/mem of=/dev/null bs=1 count=1 skip="$PID1_OFFSET" iflag=skip_bytes 2>/dev/null; then + echo "Ptrace ATTACH access to PID1 blocked: OK" + else + echo "ERROR: /proc/1/mem read should have been blocked!" >&2 + exit 1 + fi +else + echo "WARNING: Could not determine mapped address for PID1, skipping ptrace test" +fi + +# Verify READ-level access to PID1 still works (monitoring tools need this) +if cat /proc/1/status >/dev/null 2>&1; then + echo "Ptrace READ access to PID1 allowed: OK" +else + echo "ERROR: /proc/1/status should still be readable!" >&2 + exit 1 +fi + +# Verify ptrace to non-PID1 processes is unaffected +SLEEP_PID= +sleep 60 & +SLEEP_PID=$! +if cat /proc/$SLEEP_PID/status >/dev/null 2>&1; then + echo "Ptrace access to non-PID1 unaffected: OK" +else + echo "ERROR: /proc/$SLEEP_PID/status should be readable!" >&2 + kill "$SLEEP_PID" 2>/dev/null || true + exit 1 +fi +kill "$SLEEP_PID" 2>/dev/null || true +wait "$SLEEP_PID" 2>/dev/null || true +SLEEP_PID= + +# ------ Detach and verify enforcement is lifted ------ +# Kill the helper process. close() on the link FDs goes through +# bpf_link_put_direct() which synchronously detaches the trampoline. + +kill "$HELPER_PID" +wait "$HELPER_PID" 2>/dev/null || true +HELPER_PID= +echo "Helper killed, BPF programs detached synchronously" + +if [[ -x /tmp/restrict-fsaccess-test/true ]]; then + /tmp/restrict-fsaccess-test/true + echo "Execution from tmpfs after detach: OK" +fi + +umount /tmp/restrict-fsaccess-test 2>/dev/null || true +rm -rf /tmp/restrict-fsaccess-test + +echo "All enforcement tests passed" diff --git a/test/units/TEST-90-RESTRICT-FSACCESS.sh b/test/units/TEST-90-RESTRICT-FSACCESS.sh new file mode 100755 index 0000000000..9c2a033aa9 --- /dev/null +++ b/test/units/TEST-90-RESTRICT-FSACCESS.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/test-control.sh +. "$(dirname "$0")"/test-control.sh + +run_subtests + +touch /testok