// SPDX-License-Identifier: GPL-2.0 /* Copyright (c) 2026 Isovalent */ #include #include #include #include #include #include #include #include #include #include #include "bpf/libbpf_internal.h" /* for libbpf_sha256() */ #include "bpf/skel_internal.h" /* for loader ctx layout (bpf_loader_ctx etc) */ #include "test_signed_loader.skel.h" #include "test_signed_loader_map.skel.h" #include "test_signed_loader_data.skel.h" #include "test_signed_loader_lsm.skel.h" enum { BPF_SIG_UNSIGNED = 0, BPF_SIG_VERIFIED, }; enum { BPF_SIG_KEYRING_NONE = 0, BPF_SIG_KEYRING_BUILTIN, BPF_SIG_KEYRING_SECONDARY, BPF_SIG_KEYRING_PLATFORM, BPF_SIG_KEYRING_USER, }; static int load_loader(const void *insns, __u32 insns_sz, int map_fd, const void *sig, __u32 sig_sz, __s32 keyring_id, __u32 fd_array_cnt) { union bpf_attr attr; int fd; memset(&attr, 0, sizeof(attr)); attr.prog_type = BPF_PROG_TYPE_SYSCALL; attr.insns = ptr_to_u64(insns); attr.insn_cnt = insns_sz / sizeof(struct bpf_insn); attr.license = ptr_to_u64("Dual BSD/GPL"); attr.prog_flags = BPF_F_SLEEPABLE; attr.fd_array = ptr_to_u64(&map_fd); if (sig) { attr.signature = ptr_to_u64(sig); attr.signature_size = sig_sz; attr.keyring_id = keyring_id; } attr.fd_array_cnt = fd_array_cnt; memcpy(attr.prog_name, "__loader.prog", sizeof("__loader.prog")); fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, offsetofend(union bpf_attr, keyring_id)); return fd < 0 ? -errno : fd; } static int run_gen_loader(const void *insns, __u32 insns_sz, const void *data, __u32 data_sz, const void *excl, __u32 excl_sz, const void *sig, __u32 sig_sz, void *ctx, __u32 ctx_sz, bool *loader_ran) { LIBBPF_OPTS(bpf_map_create_opts, mopts, .excl_prog_hash = excl, .excl_prog_hash_size = excl_sz); __u32 key = 0; union bpf_attr attr; int map_fd, prog_fd, ret; *loader_ran = false; map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "__loader.map", 4, data_sz, 1, &mopts); if (map_fd < 0) return -errno; if (bpf_map_update_elem(map_fd, &key, data, 0)) { ret = -errno; goto out_map; } if (bpf_map_freeze(map_fd)) { ret = -errno; goto out_map; } memset(&attr, 0, sizeof(attr)); attr.prog_type = BPF_PROG_TYPE_SYSCALL; attr.insns = ptr_to_u64(insns); attr.insn_cnt = insns_sz / sizeof(struct bpf_insn); attr.license = ptr_to_u64("Dual BSD/GPL"); attr.prog_flags = BPF_F_SLEEPABLE; attr.fd_array = ptr_to_u64(&map_fd); if (sig) { attr.signature = ptr_to_u64(sig); attr.signature_size = sig_sz; attr.keyring_id = KEY_SPEC_SESSION_KEYRING; attr.fd_array_cnt = 1; } memcpy(attr.prog_name, "__loader.prog", sizeof("__loader.prog")); prog_fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, offsetofend(union bpf_attr, keyring_id)); if (prog_fd < 0) { ret = -errno; goto out_map; } memset(&attr, 0, sizeof(attr)); attr.test.prog_fd = prog_fd; attr.test.ctx_in = ptr_to_u64(ctx); attr.test.ctx_size_in = ctx_sz; if (syscall(__NR_bpf, BPF_PROG_RUN, &attr, offsetofend(union bpf_attr, test)) < 0) { ret = -errno; goto out_prog; } *loader_ran = true; ret = (int)attr.test.retval; out_prog: close(prog_fd); out_map: close(map_fd); return ret; } static void close_loader_ctx_fds(void *ctx, int nr_maps, int nr_progs) { struct bpf_map_desc *md = (struct bpf_map_desc *)((char *)ctx + sizeof(struct bpf_loader_ctx)); struct bpf_prog_desc *pd = (struct bpf_prog_desc *)(md + nr_maps); int i; for (i = 0; i < nr_maps; i++) if (md[i].map_fd > 0) close(md[i].map_fd); for (i = 0; i < nr_progs; i++) if (pd[i].prog_fd > 0) close(pd[i].prog_fd); } static int run_setup(const char *cmd, const char *dir) { int pid, status; pid = fork(); if (pid < 0) return -errno; if (pid == 0) { execlp("./verify_sig_setup.sh", "./verify_sig_setup.sh", cmd, dir, NULL); exit(1); } if (waitpid(pid, &status, 0) < 0) return -errno; return (WIFEXITED(status) && WEXITSTATUS(status) == 0) ? 0 : -EINVAL; } static int sign_buf(const char *dir, const void *buf, __u32 len, void *sig, __u32 *sig_sz) { char data_tmpl[PATH_MAX], key[PATH_MAX]; char sigpath[PATH_MAX + sizeof(".p7s")]; int fd, pid, status, ret; struct stat st; ret = snprintf(data_tmpl, sizeof(data_tmpl), "%s/dataXXXXXX", dir); if (ret < 0 || ret >= (int)sizeof(data_tmpl)) return -ENAMETOOLONG; ret = 0; fd = mkstemp(data_tmpl); if (fd < 0) return -errno; if (write(fd, buf, len) != (ssize_t)len) { close(fd); ret = -EIO; goto out; } close(fd); pid = fork(); if (pid < 0) { ret = -errno; goto out; } if (pid == 0) { snprintf(key, sizeof(key), "%s/signing_key.pem", dir); execlp("./sign-file", "./sign-file", "-d", "sha256", key, key, data_tmpl, NULL); exit(1); } if (waitpid(pid, &status, 0) < 0 || !WIFEXITED(status) || WEXITSTATUS(status)) { ret = -EINVAL; goto out; } snprintf(sigpath, sizeof(sigpath), "%s.p7s", data_tmpl); if (stat(sigpath, &st) < 0) { ret = -errno; goto out; } if (st.st_size > (off_t)*sig_sz) { ret = -E2BIG; goto out_sig; } fd = open(sigpath, O_RDONLY); if (fd < 0) { ret = -errno; goto out_sig; } if (read(fd, sig, st.st_size) != st.st_size) { close(fd); ret = -EIO; goto out_sig; } close(fd); *sig_sz = st.st_size; out_sig: unlink(sigpath); out: unlink(data_tmpl); return ret; } struct gen_loader_fixture { struct test_signed_loader *skel; struct gen_loader_opts gopts; unsigned char *blob; void *ctx; __u32 data_sz; __u32 ctx_sz; int nr_maps; int nr_progs; __u8 excl[SHA256_DIGEST_LENGTH]; }; static int gen_loader_fixture_init(struct gen_loader_fixture *f) { LIBBPF_OPTS(gen_loader_opts, gopts, .gen_hash = true); int nr_maps = 0, nr_progs = 0; struct bpf_program *p; struct bpf_map *m; memset(f, 0, sizeof(*f)); f->skel = test_signed_loader__open(); if (!ASSERT_OK_PTR(f->skel, "skel_open")) return -1; if (!ASSERT_OK(bpf_object__gen_loader(f->skel->obj, &gopts), "gen_loader")) return -1; if (!ASSERT_OK(bpf_object__load(f->skel->obj), "gen_load")) return -1; f->gopts = gopts; bpf_object__for_each_program(p, f->skel->obj) nr_progs++; bpf_object__for_each_map(m, f->skel->obj) nr_maps++; f->nr_maps = nr_maps; f->nr_progs = nr_progs; f->ctx_sz = sizeof(struct bpf_loader_ctx) + nr_maps * sizeof(struct bpf_map_desc) + nr_progs * sizeof(struct bpf_prog_desc); f->ctx = calloc(1, f->ctx_sz); if (!ASSERT_OK_PTR(f->ctx, "ctx_alloc")) return -1; ((struct bpf_loader_ctx *)f->ctx)->sz = f->ctx_sz; f->data_sz = gopts.data_sz; f->blob = malloc(f->data_sz); if (!ASSERT_OK_PTR(f->blob, "blob_alloc")) return -1; memcpy(f->blob, gopts.data, f->data_sz); /* excl_prog_hash = SHA256(loader insns) == the loader's prog->digest. */ libbpf_sha256(gopts.insns, gopts.insns_sz, f->excl); return 0; } static void gen_loader_fixture_fini(struct gen_loader_fixture *f) { if (f->ctx) close_loader_ctx_fds(f->ctx, f->nr_maps, f->nr_progs); free(f->blob); free(f->ctx); test_signed_loader__destroy(f->skel); } static void metadata_match(void) { struct gen_loader_fixture f; bool ran; int r; if (gen_loader_fixture_init(&f) == 0) { r = run_gen_loader(f.gopts.insns, f.gopts.insns_sz, f.blob, f.data_sz, f.excl, sizeof(f.excl), NULL, 0, f.ctx, f.ctx_sz, &ran); ASSERT_TRUE(ran, "loader ran"); ASSERT_EQ(r, 0, "honest loader retval"); } gen_loader_fixture_fini(&f); } static void signature_enforced(void) { static const __u8 junk[64] = { 0x30, 0x42, 0x13, 0x37, }; struct gen_loader_fixture f; int fd; if (gen_loader_fixture_init(&f) == 0) { /* * A present-but-invalid signature (the cert bytes are not a * PKCS#7 signature) must be rejected at load: the signature * path is honored, not ignored. (The valid path is covered by * the signed lskels.) Pin -EBADMSG, the PKCS#7 parse failure: * a looser fd < 0 check could also be satisfied by the sparse * fd_array rejection (-EACCES) that the loader's map reference * would trip even if the signature were silently ignored. */ fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk, sizeof(junk), KEY_SPEC_SESSION_KEYRING, 0); ASSERT_EQ(fd, -EBADMSG, "invalid signature rejected at load"); if (fd >= 0) close(fd); } gen_loader_fixture_fini(&f); } static void signed_nonexcl_fd_array_rejected(void) { static const __u8 junk[64] = { 0x30, 0x42, 0x13, 0x37, }; struct gen_loader_fixture f; int map_fd, fd; if (gen_loader_fixture_init(&f) == 0) { /* * A signed program may only bind exclusive maps through fd_array * (their contents are folded into the signature). Binding a * non-exclusive map is rejected, before the signature is even * examined. */ map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "nonexcl", 4, f.data_sz, 1, NULL); if (ASSERT_OK_FD(map_fd, "nonexcl_map")) { if (ASSERT_OK(bpf_map_freeze(map_fd), "freeze")) { fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, junk, sizeof(junk), KEY_SPEC_SESSION_KEYRING, 1); ASSERT_EQ(fd, -EPERM, "non-exclusive map in signed fd_array rejected"); if (fd >= 0) close(fd); } close(map_fd); } } gen_loader_fixture_fini(&f); } static void signed_unfrozen_fd_array_rejected(void) { static const __u8 junk[64] = { 0x30, 0x42, 0x13, 0x37, }; LIBBPF_OPTS(bpf_map_create_opts, mopts); struct gen_loader_fixture f; __u32 key = 0; int map_fd, fd; if (gen_loader_fixture_init(&f) == 0) { /* * The metadata map must be frozen before a signed load so the * folded bytes cannot change afterwards. Bind an exclusive map * with matching contents but skip the freeze: the load must be * rejected by the frozen check with -EPERM. The exclusivity * check right after it would pass, so the errno uniquely pins * the freeze requirement. */ mopts.excl_prog_hash = f.excl; mopts.excl_prog_hash_size = sizeof(f.excl); map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "unfrozen", 4, f.data_sz, 1, &mopts); if (ASSERT_OK_FD(map_fd, "unfrozen_map")) { if (ASSERT_OK(bpf_map_update_elem(map_fd, &key, f.blob, 0), "update")) { fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, junk, sizeof(junk), KEY_SPEC_SESSION_KEYRING, 1); ASSERT_EQ(fd, -EPERM, "unfrozen map in signed fd_array rejected"); if (fd >= 0) close(fd); } close(map_fd); } } gen_loader_fixture_fini(&f); } static void signed_nonarray_fd_array_rejected(void) { static const __u8 junk[64] = { 0x30, 0x42, 0x13, 0x37, }; LIBBPF_OPTS(bpf_map_create_opts, mopts); struct gen_loader_fixture f; int map_fd, fd; if (gen_loader_fixture_init(&f) == 0) { /* * Only a plain BPF_MAP_TYPE_ARRAY may be folded into the * signature. An exclusive map of any other type is rejected * (-EINVAL) rather than folded - this is the type gate that * keeps arena maps (map_direct_value_addr() returns a user * address) and insn-array maps (buffer smaller than value_size) * out of the hashed region, where the old code would have * memcpy()'d from them. A hash map stands in here: it is * exclusive (bound to the loader digest) but not an array. */ mopts.excl_prog_hash = f.excl; mopts.excl_prog_hash_size = sizeof(f.excl); map_fd = bpf_map_create(BPF_MAP_TYPE_HASH, "excl_hash", 4, 4, 1, &mopts); if (ASSERT_OK_FD(map_fd, "excl_hash_map")) { fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, junk, sizeof(junk), KEY_SPEC_SESSION_KEYRING, 1); ASSERT_EQ(fd, -EINVAL, "non-array map in signed fd_array rejected"); if (fd >= 0) close(fd); close(map_fd); } } gen_loader_fixture_fini(&f); } static int setup_meta_map(const struct gen_loader_fixture *f); static void signed_btf_fd_array_rejected(void) { char dir_tmpl[] = "/tmp/signed_loader_btfXXXXXX", *dir = NULL; __u32 sig_sz = 8192; int map_fd = -1, prog_fd = -1; unsigned char *buf = NULL; struct gen_loader_fixture f; bool have_fixture = false; struct btf *btf = NULL; union bpf_attr attr; int fds[2]; __u8 sig[8192]; syscall(__NR_request_key, "keyring", "_uid.0", NULL, KEY_SPEC_SESSION_KEYRING); dir = mkdtemp(dir_tmpl); if (!ASSERT_OK_PTR(dir, "mkdtemp")) return; if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) { rmdir(dir); return; } have_fixture = true; if (gen_loader_fixture_init(&f) != 0) goto out; /* * fd_array binds maps and BTFs alike, but only exclusive array maps are * folded into the signature. Build an otherwise genuinely signed load - * insns || metadata, exclusive frozen map at fd_array[0] - then smuggle * an extra BTF into fd_array[1]. A signed program may not bind any BTF, * so resolving the fd_array entries rejects the BTF with -EACCES (in * __add_used_btf(), before the signature is even verified). */ buf = malloc((size_t)f.gopts.insns_sz + f.data_sz); if (!ASSERT_OK_PTR(buf, "signbuf")) goto out; memcpy(buf, f.gopts.insns, f.gopts.insns_sz); memcpy(buf + f.gopts.insns_sz, f.blob, f.data_sz); if (!ASSERT_OK(sign_buf(dir, buf, f.gopts.insns_sz + f.data_sz, sig, &sig_sz), "sign insns||metadata")) goto out; map_fd = setup_meta_map(&f); if (!ASSERT_OK_FD(map_fd, "meta_map")) goto out; btf = btf__new_empty(); if (!ASSERT_OK_PTR(btf, "btf_new_empty")) goto out; btf__add_int(btf, "int", 4, BTF_INT_SIGNED); if (!ASSERT_OK(btf__load_into_kernel(btf), "btf_load")) goto out; fds[0] = map_fd; fds[1] = btf__fd(btf); memset(&attr, 0, sizeof(attr)); attr.prog_type = BPF_PROG_TYPE_SYSCALL; attr.insns = ptr_to_u64(f.gopts.insns); attr.insn_cnt = f.gopts.insns_sz / sizeof(struct bpf_insn); attr.license = ptr_to_u64("Dual BSD/GPL"); attr.prog_flags = BPF_F_SLEEPABLE; attr.fd_array = ptr_to_u64(fds); attr.fd_array_cnt = 2; attr.signature = ptr_to_u64(sig); attr.signature_size = sig_sz; attr.keyring_id = KEY_SPEC_SESSION_KEYRING; memcpy(attr.prog_name, "__loader.prog", sizeof("__loader.prog")); prog_fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, offsetofend(union bpf_attr, keyring_id)); ASSERT_EQ(prog_fd < 0 ? -errno : prog_fd, -EACCES, "BTF in signed fd_array rejected"); if (prog_fd >= 0) close(prog_fd); out: if (btf) btf__free(btf); if (map_fd >= 0) close(map_fd); if (have_fixture) gen_loader_fixture_fini(&f); if (dir) run_setup("cleanup", dir); free(buf); } static void signature_failure_logs(void) { static const __u8 junk[64] = { 0x30, 0x42, 0x13, 0x37, }; char log_buf[1024] = {}; struct gen_loader_fixture f; union bpf_attr attr; int fd; if (gen_loader_fixture_init(&f) == 0) { /* * Signature verification now runs inside bpf_check(), so a * failure is reported through the verifier log. A present-but- * invalid signature is rejected and the log says why. */ memset(&attr, 0, sizeof(attr)); attr.prog_type = BPF_PROG_TYPE_SYSCALL; attr.insns = ptr_to_u64(f.gopts.insns); attr.insn_cnt = f.gopts.insns_sz / sizeof(struct bpf_insn); attr.license = ptr_to_u64("Dual BSD/GPL"); attr.prog_flags = BPF_F_SLEEPABLE; attr.signature = ptr_to_u64(junk); attr.signature_size = sizeof(junk); attr.keyring_id = KEY_SPEC_SESSION_KEYRING; attr.log_level = 1; attr.log_buf = ptr_to_u64(log_buf); attr.log_size = sizeof(log_buf); memcpy(attr.prog_name, "__loader.prog", sizeof("__loader.prog")); fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, offsetofend(union bpf_attr, keyring_id)); ASSERT_LT(fd, 0, "invalid signature rejected at load"); if (fd >= 0) close(fd); ASSERT_HAS_SUBSTR(log_buf, "signature verification failed", "verifier logs signature failure"); } gen_loader_fixture_fini(&f); } static void signature_too_large(void) { static const __u8 junk[64] = {}; struct gen_loader_fixture f; int fd; if (gen_loader_fixture_init(&f) == 0) { /* * signature_size beyond the kernel's bound (KMALLOC_MAX_CACHE_SIZE) * is rejected before the buffer is read. */ fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk, 64 << 20, KEY_SPEC_SESSION_KEYRING, 0); ASSERT_EQ(fd, -EINVAL, "oversized signature rejected"); if (fd >= 0) close(fd); } gen_loader_fixture_fini(&f); } static void signature_zero_size(void) { static const __u8 junk[64] = {}; struct gen_loader_fixture f; int fd; if (gen_loader_fixture_init(&f) == 0) { /* * A present signature with signature_size == 0 is rejected * up front, before the keyring is resolved or the signature * buffer is read. */ fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk, 0, KEY_SPEC_SESSION_KEYRING, 0); ASSERT_EQ(fd, -EINVAL, "zero-size signature rejected"); if (fd >= 0) close(fd); } gen_loader_fixture_fini(&f); } static void signature_bad_keyring(void) { static const __u8 junk[64] = {}; struct gen_loader_fixture f; int fd; if (gen_loader_fixture_init(&f) == 0) { /* * A present signature with a keyring_id that resolves to no key is * rejected up front: bpf_prog_verify_signature() fails the keyring * lookup (-EINVAL) before it ever looks at the signature bytes. A * large positive serial takes the user-keyring path and won't exist. */ fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk, sizeof(junk), INT_MAX, 0); ASSERT_EQ(fd, -EINVAL, "signature with bad keyring_id rejected"); if (fd >= 0) close(fd); } gen_loader_fixture_fini(&f); } /* * A signed loader must ignore ctx-supplied map dimensions: the host cannot * resize a signed program's maps via the loader ctx. Drive a one-map program * through gen_loader, ask (via ctx) for every map to be resized to a bogus * value, and confirm the created maps keep their attested size. */ #define GATING_BOGUS_MAX 0x4000 static void metadata_ctx_max_entries_ignored(void) { LIBBPF_OPTS(gen_loader_opts, gopts, .gen_hash = true); struct test_signed_loader_map *skel; __u8 excl[SHA256_DIGEST_LENGTH]; int nr_maps = 0, nr_progs = 0, i, checked = 0, r; struct bpf_program *p; struct bpf_map *m; struct bpf_map_desc *md; unsigned char *blob; __u32 ctx_sz, data_sz; void *ctx; bool ran; skel = test_signed_loader_map__open(); if (!ASSERT_OK_PTR(skel, "skel_open")) return; if (!ASSERT_OK(bpf_object__gen_loader(skel->obj, &gopts), "gen_loader")) goto destroy; if (!ASSERT_OK(bpf_object__load(skel->obj), "gen_load")) goto destroy; bpf_object__for_each_program(p, skel->obj) nr_progs++; bpf_object__for_each_map(m, skel->obj) nr_maps++; ctx_sz = sizeof(struct bpf_loader_ctx) + nr_maps * sizeof(struct bpf_map_desc) + nr_progs * sizeof(struct bpf_prog_desc); ctx = calloc(1, ctx_sz); if (!ASSERT_OK_PTR(ctx, "ctx_alloc")) goto destroy; ((struct bpf_loader_ctx *)ctx)->sz = ctx_sz; md = (struct bpf_map_desc *)((char *)ctx + sizeof(struct bpf_loader_ctx)); for (i = 0; i < nr_maps; i++) md[i].max_entries = GATING_BOGUS_MAX; libbpf_sha256(gopts.insns, gopts.insns_sz, excl); data_sz = gopts.data_sz; blob = malloc(data_sz); if (!ASSERT_OK_PTR(blob, "blob_alloc")) goto free_ctx; memcpy(blob, gopts.data, data_sz); r = run_gen_loader(gopts.insns, gopts.insns_sz, blob, data_sz, excl, sizeof(excl), NULL, 0, ctx, ctx_sz, &ran); if (!ASSERT_TRUE(ran, "loader ran") || !ASSERT_EQ(r, 0, "loader retval")) goto free_blob; for (i = 0; i < nr_maps; i++) { struct bpf_map_info info; __u32 ilen = sizeof(info); int fd = md[i].map_fd; if (fd <= 0) continue; memset(&info, 0, sizeof(info)); if (ASSERT_OK(bpf_map_get_info_by_fd(fd, &info, &ilen), "map_info")) { ASSERT_NEQ(info.max_entries, GATING_BOGUS_MAX, "ctx max_entries ignored for signed loader"); checked++; } } ASSERT_GT(checked, 0, "inspected a created map"); free_blob: free(blob); free_ctx: close_loader_ctx_fds(ctx, nr_maps, nr_progs); free(ctx); destroy: test_signed_loader_map__destroy(skel); } /* * A signed loader must also ignore ctx-supplied initial_value: the host cannot * re-seed a signed program's map contents through the loader ctx. Drive a * program with one initialized global (a .data map) through gen_loader, point * every map's ctx initial_value at an adversarial buffer, and confirm the * created map still holds the attested value, never the ctx bytes. */ #define DATA_MAGIC 0x5eed1234abad1deaULL static void metadata_ctx_initial_value_ignored(void) { LIBBPF_OPTS(gen_loader_opts, gopts, .gen_hash = true); struct test_signed_loader_data *skel; __u8 excl[SHA256_DIGEST_LENGTH], evil[64]; int nr_maps = 0, nr_progs = 0, i, found = 0, r; struct bpf_program *p; struct bpf_map *m; struct bpf_map_desc *md; unsigned char *blob; __u32 ctx_sz, data_sz; void *ctx; bool ran; skel = test_signed_loader_data__open(); if (!ASSERT_OK_PTR(skel, "skel_open")) return; if (!ASSERT_OK(bpf_object__gen_loader(skel->obj, &gopts), "gen_loader")) goto destroy; if (!ASSERT_OK(bpf_object__load(skel->obj), "gen_load")) goto destroy; bpf_object__for_each_program(p, skel->obj) nr_progs++; bpf_object__for_each_map(m, skel->obj) nr_maps++; ctx_sz = sizeof(struct bpf_loader_ctx) + nr_maps * sizeof(struct bpf_map_desc) + nr_progs * sizeof(struct bpf_prog_desc); ctx = calloc(1, ctx_sz); if (!ASSERT_OK_PTR(ctx, "ctx_alloc")) goto destroy; ((struct bpf_loader_ctx *)ctx)->sz = ctx_sz; memset(evil, 0xAA, sizeof(evil)); md = (struct bpf_map_desc *)((char *)ctx + sizeof(struct bpf_loader_ctx)); for (i = 0; i < nr_maps; i++) md[i].initial_value = ptr_to_u64(evil); libbpf_sha256(gopts.insns, gopts.insns_sz, excl); data_sz = gopts.data_sz; blob = malloc(data_sz); if (!ASSERT_OK_PTR(blob, "blob_alloc")) goto free_ctx; memcpy(blob, gopts.data, data_sz); r = run_gen_loader(gopts.insns, gopts.insns_sz, blob, data_sz, excl, sizeof(excl), NULL, 0, ctx, ctx_sz, &ran); if (!ASSERT_TRUE(ran, "loader ran") || !ASSERT_EQ(r, 0, "loader retval")) goto free_blob; for (i = 0; i < nr_maps; i++) { struct bpf_map_info info; __u32 ilen = sizeof(info), key = 0; __u8 value[64] = {}; __u64 got; int fd = md[i].map_fd; if (fd <= 0) continue; memset(&info, 0, sizeof(info)); if (!ASSERT_OK(bpf_map_get_info_by_fd(fd, &info, &ilen), "map_info")) continue; if (info.value_size <= sizeof(value) && bpf_map_lookup_elem(fd, &key, value) == 0) { memcpy(&got, value, sizeof(got)); /* attested .data survives; ctx bytes (0xAA..) ignored */ if (got == DATA_MAGIC) found = 1; ASSERT_NEQ(got, 0xAAAAAAAAAAAAAAAAULL, "ctx initial_value ignored for signed loader"); } } ASSERT_EQ(found, 1, "attested .data value preserved"); free_blob: free(blob); free_ctx: close_loader_ctx_fds(ctx, nr_maps, nr_progs); free(ctx); destroy: test_signed_loader_data__destroy(skel); } /* * The load-time signature must authenticate the loader instructions: a valid * signature loads, and the very same signature over one-byte-tampered insns is * rejected. Uses ./verify_sig_setup.sh + ./sign-file at runtime, like * verify_pkcs7_sig, and verifies against the session keyring the key was added * to. (signature_enforced/_too_large only cover a malformed signature.) */ static void signature_authenticates_insns(void) { LIBBPF_OPTS(gen_loader_opts, gopts, .gen_hash = true); char dir_tmpl[] = "/tmp/signed_loaderXXXXXX", *dir; struct test_signed_loader *skel = NULL; __u8 excl[SHA256_DIGEST_LENGTH], sig[8192]; __u32 sig_sz = sizeof(sig), insns_sz, data_sz, ctx_sz; unsigned char *insns = NULL, *tampered = NULL, *blob = NULL; unsigned char *signbuf = NULL; int nr_maps = 0, nr_progs = 0, r; struct bpf_program *p; struct bpf_map *m; void *ctx = NULL; bool ran; syscall(__NR_request_key, "keyring", "_uid.0", NULL, KEY_SPEC_SESSION_KEYRING); dir = mkdtemp(dir_tmpl); if (!ASSERT_OK_PTR(dir, "mkdtemp")) return; if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) { rmdir(dir); return; } skel = test_signed_loader__open(); if (!ASSERT_OK_PTR(skel, "skel_open")) goto cleanup; if (!ASSERT_OK(bpf_object__gen_loader(skel->obj, &gopts), "gen_loader")) goto cleanup; if (!ASSERT_OK(bpf_object__load(skel->obj), "gen_load")) goto cleanup; bpf_object__for_each_program(p, skel->obj) nr_progs++; bpf_object__for_each_map(m, skel->obj) nr_maps++; ctx_sz = sizeof(struct bpf_loader_ctx) + nr_maps * sizeof(struct bpf_map_desc) + nr_progs * sizeof(struct bpf_prog_desc); insns_sz = gopts.insns_sz; data_sz = gopts.data_sz; ctx = calloc(1, ctx_sz); insns = malloc(insns_sz); tampered = malloc(insns_sz); blob = malloc(data_sz); if (!ASSERT_OK_PTR(ctx, "ctx") || !ASSERT_OK_PTR(insns, "insns") || !ASSERT_OK_PTR(tampered, "tampered") || !ASSERT_OK_PTR(blob, "blob")) goto cleanup; memcpy(insns, gopts.insns, insns_sz); memcpy(blob, gopts.data, data_sz); libbpf_sha256(insns, insns_sz, excl); signbuf = malloc((size_t)insns_sz + data_sz); if (!ASSERT_OK_PTR(signbuf, "signbuf")) goto cleanup; memcpy(signbuf, insns, insns_sz); memcpy(signbuf + insns_sz, blob, data_sz); if (!ASSERT_OK(sign_buf(dir, signbuf, insns_sz + data_sz, sig, &sig_sz), "sign-file")) goto cleanup; memset(ctx, 0, ctx_sz); ((struct bpf_loader_ctx *)ctx)->sz = ctx_sz; r = run_gen_loader(insns, insns_sz, blob, data_sz, excl, sizeof(excl), sig, sig_sz, ctx, ctx_sz, &ran); ASSERT_TRUE(ran, "valid signature: loader loaded and ran"); ASSERT_EQ(r, 0, "valid signature accepted"); close_loader_ctx_fds(ctx, nr_maps, nr_progs); memcpy(tampered, insns, insns_sz); tampered[insns_sz / 2] ^= 0xff; /* * Bind the metadata map to the tampered loader's own digest, so the * verifier's exclusive-map check (excl_prog_sha == prog->digest) passes * and the signature - verified after the maps are resolved - is what * rejects the load. This is the attacker's best case: even after * re-binding the exclusive map to their tampered loader, the signature * over the original insns || metadata still fails. (Leaving the map * bound to the original digest would instead trip the excl check first.) */ libbpf_sha256(tampered, insns_sz, excl); memset(ctx, 0, ctx_sz); ((struct bpf_loader_ctx *)ctx)->sz = ctx_sz; r = run_gen_loader(tampered, insns_sz, blob, data_sz, excl, sizeof(excl), sig, sig_sz, ctx, ctx_sz, &ran); ASSERT_FALSE(ran, "tampered loader rejected before run"); ASSERT_EQ(r, -EKEYREJECTED, "signature is bound to the instructions"); cleanup: free(insns); free(tampered); free(blob); free(signbuf); free(ctx); test_signed_loader__destroy(skel); run_setup("cleanup", dir); } static void signature_authenticates_metadata(void) { LIBBPF_OPTS(gen_loader_opts, gopts, .gen_hash = true); char dir_tmpl[] = "/tmp/signed_loaderXXXXXX", *dir; struct test_signed_loader *skel = NULL; __u8 excl[SHA256_DIGEST_LENGTH], sig[8192]; __u32 sig_sz = sizeof(sig), insns_sz, data_sz, ctx_sz; unsigned char *insns = NULL, *blob = NULL; unsigned char *signbuf = NULL; int nr_maps = 0, nr_progs = 0, r; struct bpf_program *p; struct bpf_map *m; void *ctx = NULL; bool ran; syscall(__NR_request_key, "keyring", "_uid.0", NULL, KEY_SPEC_SESSION_KEYRING); dir = mkdtemp(dir_tmpl); if (!ASSERT_OK_PTR(dir, "mkdtemp")) return; if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) { rmdir(dir); return; } skel = test_signed_loader__open(); if (!ASSERT_OK_PTR(skel, "skel_open")) goto cleanup; if (!ASSERT_OK(bpf_object__gen_loader(skel->obj, &gopts), "gen_loader")) goto cleanup; if (!ASSERT_OK(bpf_object__load(skel->obj), "gen_load")) goto cleanup; bpf_object__for_each_program(p, skel->obj) nr_progs++; bpf_object__for_each_map(m, skel->obj) nr_maps++; ctx_sz = sizeof(struct bpf_loader_ctx) + nr_maps * sizeof(struct bpf_map_desc) + nr_progs * sizeof(struct bpf_prog_desc); insns_sz = gopts.insns_sz; data_sz = gopts.data_sz; ctx = calloc(1, ctx_sz); insns = malloc(insns_sz); blob = malloc(data_sz); if (!ASSERT_OK_PTR(ctx, "ctx") || !ASSERT_OK_PTR(insns, "insns") || !ASSERT_OK_PTR(blob, "blob")) goto cleanup; memcpy(insns, gopts.insns, insns_sz); memcpy(blob, gopts.data, data_sz); libbpf_sha256(insns, insns_sz, excl); signbuf = malloc((size_t)insns_sz + data_sz); if (!ASSERT_OK_PTR(signbuf, "signbuf")) goto cleanup; memcpy(signbuf, insns, insns_sz); memcpy(signbuf + insns_sz, blob, data_sz); if (!ASSERT_OK(sign_buf(dir, signbuf, insns_sz + data_sz, sig, &sig_sz), "sign-file")) goto cleanup; memset(ctx, 0, ctx_sz); ((struct bpf_loader_ctx *)ctx)->sz = ctx_sz; r = run_gen_loader(insns, insns_sz, blob, data_sz, excl, sizeof(excl), sig, sig_sz, ctx, ctx_sz, &ran); ASSERT_TRUE(ran, "valid signature: loader loaded and ran"); ASSERT_EQ(r, 0, "valid signature accepted"); close_loader_ctx_fds(ctx, nr_maps, nr_progs); /* * Tamper the metadata after signing while leaving the instructions * and thus the exclusive hash binding untouched: the map freezes * fine and excl_prog_sha still matches the loader's digest, so the * load reaches signature verification, which folds the live frozen * map bytes into the checked payload and must reject the modified * blob. A kernel folding anything but the map contents themselves * would wrongly accept this load. */ blob[data_sz / 2] ^= 0xff; memset(ctx, 0, ctx_sz); ((struct bpf_loader_ctx *)ctx)->sz = ctx_sz; r = run_gen_loader(insns, insns_sz, blob, data_sz, excl, sizeof(excl), sig, sig_sz, ctx, ctx_sz, &ran); ASSERT_FALSE(ran, "tampered metadata rejected before run"); ASSERT_EQ(r, -EKEYREJECTED, "signature is bound to the metadata"); cleanup: free(insns); free(blob); free(signbuf); free(ctx); test_signed_loader__destroy(skel); run_setup("cleanup", dir); } static int make_excl_map(__u32 flags, __u32 value_size) { LIBBPF_OPTS(bpf_map_create_opts, opts); __u8 hash[SHA256_DIGEST_LENGTH] = { 1 }; /* any 32-byte value */ opts.excl_prog_hash = hash; opts.excl_prog_hash_size = sizeof(hash); opts.map_flags = flags; return bpf_map_create(BPF_MAP_TYPE_ARRAY, "md", 4, value_size, 1, &opts); } static void hash_requires_frozen(void) { __u8 hbuf[SHA256_DIGEST_LENGTH], val[64] = {}; struct bpf_map_info info; __u32 ilen, key = 0; int fd; fd = make_excl_map(0, sizeof(val)); if (!ASSERT_OK_FD(fd, "excl_map")) return; ASSERT_OK(bpf_map_update_elem(fd, &key, val, 0), "update"); memset(&info, 0, sizeof(info)); info.hash = ptr_to_u64(hbuf); info.hash_size = sizeof(hbuf); ilen = sizeof(info); ASSERT_EQ(bpf_map_get_info_by_fd(fd, &info, &ilen), -EPERM, "hash of unfrozen map rejected"); close(fd); } static void no_update_after_freeze(void) { __u8 val[64] = {}; __u32 key = 0; int fd; fd = make_excl_map(0, sizeof(val)); if (!ASSERT_OK_FD(fd, "excl_map")) return; ASSERT_OK(bpf_map_update_elem(fd, &key, val, 0), "update"); ASSERT_OK(bpf_map_freeze(fd), "freeze"); ASSERT_EQ(bpf_map_update_elem(fd, &key, val, 0), -EPERM, "update after freeze rejected"); close(fd); } static void freeze_writable_mmap(void) { void *w; int fd; fd = make_excl_map(BPF_F_MMAPABLE, 4096); if (!ASSERT_OK_FD(fd, "excl_mmapable_map")) return; w = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (ASSERT_OK_PTR(w, "writable_mmap")) { ASSERT_EQ(bpf_map_freeze(fd), -EBUSY, "freeze rejected while writable mmap held"); munmap(w, 4096); } close(fd); } static void no_writable_mmap_frozen(void) { void *w; int fd; fd = make_excl_map(BPF_F_MMAPABLE, 4096); if (!ASSERT_OK_FD(fd, "excl_mmapable_map")) return; ASSERT_OK(bpf_map_freeze(fd), "freeze"); w = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); ASSERT_EQ(w, MAP_FAILED, "writable mmap of frozen map rejected"); if (w != MAP_FAILED) munmap(w, 4096); close(fd); } static void map_hash_matches_libbpf(void) { __u8 kbuf[SHA256_DIGEST_LENGTH], lbuf[SHA256_DIGEST_LENGTH], val[64] = {}; struct bpf_map_info info; __u32 ilen, key = 0; int fd, i; /* * The signing scheme assumes the kernel's map hash equals what libbpf * computes over the same bytes (gen_loader bakes libbpf_sha256(blob); * the kernel recomputes via array_map_get_hash). Pin that they agree. */ for (i = 0; i < (int)sizeof(val); i++) val[i] = i * 7 + 1; fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "h", 4, sizeof(val), 1, NULL); if (!ASSERT_OK_FD(fd, "array_map")) return; ASSERT_OK(bpf_map_update_elem(fd, &key, val, 0), "update"); ASSERT_OK(bpf_map_freeze(fd), "freeze"); memset(&info, 0, sizeof(info)); info.hash = ptr_to_u64(kbuf); info.hash_size = sizeof(kbuf); ilen = sizeof(info); if (ASSERT_OK(bpf_map_get_info_by_fd(fd, &info, &ilen), "get_hash")) { libbpf_sha256(val, sizeof(val), lbuf); ASSERT_EQ(memcmp(kbuf, lbuf, sizeof(kbuf)), 0, "kernel map hash matches libbpf_sha256"); } close(fd); } static void map_hash_multi_element(void) { const __u32 nr = 8, value_size = 64; __u8 kbuf[SHA256_DIGEST_LENGTH], lbuf[SHA256_DIGEST_LENGTH]; struct bpf_map_info info; __u32 ilen, i, j; __u8 *full; int fd; /* * array_map_get_hash() hashes elem_size * max_entries (the whole value * area), not just element 0. With an 8-aligned value_size elem_size has * no padding, so pin that a >1-entry array's kernel hash equals * libbpf_sha256() over the full, concatenated element contents. */ fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "h", 4, value_size, nr, NULL); if (!ASSERT_OK_FD(fd, "array_map")) return; full = calloc(nr, value_size); if (!ASSERT_OK_PTR(full, "buf")) goto close_fd; for (i = 0; i < nr; i++) { __u8 *v = full + i * value_size; for (j = 0; j < value_size; j++) v[j] = i * 31 + j * 7 + 1; ASSERT_OK(bpf_map_update_elem(fd, &i, v, 0), "update"); } ASSERT_OK(bpf_map_freeze(fd), "freeze"); memset(&info, 0, sizeof(info)); info.hash = ptr_to_u64(kbuf); info.hash_size = sizeof(kbuf); ilen = sizeof(info); if (ASSERT_OK(bpf_map_get_info_by_fd(fd, &info, &ilen), "get_hash")) { libbpf_sha256(full, (size_t)nr * value_size, lbuf); ASSERT_EQ(memcmp(kbuf, lbuf, sizeof(kbuf)), 0, "kernel hash covers full multi-element value area"); } free(full); close_fd: close(fd); } static void map_hash_bad_size(void) { __u8 kbuf[SHA256_DIGEST_LENGTH], val[64] = {}; struct bpf_map_info info; __u32 ilen, key = 0; int fd; fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "h", 4, sizeof(val), 1, NULL); if (!ASSERT_OK_FD(fd, "array_map")) return; ASSERT_OK(bpf_map_update_elem(fd, &key, val, 0), "update"); ASSERT_OK(bpf_map_freeze(fd), "freeze"); memset(&info, 0, sizeof(info)); info.hash = ptr_to_u64(kbuf); info.hash_size = sizeof(kbuf) / 2; ilen = sizeof(info); ASSERT_EQ(bpf_map_get_info_by_fd(fd, &info, &ilen), -EINVAL, "wrong hash_size rejected"); close(fd); } static void map_hash_unsupported_type(void) { __u8 kbuf[SHA256_DIGEST_LENGTH]; struct bpf_map_info info; __u32 ilen; int fd; /* Only arrays implement map_get_hash; a hash map must be refused. */ fd = bpf_map_create(BPF_MAP_TYPE_HASH, "h", 4, 8, 4, NULL); if (!ASSERT_OK_FD(fd, "hash_map")) return; memset(&info, 0, sizeof(info)); info.hash = ptr_to_u64(kbuf); info.hash_size = sizeof(kbuf); ilen = sizeof(info); ASSERT_EQ(bpf_map_get_info_by_fd(fd, &info, &ilen), -EINVAL, "hash unsupported for non-array map"); close(fd); } static int setup_meta_map(const struct gen_loader_fixture *f) { LIBBPF_OPTS(bpf_map_create_opts, mopts, .excl_prog_hash = f->excl, .excl_prog_hash_size = sizeof(f->excl)); __u32 key = 0; int fd; fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "__loader.map", 4, f->data_sz, 1, &mopts); if (fd < 0) return -errno; if (bpf_map_update_elem(fd, &key, f->blob, 0) || bpf_map_freeze(fd)) { close(fd); return -errno; } return fd; } static void lsm_signature_verdict(void) { char dir_tmpl[] = "/tmp/signed_loader_lsmXXXXXX", *dir = NULL; struct test_signed_loader_lsm *lsm = NULL; __u32 sig_sz = 8192, msig_sz = 8192; int map_fd = -1, prog_fd = -1; bool have_fixture = false; struct gen_loader_fixture f; unsigned char *buf; __s32 ses_serial; __u8 sig[8192]; lsm = test_signed_loader_lsm__open_and_load(); if (!ASSERT_OK_PTR(lsm, "lsm_skel_load")) return; lsm->bss->monitored_tid = sys_gettid(); if (!ASSERT_OK(test_signed_loader_lsm__attach(lsm), "lsm_attach")) goto out; have_fixture = true; if (gen_loader_fixture_init(&f) != 0) goto out; map_fd = setup_meta_map(&f); if (!ASSERT_OK_FD(map_fd, "meta_map_unsigned")) goto out; lsm->bss->seen = 0; prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, NULL, 0, 0, 0); close(map_fd); map_fd = -1; if (!ASSERT_OK_FD(prog_fd, "unsigned loader load")) goto out; close(prog_fd); prog_fd = -1; if (!ASSERT_NEQ(lsm->bss->seen, 0, "bpf LSM in the active LSM set")) goto out; ASSERT_EQ(lsm->bss->seen, 1, "unsigned: one observed load"); ASSERT_EQ(lsm->bss->sig_verdict, BPF_SIG_UNSIGNED, "unsigned verdict"); ASSERT_EQ(lsm->bss->sig_keyring_type, BPF_SIG_KEYRING_NONE, "unsigned keyring type"); ASSERT_EQ(lsm->bss->sig_keyring_serial, 0, "unsigned: no keyring serial"); syscall(__NR_request_key, "keyring", "_uid.0", NULL, KEY_SPEC_SESSION_KEYRING); dir = mkdtemp(dir_tmpl); if (!ASSERT_OK_PTR(dir, "mkdtemp")) goto out; if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) { rmdir(dir); dir = NULL; goto out; } if (!ASSERT_OK(sign_buf(dir, f.gopts.insns, f.gopts.insns_sz, sig, &sig_sz), "sign-file")) goto out; map_fd = setup_meta_map(&f); if (!ASSERT_OK_FD(map_fd, "meta_map_signed")) goto out; lsm->bss->seen = 0; prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig, sig_sz, KEY_SPEC_SESSION_KEYRING, 0); close(map_fd); map_fd = -1; ASSERT_EQ(prog_fd, -EACCES, "unfolded metadata rejected"); if (prog_fd >= 0) close(prog_fd); prog_fd = -1; ses_serial = syscall(__NR_keyctl, KEYCTL_GET_KEYRING_ID, KEY_SPEC_SESSION_KEYRING, 0); ASSERT_EQ(lsm->bss->seen, 1, "signed: one observed load"); ASSERT_EQ(lsm->bss->sig_verdict, BPF_SIG_VERIFIED, "admission saw a valid signature"); ASSERT_EQ(lsm->bss->sig_keyring_type, BPF_SIG_KEYRING_USER, "signed keyring type"); ASSERT_GT(ses_serial, 0, "session keyring serial resolved"); ASSERT_EQ(lsm->bss->sig_keyring_serial, ses_serial, "signed: validated against session keyring"); buf = malloc((size_t)f.gopts.insns_sz + f.data_sz); if (!ASSERT_OK_PTR(buf, "meta_signbuf")) goto out; memcpy(buf, f.gopts.insns, f.gopts.insns_sz); memcpy(buf + f.gopts.insns_sz, f.blob, f.data_sz); if (!ASSERT_OK(sign_buf(dir, buf, f.gopts.insns_sz + f.data_sz, sig, &msig_sz), "sign insns||metadata")) { free(buf); goto out; } free(buf); map_fd = setup_meta_map(&f); if (!ASSERT_OK_FD(map_fd, "meta_map_bound")) goto out; lsm->bss->seen = 0; prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig, msig_sz, KEY_SPEC_SESSION_KEYRING, 1); close(map_fd); map_fd = -1; if (!ASSERT_OK_FD(prog_fd, "metadata-bound loader load")) goto out; close(prog_fd); prog_fd = -1; ASSERT_EQ(lsm->bss->seen, 1, "metadata: one observed load"); ASSERT_EQ(lsm->bss->sig_verdict, BPF_SIG_VERIFIED, "metadata-bound verdict"); out: if (map_fd >= 0) close(map_fd); if (prog_fd >= 0) close(prog_fd); if (have_fixture) gen_loader_fixture_fini(&f); if (dir) run_setup("cleanup", dir); test_signed_loader_lsm__destroy(lsm); } /* * Load-time metadata verification: the kernel folds the frozen metadata map * into the signature (insns || metadata) and checks it at BPF_PROG_LOAD via * fd_array_cnt, rather than the loader checking from within BPF. Sign that * concatenation, hand the kernel the map, and confirm the signed loader loads, * runs, and installs its target. */ static int loadtime_drive(const char *dir, const void *insns, __u32 insns_sz, const void *data, __u32 data_sz, const __u8 *excl, void *ctx, __u32 ctx_sz, int *load_ret, bool *ran) { LIBBPF_OPTS(bpf_map_create_opts, mopts, .excl_prog_hash = excl, .excl_prog_hash_size = SHA256_DIGEST_LENGTH); __u32 sig_sz = 8192, key = 0; unsigned char *buf = NULL; int map_fd, prog_fd, ret = 0; union bpf_attr attr; __u8 sig[8192]; *ran = false; *load_ret = 0; /* * Metadata map, bound to the loader digest and frozen, exactly as * skel_internal.h's bpf_load_and_run() sets it up. */ map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "__loader.map", 4, data_sz, 1, &mopts); if (map_fd < 0) { ret = -errno; goto out_load; } if (bpf_map_update_elem(map_fd, &key, data, 0) || bpf_map_freeze(map_fd)) { ret = -errno; goto out_load; } /* Sign insns || metadata, the same bytes the kernel reconstructs. */ buf = malloc((size_t)insns_sz + data_sz); if (!buf) { ret = -ENOMEM; goto out_load; } memcpy(buf, insns, insns_sz); memcpy(buf + insns_sz, data, data_sz); ret = sign_buf(dir, buf, insns_sz + data_sz, sig, &sig_sz); if (ret) goto out_load; memset(&attr, 0, sizeof(attr)); attr.prog_type = BPF_PROG_TYPE_SYSCALL; attr.insns = ptr_to_u64(insns); attr.insn_cnt = insns_sz / sizeof(struct bpf_insn); attr.license = ptr_to_u64("Dual BSD/GPL"); attr.prog_flags = BPF_F_SLEEPABLE; attr.fd_array = ptr_to_u64(&map_fd); attr.signature = ptr_to_u64(sig); attr.signature_size = sig_sz; attr.keyring_id = KEY_SPEC_SESSION_KEYRING; attr.fd_array_cnt = 1; memcpy(attr.prog_name, "__loader.prog", sizeof("__loader.prog")); prog_fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, offsetofend(union bpf_attr, keyring_id)); if (prog_fd < 0) { ret = -errno; goto out_load; } memset(&attr, 0, sizeof(attr)); attr.test.prog_fd = prog_fd; attr.test.ctx_in = ptr_to_u64(ctx); attr.test.ctx_size_in = ctx_sz; if (syscall(__NR_bpf, BPF_PROG_RUN, &attr, offsetofend(union bpf_attr, test)) < 0) { ret = -errno; goto out_prog; } *ran = true; ret = (int)attr.test.retval; out_prog: close(prog_fd); goto out_map; out_load: *load_ret = ret; out_map: free(buf); if (map_fd >= 0) close(map_fd); return ret; } static void loadtime_verify(struct bpf_object *obj, int expect_maps) { LIBBPF_OPTS(gen_loader_opts, gopts, .gen_hash = true); char dir_tmpl[] = "/tmp/signed_loader_ltXXXXXX", *dir = NULL; int nr_maps = 0, nr_progs = 0, load_ret = 0, r; __u8 excl[SHA256_DIGEST_LENGTH]; struct bpf_prog_desc *pd; struct bpf_map_desc *md; unsigned char *blob = NULL; struct bpf_program *p; struct bpf_map *m; __u32 ctx_sz, data_sz; void *ctx = NULL; bool ran = false; syscall(__NR_request_key, "keyring", "_uid.0", NULL, KEY_SPEC_SESSION_KEYRING); dir = mkdtemp(dir_tmpl); if (!ASSERT_OK_PTR(dir, "mkdtemp")) return; if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) { rmdir(dir); return; } if (!ASSERT_OK(bpf_object__gen_loader(obj, &gopts), "gen_loader")) goto out; if (!ASSERT_OK(bpf_object__load(obj), "gen_load")) goto out; bpf_object__for_each_program(p, obj) nr_progs++; bpf_object__for_each_map(m, obj) nr_maps++; if (!ASSERT_EQ(nr_maps, expect_maps, "fixture map count")) goto out; ctx_sz = sizeof(struct bpf_loader_ctx) + nr_maps * sizeof(struct bpf_map_desc) + nr_progs * sizeof(struct bpf_prog_desc); ctx = calloc(1, ctx_sz); if (!ASSERT_OK_PTR(ctx, "ctx_alloc")) goto out; ((struct bpf_loader_ctx *)ctx)->sz = ctx_sz; data_sz = gopts.data_sz; blob = malloc(data_sz); if (!ASSERT_OK_PTR(blob, "blob_alloc")) goto out; memcpy(blob, gopts.data, data_sz); /* excl_prog_hash = SHA256(loader insns) == the loader's prog->digest. */ libbpf_sha256(gopts.insns, gopts.insns_sz, excl); r = loadtime_drive(dir, gopts.insns, gopts.insns_sz, blob, data_sz, excl, ctx, ctx_sz, &load_ret, &ran); ASSERT_OK(load_ret, "signed loader loaded (insns || metadata)"); ASSERT_TRUE(ran, "loader ran"); ASSERT_EQ(r, 0, "loader installed its target"); md = (struct bpf_map_desc *)((char *)ctx + sizeof(struct bpf_loader_ctx)); pd = (struct bpf_prog_desc *)(md + nr_maps); ASSERT_GT(pd[0].prog_fd, 0, "target program installed"); if (nr_maps) ASSERT_GT(md[0].map_fd, 0, "target map installed"); close_loader_ctx_fds(ctx, nr_maps, nr_progs); out: free(blob); free(ctx); if (dir) run_setup("cleanup", dir); } static void loadtime_no_map(void) { struct test_signed_loader *skel = test_signed_loader__open(); if (!ASSERT_OK_PTR(skel, "skel_open")) return; loadtime_verify(skel->obj, 0); test_signed_loader__destroy(skel); } static void loadtime_with_map(void) { struct test_signed_loader_map *skel = test_signed_loader_map__open(); if (!ASSERT_OK_PTR(skel, "skel_open")) return; loadtime_verify(skel->obj, 1); test_signed_loader_map__destroy(skel); } /* * A signed program need not bind any map. A plain BPF_PROG_TYPE_SYSCALL * program with no fd_array is signed over its instructions alone: the kernel * verifies the signature, folds no metadata, and the program loads. Exercise * the fd_array == NULL / fd_array_cnt == 0 path, and confirm the signature * still authenticates the instructions (a tampered copy is rejected). */ static void signed_no_fd_array(void) { struct bpf_insn insns[] = { BPF_MOV64_IMM(BPF_REG_0, 0), BPF_EXIT_INSN(), }; char dir_tmpl[] = "/tmp/signed_loaderXXXXXX", *dir; __u32 sig_sz = 8192; union bpf_attr attr; __u8 sig[8192]; int prog_fd, err; syscall(__NR_request_key, "keyring", "_uid.0", NULL, KEY_SPEC_SESSION_KEYRING); dir = mkdtemp(dir_tmpl); if (!ASSERT_OK_PTR(dir, "mkdtemp")) return; if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) { rmdir(dir); return; } /* No metadata map: the signed payload is the instructions alone. */ if (!ASSERT_OK(sign_buf(dir, insns, sizeof(insns), sig, &sig_sz), "sign-file")) goto cleanup; memset(&attr, 0, sizeof(attr)); attr.prog_type = BPF_PROG_TYPE_SYSCALL; attr.insns = ptr_to_u64(insns); attr.insn_cnt = ARRAY_SIZE(insns); attr.license = ptr_to_u64("Dual BSD/GPL"); attr.prog_flags = BPF_F_SLEEPABLE; attr.signature = ptr_to_u64(sig); attr.signature_size = sig_sz; attr.keyring_id = KEY_SPEC_SESSION_KEYRING; /* fd_array and fd_array_cnt deliberately left NULL/0. */ memcpy(attr.prog_name, "signed_nomap", sizeof("signed_nomap")); prog_fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, offsetofend(union bpf_attr, keyring_id)); if (!ASSERT_GE(prog_fd, 0, "map-less signed program loaded")) { if (prog_fd >= 0) close(prog_fd); goto cleanup; } close(prog_fd); /* The signature covers the instructions, so tampering must be rejected. */ insns[0].imm = 1; prog_fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, offsetofend(union bpf_attr, keyring_id)); err = prog_fd < 0 ? -errno : prog_fd; ASSERT_EQ(err, -EKEYREJECTED, "tampered map-less program rejected"); if (prog_fd >= 0) close(prog_fd); cleanup: run_setup("cleanup", dir); } /* * A signed program may reach maps only through fd_array indices, so the kernel * folds (and thus attests) them. A direct BPF_PSEUDO_MAP_FD reference - a raw, * unfolded fd baked into the signed instructions - is rejected by the verifier. */ static void signed_map_by_fd_rejected(void) { struct bpf_insn insns[] = { BPF_LD_MAP_FD(BPF_REG_1, 0), BPF_MOV64_IMM(BPF_REG_0, 0), BPF_EXIT_INSN(), }; char dir_tmpl[] = "/tmp/signed_loaderXXXXXX", *dir; __u32 sig_sz = 8192; union bpf_attr attr; __u8 sig[8192]; int map_fd, prog_fd, err; map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "sig_mapfd", 4, 4, 1, NULL); if (!ASSERT_GE(map_fd, 0, "map_create")) return; insns[0].imm = map_fd; /* bake the raw map fd into the ld_imm64 */ syscall(__NR_request_key, "keyring", "_uid.0", NULL, KEY_SPEC_SESSION_KEYRING); dir = mkdtemp(dir_tmpl); if (!ASSERT_OK_PTR(dir, "mkdtemp")) goto out_map; if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) { rmdir(dir); goto out_map; } /* Sign the instructions, raw map fd and all. */ if (!ASSERT_OK(sign_buf(dir, insns, sizeof(insns), sig, &sig_sz), "sign-file")) goto cleanup; memset(&attr, 0, sizeof(attr)); attr.prog_type = BPF_PROG_TYPE_SYSCALL; attr.insns = ptr_to_u64(insns); attr.insn_cnt = ARRAY_SIZE(insns); attr.license = ptr_to_u64("Dual BSD/GPL"); attr.prog_flags = BPF_F_SLEEPABLE; attr.signature = ptr_to_u64(sig); attr.signature_size = sig_sz; attr.keyring_id = KEY_SPEC_SESSION_KEYRING; /* No fd_array: the map is reached by a raw fd in the instructions. */ memcpy(attr.prog_name, "signed_mapfd", sizeof("signed_mapfd")); prog_fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, offsetofend(union bpf_attr, keyring_id)); err = prog_fd < 0 ? -errno : prog_fd; ASSERT_EQ(err, -EINVAL, "signed program referencing a map by fd rejected"); if (prog_fd >= 0) close(prog_fd); cleanup: run_setup("cleanup", dir); out_map: close(map_fd); } /* * A signed program may reach maps only through the continuous fd_array, so the * kernel folds (and thus attests) them. Referencing a map by fd_array *index* * while leaving fd_array_cnt at 0 selects the sparse path, which resolves a map * the signature never covered; the verifier rejects it up front with -EACCES. */ static void signed_sparse_fd_array_rejected(void) { struct bpf_insn insns[] = { BPF_LD_IMM64_RAW(BPF_REG_1, BPF_PSEUDO_MAP_IDX, 0), BPF_MOV64_IMM(BPF_REG_0, 0), BPF_EXIT_INSN(), }; char dir_tmpl[] = "/tmp/signed_loader_spXXXXXX", *dir; __u32 sig_sz = 8192; union bpf_attr attr; __u8 sig[8192]; int map_fd, prog_fd, err; map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "sig_sparse", 4, 4, 1, NULL); if (!ASSERT_GE(map_fd, 0, "map_create")) return; syscall(__NR_request_key, "keyring", "_uid.0", NULL, KEY_SPEC_SESSION_KEYRING); dir = mkdtemp(dir_tmpl); if (!ASSERT_OK_PTR(dir, "mkdtemp")) goto out_map; if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) { rmdir(dir); goto out_map; } /* Sign the instructions alone; the sparse map is not folded. */ if (!ASSERT_OK(sign_buf(dir, insns, sizeof(insns), sig, &sig_sz), "sign-file")) goto cleanup; memset(&attr, 0, sizeof(attr)); attr.prog_type = BPF_PROG_TYPE_SYSCALL; attr.insns = ptr_to_u64(insns); attr.insn_cnt = ARRAY_SIZE(insns); attr.license = ptr_to_u64("Dual BSD/GPL"); attr.prog_flags = BPF_F_SLEEPABLE; attr.fd_array = ptr_to_u64(&map_fd); attr.fd_array_cnt = 0; /* sparse: force lazy map resolution */ attr.signature = ptr_to_u64(sig); attr.signature_size = sig_sz; attr.keyring_id = KEY_SPEC_SESSION_KEYRING; memcpy(attr.prog_name, "signed_sparse", sizeof("signed_sparse")); prog_fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, offsetofend(union bpf_attr, keyring_id)); err = prog_fd < 0 ? -errno : prog_fd; ASSERT_EQ(err, -EACCES, "signed program binding a sparse fd_array map rejected"); if (prog_fd >= 0) close(prog_fd); cleanup: run_setup("cleanup", dir); out_map: close(map_fd); } static void signed_module_kfunc_rejected(void) { struct bpf_insn insns[] = { BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, BPF_PSEUDO_KFUNC_CALL, 1, 1), BPF_MOV64_IMM(BPF_REG_0, 0), BPF_EXIT_INSN(), }; char dir_tmpl[] = "/tmp/signed_loader_kfnXXXXXX", *dir; int prog_fd, err, fds[2]; struct btf *btf = NULL; __u32 sig_sz = 8192; union bpf_attr attr; __u8 sig[8192]; syscall(__NR_request_key, "keyring", "_uid.0", NULL, KEY_SPEC_SESSION_KEYRING); dir = mkdtemp(dir_tmpl); if (!ASSERT_OK_PTR(dir, "mkdtemp")) return; if (!ASSERT_OK(run_setup("setup", dir), "verify_sig_setup")) { rmdir(dir); return; } if (!ASSERT_OK(sign_buf(dir, insns, sizeof(insns), sig, &sig_sz), "sign-file")) goto cleanup; btf = btf__new_empty(); if (!ASSERT_OK_PTR(btf, "btf_new_empty")) goto cleanup; btf__add_int(btf, "int", 4, BTF_INT_SIGNED); if (!ASSERT_OK(btf__load_into_kernel(btf), "btf_load")) goto cleanup; fds[0] = -1; fds[1] = btf__fd(btf); memset(&attr, 0, sizeof(attr)); attr.prog_type = BPF_PROG_TYPE_SYSCALL; attr.insns = ptr_to_u64(insns); attr.insn_cnt = ARRAY_SIZE(insns); attr.license = ptr_to_u64("Dual BSD/GPL"); attr.prog_flags = BPF_F_SLEEPABLE; attr.fd_array = ptr_to_u64(fds); attr.fd_array_cnt = 0; /* sparse: force lazy kfunc BTF resolution */ attr.signature = ptr_to_u64(sig); attr.signature_size = sig_sz; attr.keyring_id = KEY_SPEC_SESSION_KEYRING; memcpy(attr.prog_name, "signed_kfunc", sizeof("signed_kfunc")); prog_fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, offsetofend(union bpf_attr, keyring_id)); err = prog_fd < 0 ? -errno : prog_fd; if (prog_fd >= 0) close(prog_fd); ASSERT_EQ(err, -EACCES, "module kfunc BTF in signed program rejected"); cleanup: if (btf) btf__free(btf); run_setup("cleanup", dir); } void test_signed_loader(void) { if (test__start_subtest("loadtime_no_map")) loadtime_no_map(); if (test__start_subtest("loadtime_with_map")) loadtime_with_map(); if (test__start_subtest("metadata_match")) metadata_match(); if (test__start_subtest("signature_enforced")) signature_enforced(); if (test__start_subtest("signed_nonexcl_fd_array_rejected")) signed_nonexcl_fd_array_rejected(); if (test__start_subtest("signed_unfrozen_fd_array_rejected")) signed_unfrozen_fd_array_rejected(); if (test__start_subtest("signed_nonarray_fd_array_rejected")) signed_nonarray_fd_array_rejected(); if (test__start_subtest("signed_btf_fd_array_rejected")) signed_btf_fd_array_rejected(); if (test__start_subtest("signed_module_kfunc_rejected")) signed_module_kfunc_rejected(); if (test__start_subtest("signature_failure_logs")) signature_failure_logs(); if (test__start_subtest("signature_too_large")) signature_too_large(); if (test__start_subtest("signature_zero_size")) signature_zero_size(); if (test__start_subtest("signature_bad_keyring")) signature_bad_keyring(); if (test__start_subtest("metadata_ctx_max_entries_ignored")) metadata_ctx_max_entries_ignored(); if (test__start_subtest("metadata_ctx_initial_value_ignored")) metadata_ctx_initial_value_ignored(); if (test__start_subtest("signature_authenticates_insns")) signature_authenticates_insns(); if (test__start_subtest("signature_authenticates_metadata")) signature_authenticates_metadata(); if (test__start_subtest("hash_requires_frozen")) hash_requires_frozen(); if (test__start_subtest("no_update_after_freeze")) no_update_after_freeze(); if (test__start_subtest("freeze_writable_mmap")) freeze_writable_mmap(); if (test__start_subtest("no_writable_mmap_frozen")) no_writable_mmap_frozen(); if (test__start_subtest("map_hash_matches_libbpf")) map_hash_matches_libbpf(); if (test__start_subtest("map_hash_multi_element")) map_hash_multi_element(); if (test__start_subtest("map_hash_bad_size")) map_hash_bad_size(); if (test__start_subtest("map_hash_unsupported_type")) map_hash_unsupported_type(); if (test__start_subtest("lsm_signature_verdict")) lsm_signature_verdict(); if (test__start_subtest("signed_no_fd_array")) signed_no_fd_array(); if (test__start_subtest("signed_map_by_fd_rejected")) signed_map_by_fd_rejected(); if (test__start_subtest("signed_sparse_fd_array_rejected")) signed_sparse_fd_array_rejected(); }