| // SPDX-License-Identifier: GPL-2.0 | |
| /* Copyright (c) 2026 Isovalent */ | |
| #include <test_progs.h> | |
| #include <sys/syscall.h> | |
| #include <sys/mman.h> | |
| #include <sys/wait.h> | |
| #include <sys/stat.h> | |
| #include <fcntl.h> | |
| #include <limits.h> | |
| #include <linux/keyctl.h> | |
| #include <linux/bpf.h> | |
| #include <bpf/btf.h> | |
| #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(); | |
| } |