crypto/asymmetric_keys/verify_pefile.c | 4 ++++ 1 file changed, 4 insertions(+)
pefile_parse_binary() reads the size field of the certificate table's
data-directory entry, which sits at fixed index 4 of the PE optional
header's data directory:
ctx->certs_size = ddir->certs.size;
but nothing ensures index 4 is present. n_data_dirents (the untrusted
NumberOfRvaAndSizes) is only upper-bounded against header_size and may be
0, and header_size need only satisfy cursor < header_size < datalen. A
crafted PE with n_data_dirents = 0 and a tiny header_size therefore causes
the ddir->certs.size read to land past the end of the image (CWE-125). The
chkaddr() that bounds the certificate blob runs only after this read.
verify_pefile_signature() is reached from kexec_file_load() (the
lockdown/secure-boot enforced PE-image signature path), and the image is
parsed before its signature is checked. The trigger needs CAP_SYS_BOOT and
the access is out-of-bounds read only (no write).
Require the certificate table's data-directory entry (index 4) to be
present; the existing upper-bound check then keeps ddir->certs within
[cursor, header_size).
Fixes: 26d1164be37f ("pefile: Parse a PE binary to find a key and a signature contained therein")
Assisted-by: copilot-cli:claude-opus-4-6 frama-c
Signed-off-by: Fabrice Derepas <fabrice.derepas@canonical.com>
---
Reproduced under KASAN (CONFIG_KASAN_GENERIC, x86-64) with a KUnit case that
builds a crafted PE (valid MZ/PE/PE32 magics, data_dirs = 0, header_size just
past the optional header) and calls verify_pefile_signature() with a NULL
keyring -- the parse runs before any signature check. On an unpatched kernel:
BUG: KASAN: slab-out-of-bounds in verify_pefile_signature+0x1d6/0x950
Read of size 4 ... in pefile_parse_binary() (inlined)
With this patch the crafted image is rejected (-ELIBBAD) before the read and
the case passes with no KASAN report. The test is not included here; happy to
submit it separately.
crypto/asymmetric_keys/verify_pefile.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/crypto/asymmetric_keys/verify_pefile.c b/crypto/asymmetric_keys/verify_pefile.c
index cec99db..e7f8bdb 100644
--- a/crypto/asymmetric_keys/verify_pefile.c
+++ b/crypto/asymmetric_keys/verify_pefile.c
@@ -87,6 +87,10 @@ static int pefile_parse_binary(const void *pebuf, unsigned int pelen,
if (ctx->n_data_dirents > (ctx->header_size - cursor) / sizeof(*dde))
return -ELIBBAD;
+ /* The certificate table entry is at fixed index 4 of the data directory. */
+ if (ctx->n_data_dirents <= 4)
+ return -ELIBBAD;
+
ddir = pebuf + cursor;
cursor += sizeof(*dde) * ctx->n_data_dirents;
base-commit: 3d6d817622b0a9721e3cc404df3469171582be13
--
2.53.0
On Thu, Aug 13, 2026 at 5:57 PM Fabrice Derepas
<fabrice.derepas@canonical.com> wrote:
>
> pefile_parse_binary() reads the size field of the certificate table's
> data-directory entry, which sits at fixed index 4 of the PE optional
> header's data directory:
>
> ctx->certs_size = ddir->certs.size;
>
> but nothing ensures index 4 is present. n_data_dirents (the untrusted
> NumberOfRvaAndSizes) is only upper-bounded against header_size and may be
> 0, and header_size need only satisfy cursor < header_size < datalen. A
> crafted PE with n_data_dirents = 0 and a tiny header_size therefore causes
> the ddir->certs.size read to land past the end of the image (CWE-125). The
> chkaddr() that bounds the certificate blob runs only after this read.
>
> verify_pefile_signature() is reached from kexec_file_load() (the
> lockdown/secure-boot enforced PE-image signature path), and the image is
> parsed before its signature is checked. The trigger needs CAP_SYS_BOOT and
> the access is out-of-bounds read only (no write).
>
> Require the certificate table's data-directory entry (index 4) to be
> present; the existing upper-bound check then keeps ddir->certs within
> [cursor, header_size).
>
> Fixes: 26d1164be37f ("pefile: Parse a PE binary to find a key and a signature contained therein")
> Assisted-by: copilot-cli:claude-opus-4-6 frama-c
> Signed-off-by: Fabrice Derepas <fabrice.derepas@canonical.com>
> ---
> Reproduced under KASAN (CONFIG_KASAN_GENERIC, x86-64) with a KUnit case that
Any reason not to commit this as well?
> builds a crafted PE (valid MZ/PE/PE32 magics, data_dirs = 0, header_size just
> past the optional header) and calls verify_pefile_signature() with a NULL
> keyring -- the parse runs before any signature check. On an unpatched kernel:
>
> BUG: KASAN: slab-out-of-bounds in verify_pefile_signature+0x1d6/0x950
> Read of size 4 ... in pefile_parse_binary() (inlined)
>
> With this patch the crafted image is rejected (-ELIBBAD) before the read and
> the case passes with no KASAN report. The test is not included here; happy to
> submit it separately.
>
> crypto/asymmetric_keys/verify_pefile.c | 4 ++++
> 1 file changed, 4 insertions(+)
>
> diff --git a/crypto/asymmetric_keys/verify_pefile.c b/crypto/asymmetric_keys/verify_pefile.c
> index cec99db..e7f8bdb 100644
> --- a/crypto/asymmetric_keys/verify_pefile.c
> +++ b/crypto/asymmetric_keys/verify_pefile.c
> @@ -87,6 +87,10 @@ static int pefile_parse_binary(const void *pebuf, unsigned int pelen,
> if (ctx->n_data_dirents > (ctx->header_size - cursor) / sizeof(*dde))
> return -ELIBBAD;
>
> + /* The certificate table entry is at fixed index 4 of the data directory. */
> + if (ctx->n_data_dirents <= 4)
My agent suggested replacing 4 with offsetof(struct data_directory,
certs) / sizeof(*dde) to make it more obvious, however not married to
it.
> + return -ELIBBAD;
> +
> ddir = pebuf + cursor;
> cursor += sizeof(*dde) * ctx->n_data_dirents;
>
> base-commit: 3d6d817622b0a9721e3cc404df3469171582be13
> --
> 2.53.0
>
>
Ignat
pefile_parse_binary() reads the size field of the certificate table's
data-directory entry, which sits at fixed index 4 of the PE optional
header's data directory:
ctx->certs_size = ddir->certs.size;
but nothing ensures index 4 is present. n_data_dirents (the untrusted
NumberOfRvaAndSizes) is only upper-bounded against header_size and may be
0, and header_size need only satisfy cursor < header_size < datalen. A
crafted PE with n_data_dirents = 0 and a tiny header_size therefore causes
the ddir->certs.size read to land past the end of the image (CWE-125). The
chkaddr() that bounds the certificate blob runs only after this read.
verify_pefile_signature() is reached from kexec_file_load() (the
lockdown/secure-boot enforced PE-image signature path), and the image is
parsed before its signature is checked. The trigger needs CAP_SYS_BOOT and
the access is out-of-bounds read only (no write).
Require the certificate table's data-directory entry (index 4) to be
present; the existing upper-bound check then keeps ddir->certs within
[cursor, header_size).
Fixes: 26d1164be37f ("pefile: Parse a PE binary to find a key and a signature contained therein")
Assisted-by: copilot-cli:claude-opus-4-6 frama-c
Signed-off-by: Fabrice Derepas <fabrice.derepas@canonical.com>
---
v2: express the "index 4" bound as
offsetof(struct data_directory, certs) / sizeof(*dde) rather than a
literal 4, per Ignat Korchagin's review [1] -- self-documenting and it
tracks the struct layout. It is the same value (offsetof is 32,
sizeof(*dde) is 8, so the bound is 4). A KUnit test is added as 2/2.
[1] https://lore.kernel.org/all/CAOs+rJVztmvHSkNxP_voc7E=girsstCKmqxG37pvO2kTaEk1TQ@mail.gmail.com/
Reproduced under KASAN (CONFIG_KASAN_GENERIC, x86-64) with the KUnit case in
2/2: a crafted PE with data_dirs = 0 takes a slab-out-of-bounds read in
pefile_parse_binary() on an unpatched kernel, and is rejected with -ELIBBAD
(no KASAN report) with this patch.
crypto/asymmetric_keys/verify_pefile.c | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/crypto/asymmetric_keys/verify_pefile.c b/crypto/asymmetric_keys/verify_pefile.c
index cec99db14..1efd5c0d0 100644
--- a/crypto/asymmetric_keys/verify_pefile.c
+++ b/crypto/asymmetric_keys/verify_pefile.c
@@ -87,6 +87,11 @@ static int pefile_parse_binary(const void *pebuf, unsigned int pelen,
if (ctx->n_data_dirents > (ctx->header_size - cursor) / sizeof(*dde))
return -ELIBBAD;
+ /* the certificate table entry must be present in the data directory */
+ if (ctx->n_data_dirents <=
+ offsetof(struct data_directory, certs) / sizeof(*dde))
+ return -ELIBBAD;
+
ddir = pebuf + cursor;
cursor += sizeof(*dde) * ctx->n_data_dirents;
base-commit: d58772d8520c7ef247c4b95c9bd76d3a25da9ff5
--
2.53.0
On Sat, Aug 15, 2026 at 04:00:19PM +0200, Fabrice Derepas wrote:
> pefile_parse_binary() reads the size field of the certificate table's
> data-directory entry, which sits at fixed index 4 of the PE optional
> header's data directory:
>
> ctx->certs_size = ddir->certs.size;
>
> but nothing ensures index 4 is present. n_data_dirents (the untrusted
> NumberOfRvaAndSizes) is only upper-bounded against header_size and may be
> 0, and header_size need only satisfy cursor < header_size < datalen. A
> crafted PE with n_data_dirents = 0 and a tiny header_size therefore causes
> the ddir->certs.size read to land past the end of the image (CWE-125). The
> chkaddr() that bounds the certificate blob runs only after this read.
>
> verify_pefile_signature() is reached from kexec_file_load() (the
> lockdown/secure-boot enforced PE-image signature path), and the image is
> parsed before its signature is checked. The trigger needs CAP_SYS_BOOT and
> the access is out-of-bounds read only (no write).
>
> Require the certificate table's data-directory entry (index 4) to be
> present; the existing upper-bound check then keeps ddir->certs within
> [cursor, header_size).
>
> Fixes: 26d1164be37f ("pefile: Parse a PE binary to find a key and a signature contained therein")
> Assisted-by: copilot-cli:claude-opus-4-6 frama-c
> Signed-off-by: Fabrice Derepas <fabrice.derepas@canonical.com>
> ---
> v2: express the "index 4" bound as
> offsetof(struct data_directory, certs) / sizeof(*dde) rather than a
> literal 4, per Ignat Korchagin's review [1] -- self-documenting and it
> tracks the struct layout. It is the same value (offsetof is 32,
> sizeof(*dde) is 8, so the bound is 4). A KUnit test is added as 2/2.
>
> [1] https://lore.kernel.org/all/CAOs+rJVztmvHSkNxP_voc7E=girsstCKmqxG37pvO2kTaEk1TQ@mail.gmail.com/
>
> Reproduced under KASAN (CONFIG_KASAN_GENERIC, x86-64) with the KUnit case in
> 2/2: a crafted PE with data_dirs = 0 takes a slab-out-of-bounds read in
> pefile_parse_binary() on an unpatched kernel, and is rejected with -ELIBBAD
> (no KASAN report) with this patch.
>
> crypto/asymmetric_keys/verify_pefile.c | 5 +++++
> 1 file changed, 5 insertions(+)
All applied. Thanks.
--
Email: Herbert Xu <herbert@gondor.apana.org.au>
Home Page: http://gondor.apana.org.au/~herbert/
PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt
Add self-contained KUnit tests for pefile_parse_binary(), exercised
through verify_pefile_signature() -- which parses the image before any
signature check, so no keyring or real signature is needed. The cases
build small malformed PE images in memory:
- a PE declaring 0 data-directory entries: the certificate table entry
at fixed index 4 is absent and must be rejected (-ELIBBAD), not read
out of bounds;
- a PE declaring 5 entries with a zero (unsigned) certificate entry
gets past the directory bound and is rejected as unsigned (-ENODATA).
verify_pefile_signature() is not exported, so the test is built-in only
(bool, not tristate).
Signed-off-by: Fabrice Derepas <fabrice.derepas@canonical.com>
---
crypto/asymmetric_keys/Kconfig | 12 +++
crypto/asymmetric_keys/Makefile | 2 +
crypto/asymmetric_keys/verify_pefile_test.c | 100 ++++++++++++++++++++
3 files changed, 114 insertions(+)
create mode 100644 crypto/asymmetric_keys/verify_pefile_test.c
diff --git a/crypto/asymmetric_keys/Kconfig b/crypto/asymmetric_keys/Kconfig
index 6a2f66404..c73979b2e 100644
--- a/crypto/asymmetric_keys/Kconfig
+++ b/crypto/asymmetric_keys/Kconfig
@@ -114,4 +114,16 @@ config FIPS_SIGNATURE_SELFTEST_ECDSA
depends on CRYPTO_SHA256=y || CRYPTO_SHA256=FIPS_SIGNATURE_SELFTEST
depends on CRYPTO_ECDSA=y || CRYPTO_ECDSA=FIPS_SIGNATURE_SELFTEST
+config VERIFY_PEFILE_KUNIT_TEST
+ bool "KUnit tests for signed PE file parsing" if !KUNIT_ALL_TESTS
+ depends on KUNIT=y && SIGNED_PE_FILE_VERIFICATION
+ default KUNIT_ALL_TESTS
+ help
+ Enable KUnit tests for the PE binary parser used by signed PE file
+ signature verification. The tests build small malformed PE images in
+ memory and check that pefile_parse_binary() rejects them instead of
+ reading out of bounds.
+
+ If unsure, say N.
+
endif # ASYMMETRIC_KEY_TYPE
diff --git a/crypto/asymmetric_keys/Makefile b/crypto/asymmetric_keys/Makefile
index bc65d3b98..35172685c 100644
--- a/crypto/asymmetric_keys/Makefile
+++ b/crypto/asymmetric_keys/Makefile
@@ -79,3 +79,5 @@ verify_signed_pefile-y := \
$(obj)/mscode_parser.o: $(obj)/mscode.asn1.h $(obj)/mscode.asn1.h
$(obj)/mscode.asn1.o: $(obj)/mscode.asn1.c $(obj)/mscode.asn1.h
+
+obj-$(CONFIG_VERIFY_PEFILE_KUNIT_TEST) += verify_pefile_test.o
diff --git a/crypto/asymmetric_keys/verify_pefile_test.c b/crypto/asymmetric_keys/verify_pefile_test.c
new file mode 100644
index 000000000..8b42eb41e
--- /dev/null
+++ b/crypto/asymmetric_keys/verify_pefile_test.c
@@ -0,0 +1,100 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for the signed PE binary parser in verify_pefile.c.
+ *
+ * The cases build small malformed PE images in memory and call
+ * verify_pefile_signature(), which parses the image before any signature
+ * check, so the parser is exercised without a keyring or a real signature.
+ */
+#include <kunit/test.h>
+#include <linux/slab.h>
+#include <linux/pe.h>
+#include <linux/verification.h>
+
+/*
+ * Build a minimal PE32 image whose optional header declares @data_dirs data
+ * directory entries. header_size is sized to hold them and the image is a few
+ * bytes larger. With @data_dirs < 5 the certificate table entry (fixed index
+ * 4) lies outside the declared directory.
+ */
+static void *build_pe32(u16 data_dirs, unsigned int *out_len)
+{
+ size_t cursor = sizeof(struct mz_hdr) + sizeof(struct pe_hdr) +
+ sizeof(struct pe32_opt_hdr);
+ size_t header_size = cursor + ((size_t)data_dirs + 1) *
+ sizeof(struct data_dirent);
+ size_t pelen = header_size + 8;
+ struct mz_hdr *mz;
+ struct pe_hdr *pe;
+ struct pe32_opt_hdr *opt;
+ void *buf = kzalloc(pelen, GFP_KERNEL);
+
+ if (!buf)
+ return NULL;
+
+ mz = buf;
+ mz->magic = IMAGE_DOS_SIGNATURE;
+ mz->peaddr = sizeof(struct mz_hdr);
+
+ pe = buf + sizeof(struct mz_hdr);
+ pe->magic = IMAGE_NT_SIGNATURE;
+
+ opt = buf + sizeof(struct mz_hdr) + sizeof(struct pe_hdr);
+ opt->magic = IMAGE_NT_OPTIONAL_HDR32_MAGIC;
+ opt->header_size = header_size;
+ opt->data_dirs = data_dirs;
+
+ *out_len = pelen;
+ return buf;
+}
+
+/*
+ * data_dirs = 0: the certificate table entry (index 4) is absent, so the
+ * parser must reject the image instead of reading ddir->certs past the end.
+ */
+static void pefile_missing_certs_dirent(struct kunit *test)
+{
+ unsigned int len;
+ void *buf = build_pe32(0, &len);
+ int ret;
+
+ KUNIT_ASSERT_NOT_NULL(test, buf);
+ ret = verify_pefile_signature(buf, len, NULL,
+ VERIFYING_KEXEC_PE_SIGNATURE);
+ KUNIT_EXPECT_EQ(test, ret, -ELIBBAD);
+ kfree(buf);
+}
+
+/*
+ * data_dirs = 5: the certificate table entry is present (and zero, i.e.
+ * unsigned), so the parser gets past the directory bound and rejects the image
+ * as unsigned (-ENODATA) rather than -ELIBBAD.
+ */
+static void pefile_present_certs_dirent(struct kunit *test)
+{
+ unsigned int len;
+ void *buf = build_pe32(5, &len);
+ int ret;
+
+ KUNIT_ASSERT_NOT_NULL(test, buf);
+ ret = verify_pefile_signature(buf, len, NULL,
+ VERIFYING_KEXEC_PE_SIGNATURE);
+ KUNIT_EXPECT_EQ(test, ret, -ENODATA);
+ kfree(buf);
+}
+
+static struct kunit_case verify_pefile_cases[] = {
+ KUNIT_CASE(pefile_missing_certs_dirent),
+ KUNIT_CASE(pefile_present_certs_dirent),
+ {}
+};
+
+static struct kunit_suite verify_pefile_suite = {
+ .name = "verify_pefile",
+ .test_cases = verify_pefile_cases,
+};
+
+kunit_test_suite(verify_pefile_suite);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("KUnit tests for the signed PE binary parser");
--
2.53.0
© 2016 - 2026 Red Hat, Inc.