test-bpf-restrict-fsaccess: differentiate between a denial and an unrunnable probe

Make sure the tests can tell the difference between an actual denial and
a probe that simply cannot be run.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
Christian Brauner
2026-08-31 23:33:39 +02:00
parent 9064524e44
commit c35f061634
2 changed files with 158 additions and 81 deletions
+140 -67
View File
@@ -12,6 +12,10 @@
* 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
*
* The probes exit with 0 if the operation was allowed (the bypass works), 1 if
* it was refused with the errno the mechanism under test produces, 2 if it
* could not be attempted and 3 if it was refused with an unexpected errno.
*
* When "attach" is used, the BPF LSM program is loaded with initramfs_s_dev
* set to the current rootfs s_dev, so the calling test script (running from
* the rootfs) continues to work. The process holds all link FDs and blocks;
@@ -27,71 +31,142 @@
#include "bpf-link.h"
#include "bpf-restrict-fsaccess.h"
#include "devnum-util.h"
#include "errno-util.h"
#include "fd-util.h"
#include "log.h"
#include "lsm-util.h"
#include "string-util.h"
#include "tests.h"
/* ---- mmap/mprotect probe commands (no BPF dependency) ----
/* ---- Probe commands (no BPF dependency) ----
*
* These exercise the mmap_file, file_mprotect, and anonymous-mmap LSM hooks.
* The test script copies a file to tmpfs and passes its path here.
* Returns 0 if the operation was allowed, negative errno if denied. */
* Each attempts one operation the hooks are supposed to deny. The exit codes
* let the test script tell a denial by the mechanism under test from a probe
* that could not run at all. */
enum {
PROBE_ALLOWED = 0, /* the operation succeeded, i.e. the bypass works */
PROBE_DENIED = 1, /* refused with the errno the mechanism under test produces */
PROBE_ERROR = 2, /* the operation could not be attempted */
PROBE_DENIED_OTHER = 3, /* refused, but with an unexpected errno (another LSM?) */
};
static int do_mmap_exec(const char *path) {
_cleanup_close_ int fd = -EBADF;
void *addr;
/* Only the errno the mechanism under test produces counts as a denial. */
static int probe_denied(int error, int expected) {
assert(error < 0);
fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0)
return log_error_errno(errno, "Failed to open %s: %m", path);
if (error == expected)
return PROBE_DENIED;
addr = mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED)
return log_info_errno(errno, "PROT_EXEC mmap of %s denied: %m", path);
(void) munmap(addr, 4096);
log_info("PROT_EXEC mmap of %s succeeded", path);
return 0;
log_warning("Refused with unexpected error %s, expected %s.", STRERROR(error), STRERROR(expected));
return PROBE_DENIED_OTHER;
}
static int do_anon_mmap_exec(void) {
void *addr;
addr = mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
return log_info_errno(errno, "Anonymous PROT_EXEC mmap denied: %m");
(void) munmap(addr, 4096);
log_info("Anonymous PROT_EXEC mmap succeeded");
return 0;
}
static int do_mprotect_exec(const char *path) {
static int mmap_probe(const char *path, int prot, const char *what) {
_cleanup_close_ int fd = -EBADF;
void *addr;
int r;
fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0)
return log_error_errno(errno, "Failed to open %s: %m", path);
if (fd < 0) {
log_error_errno(errno, "Failed to open %s: %m", path);
return PROBE_ERROR;
}
addr = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED)
return log_error_errno(errno, "PROT_READ mmap of %s failed: %m", path);
r = mprotect(addr, 4096, PROT_READ | PROT_EXEC);
if (r < 0)
r = -errno;
addr = mmap(NULL, 4096, prot, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) {
r = log_info_errno(errno, "%s of %s denied: %m", what, path);
return probe_denied(r, -EPERM);
}
(void) munmap(addr, 4096);
log_info("%s of %s succeeded", what, path);
return PROBE_ALLOWED;
}
if (r < 0)
return log_info_errno(r, "mprotect PROT_EXEC on %s denied: %m", path);
/* Maps PATH with 'prot', then changes the protection to 'new_prot'. With 'poke' the mapping is written to
* first, so that the page is a private copy. */
static int mprotect_probe(const char *path, int prot, bool poke, int new_prot, const char *what) {
_cleanup_close_ int fd = -EBADF;
void *addr;
int r;
log_info("mprotect PROT_EXEC on %s succeeded", path);
return 0;
fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) {
log_error_errno(errno, "Failed to open %s: %m", path);
return PROBE_ERROR;
}
addr = mmap(NULL, 4096, prot, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) {
log_error_errno(errno, "Initial mmap of %s failed, cannot probe: %m", path);
return PROBE_ERROR;
}
if (poke)
*(volatile uint8_t*) addr = 0x90; /* trigger copy-on-write */
r = RET_NERRNO(mprotect(addr, 4096, new_prot));
(void) munmap(addr, 4096);
if (r < 0) {
log_info_errno(r, "%s on %s denied: %m", what, path);
return probe_denied(r, -EPERM);
}
log_info("%s on %s succeeded", what, path);
return PROBE_ALLOWED;
}
static int do_mmap_exec(const char *path) {
return mmap_probe(path, PROT_READ | PROT_EXEC, "PROT_EXEC mmap");
}
static int do_anon_mmap_exec(void) {
void *addr;
int r;
addr = mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
r = log_info_errno(errno, "Anonymous PROT_EXEC mmap denied: %m");
return probe_denied(r, -EPERM);
}
(void) munmap(addr, 4096);
log_info("Anonymous PROT_EXEC mmap succeeded");
return PROBE_ALLOWED;
}
/* mmap PROT_READ, then add PROT_EXEC: denied for untrusted files, a positive control for trusted ones. */
static int do_mprotect_exec(const char *path) {
return mprotect_probe(path, PROT_READ, /* poke= */ false, PROT_READ | PROT_EXEC, "mprotect PROT_EXEC");
}
static int usage(void) {
log_error("Usage: %s attach|check|mmap-exec PATH|anon-mmap-exec|mprotect-exec PATH",
program_invocation_short_name);
/* Not EXIT_FAILURE, which is PROBE_DENIED. */
return PROBE_ERROR;
}
/* The probes need no BPF support. Returns the probe's exit code, or -ENOENT if the verb is not a probe. */
static int dispatch_probe(int argc, char *argv[]) {
const char *verb = argv[1];
if (argc == 2) {
if (streq(verb, "anon-mmap-exec"))
return do_anon_mmap_exec();
return -ENOENT;
}
if (argc != 3)
return -ENOENT;
if (streq(verb, "mmap-exec"))
return do_mmap_exec(argv[2]);
if (streq(verb, "mprotect-exec"))
return do_mprotect_exec(argv[2]);
return -ENOENT;
}
#if BPF_FRAMEWORK && HAVE_LSM_INTEGRITY_TYPE
@@ -192,47 +267,45 @@ static int do_check(void) {
}
int main(int argc, char *argv[]) {
int r;
test_setup_logging(LOG_DEBUG);
if (argc < 2) {
log_error("Usage: %s attach|check|mmap-exec|anon-mmap-exec|mprotect-exec",
program_invocation_short_name);
return EXIT_FAILURE;
}
if (argc < 2)
return usage();
if (streq(argv[1], "attach"))
return do_attach() < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (streq(argv[1], "check"))
return do_check() < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (streq(argv[1], "mmap-exec") && argc == 3)
return do_mmap_exec(argv[2]) < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (streq(argv[1], "anon-mmap-exec"))
return do_anon_mmap_exec() < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (streq(argv[1], "mprotect-exec") && argc == 3)
return do_mprotect_exec(argv[2]) < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
log_error("Usage: %s attach|check|mmap-exec PATH|anon-mmap-exec|mprotect-exec PATH",
program_invocation_short_name);
return EXIT_FAILURE;
r = dispatch_probe(argc, argv);
if (r >= 0)
return r;
return usage();
}
#else /* ! BPF_FRAMEWORK || ! HAVE_LSM_INTEGRITY_TYPE */
int main(int argc, char *argv[]) {
int r;
test_setup_logging(LOG_DEBUG);
/* mmap/mprotect probes work without BPF */
if (argc >= 2) {
if (streq(argv[1], "mmap-exec") && argc == 3)
return do_mmap_exec(argv[2]) < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (streq(argv[1], "anon-mmap-exec"))
return do_anon_mmap_exec() < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (streq(argv[1], "mprotect-exec") && argc == 3)
return do_mprotect_exec(argv[2]) < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
if (argc < 2)
return usage();
if (streq(argv[1], "attach") || streq(argv[1], "check")) {
log_info("BPF framework not available, attach/check not supported");
return EXIT_TEST_SKIP;
}
log_info("BPF framework not available, attach/check not supported");
return 77; /* skip */
r = dispatch_probe(argc, argv);
if (r >= 0)
return r;
return usage();
}
#endif
+18 -14
View File
@@ -47,6 +47,20 @@ if [[ ! -x "$HELPER" ]]; then
exit 1
fi
# The helper's probes exit with 0 if the operation was allowed, 1 if it was
# refused with the errno the mechanism under test produces, 2 if it could not
# be attempted and 3 if it was refused with an unexpected errno. Only the exact
# code counts, so a broken probe cannot pass as a denial.
expect_probe() {
local expected="$1" what="$2" rc=0
shift 2
"$HELPER" "$@" || rc=$?
if [[ "$rc" -ne "$expected" ]]; then
echo "ERROR: $what: helper '$*' exited with $rc, expected $expected" >&2
exit 1
fi
}
# 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
@@ -162,28 +176,18 @@ echo "Execution from tmpfs blocked: OK"
# Write a test file on the tmpfs mount for mmap/mprotect tests
dd if=/dev/zero of=/tmp/restrict-fsaccess-test/testfile bs=4096 count=1 2>/dev/null
# File-backed PROT_EXEC mmap should be denied.
# The helper exits 0 if mmap succeeds (bad), 1 if denied (good).
if "$HELPER" mmap-exec /tmp/restrict-fsaccess-test/testfile; then
echo "ERROR: PROT_EXEC mmap of tmpfs file should have been blocked!" >&2
exit 1
fi
# File-backed PROT_EXEC mmap should be denied (with EPERM, see expect_probe).
expect_probe 1 "PROT_EXEC mmap of tmpfs file" mmap-exec /tmp/restrict-fsaccess-test/testfile
echo "PROT_EXEC mmap from tmpfs blocked: OK"
# Anonymous PROT_EXEC mmap should be denied (NULL file — mmap_file hook)
if "$HELPER" anon-mmap-exec; then
echo "ERROR: Anonymous PROT_EXEC mmap should have been blocked!" >&2
exit 1
fi
expect_probe 1 "anonymous PROT_EXEC mmap" anon-mmap-exec
echo "Anonymous PROT_EXEC mmap blocked: OK"
# ------ Test: mprotect adding PROT_EXEC is blocked (file_mprotect hook) ------
# mmap PROT_READ then mprotect to PROT_EXEC — the file_mprotect hook should deny this.
if "$HELPER" mprotect-exec /tmp/restrict-fsaccess-test/testfile; then
echo "ERROR: mprotect PROT_EXEC on tmpfs file should have been blocked!" >&2
exit 1
fi
expect_probe 1 "mprotect PROT_EXEC on tmpfs file" mprotect-exec /tmp/restrict-fsaccess-test/testfile
echo "mprotect PROT_EXEC from tmpfs blocked: OK"
# ------ Test: Execution from signed dm-verity device ------