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:
Christian Brauner
2026-05-13 13:58:46 +02:00
committed by GitHub
28 changed files with 1955 additions and 14 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -101,6 +101,7 @@ Packages=
gzip
jq
kbd
keyutils
kmod
less
lsof

View File

@@ -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',

View 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";

View File

@@ -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;

View 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

View 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);

View File

@@ -52,3 +52,4 @@ typedef struct Unit Unit;
typedef struct UnitRef UnitRef;
struct restrict_fs_bpf;
struct restrict_fsaccess_bpf;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 */

View File

@@ -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(

View File

@@ -41,6 +41,7 @@
#CapabilityBoundingSet=
#NoNewPrivileges=no
#ProtectSystem=auto
#RestrictFileSystemAccess=no
#SystemCallArchitectures=
#TimerSlackNSec=
#StatusUnitFormat={{STATUS_UNIT_FORMAT_DEFAULT_STR}}

View File

@@ -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),

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,

View 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

View 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'},
},
},
]

View File

@@ -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

View 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

View 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"

View 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"

View 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