[PATCH 01/11] hornet: fix TOCTOU in signed program verification

Fan Wu wufan at kernel.org
Sat May 30 01:11:42 UTC 2026


On Wed, May 27, 2026 at 8:09 PM Blaise Boscaccy
<bboscaccy at linux.microsoft.com> wrote:
>
> The signature verification path was vulnerable to a time-of-check vs
> time-of-use race at both the program load and program run hook sites:
> between the moment a map's contents were hashed for signature
> verification and the moment the program run hook re-verified them, an
> attacker with sufficient privileges could swap or mutate the map
> contents.
>
> Close the race by snapshotting the map hashes during program load,
> attaching them to the program, and re-verifying them from the
> security_bpf_prog hook against prog->aux->used_maps. Because used_maps
> is the same map set the verifier and runtime resolve against, there is
> no longer a window in which the verified set and the executed set can
> diverge.
>
> Since we are no longer targeting the fd_array passed in, drop the map
> index data entirely and check for whether or not the set of requested
> map hashes is a subset of prog->aux->used_maps.
>
> Reported-by: Eric Biggers <ebiggers at kernel.org>
> Signed-off-by: Blaise Boscaccy <bboscaccy at linux.microsoft.com>
> ---
>  Documentation/admin-guide/LSM/Hornet.rst |  39 +++-----
>  scripts/hornet/gen_sig.c                 |  17 +---
>  security/hornet/hornet.asn1              |   1 -
>  security/hornet/hornet_lsm.c             | 121 +++--------------------
>  tools/testing/selftests/hornet/Makefile  |   2 +-
>  5 files changed, 35 insertions(+), 145 deletions(-)
>
> diff --git a/Documentation/admin-guide/LSM/Hornet.rst b/Documentation/admin-guide/LSM/Hornet.rst
> index 0ade4c17374c6..a369bc11408f4 100644
> --- a/Documentation/admin-guide/LSM/Hornet.rst
> +++ b/Documentation/admin-guide/LSM/Hornet.rst
> @@ -86,15 +86,14 @@ Hornet protects against the following threats:
>
>  - **Tampering with map data**: When map hashes are included in the
>    signature, Hornet verifies that frozen BPF maps match their expected
> -  SHA-256 hashes at load time. Maps are also re-verified before program
> -  execution via ``BPF_PROG_RUN``.
> +  SHA-256 hashes at load time after the program is publically exposed.
>
>  Hornet does **not** protect against:
>
>  - Compromise of the signing key itself.
>  - Attacks that occur after a program has been loaded and verified.
>  - Programs loaded by the kernel itself (kernel-internal loads bypass
> -  the ``BPF_PROG_RUN`` map check).
> +  the map check).
>
>  Known Limitations
>  =================
> @@ -117,6 +116,10 @@ Known Limitations
>    data. It does not guarantee positional binding of maps to specific
>    fd_array slots.
>
> +- Map hash verification does not enforce any ordering. It simply asserts
> +  that the set of map hashes requested to be verified exist in the used
> +  array.
> +
>  - BPF_MAP_TYPE_PROG_ARRAY maps must be frozen for Hornet to verify
>    them. Unfrozen prog array maps are not covered by verification.
>
> @@ -159,24 +162,19 @@ The following describes what happens when a userspace program calls
>  5. Hornet extracts the authenticated attribute identified by
>     ``OID_hornet_data`` (OID ``2.25.316487325684022475439036912669789383960``)
>     from the PKCS#7 message. This attribute contains an ASN.1-encoded set
> -   of map index/hash pairs.
> +   of map hash hashes
>
> -6. For each map hash entry, Hornet retrieves the corresponding BPF map
> -   via its file descriptor, confirms it is frozen, computes its SHA-256
> -   hash, and compares it against the signed hash.
> +6. For each map hash entry, Hornet retrieves stores the target map hash in
> +   the program's LSM blob.
>
>  7. The resulting integrity verdict is passed to the
>     ``bpf_prog_load_post_integrity`` hook so that downstream LSMs can
>     enforce policy.
>
> -Runtime Map Verification
> -------------------------
> -
> -When ``bpf(BPF_PROG_RUN, ...)`` is called from userspace, Hornet
> -re-verifies the hashes of all maps associated with the program. This
> -ensures that map contents have not been modified between program load
> -and execution. If any map hash no longer matches, the ``BPF_PROG_RUN``
> -command is denied.
> +8. After the verifier processes the program, once it's ready to be published,
> +   Hornet intercepts the ``bpf_prog`` hook, and verifies that the set of
> +   required hashes exist in the programs used maps. If the map hashes are
> +   unable to be found, the command is denied.
>
>  Userspace Interface
>  -------------------
> @@ -199,14 +197,10 @@ the following ASN.1 schema::
>    HornetData ::= SET OF Map
>
>    Map ::= SEQUENCE {
> -      index   INTEGER,
>        sha     OCTET STRING
>    }
>
> -Each ``Map`` entry contains the index of the map in the program's
> -``fd_array`` and its expected SHA-256 hash. A zero-length ``sha`` field
> -indicates that the map at that index should be skipped during
> -verification.
> +Each ``Map`` entry contains an expected SHA-256 hash.
>
>  Tooling
>  =======
> @@ -229,7 +223,7 @@ Usage::
>            --key <signer.key> \
>            [--pass <passphrase>] \
>            --out <signature.p7b> \
> -          [--add <mapfile.bin>:<index> ...]
> +          [--add <mapfile.bin> ...]
>
>  ``--data``
>    Path to the binary file containing eBPF program instructions to sign.
> @@ -248,8 +242,7 @@ Usage::
>
>  ``--add``
>    Attach a map hash as a signed attribute. The argument is a path to a
> -  binary map file followed by a colon and the map's index in the
> -  ``fd_array``. This option may be specified multiple times.
> +  binary map file. This option may be specified multiple times.
>
>  extract-skel.sh
>  ---------------
> diff --git a/scripts/hornet/gen_sig.c b/scripts/hornet/gen_sig.c
> index 8dd9ed66346a2..b4f983ab24bcd 100644
> --- a/scripts/hornet/gen_sig.c
> +++ b/scripts/hornet/gen_sig.c
> @@ -55,7 +55,6 @@
>
>  struct hash_spec {
>         char *file;
> -       int index;
>  };
>
>  typedef struct {
> @@ -66,7 +65,6 @@ typedef struct {
>
>  DECLARE_ASN1_FUNCTIONS(HORNET_MAP)
>  ASN1_SEQUENCE(HORNET_MAP) = {
> -       ASN1_SIMPLE(HORNET_MAP, index, ASN1_INTEGER),
>         ASN1_SIMPLE(HORNET_MAP, hash, ASN1_OCTET_STRING)
>  } ASN1_SEQUENCE_END(HORNET_MAP);
>
> @@ -253,12 +251,11 @@ static int sha256(const char *path, unsigned char out[SHA256_LEN], unsigned int
>         return rc;
>  }
>
> -static void add_hash(MAP_SET *set, unsigned char *buffer, int buffer_len, int index)
> +static void add_hash(MAP_SET *set, unsigned char *buffer, int buffer_len)
>  {
>         HORNET_MAP *map = NULL;
>
>         map = HORNET_MAP_new();
> -       ASN1_INTEGER_set(map->index, index);
>         ASN1_OCTET_STRING_set(map->hash, buffer, buffer_len);
>         sk_HORNET_MAP_push(set->maps, map);
>  }
> @@ -320,14 +317,8 @@ int main(int argc, char **argv)
>                         data_path = optarg;
>                         break;
>                 case 'A':
> -                       if (strchr(optarg, ':')) {
> -                               hashes[hash_count].file = strsep(&optarg, ":");
> -                               hashes[hash_count].index = atoi(optarg);
> -                               if (++hash_count >= MAX_HASHES) {
> -                                       usage(argv[0]);
> -                                       return EXIT_FAILURE;
> -                               }
> -                       } else {
> +                       hashes[hash_count].file = optarg;
> +                       if (++hash_count >= MAX_HASHES) {
>                                 usage(argv[0]);
>                                 return EXIT_FAILURE;
>                         }
> @@ -371,7 +362,7 @@ int main(int argc, char **argv)
>                 if (sha256(hashes[i].file, hash_buffer, &hash_len) != 0) {
>                         DIE("failed to hash input");
>                 }
> -               add_hash(set, hash_buffer, hash_len, hashes[i].index);
> +               add_hash(set, hash_buffer, hash_len);
>         }
>
>         oid = OBJ_txt2obj("2.25.316487325684022475439036912669789383960", 1);
> diff --git a/security/hornet/hornet.asn1 b/security/hornet/hornet.asn1
> index e60abf451ae23..3cf50379f5e7c 100644
> --- a/security/hornet/hornet.asn1
> +++ b/security/hornet/hornet.asn1
> @@ -7,6 +7,5 @@
>  HornetData ::= SET OF Map
>
>  Map ::= SEQUENCE {
> -       index                   INTEGER ({ hornet_map_index }),
>         sha                     OCTET STRING ({ hornet_map_hash })
>  } ({ hornet_next_map })
> diff --git a/security/hornet/hornet_lsm.c b/security/hornet/hornet_lsm.c
> index a4d11fa5b0889..516038413f321 100644
> --- a/security/hornet/hornet_lsm.c
> +++ b/security/hornet/hornet_lsm.c
> @@ -21,26 +21,18 @@
>
>  #define MAX_USED_MAPS 64
>
> -struct hornet_maps {
> -       bpfptr_t fd_array;
> -};
> -
>  /* The only hashing algorithm available is SHA256 due to it be hardcoded
>   * in the bpf subsystem.
>   */
> -
> -struct hornet_parse_context {
> -       int indexes[MAX_USED_MAPS];
> -       bool skips[MAX_USED_MAPS];
> -       unsigned char hashes[SHA256_DIGEST_SIZE * MAX_USED_MAPS];
> -       int hash_count;
> -};
> -
>  struct hornet_prog_security_struct {
>         int signed_hash_count;
>         unsigned char signed_hashes[SHA256_DIGEST_SIZE * MAX_USED_MAPS];
>  };
>
> +struct hornet_parse_context {
> +       struct hornet_prog_security_struct *security;
> +};
> +
>  struct lsm_blob_sizes hornet_blob_sizes __ro_after_init = {
>         .lbs_bpf_prog = sizeof(struct hornet_prog_security_struct),
>  };
> @@ -51,79 +43,17 @@ hornet_bpf_prog_security(struct bpf_prog *prog)
>         return prog->aux->security + hornet_blob_sizes.lbs_bpf_prog;
>  }
>
> -static int hornet_verify_hashes(struct hornet_maps *maps,
> -                               struct hornet_parse_context *ctx,
> -                               struct bpf_prog *prog)
> -{
> -       int map_fd;
> -       u32 i;
> -       struct bpf_map *map;
> -       int err = 0;
> -       unsigned char hash[SHA256_DIGEST_SIZE];
> -       struct hornet_prog_security_struct *security = hornet_bpf_prog_security(prog);
> -
> -       for (i = 0; i < ctx->hash_count; i++) {
> -               if (ctx->skips[i])
> -                       continue;
> -
> -               err = copy_from_bpfptr_offset(&map_fd, maps->fd_array,
> -                                             ctx->indexes[i] * sizeof(map_fd),
> -                                             sizeof(map_fd));
> -               if (err != 0)
> -                       return LSM_INT_VERDICT_FAULT;

It seems this verdict is no longer used.

> -
> -               CLASS(fd, f)(map_fd);
> -               if (fd_empty(f))
> -                       return LSM_INT_VERDICT_FAULT;
> -               if (unlikely(fd_file(f)->f_op != &bpf_map_fops))
> -                       return LSM_INT_VERDICT_FAULT;
> -
> -               map = fd_file(f)->private_data;
> -               if (!READ_ONCE(map->frozen))
> -                       return LSM_INT_VERDICT_FAULT;
> -
> -               if (!map->ops->map_get_hash)
> -                       return LSM_INT_VERDICT_FAULT;
> -
> -               if (map->ops->map_get_hash(map, SHA256_DIGEST_SIZE, hash))
> -                       return LSM_INT_VERDICT_FAULT;
> -
> -               err = memcmp(hash, &ctx->hashes[i * SHA256_DIGEST_SIZE],
> -                             SHA256_DIGEST_SIZE);
> -               if (err)
> -                       return LSM_INT_VERDICT_UNEXPECTED;

similar above, they should be removed for the header and for the ipe policy.
-Fan

> -
> -               memcpy(&security->signed_hashes[security->signed_hash_count * SHA256_DIGEST_SIZE],
> -                      &ctx->hashes[i * SHA256_DIGEST_SIZE], SHA256_DIGEST_SIZE);
> -               security->signed_hash_count++;
> -       }
> -       return LSM_INT_VERDICT_OK;
> -}
> -
>  int hornet_next_map(void *context, size_t hdrlen,
>                      unsigned char tag,
>                      const void *value, size_t vlen)
>  {
>         struct hornet_parse_context *ctx = (struct hornet_parse_context *)context;
>
> -       if (++ctx->hash_count >= MAX_USED_MAPS)
> +       if (++ctx->security->signed_hash_count >= MAX_USED_MAPS)
>                 return -EINVAL;
>         return 0;
>  }
>
> -int hornet_map_index(void *context, size_t hdrlen,
> -                    unsigned char tag,
> -                    const void *value, size_t vlen)
> -{
> -       struct hornet_parse_context *ctx = (struct hornet_parse_context *)context;
> -
> -       if (vlen != 1)
> -               return -EINVAL;
> -
> -       ctx->indexes[ctx->hash_count] = *(u8 *)value;
> -       return 0;
> -}
> -
>  int hornet_map_hash(void *context, size_t hdrlen,
>                     unsigned char tag,
>                     const void *value, size_t vlen)
> @@ -134,11 +64,8 @@ int hornet_map_hash(void *context, size_t hdrlen,
>         if (vlen != SHA256_DIGEST_SIZE && vlen != 0)
>                 return -EINVAL;
>
> -       if (vlen) {
> -               ctx->skips[ctx->hash_count] = false;
> -               memcpy(&ctx->hashes[ctx->hash_count * SHA256_DIGEST_SIZE], value, vlen);
> -       } else
> -               ctx->skips[ctx->hash_count] = true;
> +       memcpy(&ctx->security->signed_hashes[ctx->security->signed_hash_count * SHA256_DIGEST_SIZE],
> +              value, vlen);
>
>         return 0;
>  }
> @@ -147,7 +74,6 @@ static int hornet_check_program(struct bpf_prog *prog, union bpf_attr *attr,
>                                 struct bpf_token *token, bool is_kernel,
>                                 enum lsm_integrity_verdict *verdict)
>  {
> -       struct hornet_maps maps = {0};
>         bpfptr_t usig = make_bpfptr(attr->signature, is_kernel);
>         struct pkcs7_message *msg;
>         struct hornet_parse_context *ctx;
> @@ -172,7 +98,8 @@ static int hornet_check_program(struct bpf_prog *prog, union bpf_attr *attr,
>         if (!ctx)
>                 return -ENOMEM;
>
> -       maps.fd_array = make_bpfptr(attr->fd_array, is_kernel);
> +       ctx->security = hornet_bpf_prog_security(prog);
> +
>         sig = kzalloc(attr->signature_size, GFP_KERNEL);
>         if (!sig) {
>                 err = -ENOMEM;
> @@ -225,7 +152,7 @@ static int hornet_check_program(struct bpf_prog *prog, union bpf_attr *attr,
>                 goto cleanup_msg;
>         }
>
> -       *verdict = hornet_verify_hashes(&maps, ctx, prog);
> +       *verdict = LSM_INT_VERDICT_OK;
>         err = 0;
>
>  cleanup_msg:
> @@ -257,10 +184,8 @@ static int hornet_bpf_prog_load_integrity(struct bpf_prog *prog, union bpf_attr
>                                                      &hornet_lsmid, verdict);
>  }
>
> -static int hornet_check_prog_maps(u32 ufd)
> +static int hornet_check_prog_maps(struct bpf_prog *prog)
>  {
> -       CLASS(fd, f)(ufd);
> -       struct bpf_prog *prog;
>         struct hornet_prog_security_struct *security;
>         unsigned char hash[SHA256_DIGEST_SIZE];
>         struct bpf_map *map;
> @@ -268,12 +193,6 @@ static int hornet_check_prog_maps(u32 ufd)
>         bool found;
>         int covered_count = 0;
>
> -       if (fd_empty(f))
> -               return -EBADF;
> -       if (fd_file(f)->f_op != &bpf_prog_fops)
> -               return -EINVAL;
> -
> -       prog = fd_file(f)->private_data;
>         security = hornet_bpf_prog_security(prog);
>
>         if (!security->signed_hash_count)
> @@ -316,26 +235,14 @@ static int hornet_check_prog_maps(u32 ufd)
>         return 0;
>  }
>
> -static int hornet_bpf(int cmd, union bpf_attr *attr, unsigned int size, bool kernel)
> +static int hornet_bpf_prog(struct bpf_prog *prog)
>  {
> -       /* in horent_bpf(), anything that had originated from kernel space we assume
> -        * has already been checked, in some form or another, so we don't bother
> -        * checking the intergity of any maps. In hornet_bpf_prog_load_integrity(),
> -        * hornet doesn't make any opinion on that and delegates that to the downstream
> -        * policy enforcement.
> -        */
> -
> -       if (cmd != BPF_PROG_RUN)
> -               return 0;
> -       if (kernel)
> -               return 0;
> -
> -       return hornet_check_prog_maps(attr->test.prog_fd);
> +       return hornet_check_prog_maps(prog);
>  }
>
>  static struct security_hook_list hornet_hooks[] __ro_after_init = {
>         LSM_HOOK_INIT(bpf_prog_load_integrity, hornet_bpf_prog_load_integrity),
> -       LSM_HOOK_INIT(bpf, hornet_bpf),
> +       LSM_HOOK_INIT(bpf_prog, hornet_bpf_prog),
>  };
>
>  static int __init hornet_init(void)
> diff --git a/tools/testing/selftests/hornet/Makefile b/tools/testing/selftests/hornet/Makefile
> index 432bce59f54e7..316364f95f28c 100644
> --- a/tools/testing/selftests/hornet/Makefile
> +++ b/tools/testing/selftests/hornet/Makefile
> @@ -51,7 +51,7 @@ $(OUTPUT)/gen_sig: ../../../../scripts/hornet/gen_sig.c
>
>  sig.bin: insn.bin map.bin $(OUTPUT)/gen_sig
>         $(OUTPUT)/gen_sig --key $(CERTDIR)/signing_key.pem --cert $(CERTDIR)/signing_key.x509 \
> -               --data insn.bin --add map.bin:0 --out sig.bin
> +               --data insn.bin --add map.bin --out sig.bin
>
>  signed_loader.h: sig.bin
>         $(SCRIPTSDIR)/write-sig.sh loader.h sig.bin > $@
> --
> 2.53.0
>



More information about the Linux-security-module-archive mailing list