From: Yuqi Xu <xuyuqiabc@gmail.com>
To: linux-crypto@vger.kernel.org, keyrings@vger.kernel.org
Cc: David Howells <dhowells@redhat.com>,
Lukas Wunner <lukas@wunner.de>, Ignat Korchagin <ignat@linux.win>,
Herbert Xu <herbert@gondor.apana.org.au>,
"David S. Miller" <davem@davemloft.net>,
Rusty Russell <rusty@rustcorp.com.au>,
stable@vger.kernel.org, Vega <vega@nebusec.ai>,
Ren Wei <weir@nebusec.ai>,
xuyq21@lenovo.com
Subject: [PATCH v4] KEYS: Account for asymmetric key payload data in the quota
Date: Mon, 26 Oct 2026 10:49:48 +0800 [thread overview]
Message-ID: <20261026024840.23482-1-xuyuqiabc@gmail.com> (raw)
add_key("asymmetric", ...) preparses the supplied blob 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() duplicates the attacker-controlled PrivateKey OCTET
STRING with kmemdup() into pub->key. pkcs8_key_preparse() stores that
object in the asymmetric-key payload but hard-codes prep->quotalen to
100, so the quota records 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 while consuming 100 quota bytes. With the
default non-root limits (200 keys / 20000 bytes) a user can retain
roughly 12 MiB instead of the 20 KiB the limit is meant to allow.
x509_key_preparse() keeps the parsed public key, its parameters, the
signature and the authority key IDs, but also charges a fixed 100
bytes. PKCS#8 later repeated the same fixed-100 accounting. The same
add_key() call reaches the X.509 parser, so fixing only PKCS#8 would
leave the bypass reachable.
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. A charge which does not fit
in unsigned short is rejected with -EINVAL before the payload is
installed, because key_payload_reserve() keeps the full delta in the
user counter and refunds only the truncated key->quotalen. key_alloc()
also charges the key description to the same field, so include the
effective description length in that limit as well.
Measured on v7.3-rc1 (10396a2d6d41), 2 vCPU / 2 GiB QEMU guest, as uid
1000, adding PKCS#8 keys with a 65000-byte PrivateKey field. Before
the patch, 176 keys were retained (about 11.4 MiB) while
/proc/key-users charged only 19712 bytes. After the patch the
oversized key is rejected immediately, and a 200-byte PrivateKey still
loads and is charged 264 bytes. A small X.509 certificate whose only
large object is a 26000-byte authorityKeyIdentifier is likewise
rejected, while the unpatched kernel accepted it and charged 109
bytes.
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>
---
Changes in v4:
- Sashiko (High): "keys: integer truncation of key->quotalen causes
permanent quota leak" on key_alloc() and key_payload_reserve().
Include the effective key description length in the USHRT_MAX
limit. key_alloc() and key_payload_reserve() add the description
and the payload to the same unsigned short field, so checking the
payload alone was not enough.
- v3 Link: https://lore.kernel.org/all/20261019041314.48569-1-xuyuqiabc@gmail.com/
Changes in v3:
- Sashiko (High): "keys: integer truncation of key->quotalen causes
permanent quota leak" on pkcs8_key_preparse() and
x509_key_preparse(). A payload charge above USHRT_MAX is rejected
with -EINVAL before the payload is installed. key_payload_reserve()
keeps the full delta in the user quota and refunds only the
truncated key->quotalen, so that charge would leak quota.
- Reposted so Sashiko records a review against this Message-ID, as
requested by Herbert Xu. v2 was posted and Ignat Korchagin replied.
- v2 Link: https://lore.kernel.org/all/4a0b2e00f5cc62c173a01fec2f56eab1b04d70bb.1790568982.git.xuyuqiabc@gmail.com/
Changes in v2:
- Move the bug details and the measured quota numbers into this
commit message, as requested by Ignat Korchagin.
- Sashiko (High): "keys: integer truncation in key->quotalen causes
permanent quota leak" on pkcs8_key_preparse() and
x509_key_preparse(). Not changed in v2. The same finding is
addressed in v3 and v4.
- v1 Link: https://lore.kernel.org/all/cover.1789801335.git.xuyuqiabc@gmail.com/
diff --git a/crypto/asymmetric_keys/pkcs8_parser.c b/crypto/asymmetric_keys/pkcs8_parser.c
index 9dd1c181789a..db2d57489d96 100644
--- a/crypto/asymmetric_keys/pkcs8_parser.c
+++ b/crypto/asymmetric_keys/pkcs8_parser.c
@@ -10,6 +10,7 @@
#include <linux/kernel.h>
#include <linux/export.h>
#include <linux/slab.h>
+#include <linux/string.h>
#include <linux/err.h>
#include <linux/oid_registry.h>
#include <keys/asymmetric-subtype.h>
@@ -136,6 +137,8 @@ static struct public_key *pkcs8_parse(const void *data, size_t datalen)
static int pkcs8_key_preparse(struct key_preparsed_payload *prep)
{
struct public_key *pub;
+ const char *description;
+ size_t desclen, quotalen;
pub = pkcs8_parse(prep->data, prep->datalen);
if (IS_ERR(pub))
@@ -144,13 +147,26 @@ static int pkcs8_key_preparse(struct key_preparsed_payload *prep)
pr_devel("Cert Key Algo: %s\n", pub->pkey_algo);
pub->id_type = "PKCS8";
+ /*
+ * key_alloc() charges the description to the same unsigned short
+ * field that key_payload_reserve() later adds the payload to.
+ */
+ description = prep->orig_description ?: prep->description;
+ desclen = description ? strlen(description) + 1 : 0;
+
+ quotalen = sizeof(*pub) + pub->keylen;
+ if (desclen > USHRT_MAX || quotalen > USHRT_MAX - desclen) {
+ public_key_free(pub);
+ return -EINVAL;
+ }
+
/* We're pinning the module by being linked against it */
__module_get(public_key_subtype.owner);
prep->payload.data[asym_subtype] = &public_key_subtype;
prep->payload.data[asym_key_ids] = NULL;
prep->payload.data[asym_crypto] = pub;
prep->payload.data[asym_auth] = NULL;
- prep->quotalen = 100;
+ prep->quotalen = quotalen;
return 0;
}
diff --git a/crypto/asymmetric_keys/x509_public_key.c b/crypto/asymmetric_keys/x509_public_key.c
index 25cf8ac7f257..4a9c85277ce0 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;
+ const char *description, *q;
+ size_t desclen, srlen, sulen, quotalen;
+ int i;
cert = x509_cert_parse(prep->data, prep->datalen);
if (IS_ERR(cert))
@@ -221,6 +223,36 @@ static int x509_key_preparse(struct key_preparsed_payload *prep)
if (IS_ERR(kids->id[2]))
return PTR_ERR(kids->id[2]);
+ /* Charge the memory retained in the payload, not a fixed estimate */
+ quotalen = sizeof(*cert->pub) + cert->pub->keylen +
+ cert->pub->paramlen;
+ if (cert->sig) {
+ quotalen += sizeof(*cert->sig) + cert->sig->s_size;
+ if (cert->sig->m_free)
+ quotalen += cert->sig->m_size;
+ for (i = 0; i < ARRAY_SIZE(cert->sig->auth_ids); i++) {
+ id = cert->sig->auth_ids[i];
+ if (id)
+ quotalen += sizeof(*id) + id->len;
+ }
+ }
+ quotalen += sizeof(*kids);
+ for (i = 0; i < ARRAY_SIZE(kids->id); i++) {
+ id = kids->id[i];
+ if (id)
+ quotalen += sizeof(*id) + id->len;
+ }
+ /*
+ * key_alloc() charges the description to the same unsigned short
+ * field that key_payload_reserve() later adds the payload to.
+ */
+ description = prep->orig_description ?: desc;
+ desclen = strlen(description) + 1;
+ if (desclen > USHRT_MAX || quotalen > USHRT_MAX - desclen) {
+ kfree(kids->id[2]);
+ return -EINVAL;
+ }
+
/* We're pinning the module by being linked against it */
__module_get(public_key_subtype.owner);
prep->payload.data[asym_subtype] = &public_key_subtype;
@@ -228,7 +260,7 @@ 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;
+ prep->quotalen = quotalen;
/* We've finished with the certificate */
cert->pub = NULL;
--
2.56.0
next reply other threads:[~2026-10-26 2:50 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-26 2:49 Yuqi Xu [this message]
2026-11-06 18:23 ` [PATCH v4] KEYS: Account for asymmetric key payload data in the quota Ignat Korchagin
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=20261026024840.23482-1-xuyuqiabc@gmail.com \
--to=xuyuqiabc@gmail.com \
--cc=davem@davemloft.net \
--cc=dhowells@redhat.com \
--cc=herbert@gondor.apana.org.au \
--cc=ignat@linux.win \
--cc=keyrings@vger.kernel.org \
--cc=linux-crypto@vger.kernel.org \
--cc=lukas@wunner.de \
--cc=rusty@rustcorp.com.au \
--cc=stable@vger.kernel.org \
--cc=vega@nebusec.ai \
--cc=weir@nebusec.ai \
--cc=xuyq21@lenovo.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