root/tools/testing/selftests/bpf/prog_tests/signed_loader.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();
}