diff --git a/src/test/test-bpf-restrict-fsaccess.c b/src/test/test-bpf-restrict-fsaccess.c index 51c8722c68..0e2358c2ea 100644 --- a/src/test/test-bpf-restrict-fsaccess.c +++ b/src/test/test-bpf-restrict-fsaccess.c @@ -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 diff --git a/test/units/TEST-90-RESTRICT-FSACCESS.enforce.sh b/test/units/TEST-90-RESTRICT-FSACCESS.enforce.sh index 6a69ca9248..77d761eba1 100755 --- a/test/units/TEST-90-RESTRICT-FSACCESS.enforce.sh +++ b/test/units/TEST-90-RESTRICT-FSACCESS.enforce.sh @@ -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 ------