test: add integration tests for RestrictFileSystemAccess= BPF LSM
Add TEST-90-RESTRICT-FSACCESS with two subtests: config subtest — Tests PID1's RestrictFileSystemAccess= configuration parsing and failure modes via system.conf drop-ins and daemon-reexec: - Default RestrictFileSystemAccess=no produces no log messages - RestrictFileSystemAccess=yes without BPF LSM logs appropriate warning - RestrictFileSystemAccess=yes without require_signatures is correctly rejected by the test helper binary's precondition check enforce subtest — Tests actual BPF LSM enforcement using a test helper binary (test-bpf-restrict-fsaccess) that loads the BPF skeleton with initramfs_s_dev set to the rootfs s_dev, pins BPF links, and exits: - Execution from rootfs continues to work (trusted via initramfs_s_dev) - Execution from tmpfs is blocked with EPERM - Execution from a signed dm-verity device is allowed, driven via systemd-run -p RootImage= against the pre-built signed minimal_0 images that mkosi ships and signs at image build time (no on-the-fly squashfs / verity hash tree / signature build required) - After BPF detach, enforcement is lifted All tests skip gracefully when prerequisites are not met (BPF LSM, BPF framework, dm-verity tools, signing keys). Signed-off-by: Christian Brauner <brauner@kernel.org>
This commit is contained in:
@@ -101,6 +101,7 @@ Packages=
|
||||
gzip
|
||||
jq
|
||||
kbd
|
||||
keyutils
|
||||
kmod
|
||||
less
|
||||
lsof
|
||||
|
||||
@@ -544,6 +544,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,
|
||||
|
||||
@@ -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
|
||||
@@ -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'},
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -103,6 +103,7 @@ foreach dirname : [
|
||||
'TEST-87-AUX-UTILS-VM',
|
||||
'TEST-88-UPGRADE',
|
||||
'TEST-89-RESOLVED-MDNS',
|
||||
'TEST-90-RESTRICT-FSACCESS',
|
||||
]
|
||||
subdir(dirname)
|
||||
endforeach
|
||||
|
||||
+103
@@ -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
@@ -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
@@ -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"
|
||||
Executable
+11
@@ -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