mirror of
https://github.com/torvalds/linux.git
synced 2026-10-08 11:36:02 +02:00
The signature_enforced, signature_too_large, signature_zero_size and signature_bad_keyring subtests load a program that must be rejected, but leave the fd open if the kernel unexpectedly accepts the load: test_progs asserts record the failure and continue, so the fd would linger for the rest of the run. Just close it. Signed-off-by: Daniel Borkmann <daniel@iogearbox.net> Link: https://lore.kernel.org/bpf/20260708184107.369182-1-daniel@iogearbox.net Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
1841 lines
54 KiB
C
1841 lines
54 KiB
C
// 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();
|
|
}
|