blob: e43dba8fed8e415535fe5f22411f01e87e9be12c [file] [edit]
// 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,
BPF_SIG_KEYRING_BPF,
};
#ifndef KEY_SPEC_BPF_KEYRING
#define KEY_SPEC_BPF_KEYRING -9
#endif
/* verify_sig_setup.sh exits with this when openssl cannot do ML-DSA. */
#define SETUP_SKIP (-77)
/* FIPS-204 ML-DSA-87 signature size, see include/crypto/mldsa.h. */
#define MLDSA87_SIGNATURE_SIZE 4627
static int load_loader_log(const void *insns, __u32 insns_sz, int map_fd,
const void *sig, __u32 sig_sz, __s32 keyring_id,
__u32 fd_array_cnt, char *log_buf, __u32 log_sz)
{
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);
attr.fd_array_cnt = fd_array_cnt;
if (sig) {
attr.signature = ptr_to_u64(sig);
attr.signature_size = sig_sz;
attr.keyring_id = keyring_id;
}
if (log_buf) {
attr.log_level = 1;
attr.log_buf = ptr_to_u64(log_buf);
attr.log_size = log_sz;
}
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 load_loader(const void *insns, __u32 insns_sz, int map_fd,
const void *sig, __u32 sig_sz, __s32 keyring_id,
__u32 fd_array_cnt)
{
return load_loader_log(insns, insns_sz, map_fd, sig, sig_sz, keyring_id,
fd_array_cnt, NULL, 0);
}
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;
if (!WIFEXITED(status))
return -EINVAL;
return -WEXITSTATUS(status);
}
static void genkey_dir_fini(const char *dir)
{
static const char * const files[] = {
"signing_key.der", "signing_key.pem", "x509.genkey",
};
char path[PATH_MAX];
size_t i;
if (!dir)
return;
for (i = 0; i < ARRAY_SIZE(files); i++) {
snprintf(path, sizeof(path), "%s/%s", dir, files[i]);
unlink(path);
}
rmdir(dir);
}
static int sign_buf_digest(const char *dir, const void *buf, __u32 len,
void *sig, __u32 *sig_sz, const char *digest)
{
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;
snprintf(sigpath, sizeof(sigpath), "%s.p7s", data_tmpl);
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", digest,
key, key, data_tmpl, NULL);
exit(1);
}
if (waitpid(pid, &status, 0) < 0 ||
!WIFEXITED(status) || WEXITSTATUS(status)) {
ret = -EINVAL;
goto out;
}
if (stat(sigpath, &st) < 0) {
ret = -errno;
goto out;
}
if (st.st_size > (off_t)*sig_sz) {
ret = -E2BIG;
goto out;
}
fd = open(sigpath, O_RDONLY);
if (fd < 0) {
ret = -errno;
goto out;
}
if (read(fd, sig, st.st_size) != st.st_size) {
close(fd);
ret = -EIO;
goto out;
}
close(fd);
*sig_sz = st.st_size;
out:
unlink(sigpath);
unlink(data_tmpl);
return ret;
}
static int sign_buf(const char *dir, const void *buf, __u32 len,
void *sig, __u32 *sig_sz)
{
return sign_buf_digest(dir, buf, len, sig, sig_sz, "sha256");
}
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);
}
/*
* CO-RE relocations would have the kernel rewrite the insns after the
* signature was checked over them, with values the loader picks through the
* BTF it supplies. A signed load that carries any is rejected before the
* signature is examined, so junk serves as one.
*/
static void signed_core_relo_rejected(void)
{
static const __u8 junk[64] = { 0x30, 0x42, 0x13, 0x37, };
struct bpf_insn insns[] = {
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_EXIT_INSN(),
};
struct bpf_core_relo relo = {
.insn_off = 0,
.type_id = 1,
.kind = BPF_CORE_TYPE_ID_LOCAL,
};
char log_buf[1024] = {};
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 = ARRAY_SIZE(insns);
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.core_relos = ptr_to_u64(&relo);
attr.core_relo_cnt = 1;
attr.core_relo_rec_size = sizeof(relo);
attr.log_level = 1;
attr.log_buf = ptr_to_u64(log_buf);
attr.log_size = sizeof(log_buf);
fd = syscall(__NR_bpf, BPF_PROG_LOAD, &attr, offsetofend(union bpf_attr, keyring_id));
fd = fd < 0 ? -errno : fd;
ASSERT_EQ(fd, -EACCES, "signed load with CO-RE relocations rejected");
ASSERT_HAS_SUBSTR(log_buf, "signed program cannot carry CO-RE relocations", "log");
if (fd >= 0)
close(fd);
}
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-rsa", 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;
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.
*/
fd = load_loader_log(f.gopts.insns, f.gopts.insns_sz, -1, junk,
sizeof(junk), KEY_SPEC_SESSION_KEYRING, 0,
log_buf, sizeof(log_buf));
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
* (BPF_PROG_MAX_SIGNATURE_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);
}
static bool keyring_unsealed_boot(void)
{
char val = 0;
int fd;
fd = open("/sys/module/bpf/parameters/keyring_unsealed", O_RDONLY);
if (fd < 0)
return false;
if (read(fd, &val, 1) != 1)
val = 0;
close(fd);
return val == 'Y' || val == '1';
}
static int bpf_keyring_lookup(int *nr_keys)
{
char line[512], type[32], desc[64];
int serial = -ENOENT;
FILE *f;
f = fopen("/proc/keys", "r");
if (!f)
return -errno;
while (fgets(line, sizeof(line), f)) {
unsigned int hex;
char *sum;
if (sscanf(line, "%x %*s %*s %*s %*s %*s %*s %31s %63s",
&hex, type, desc) != 3)
continue;
if (strcmp(type, "keyring") || strcmp(desc, ".bpf:"))
continue;
serial = (int)hex;
if (nr_keys) {
sum = strstr(line, ".bpf: ");
*nr_keys = !sum || !strncmp(sum + 6, "empty", 5) ?
0 : atoi(sum + 6);
}
break;
}
fclose(f);
return serial;
}
static long keyctl_ret(int cmd, unsigned long arg2, unsigned long arg3)
{
long ret = syscall(__NR_keyctl, cmd, arg2, arg3);
return ret < 0 ? -errno : ret;
}
/*
* What the bpf keyring still needs once it got provisioned: KEY_POS_SEARCH
* for the in-kernel search during verification, and the user view/read bits
* so it stays visible in /proc/keys, rest is dropped so the enrolled is
* therefore final.
*/
#define BPF_KEYRING_PERM_LOCKED 0x08030000
/* What bpf_keyring_init() grants at boot. */
#define BPF_KEYRING_PERM_INITIAL 0x082f0000
static void bpf_keyring_sealed(void)
{
static const __u8 junk[64] = {};
struct gen_loader_fixture f;
int serial, key, fd;
if (keyring_unsealed_boot()) {
printf("%s:SKIP:the bpf keyring was unsealed at boot\n", __func__);
test__skip();
return;
}
serial = bpf_keyring_lookup(NULL);
if (serial >= 0) {
ASSERT_EQ(keyctl_ret(KEYCTL_GET_KEYRING_ID,
KEY_SPEC_BPF_KEYRING, 0), serial,
"KEY_SPEC_BPF_KEYRING resolves to the bpf keyring");
key = syscall(__NR_add_key, "user", "sealprobe", "x", 1,
KEY_SPEC_BPF_KEYRING);
if (key >= 0)
syscall(__NR_keyctl, KEYCTL_UNLINK, key,
KEY_SPEC_BPF_KEYRING);
ASSERT_EQ(key < 0 ? -errno : 0, -EPERM,
"nothing links into a sealed keyring");
}
if (gen_loader_fixture_init(&f) == 0) {
fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk,
sizeof(junk), KEY_SPEC_BPF_KEYRING, 0);
ASSERT_EQ(fd, -ENOKEY, "sealed bpf keyring rejected");
if (fd >= 0)
close(fd);
}
gen_loader_fixture_fini(&f);
}
static int try_load(const struct gen_loader_fixture *f, const void *sig,
__u32 sig_sz, __s32 keyring_id, char *log_buf, __u32 log_sz)
{
int map_fd, prog_fd;
map_fd = setup_meta_map(f);
if (!ASSERT_OK_FD(map_fd, "meta_map"))
return map_fd;
prog_fd = load_loader_log(f->gopts.insns, f->gopts.insns_sz, map_fd,
sig, sig_sz, keyring_id, 1, log_buf, log_sz);
close(map_fd);
if (prog_fd >= 0)
close(prog_fd);
return prog_fd;
}
/*
* This needs bpf.keyring_unsealed=1 on the guest kernel command line, which
* vmtest.sh can pass via KERNEL_CMDLINE_EXTRA. There is no way to unseal the
* keyring from here, so without it the test skips. It also only works once
* per boot, as restricting a keyring cannot be undone.
*/
static void bpf_keyring_provisioned(void)
{
char dir_tmpl[] = "/tmp/bpfkeyringXXXXXX";
char bad_tmpl[] = "/tmp/bpfkeyringbadXXXXXX";
__u8 *sig = NULL, *bad = NULL, *buf = NULL;
int serial, err;
int nr_keys = 0, der_fd = -1;
struct gen_loader_fixture f;
__u32 sig_sz = 8192, bad_sz;
bool have_fixture = false;
char *dir, *bad_dir = NULL;
char log_buf[1024] = {};
char path[PATH_MAX];
__u8 der[4096];
ssize_t der_sz;
serial = bpf_keyring_lookup(&nr_keys);
if (serial < 0) {
printf("%s:SKIP:no bpf keyring (needs CONFIG_KEYS)\n", __func__);
test__skip();
return;
}
if (nr_keys != 0) {
printf("%s:SKIP:the bpf keyring has already been provisioned\n",
__func__);
test__skip();
return;
}
dir = mkdtemp(dir_tmpl);
if (!ASSERT_OK_PTR(dir, "mkdtemp"))
return;
if (!ASSERT_OK(run_setup("genkey", dir), "verify_sig_setup genkey"))
goto rmdir;
snprintf(path, sizeof(path), "%s/signing_key.der", dir);
der_fd = open(path, O_RDONLY);
if (!ASSERT_OK_FD(der_fd, "open signing_key.der"))
goto rmdir;
der_sz = read(der_fd, der, sizeof(der));
close(der_fd);
if (!ASSERT_GT(der_sz, 0, "read signing_key.der"))
goto rmdir;
ASSERT_EQ(keyctl_ret(KEYCTL_GET_KEYRING_ID, KEY_SPEC_BPF_KEYRING, 0),
serial, "KEY_SPEC_BPF_KEYRING resolves to the bpf keyring");
err = syscall(__NR_add_key, "asymmetric", "", der, (size_t)der_sz,
KEY_SPEC_BPF_KEYRING);
if (err < 0 && errno == EPERM) {
printf("%s:SKIP:the bpf keyring is sealed, need bpf.keyring_unsealed=1\n",
__func__);
test__skip();
goto rmdir;
}
if (!ASSERT_GE(err, 0, "add the signing key to the bpf keyring"))
goto rmdir;
sig = malloc(sig_sz);
if (!ASSERT_OK_PTR(sig, "sig buf"))
goto out;
have_fixture = true;
if (gen_loader_fixture_init(&f) != 0)
goto out;
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;
ASSERT_EQ(try_load(&f, sig, sig_sz, KEY_SPEC_BPF_KEYRING, NULL, 0),
-ENOKEY, "unrestricted keyring still not consulted");
ASSERT_EQ(try_load(&f, sig, sig_sz, KEY_SPEC_SESSION_KEYRING, NULL, 0),
-EPERM, "caller-supplied keyring refused before provisioning");
if (!ASSERT_OK(syscall(__NR_keyctl, KEYCTL_RESTRICT_KEYRING,
KEY_SPEC_BPF_KEYRING, NULL, NULL),
"restrict bpf keyring"))
goto out;
if (!ASSERT_OK_FD(try_load(&f, sig, sig_sz, KEY_SPEC_BPF_KEYRING,
NULL, 0),
"load signed by a key in the .bpf keyring"))
goto out;
bad_dir = mkdtemp(bad_tmpl);
if (!ASSERT_OK_PTR(bad_dir, "mkdtemp unenrolled"))
goto out;
if (!ASSERT_OK(run_setup("genkey", bad_dir), "verify_sig_setup genkey unenrolled"))
goto out;
bad_sz = 8192;
bad = malloc(bad_sz);
if (!ASSERT_OK_PTR(bad, "bad sig buf"))
goto out;
if (!ASSERT_OK(sign_buf(bad_dir, buf, f.gopts.insns_sz + f.data_sz, bad,
&bad_sz), "sign with an unenrolled key"))
goto out;
ASSERT_EQ(try_load(&f, bad, bad_sz, KEY_SPEC_BPF_KEYRING, log_buf,
sizeof(log_buf)), -ENOKEY,
"key outside the bpf keyring refused");
ASSERT_HAS_SUBSTR(log_buf, "signature verification failed",
"the bpf keyring was consulted");
f.blob[0] ^= 0xff;
err = try_load(&f, sig, sig_sz, KEY_SPEC_BPF_KEYRING, NULL, 0);
f.blob[0] ^= 0xff;
ASSERT_EQ(err, -EKEYREJECTED, "tampered metadata refused");
ASSERT_EQ(try_load(&f, sig, sig_sz, KEY_SPEC_SESSION_KEYRING, NULL, 0),
-EPERM, "caller-supplied keyring refused once .bpf is in use");
err = keyctl_ret(KEYCTL_UNLINK, KEY_SPEC_SESSION_KEYRING, serial);
ASSERT_EQ(err, -ENOENT, "keyring writable while the user bits are there");
err = keyctl_ret(KEYCTL_SETPERM, serial, BPF_KEYRING_PERM_LOCKED);
if (!ASSERT_OK(err, "drop the user bits on the bpf keyring"))
goto out;
ASSERT_OK_FD(try_load(&f, sig, sig_sz, KEY_SPEC_BPF_KEYRING, NULL, 0),
"load still verified against the locked keyring");
ASSERT_EQ(keyctl_ret(KEYCTL_GET_KEYRING_ID, KEY_SPEC_BPF_KEYRING, 0),
-EACCES, "the special id grants no rights of its own");
err = keyctl_ret(KEYCTL_UNLINK, KEY_SPEC_SESSION_KEYRING, serial);
ASSERT_EQ(err, -EACCES, "unlink refused");
err = keyctl_ret(KEYCTL_CLEAR, serial, 0);
ASSERT_EQ(err, -EACCES, "clear refused");
err = keyctl_ret(KEYCTL_REVOKE, serial, 0);
ASSERT_EQ(err, -EACCES, "revoke refused");
err = keyctl_ret(KEYCTL_INVALIDATE, serial, 0);
ASSERT_EQ(err, -EACCES, "invalidate refused");
err = keyctl_ret(KEYCTL_SET_TIMEOUT, serial, 1);
ASSERT_EQ(err, -EACCES, "timeout refused");
err = keyctl_ret(KEYCTL_SETPERM, serial, BPF_KEYRING_PERM_INITIAL);
ASSERT_EQ(err, -EACCES, "the bits cannot be granted back");
ASSERT_EQ(bpf_keyring_lookup(&nr_keys), serial, "keyring still there");
ASSERT_EQ(nr_keys, 1, "the enrolled key survived");
out:
if (have_fixture)
gen_loader_fixture_fini(&f);
genkey_dir_fini(bad_dir);
free(buf);
free(bad);
free(sig);
rmdir:
genkey_dir_fini(dir);
}
/*
* 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-rsa", 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-rsa", 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-rsa", 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-rsa", 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);
}
/*
* End-to-end signed load with a post-quantum key. ML-DSA (FIPS-204) is wired
* through the X.509 and PKCS#7 parsers, and BPF reaches them via
* verify_pkcs7_signature() without knowing the algorithm, so an ML-DSA key in
* the keyring should verify an ML-DSA signed program with no BPF-side work.
*/
static void mldsa_signed_load(void)
{
char dir_tmpl[] = "/tmp/bpfmldsaXXXXXX";
int map_fd = -1, prog_fd = -1, err;
__u8 *sig = NULL, *buf = NULL;
struct gen_loader_fixture f;
bool have_fixture = false;
__u32 sig_sz = 16384;
char *dir;
syscall(__NR_request_key, "keyring", "_uid.0", NULL,
KEY_SPEC_SESSION_KEYRING);
dir = mkdtemp(dir_tmpl);
if (!ASSERT_OK_PTR(dir, "mkdtemp"))
return;
err = run_setup("setup-mldsa", dir);
if (err == SETUP_SKIP) {
printf("%s:SKIP:no working ML-DSA signing, set SELFTESTS_VERBOSE=1\n",
__func__);
test__skip();
genkey_dir_fini(dir);
return;
}
if (!ASSERT_OK(err, "verify_sig_setup setup-mldsa")) {
genkey_dir_fini(dir);
return;
}
sig = malloc(sig_sz);
if (!ASSERT_OK_PTR(sig, "sig buf"))
goto out;
have_fixture = true;
if (gen_loader_fixture_init(&f) != 0)
goto out;
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);
/*
* ML-DSA hashes the message itself, but openssl before 4.0 cannot
* produce a CMS message without signedAttrs for it, and with those in
* play only SHA-512 is permitted for the messageDigest attribute.
*/
if (!ASSERT_OK(sign_buf_digest(dir, buf, f.gopts.insns_sz + f.data_sz,
sig, &sig_sz, "sha512"),
"sign insns||metadata with ML-DSA"))
goto out;
/*
* Guard against the setup silently handing back some other key type:
* an RSA or ECDSA signature is a few hundred bytes, where an ML-DSA-87
* one cannot be smaller than the raw signature it carries.
*/
ASSERT_GT(sig_sz, MLDSA87_SIGNATURE_SIZE, "ML-DSA-87 signature size");
map_fd = setup_meta_map(&f);
if (!ASSERT_OK_FD(map_fd, "meta_map"))
goto out;
prog_fd = load_loader(f.gopts.insns, f.gopts.insns_sz, map_fd, sig,
sig_sz, KEY_SPEC_SESSION_KEYRING, 1);
ASSERT_OK_FD(prog_fd, "ML-DSA signed loader load");
out:
if (prog_fd >= 0)
close(prog_fd);
if (map_fd >= 0)
close(map_fd);
if (have_fixture)
gen_loader_fixture_fini(&f);
free(buf);
free(sig);
run_setup("cleanup", dir);
}
/*
* 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-rsa", 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-rsa", 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-rsa", 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-rsa", 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);
}
enum subtest_boot {
BOOT_ANY,
BOOT_SEALED,
BOOT_UNSEALED,
};
static const struct {
const char *name;
void (*fn)(void);
enum subtest_boot boot;
} subtests[] = {
{ "loadtime_no_map", loadtime_no_map, BOOT_SEALED },
{ "loadtime_with_map", loadtime_with_map, BOOT_SEALED },
{ "metadata_match", metadata_match, BOOT_ANY },
{ "signature_enforced", signature_enforced, BOOT_SEALED },
{ "signed_nonexcl_fd_array_rejected", signed_nonexcl_fd_array_rejected, BOOT_SEALED },
{ "signed_unfrozen_fd_array_rejected", signed_unfrozen_fd_array_rejected, BOOT_SEALED },
{ "signed_nonarray_fd_array_rejected", signed_nonarray_fd_array_rejected, BOOT_SEALED },
{ "signed_btf_fd_array_rejected", signed_btf_fd_array_rejected, BOOT_ANY },
{ "signed_core_relo_rejected", signed_core_relo_rejected, BOOT_ANY },
{ "signed_module_kfunc_rejected", signed_module_kfunc_rejected, BOOT_SEALED },
{ "signature_failure_logs", signature_failure_logs, BOOT_SEALED },
{ "signature_too_large", signature_too_large, BOOT_ANY },
{ "signature_zero_size", signature_zero_size, BOOT_ANY },
{ "signature_bad_keyring", signature_bad_keyring, BOOT_SEALED },
{ "bpf_keyring_sealed", bpf_keyring_sealed, BOOT_ANY },
{ "mldsa_signed_load", mldsa_signed_load, BOOT_SEALED },
{ "metadata_ctx_max_entries_ignored", metadata_ctx_max_entries_ignored, BOOT_ANY },
{ "metadata_ctx_initial_value_ignored", metadata_ctx_initial_value_ignored, BOOT_ANY },
{ "signature_authenticates_insns", signature_authenticates_insns, BOOT_SEALED },
{ "signature_authenticates_metadata", signature_authenticates_metadata, BOOT_SEALED },
{ "hash_requires_frozen", hash_requires_frozen, BOOT_ANY },
{ "no_update_after_freeze", no_update_after_freeze, BOOT_ANY },
{ "freeze_writable_mmap", freeze_writable_mmap, BOOT_ANY },
{ "no_writable_mmap_frozen", no_writable_mmap_frozen, BOOT_ANY },
{ "map_hash_matches_libbpf", map_hash_matches_libbpf, BOOT_ANY },
{ "map_hash_multi_element", map_hash_multi_element, BOOT_ANY },
{ "map_hash_bad_size", map_hash_bad_size, BOOT_ANY },
{ "map_hash_unsupported_type", map_hash_unsupported_type, BOOT_ANY },
{ "lsm_signature_verdict", lsm_signature_verdict, BOOT_SEALED },
{ "signed_no_fd_array", signed_no_fd_array, BOOT_SEALED },
{ "signed_map_by_fd_rejected", signed_map_by_fd_rejected, BOOT_SEALED },
{ "signed_sparse_fd_array_rejected", signed_sparse_fd_array_rejected, BOOT_SEALED },
{ "bpf_keyring_provisioned", bpf_keyring_provisioned, BOOT_UNSEALED },
};
void test_signed_loader(void)
{
bool unsealed = keyring_unsealed_boot();
unsigned int i;
for (i = 0; i < ARRAY_SIZE(subtests); i++) {
if (!test__start_subtest(subtests[i].name))
continue;
if (subtests[i].boot == BOOT_SEALED && unsealed) {
printf("%s:SKIP:needs a boot without bpf.keyring_unsealed=1\n",
subtests[i].name);
test__skip();
continue;
}
if (subtests[i].boot == BOOT_UNSEALED && !unsealed) {
printf("%s:SKIP:needs bpf.keyring_unsealed=1\n",
subtests[i].name);
test__skip();
continue;
}
subtests[i].fn();
}
}