From: Jarkko Sakkinen <jarkko@kernel.org>
To: Kyle Zeng <kylebot@openai.com>, dhowells@redhat.com
Cc: keyrings@vger.kernel.org, linux-kernel@vger.kernel.org,
linux-security-module@vger.kernel.org, dhowells@redhat.com,
outbounddisclosures@openai.com
Subject: Re: [PATCH] keys: make keyring indexes independent of kernel addresses
Date: Thu, 8 Oct 2026 20:28:00 +0300 [thread overview]
Message-ID: <asfSoKL_0NreaEnz@kernel.org> (raw)
In-Reply-To: <20261006223746.48940-1-kylebot@openai.com>
On Tue, Oct 06, 2026 at 03:37:46PM -0700, Kyle Zeng wrote:
> KEYCTL_READ returns the serial numbers in a keyring in associative-array
> traversal order. The first word of the index is a public, deterministic
> hash of the type pointer, domain pointer and description. An unprivileged
> user can therefore insert keys with known descriptions, predict their
> order for each possible KASLR slide, and recover the kernel image address.
> On an x86-64 build with 16 MiB alignment, 45 user keys suffice to
> distinguish the candidate slides.
>
> Remove the addresses from the whole index, not just its first hash word.
> Assign each key type and domain a nonzero 64-bit ID and hash those IDs and
> the description with a once-initialized SipHash key. Use the full IDs in
> the later index chunks as well. Truncating or hashing the IDs there would
> lose the guarantee that distinct type/domain/description tuples have a
> differing index bit.
>
> Allocate IDs lazily since private key types need not be registered. Cache
> them in the index key so tree walks and copies of an existing index do not
> need to dereference a type descriptor that may have been unregistered.
> Keep the 32-bit chunk layout and diff_objects() bit offsets consistent,
> and retain the separate root slot for keyrings used by recursive search.
> The key comparison, permissions and userspace data format are unchanged;
> only the address-dependent in-memory ordering is replaced.
>
> Fixes: b2a4df200d57 ("KEYS: Expand the capacity of a keyring")
> Assisted-by: Codex:gpt-6-astra
> Signed-off-by: Kyle Zeng <kylebot@openai.com>
> ---
I personally am for this approach but I'd like to have ACK or feedback
from David for this before moving further.
David, please take a look at this.
> include/linux/key-type.h | 1 +
> include/linux/key.h | 5 ++
> security/keys/keyring.c | 145 +++++++++++++++++++++------------------
> 3 files changed, 83 insertions(+), 68 deletions(-)
>
> diff --git a/include/linux/key-type.h b/include/linux/key-type.h
> index bb97bd3e5af4..c4f329b6ae3f 100644
> --- a/include/linux/key-type.h
> +++ b/include/linux/key-type.h
> @@ -161,6 +161,7 @@ struct key_type {
> const void *in, const void *in2);
>
> /* internal fields */
> + atomic64_t index_id; /* Opaque keyring index ID */
> struct list_head link; /* link in types list */
> struct lock_class_key lock_class; /* key->sem lock class */
> } __randomize_layout;
> diff --git a/include/linux/key.h b/include/linux/key.h
> index 81b8f05c6898..e40b5267658f 100644
> --- a/include/linux/key.h
> +++ b/include/linux/key.h
> @@ -107,6 +107,7 @@ struct keyring_name;
>
> struct key_tag {
> struct rcu_head rcu;
> + atomic64_t index_id; /* Opaque keyring index ID */
> refcount_t usage;
> bool removed; /* T when subject removed */
> };
> @@ -129,6 +130,8 @@ struct keyring_index_key {
> struct key_type *type;
> struct key_tag *domain_tag; /* Domain of operation */
> const char *description;
> + u64 type_id; /* Cached index IDs */
> + u64 domain_id;
> };
>
> union key_payload {
> @@ -251,6 +254,8 @@ struct key {
> struct key_type *type; /* type of key */
> struct key_tag *domain_tag; /* Domain of operation */
> char *description;
> + u64 type_id;
> + u64 domain_id;
> };
> };
>
> diff --git a/security/keys/keyring.c b/security/keys/keyring.c
> index 15bf4af8f282..c03e394b885d 100644
> --- a/security/keys/keyring.c
> +++ b/security/keys/keyring.c
> @@ -14,6 +14,9 @@
> #include <linux/err.h>
> #include <linux/user_namespace.h>
> #include <linux/nsproxy.h>
> +#include <linux/once.h>
> +#include <linux/random.h>
> +#include <linux/siphash.h>
> #include <keys/keyring-type.h>
> #include <keys/user-type.h>
> #include <linux/assoc_array_priv.h>
> @@ -147,49 +150,46 @@ static int keyring_instantiate(struct key *keyring,
> }
>
> /*
> - * Multiply 64-bits by 32-bits to 96-bits and fold back to 64-bit. Ideally we'd
> - * fold the carry back too, but that requires inline asm.
> + * Give key types and domains address-independent identities. Zero means that
> + * an ID has not yet been allocated. As with inode sequence numbers used by
> + * futexes, this relies on the 64-bit counter not wrapping in a machine's
> + * lifetime.
> */
> -static u64 mult_64x32_and_fold(u64 x, u32 y)
> +static u64 key_index_id(atomic64_t *id)
> {
> - u64 hi = (u64)(u32)(x >> 32) * y;
> - u64 lo = (u64)(u32)(x) * y;
> - return lo + ((u64)(u32)hi << 32) + (u32)(hi >> 32);
> + static atomic64_t next_id;
> + u64 old, new;
> +
> + old = atomic64_read(id);
> + if (likely(old))
> + return old;
> +
> + do {
> + new = atomic64_inc_return(&next_id);
> + } while (WARN_ON_ONCE(!new));
> +
> + old = 0;
> + if (!atomic64_try_cmpxchg_relaxed(id, &old, new))
> + return old;
> + return new;
> }
>
> /*
> - * Hash a key type and description.
> + * Hash a key type, domain and description. KEYCTL_READ exposes the tree's
> + * traversal order, so no part of the index may contain kernel addresses.
> */
> static void hash_key_type_and_desc(struct keyring_index_key *index_key)
> {
> + static siphash_key_t hash_key __read_mostly;
> const unsigned level_shift = ASSOC_ARRAY_LEVEL_STEP;
> const unsigned long fan_mask = ASSOC_ARRAY_FAN_MASK;
> - const char *description = index_key->description;
> - unsigned long hash, type;
> - u32 piece;
> + unsigned long hash;
> u64 acc;
> - int n, desc_len = index_key->desc_len;
> -
> - type = (unsigned long)index_key->type;
> - acc = mult_64x32_and_fold(type, desc_len + 13);
> - acc = mult_64x32_and_fold(acc, 9207);
> - piece = (unsigned long)index_key->domain_tag;
> - acc = mult_64x32_and_fold(acc, piece);
> - acc = mult_64x32_and_fold(acc, 9207);
> -
> - for (;;) {
> - n = desc_len;
> - if (n <= 0)
> - break;
> - if (n > 4)
> - n = 4;
> - piece = 0;
> - memcpy(&piece, description, n);
> - description += n;
> - desc_len -= n;
> - acc = mult_64x32_and_fold(acc, piece);
> - acc = mult_64x32_and_fold(acc, 9207);
> - }
> +
> + get_random_once(&hash_key, sizeof(hash_key));
> + acc = siphash(index_key->description, index_key->desc_len, &hash_key);
> + acc = siphash_3u64(index_key->type_id, index_key->domain_id, acc,
> + &hash_key);
>
> /* Fold the hash down to 32 bits if need be. */
> hash = acc;
> @@ -209,7 +209,7 @@ static void hash_key_type_and_desc(struct keyring_index_key *index_key)
>
> /*
> * Finalise an index key to include a part of the description actually in the
> - * index key, to set the domain tag and to calculate the hash.
> + * index key, to set the domain tag and index IDs, and to calculate the hash.
> */
> void key_set_index_key(struct keyring_index_key *index_key)
> {
> @@ -225,6 +225,14 @@ void key_set_index_key(struct keyring_index_key *index_key)
> index_key->domain_tag = &default_domain_tag;
> }
>
> + /* Preserve the IDs when copying an existing key's index. In
> + * particular, its type may already have been unregistered.
> + */
> + if (!index_key->type_id)
> + index_key->type_id = key_index_id(&index_key->type->index_id);
> + if (!index_key->domain_id)
> + index_key->domain_id = key_index_id(&index_key->domain_tag->index_id);
> +
> hash_key_type_and_desc(index_key);
> }
>
> @@ -263,11 +271,14 @@ void key_remove_domain(struct key_tag *domain_tag)
> /*
> * Build the next index key chunk.
> *
> + * The hash and inline description are followed by the 64-bit type and domain
> + * IDs, low word first, and then by the rest of the description.
> * We return it one word-sized chunk at a time.
> */
> static unsigned long keyring_get_key_chunk(const void *data, int level)
> {
> const struct keyring_index_key *index_key = data;
> + const unsigned int id_chunks = sizeof(u64) / sizeof(unsigned long);
> unsigned long chunk = 0;
> const u8 *d;
> int desc_len = index_key->desc_len, n = sizeof(chunk);
> @@ -279,27 +290,30 @@ static unsigned long keyring_get_key_chunk(const void *data, int level)
> return index_key->hash;
> case 1:
> return index_key->x;
> - case 2:
> - return (unsigned long)index_key->type;
> - case 3:
> - return (unsigned long)index_key->domain_tag;
> - default:
> - level -= 4;
> - offset = sizeof(index_key->desc) + level * sizeof(long);
> - if (desc_len <= offset)
> - return 0;
> -
> - d = index_key->description + offset;
> - desc_len -= offset;
> - if (desc_len > n)
> - desc_len = n;
> - d += desc_len;
> - do {
> - chunk <<= 8;
> - chunk |= *--d;
> - } while (--desc_len > 0);
> - return chunk;
> }
> +
> + level -= 2;
> + if (level < id_chunks)
> + return index_key->type_id >> (level * ASSOC_ARRAY_KEY_CHUNK_SIZE);
> + level -= id_chunks;
> + if (level < id_chunks)
> + return index_key->domain_id >> (level * ASSOC_ARRAY_KEY_CHUNK_SIZE);
> + level -= id_chunks;
> +
> + offset = sizeof(index_key->desc) + level * sizeof(long);
> + if (desc_len <= offset)
> + return 0;
> +
> + d = index_key->description + offset;
> + desc_len -= offset;
> + if (desc_len > n)
> + desc_len = n;
> + d += desc_len;
> + do {
> + chunk <<= 8;
> + chunk |= *--d;
> + } while (--desc_len > 0);
> + return chunk;
> }
>
> static unsigned long keyring_get_object_key_chunk(const void *object, int level)
> @@ -330,6 +344,7 @@ static int keyring_diff_objects(const void *object, const void *data)
> const struct keyring_index_key *a = &key_a->index_key;
> const struct keyring_index_key *b = data;
> unsigned long seg_a, seg_b;
> + u64 diff;
> int level, i;
>
> level = 0;
> @@ -339,28 +354,22 @@ static int keyring_diff_objects(const void *object, const void *data)
> goto differ;
> level += ASSOC_ARRAY_KEY_CHUNK_SIZE / 8;
>
> - /* The number of bits contributed by the hash is controlled by a
> - * constant in the assoc_array headers. Everything else thereafter we
> - * can deal with as being machine word-size dependent.
> - */
> + /* The inline description contributes one machine-word-sized chunk. */
> seg_a = a->x;
> seg_b = b->x;
> if ((seg_a ^ seg_b) != 0)
> goto differ;
> level += sizeof(unsigned long);
>
> - /* The next bit may not work on big endian */
> - seg_a = (unsigned long)a->type;
> - seg_b = (unsigned long)b->type;
> - if ((seg_a ^ seg_b) != 0)
> - goto differ;
> - level += sizeof(unsigned long);
> + diff = a->type_id ^ b->type_id;
> + if (diff)
> + return level * 8 + __ffs64(diff);
> + level += sizeof(u64);
>
> - seg_a = (unsigned long)a->domain_tag;
> - seg_b = (unsigned long)b->domain_tag;
> - if ((seg_a ^ seg_b) != 0)
> - goto differ;
> - level += sizeof(unsigned long);
> + diff = a->domain_id ^ b->domain_id;
> + if (diff)
> + return level * 8 + __ffs64(diff);
> + level += sizeof(u64);
>
> i = sizeof(a->desc);
> if (a->desc_len <= i)
> --
> 2.53.0
>
Br, Jarkko
prev parent reply other threads:[~2026-10-08 17:28 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-06 22:37 Kyle Zeng
2026-10-08 17:28 ` Jarkko Sakkinen [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=asfSoKL_0NreaEnz@kernel.org \
--to=jarkko@kernel.org \
--cc=dhowells@redhat.com \
--cc=keyrings@vger.kernel.org \
--cc=kylebot@openai.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-security-module@vger.kernel.org \
--cc=outbounddisclosures@openai.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®