mirror of
https://github.com/systemd/systemd.git
synced 2026-08-13 17:08:14 +00:00
RestrictFileSystemAccess= — dm-verity filesystem access enforcement via BPF LSM (#41340)
This series adds a new `RestrictFileSystemAccess=` setting in the `[Manager]` section of `system.conf` 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. Everything else — tmpfs, procfs, sysfs, anonymous executable mappings, unsigned dm-verity devices — is denied. The directive takes the values `no` (default), `exec` (lock down execution), and accepts `yes` as an alias for `exec`. The name is deliberately broader than what the initial values cover so the same setting can grow to restrict other filesystem access categories in the future (e.g. `any` to deny all access from untrusted filesystems, not just execution). ### How it works The BPF program is entirely self-contained; PID1 loads it and the kernel does the rest. When dm-verity brings up a device, the kernel calls `security_bdev_setintegrity()` twice during `verity_preresume()`: once with the root hash and once with the signature validity status. Our `lsm/bdev_setintegrity` hook captures the second call and records the device number in a BPF hash map if the signature is valid. When a device is torn down, `lsm/bdev_free_security` cleans up the map entry. No userspace map population is needed at any point. The enforcement side hooks `bprm_check_security` (execve), `mmap_file` (PROT_EXEC mappings including shared libraries), and `file_mprotect` (W→X transitions like JIT and libffi). Each hook resolves the file's backing device via `file->f_inode->i_sb->s_dev` and looks it up in the verity device map. For block-backed filesystems, `s_dev` equals `s_bdev->bd_dev`, which avoids an extra pointer chase and NULL check on `s_bdev` — non-block filesystems simply miss in the map and get denied by the default policy. During early boot the initramfs needs to be trusted as well, since it runs before any dm-verity volume is mounted. PID1 writes the initramfs superblock's device number into a BPF global before attaching the programs, and clears it after `switch_root` to close the trust window. As a prerequisite, PID1 also verifies that `dm_verity.require_signatures=1` is active — without it, unsigned dm-verity devices could be created, which would weaken the security model even though the BPF program would correctly deny execution from them. ### Surviving daemon-reexec The BPF programs and their verity device map must survive PID1 re-execution (daemon-reexec, switch_root, soft-reboot). Without preservation, `manager_free()` would destroy the skeleton, the link FDs would close, programs would detach, and the map would be freed. After exec, a fresh skeleton would have an empty map — but existing dm-verity devices have already signaled their integrity and won't do so again. A deny-default policy plus an empty map means all execution denied and the system is bricked. We solve this by serializing the raw BPF link FDs and the `.bss` map FD across exec using systemd's existing `serialize_fd` / `fdset_cloexec` / `deserialize_fd` infrastructure. The kernel reference chain (link FD → `struct bpf_link` → `struct bpf_prog` → `struct bpf_map`) keeps programs attached and map data intact as long as the dup'd FDs survive. After exec, PID1 detects the deserialized FDs and skips skeleton re-creation entirely. If switching root, it uses the deserialized `.bss` map FD to clear `initramfs_s_dev` via a targeted `mmap()` write, preserving the other guard globals in `.bss`. We intentionally avoid bpffs pinning. Pinned objects are discoverable and manipulable by any process with sufficient privileges (`BPF_OBJ_GET`, unlink). FD serialization keeps everything private to PID1 with no external attack surface. ### Self-protection BPF LSM programs attached via the tracing trampoline (`BPF_LSM_MAC`) are inherently tamper-resistant — `bpf_tracing_link_lops` has no `.update_prog` and no `.detach` callbacks, so the kernel rejects `BPF_LINK_UPDATE` with `-EINVAL` and `BPF_LINK_DETACH` with `-EOPNOTSUPP`. Once attached, our programs cannot be modified or detached through the `bpf()` syscall. The remaining attack vector is map injection: `BPF_MAP_GET_FD_BY_ID` to obtain an FD to `verity_devices`, then `BPF_MAP_UPDATE_ELEM` to insert a fake trusted device. The self-protection guard blocks this with three hooks. `lsm/bpf_map` fires inside `bpf_map_new_fd()`, the chokepoint for all code paths that produce a map FD, and denies access to our map IDs from any process other than PID1 (identified via `tgid == 1`, which is unspoofable — `bpf_get_current_pid_tgid()` reads `current->tgid` from `pid->numbers[0].nr`, the init-namespace PID). `lsm/bpf_prog` provides analogous protection for program FDs as defense-in-depth. `lsm/bpf` handles `BPF_LINK_GET_FD_BY_ID` at the command level since there is no `security_bpf_link()` hook in the kernel. The guard starts inactive — all protected IDs default to 0 in `.bss`, and no real BPF object has ID 0 — so there is no window where it interferes with PID1's own setup. After attaching all programs, PID1 queries the kernel-assigned IDs via `bpf_obj_get_info_by_fd()` and writes them into the guard's globals. From that point on, the guard is active. The guard has zero collateral damage: it only denies access to our specific object IDs, leaving bpftrace, bpftool, `RestrictFileSystems=`, and all other BPF usage completely unaffected. Additionally, a ptrace guard (`lsm/ptrace_access_check`) blocks `PTRACE_MODE_ATTACH` to PID1 from other processes, preventing extraction of sensitive state from PID1's address space via ptrace, `/proc/1/mem`, `process_vm_readv()`, or `pidfd_getfd()`. `PTRACE_MODE_READ` is allowed so that monitoring tools and `systemctl` continue to work normally. ### Limitations - The enforcement hooks resolve trust by looking at `file->f_inode->i_sb->s_dev` — the device number of the superblock that owns the file's inode. This works correctly for files directly on a dm-verity block device, but it does not see through overlayfs. When a file is accessed on an overlay mount, `f_inode` points to the overlay inode, and `i_sb->s_dev` is the overlay superblock's anonymous device number — not the underlying dm-verity device. The overlay superblock has no backing block device, so the lookup misses in the verity map and execution is denied by the default policy. This means that overlayfs mounts whose lower layers are on dm-verity-protected volumes will currently have execution blocked, even though the actual data is integrity-protected. The correct fix requires a kernel extension that allows the BPF program to call something like `d_real_inode()` to resolve through the overlay to the real inode on the underlying filesystem, and then check that inode's superblock device number against the verity map. I plan to add a BPF kfunc exposing this functionality in a follow-up kernel series. - Multi-device filesystems such as btrfs use entirely synthetic device numbers and there is no way to reach the actual device backing the inode from the inode itself. So `RestrictFileSystemAccess=` only works reliably with a subset of filesystems. In practice this isn't a problem because the feature is tailored to erofs; using it on arbitrary filesystems requires careful vetting of the actual filesystem behaviour. - The initial implementation also blocks JIT-style execution that relies on memory mapped executable. This is part of `exec` semantics today and can be loosened later by introducing finer-grained values (a common pattern in systemd — following the precedent of `ProtectSystem=`, which started as a boolean and later grew `auto`/`yes`/`full`/`strict` semantics). - The configuration is a system-wide setting with no per-unit opt-out. This is intentional for the initial implementation: a global invariant is easier to reason about and harder to accidentally weaken. Per-unit relaxation can be added later if a concrete need arises. ### Testing The series includes unit tests and integration tests covering both the core enforcement logic and the self-protection guard. The unit test loads the skeleton, attaches programs, populates guard globals, and verifies that protected IDs are set correctly. The integration tests exercise the guard by attempting `BPF_MAP_GET_FD_BY_ID` and `BPF_PROG_GET_FD_BY_ID` from a non-PID1 process and verifying that access is denied. What we cannot currently test end-to-end is actual execution enforcement against a dm-verity-signed root filesystem. The systemd test suite does not yet have infrastructure for booting a VM with a signed dm-verity rootfs image — the existing mkosi-based test framework lacks the ability to produce and boot such images. This will hopefully change soon when Daan integrates barrage into the test suite. Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
4
.github/workflows/build-test.sh
vendored
4
.github/workflows/build-test.sh
vendored
@@ -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
|
||||
|
||||
@@ -79,6 +79,17 @@
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>systemd.restrict_filesystem_access=</varname></term>
|
||||
<listitem>
|
||||
<para>Controls the <varname>RestrictFileSystemAccess=</varname> execution enforcement policy. For
|
||||
details, see
|
||||
<citerefentry><refentrytitle>systemd-system.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v261"/>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>systemd.mask=</varname></term>
|
||||
<term><varname>systemd.wants=</varname></term>
|
||||
|
||||
@@ -532,6 +532,49 @@
|
||||
<xi:include href="version-info.xml" xpointer="v256"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>RestrictFileSystemAccess=</varname></term>
|
||||
|
||||
<listitem><para>Takes a boolean argument or the special value <literal>exec</literal>. Defaults to
|
||||
<literal>no</literal>. 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.</para>
|
||||
|
||||
<para>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
|
||||
<filename>system.conf</filename>, modify the kernel command line, or kexec into an unsigned initrd
|
||||
can disable or bypass the policy.</para>
|
||||
|
||||
<para>The enforcement hooks block <function>execve()</function> of untrusted binaries
|
||||
(<literal>bprm_check_security</literal>), <constant>PROT_EXEC</constant> memory mappings including
|
||||
shared libraries (<literal>mmap_file</literal>), and write-to-execute transitions such as JIT
|
||||
compilation (<literal>file_mprotect</literal>).</para>
|
||||
|
||||
<para>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.</para>
|
||||
|
||||
<para>Note that, without further measures to secure the system, kexec can be used to circumvent 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>
|
||||
|
||||
<para>The value <literal>yes</literal> is equivalent to <literal>exec</literal>. Additional
|
||||
modes may be added in the future.</para>
|
||||
|
||||
<para>This option may also be set via the <varname>systemd.restrict_filesystem_access=</varname> kernel command
|
||||
line option, see
|
||||
<citerefentry><refentrytitle>kernel-command-line</refentrytitle><manvolnum>7</manvolnum></citerefentry>.
|
||||
</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>SystemCallArchitectures=</varname></term>
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ Packages=
|
||||
gzip
|
||||
jq
|
||||
kbd
|
||||
keyutils
|
||||
kmod
|
||||
less
|
||||
lsof
|
||||
|
||||
@@ -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',
|
||||
|
||||
300
src/bpf/restrict-fsaccess.bpf.c
Normal file
300
src/bpf/restrict-fsaccess.bpf.c
Normal file
@@ -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 <errno.h> /* IWYU pragma: keep */
|
||||
#include <bpf/bpf_core_read.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
|
||||
#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";
|
||||
@@ -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;
|
||||
|
||||
562
src/core/bpf-restrict-fsaccess.c
Normal file
562
src/core/bpf-restrict-fsaccess.c
Normal file
@@ -0,0 +1,562 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#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
|
||||
61
src/core/bpf-restrict-fsaccess.h
Normal file
61
src/core/bpf-restrict-fsaccess.h
Normal file
@@ -0,0 +1,61 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include <sys/sysmacros.h>
|
||||
|
||||
#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);
|
||||
@@ -52,3 +52,4 @@ typedef struct Unit Unit;
|
||||
typedef struct UnitRef UnitRef;
|
||||
|
||||
struct restrict_fs_bpf;
|
||||
struct restrict_fsaccess_bpf;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#CapabilityBoundingSet=
|
||||
#NoNewPrivileges=no
|
||||
#ProtectSystem=auto
|
||||
#RestrictFileSystemAccess=no
|
||||
#SystemCallArchitectures=
|
||||
#TimerSlackNSec=
|
||||
#StatusUnitFormat={{STATUS_UNIT_FORMAT_DEFAULT_STR}}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
222
src/test/test-bpf-restrict-fsaccess.c
Normal file
222
src/test/test-bpf-restrict-fsaccess.c
Normal file
@@ -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 <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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
|
||||
50
test/integration-tests/TEST-90-RESTRICT-FSACCESS/meson.build
Normal file
50
test/integration-tests/TEST-90-RESTRICT-FSACCESS/meson.build
Normal file
@@ -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'},
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -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
|
||||
|
||||
103
test/units/TEST-90-RESTRICT-FSACCESS.config.sh
Executable file
103
test/units/TEST-90-RESTRICT-FSACCESS.config.sh
Executable file
@@ -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
|
||||
107
test/units/TEST-90-RESTRICT-FSACCESS.dm-verity-keyring.sh
Executable file
107
test/units/TEST-90-RESTRICT-FSACCESS.dm-verity-keyring.sh
Executable file
@@ -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"
|
||||
311
test/units/TEST-90-RESTRICT-FSACCESS.enforce.sh
Executable file
311
test/units/TEST-90-RESTRICT-FSACCESS.enforce.sh
Executable file
@@ -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"
|
||||
11
test/units/TEST-90-RESTRICT-FSACCESS.sh
Executable file
11
test/units/TEST-90-RESTRICT-FSACCESS.sh
Executable file
@@ -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
|
||||
Reference in New Issue
Block a user