mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2026-08-09 06:14:34 +02:00
Add a minimal BPF LSM program on lsm/bpf_prog_load that, for loads on
the monitored thread, reads back prog->aux->sig.{verdict,keyring_type,
keyring_serial}, and a signed_loader subtest that drives the same
gen_loader loader through the hook twice: i) /unsigned/ where the LSM
must observe UNSIGNED, no keyring and serial 0; ii) /signed/ where the
very same insns signed against the session keyring must be observed as
VERIFIED with a user keyring, and the recorded keyring_serial must be
equal to the resolved session keyring serial. Loading (not running) the
loader is sufficient since the verdict is attached at load time.
# LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t signed_loader
[ 1.970530] clocksource: Switched to clocksource tsc
#405/1 signed_loader/metadata_check_shape:OK
#405/2 signed_loader/metadata_match:OK
#405/3 signed_loader/metadata_sha_mismatch:OK
#405/4 signed_loader/metadata_not_exclusive:OK
#405/5 signed_loader/metadata_hash_not_computed:OK
#405/6 signed_loader/signature_enforced:OK
#405/7 signed_loader/signature_too_large:OK
#405/8 signed_loader/signature_bad_keyring:OK
#405/9 signed_loader/metadata_ctx_max_entries_ignored:OK
#405/10 signed_loader/metadata_ctx_initial_value_ignored:OK
#405/11 signed_loader/signature_authenticates_insns:OK
#405/12 signed_loader/hash_requires_frozen:OK
#405/13 signed_loader/no_update_after_freeze:OK
#405/14 signed_loader/freeze_writable_mmap:OK
#405/15 signed_loader/no_writable_mmap_frozen:OK
#405/16 signed_loader/map_hash_matches_libbpf:OK
#405/17 signed_loader/map_hash_multi_element:OK
#405/18 signed_loader/map_hash_bad_size:OK
#405/19 signed_loader/map_hash_unsupported_type:OK
#405/20 signed_loader/lsm_signature_verdict:OK
#405 signed_loader:OK
Summary: 1/20 PASSED, 0 SKIPPED, 0 FAILED
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Link: https://lore.kernel.org/r/20260605213518.544262-2-daniel@iogearbox.net
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
1136 lines
32 KiB
C
1136 lines
32 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/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"
|
|
|
|
#define SIG_MATCH_INSNS 33 /* excl (5) + 4 * sha-dword (7) */
|
|
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
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,
|
|
bool get_hash, 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);
|
|
__u8 hbuf[SHA256_DIGEST_LENGTH];
|
|
struct bpf_map_info info;
|
|
__u32 ilen = sizeof(info), 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;
|
|
}
|
|
if (get_hash) {
|
|
memset(&info, 0, sizeof(info));
|
|
info.hash = ptr_to_u64(hbuf);
|
|
info.hash_size = sizeof(hbuf);
|
|
if (bpf_map_get_info_by_fd(map_fd, &info, &ilen)) {
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
|
|
static void check_sig_match_shape(const struct bpf_insn *in, int n)
|
|
{
|
|
int a = -1, cleanup = -1, i, base, t, br[5], nb = 0;
|
|
|
|
/* BPF_PSEUDO_MAP_IDX (the struct bpf_map * form) is used only here. */
|
|
for (i = 0; i + 1 < n; i++) {
|
|
if (in[i].code == (BPF_LD | BPF_IMM | BPF_DW) &&
|
|
in[i].src_reg == BPF_PSEUDO_MAP_IDX) {
|
|
a = i;
|
|
break;
|
|
}
|
|
}
|
|
if (!ASSERT_GE(a, 0, "emit_signature_match present"))
|
|
return;
|
|
if (!ASSERT_LE(a + SIG_MATCH_INSNS, n, "block fits in program"))
|
|
return;
|
|
|
|
/* excl check: r2 = *(u32 *)(map + 32); if r2 != 1 goto cleanup */
|
|
ASSERT_EQ(in[a + 2].code, (BPF_LDX | BPF_MEM | BPF_W), "excl load width");
|
|
ASSERT_EQ(in[a + 2].off, SHA256_DIGEST_LENGTH, "excl field offset");
|
|
ASSERT_EQ(in[a + 4].code, (BPF_JMP | BPF_JNE | BPF_K), "excl branch op");
|
|
ASSERT_EQ(in[a + 4].imm, 1, "excl compared to 1");
|
|
br[nb++] = a + 4;
|
|
|
|
/* 4 sha-dword checks: r2 = *(u64 *)(map + i*8); if r2 != r3 goto cleanup */
|
|
for (i = 0; i < 4; i++) {
|
|
base = a + 5 + i * 7;
|
|
ASSERT_EQ(in[base + 2].code, (BPF_LDX | BPF_MEM | BPF_DW), "sha load width");
|
|
ASSERT_EQ(in[base + 2].off, i * 8, "sha dword offset");
|
|
ASSERT_EQ(in[base + 3].code, (BPF_LD | BPF_IMM | BPF_DW), "sha imm64 (H_meta)");
|
|
ASSERT_EQ(in[base + 6].code, (BPF_JMP | BPF_JNE | BPF_X), "sha branch op");
|
|
br[nb++] = base + 6;
|
|
}
|
|
|
|
/*
|
|
* Locate the real cleanup label so we can pin the exact jump target,
|
|
* not just "some backward label". bpf_gen__init() emits the cleanup
|
|
* block as a prog-fd close loop whose first instruction is the label
|
|
* every error branch jumps to.
|
|
*/
|
|
for (i = 0; i + 2 < a; i++) {
|
|
if (in[i].code == (BPF_LDX | BPF_MEM | BPF_W) &&
|
|
in[i].dst_reg == BPF_REG_1 && in[i].src_reg == BPF_REG_10 &&
|
|
in[i + 1].code == (BPF_JMP | BPF_JSLE | BPF_K) &&
|
|
in[i + 1].dst_reg == BPF_REG_1 && in[i + 1].imm == 0 &&
|
|
in[i + 1].off == 1 &&
|
|
in[i + 2].code == (BPF_JMP | BPF_CALL) &&
|
|
in[i + 2].imm == BPF_FUNC_sys_close) {
|
|
cleanup = i;
|
|
break;
|
|
}
|
|
}
|
|
if (!ASSERT_GE(cleanup, 0, "cleanup label located"))
|
|
return;
|
|
for (i = 0; i < nb; i++) {
|
|
t = br[i] + 1 + in[br[i]].off;
|
|
ASSERT_EQ(t, cleanup, "sig-match lands on cleanup");
|
|
}
|
|
/*
|
|
* Same invariant for every other cleanup-bound jump in the program:
|
|
* emit_check_err() is the only source of "if (r7 < 0) goto cleanup",
|
|
* so each of those must also resolve exactly to cleanup.
|
|
*/
|
|
for (i = 0, t = 0; i < n; i++) {
|
|
if (in[i].code != (BPF_JMP | BPF_JSLT | BPF_K) ||
|
|
in[i].dst_reg != BPF_REG_7 || in[i].imm != 0 || in[i].off >= 0)
|
|
continue;
|
|
ASSERT_EQ(i + 1 + in[i].off, cleanup, "err-check lands on cleanup");
|
|
t++;
|
|
}
|
|
ASSERT_GT(t, 0, "found emit_check_err jumps");
|
|
}
|
|
|
|
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_check_shape(void)
|
|
{
|
|
struct gen_loader_fixture f;
|
|
|
|
if (gen_loader_fixture_init(&f) == 0)
|
|
check_sig_match_shape((const struct bpf_insn *)f.gopts.insns,
|
|
f.gopts.insns_sz / sizeof(struct bpf_insn));
|
|
gen_loader_fixture_fini(&f);
|
|
}
|
|
|
|
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,
|
|
true, 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 metadata_sha_mismatch(void)
|
|
{
|
|
struct gen_loader_fixture f;
|
|
bool ran;
|
|
int r;
|
|
|
|
if (gen_loader_fixture_init(&f) == 0) {
|
|
/*
|
|
* blob[0] lives in the loader's fd_array scratch (first add_data in
|
|
* bpf_gen__init); a 0-map program never reads it, so flipping it
|
|
* changes only map->sha. The metadata check is the only thing that
|
|
* can notice -> isolates emit_signature_match.
|
|
*/
|
|
f.blob[0] ^= 0xff;
|
|
r = run_gen_loader(f.gopts.insns, f.gopts.insns_sz, f.blob,
|
|
f.data_sz, f.excl, sizeof(f.excl), NULL, 0,
|
|
true, f.ctx, f.ctx_sz, &ran);
|
|
ASSERT_TRUE(ran, "loader ran");
|
|
ASSERT_EQ(r, -EINVAL, "tampered blob rejected by emit_signature_match");
|
|
}
|
|
gen_loader_fixture_fini(&f);
|
|
}
|
|
|
|
static void metadata_not_exclusive(void)
|
|
{
|
|
struct gen_loader_fixture f;
|
|
bool ran;
|
|
int r;
|
|
|
|
if (gen_loader_fixture_init(&f) == 0) {
|
|
/*
|
|
* Correct blob but a non-exclusive metadata map: the verifier does
|
|
* not reject (excl_prog_sha unset), so the runtime map->excl == 1
|
|
* check in the loader must.
|
|
*/
|
|
r = run_gen_loader(f.gopts.insns, f.gopts.insns_sz, f.blob,
|
|
f.data_sz, NULL, 0, NULL, 0, true, f.ctx,
|
|
f.ctx_sz, &ran);
|
|
ASSERT_TRUE(ran, "loader ran");
|
|
ASSERT_EQ(r, -EINVAL, "non-exclusive metadata map rejected");
|
|
}
|
|
gen_loader_fixture_fini(&f);
|
|
}
|
|
|
|
static void metadata_hash_not_computed(void)
|
|
{
|
|
struct gen_loader_fixture f;
|
|
bool ran;
|
|
int r;
|
|
|
|
if (gen_loader_fixture_init(&f) == 0) {
|
|
/*
|
|
* Correct, exclusive, frozen map, but its hash was never computed
|
|
* (no OBJ_GET_INFO_BY_FD), so map->sha stays zero. The loader must
|
|
* fail closed rather than treat an unset hash as a match.
|
|
*/
|
|
r = run_gen_loader(f.gopts.insns, f.gopts.insns_sz, f.blob,
|
|
f.data_sz, f.excl, sizeof(f.excl), NULL, 0,
|
|
false, f.ctx, f.ctx_sz, &ran);
|
|
ASSERT_TRUE(ran, "loader ran");
|
|
ASSERT_EQ(r, -EINVAL, "uncomputed metadata hash rejected");
|
|
}
|
|
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.)
|
|
*/
|
|
fd = load_loader(f.gopts.insns, f.gopts.insns_sz, -1, junk,
|
|
sizeof(junk), KEY_SPEC_SESSION_KEYRING);
|
|
ASSERT_LT(fd, 0, "invalid signature rejected at load");
|
|
}
|
|
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);
|
|
ASSERT_EQ(fd, -EINVAL, "oversized signature rejected");
|
|
}
|
|
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);
|
|
ASSERT_EQ(fd, -EINVAL, "signature with bad keyring_id rejected");
|
|
}
|
|
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, true, 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, true, 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;
|
|
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);
|
|
|
|
if (!ASSERT_OK(sign_buf(dir, insns, insns_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, true, 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;
|
|
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, true, 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(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;
|
|
int map_fd = -1, prog_fd = -1;
|
|
bool have_fixture = false;
|
|
struct gen_loader_fixture f;
|
|
__u32 sig_sz = 8192;
|
|
__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);
|
|
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);
|
|
close(map_fd);
|
|
map_fd = -1;
|
|
if (!ASSERT_OK_FD(prog_fd, "signed loader load"))
|
|
goto out;
|
|
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, "signed verdict");
|
|
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");
|
|
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);
|
|
}
|
|
|
|
void test_signed_loader(void)
|
|
{
|
|
if (test__start_subtest("metadata_check_shape"))
|
|
metadata_check_shape();
|
|
if (test__start_subtest("metadata_match"))
|
|
metadata_match();
|
|
if (test__start_subtest("metadata_sha_mismatch"))
|
|
metadata_sha_mismatch();
|
|
if (test__start_subtest("metadata_not_exclusive"))
|
|
metadata_not_exclusive();
|
|
if (test__start_subtest("metadata_hash_not_computed"))
|
|
metadata_hash_not_computed();
|
|
if (test__start_subtest("signature_enforced"))
|
|
signature_enforced();
|
|
if (test__start_subtest("signature_too_large"))
|
|
signature_too_large();
|
|
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("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();
|
|
}
|