Skip to contentSkip to conversation

crypto: asymmetric_keys - bound section data range in pefile_digest_pe_contents

PATCH In review Fabrice Derepas 2 participants 6 messages Aug 13 – Aug 26

Subsystems (5)
lore
Share & tools
lore
#0 lore raw

PATCH 1/1 crypto: asymmetric_keys - bound section data range in pefile_digest_pe_contents

pefile_digest_pe_contents() hashes each PE section with

	crypto_shash_update(desc, pebuf + ctx->secs[i].data_addr,
			    ctx->secs[i].raw_data_size);

where both data_addr and raw_data_size come straight from the untrusted PE section table. pefile_parse_binary() bounds only the section table's location, never the sections' data ranges, and nothing else on the path checks them. A section with data_addr and/or raw_data_size pointing outside the image therefore reads past the pelen-byte buffer (CWE-125); a large raw_data_size makes it read far past the end and fault.

Commit f7dd32c5179d ("crypto: asymmetric_keys - fix OOB read in pefile_digest_pe_contents") recently fixed a sibling underflow in this same function (the hashed_bytes + certs_size trailer computation); the per-section read below was left unchecked.

This loop runs only after verify_pkcs7_signature() succeeds, so it requires a validly signed image -- but not an attacker signing key. The PKCS#7 signature covers the SpcIndirectDataContent (a self-contained digest in the certificate table), not the live section bytes, so tampering data_addr in a publicly available signed image leaves the signature valid at that step while the out-of-bounds read fires when this function recomputes the digest. It is thus reachable by a local CAP_SYS_BOOT user via kexec_file_load() with a tampered signed image; the access is an out-of-bounds read only, but a large raw_data_size faults, which is a denial of service when panic_on_oops is set.

Reject a section whose data range does not lie within the image before hashing it.

Fixes af316fc442ef ("pefile: Digest the PE binary and compare to the PKCS#7 data")
Assisted-by copilot-cli:claude-opus-4-6 frama-c

---
Note: f7dd32c5179d, which fixed the sibling underflow in this function, was assigned CVE-2026-64544; this closes the per-section read it did not cover.

Reproduced under KASAN (CONFIG_KASAN_GENERIC, x86-64). Two things were shown:

1. The defect + fix: a KUnit case calls pefile_digest_pe_contents() with a crafted context whose section has data_addr = pelen. Unpatched:
slab-out-of-bounds read; patched: -ELIBBAD before the hash.

2. The reachability, end-to-end against a real trusted keyring: a real EFI binary was signed with a test key embedded via CONFIG_SYSTEM_TRUSTED_KEYS, then its section data_addr was tampered (leaving the PKCS#7 byte-identical). Results from verify_pefile_signature(): the untampered image verifies (0), one signed by an untrusted key is rejected at verify_pkcs7_signature() (-ENOKEY), and the tampered-but-trusted image passes that step and takes the OOB read in pefile_digest_pe_contents(). So no signing key is required.

The KUnit case (item 1) is easy to package and I can submit it separately; the end-to-end setup (item 2) needs an embedded trusted key and signed images, so it is harder to ship standalone. Neither is included here.

 crypto/asymmetric_keys/verify_pefile.c | 5 +++++
 1 file changed, 5 insertions(+)
crypto/asymmetric_keys/verify_pefile.c +5 -0
index cec99db..4e84fcf 100644
@@ -292,6 +292,11 @@ static int pefile_digest_pe_contents(const void *pebuf, unsigned int pelen,
 		i = canon[loop];
 		if (ctx->secs[i].raw_data_size == 0)
 			continue;
+		if (ctx->secs[i].data_addr > pelen ||
+		    ctx->secs[i].raw_data_size > pelen - ctx->secs[i].data_addr) {
+			kfree(canon);
+			return -ELIBBAD;
+		}
 		ret = crypto_shash_update(desc,
 					  pebuf + ctx->secs[i].data_addr,
 					  ctx->secs[i].raw_data_size);

base-commit: 3d6d817622b0a9721e3cc404df3469171582be13

Signature · 1 line
2.53.0
maintainer
#1 lore raw
Quoted 65 lines · Fabrice Derepas

pefile_digest_pe_contents() hashes each PE section with

        crypto_shash_update(desc, pebuf + ctx->secs[i].data_addr,
                            ctx->secs[i].raw_data_size);

where both data_addr and raw_data_size come straight from the untrusted PE section table. pefile_parse_binary() bounds only the section table's location, never the sections' data ranges, and nothing else on the path checks them. A section with data_addr and/or raw_data_size pointing outside the image therefore reads past the pelen-byte buffer (CWE-125); a large raw_data_size makes it read far past the end and fault.

Commit f7dd32c5179d ("crypto: asymmetric_keys - fix OOB read in pefile_digest_pe_contents") recently fixed a sibling underflow in this same function (the hashed_bytes + certs_size trailer computation); the per-section read below was left unchecked.

This loop runs only after verify_pkcs7_signature() succeeds, so it requires a validly signed image -- but not an attacker signing key. The PKCS#7 signature covers the SpcIndirectDataContent (a self-contained digest in the certificate table), not the live section bytes, so tampering data_addr in a publicly available signed image leaves the signature valid at that step while the out-of-bounds read fires when this function recomputes the digest. It is thus reachable by a local CAP_SYS_BOOT user via kexec_file_load() with a tampered signed image; the access is an out-of-bounds read only, but a large raw_data_size faults, which is a denial of service when panic_on_oops is set.

Reject a section whose data range does not lie within the image before hashing it.

Fixes af316fc442ef ("pefile: Digest the PE binary and compare to the PKCS#7 data")
Assisted-by copilot-cli:claude-opus-4-6 frama-c

---
Note: f7dd32c5179d, which fixed the sibling underflow in this function, was assigned CVE-2026-64544; this closes the per-section read it did not cover.

Reproduced under KASAN (CONFIG_KASAN_GENERIC, x86-64). Two things were shown:

1. The defect + fix: a KUnit case calls pefile_digest_pe_contents() with a crafted context whose section has data_addr = pelen. Unpatched:
slab-out-of-bounds read; patched: -ELIBBAD before the hash.

2. The reachability, end-to-end against a real trusted keyring: a real EFI binary was signed with a test key embedded via CONFIG_SYSTEM_TRUSTED_KEYS, then its section data_addr was tampered (leaving the PKCS#7 byte-identical). Results from verify_pefile_signature(): the untampered image verifies (0), one signed by an untrusted key is rejected at verify_pkcs7_signature() (-ENOKEY), and the tampered-but-trusted image passes that step and takes the OOB read in pefile_digest_pe_contents(). So no signing key is required.

The KUnit case (item 1) is easy to package and I can submit it separately; the end-to-end setup (item 2) needs an embedded trusted key and signed images, so it is harder to ship standalone. Neither is included here.

 crypto/asymmetric_keys/verify_pefile.c | 5 +++++
 1 file changed, 5 insertions(+)
crypto/asymmetric_keys/verify_pefile.c +0 -0
index cec99db..4e84fcf 100644

crypto/asymmetric_keys/verify_pefile.c static int pefile_digest_pe_contents(const void *pebuf, unsigned int pelen, · line 296

Quoted 5 lines
crypto/asymmetric_keys/verify_pefile.c +2 -0
@@ -292,6 +292,11 @@ static int pefile_digest_pe_contents(const void *pebuf, unsigned int pelen,
                i = canon[loop];
                if (ctx->secs[i].raw_data_size == 0)
                        continue;
+               if (ctx->secs[i].data_addr > pelen ||
+                   ctx->secs[i].raw_data_size > pelen - ctx->secs[i].data_addr) {

Can we do this validation earlier? Perhaps in pefile_parse_binary()? The idea is that if the data is malformed we should probably not start hashing at all. You could also reuse chaddr() macro for the check.

Quoted 11 lines
crypto/asymmetric_keys/verify_pefile.c +3 -0
+                       kfree(canon);
+                       return -ELIBBAD;
+               }
                ret = crypto_shash_update(desc,
                                          pebuf + ctx->secs[i].data_addr,
                                          ctx->secs[i].raw_data_size);

base-commit: 3d6d817622b0a9721e3cc404df3469171582be13 --
2.53.0

Ignat

#2 lore raw

PATCH v2 1/1 crypto: asymmetric_keys - bound PE section ranges in pefile_parse_binary

pefile_digest_pe_contents() hashes each PE section with

	crypto_shash_update(desc, pebuf + ctx->secs[i].data_addr,
			    ctx->secs[i].raw_data_size);

where both data_addr and raw_data_size come straight from the untrusted PE section table. pefile_parse_binary() bounds the section table's location but not the sections' data ranges, and nothing else on the path checks them. A section with data_addr and/or raw_data_size pointing outside the image therefore reads past the pelen-byte buffer (CWE-125); a large raw_data_size makes it read far past the end and fault.

Commit f7dd32c5179d ("crypto: asymmetric_keys - fix OOB read in pefile_digest_pe_contents") recently fixed a sibling underflow in this same function (the hashed_bytes + certs_size trailer computation); the per-section read was left unchecked.

The digest loop runs only after verify_pkcs7_signature() succeeds, so it requires a validly signed image -- but not an attacker signing key. The PKCS#7 signature covers the SpcIndirectDataContent (a self-contained digest in the certificate table), not the live section bytes, so tampering data_addr in a publicly available signed image leaves the signature valid at that step while the out-of-bounds read fires when the digest is recomputed. It is thus reachable by a local CAP_SYS_BOOT user via kexec_file_load() with a tampered signed image; the access is an out-of-bounds read only, but a large raw_data_size faults, which is a denial of service when panic_on_oops is set.

Validate each section's data range against the image when the section table is parsed, reusing the existing chkaddr() bounds macro. A malformed range is rejected with -ELIBBAD at parse time -- before signature verification and before any hashing -- so a bad image is never digested.

Fixes af316fc442ef ("pefile: Digest the PE binary and compare to the PKCS#7 data")
Assisted-by copilot-cli:claude-opus-4-6 frama-c

---
v2:
- move the check into pefile_parse_binary() and reuse chkaddr(), per Ignat Korchagin's review [1], so a malformed image is rejected at parse time -- before signature verification and before hashing. v1 put the check in the pefile_digest_pe_contents() section loop.
- because chkaddr() runs on every section, v2 also validates zero-size (raw_data_size == 0) sections, which v1 and the shipping digest loop skip. This only rejects a zero-size section whose data_addr points past the image -- malformed input that real PEs never produce (.bss uses PointerToRawData = 0, which passes) -- and matches the "don't start on malformed data" intent.

[1] https://lore.kernel.org/all/CAOs+rJXZ++7_o4L6LuT567FjdYsa=6NEki3qcFSRX+ehJxpoVg@mail.gmail.com/

Tested under KASAN (CONFIG_KASAN_GENERIC, x86-64) with a KUnit case that calls verify_pefile_signature() on real images: a signed-then-tampered image (a section's data_addr set to pelen) is now rejected with -ELIBBAD in pefile_parse_binary(), before verify_pkcs7_signature() and with no KASAN splat, while an untampered signed image passes parse and reaches the signature check (-ENOKEY, as the test key is not trusted in this build). v1's KASAN repro -- a crafted context taken straight into the digest loop -- showed the slab-out-of-bounds this prevents.

 crypto/asymmetric_keys/verify_pefile.c | 9 +++++++++
 1 file changed, 9 insertions(+)
crypto/asymmetric_keys/verify_pefile.c +9 -0
index cec99db14..c447597f7 100644
@@ -30,6 +30,7 @@ static int pefile_parse_binary(const void *pebuf, unsigned int pelen,
 	const struct data_dirent *dde;
 	const struct section_header *sec;
 	size_t cursor, datalen = pelen;
+	unsigned int loop;
 
 	kenter("");
 
@@ -112,6 +113,14 @@ static int pefile_parse_binary(const void *pebuf, unsigned int pelen,
 		return -ELIBBAD;
 	ctx->secs = pebuf + cursor;
 
+	/* pefile_digest_pe_contents() hashes each section's raw data using
+	 * these fields directly; reject a section whose data range falls
+	 * outside the image so hashing never reads past the buffer.
+	 */
+	for (loop = 0; loop < ctx->n_sections; loop++)
+		chkaddr(0, ctx->secs[loop].data_addr,
+			ctx->secs[loop].raw_data_size);
+
 	return 0;
 }
 

base-commit: d58772d8520c7ef247c4b95c9bd76d3a25da9ff5

Signature · 1 line
2.53.0
maintainer
#3 lore raw

Re: [PATCH v2] crypto: asymmetric_keys - bound PE section ranges in pefile_parse_binary

Quoted 50 lines · Fabrice Derepas

pefile_digest_pe_contents() hashes each PE section with

        crypto_shash_update(desc, pebuf + ctx->secs[i].data_addr,
                            ctx->secs[i].raw_data_size);

where both data_addr and raw_data_size come straight from the untrusted PE section table. pefile_parse_binary() bounds the section table's location but not the sections' data ranges, and nothing else on the path checks them. A section with data_addr and/or raw_data_size pointing outside the image therefore reads past the pelen-byte buffer (CWE-125); a large raw_data_size makes it read far past the end and fault.

Commit f7dd32c5179d ("crypto: asymmetric_keys - fix OOB read in pefile_digest_pe_contents") recently fixed a sibling underflow in this same function (the hashed_bytes + certs_size trailer computation); the per-section read was left unchecked.

The digest loop runs only after verify_pkcs7_signature() succeeds, so it requires a validly signed image -- but not an attacker signing key. The PKCS#7 signature covers the SpcIndirectDataContent (a self-contained digest in the certificate table), not the live section bytes, so tampering data_addr in a publicly available signed image leaves the signature valid at that step while the out-of-bounds read fires when the digest is recomputed. It is thus reachable by a local CAP_SYS_BOOT user via kexec_file_load() with a tampered signed image; the access is an out-of-bounds read only, but a large raw_data_size faults, which is a denial of service when panic_on_oops is set.

Validate each section's data range against the image when the section table is parsed, reusing the existing chkaddr() bounds macro. A malformed range is rejected with -ELIBBAD at parse time -- before signature verification and before any hashing -- so a bad image is never digested.

Fixes af316fc442ef ("pefile: Digest the PE binary and compare to the PKCS#7 data")
Assisted-by copilot-cli:claude-opus-4-6 frama-c

---
v2:
- move the check into pefile_parse_binary() and reuse chkaddr(), per Ignat Korchagin's review [1], so a malformed image is rejected at parse time -- before signature verification and before hashing. v1 put the check in the pefile_digest_pe_contents() section loop.
- because chkaddr() runs on every section, v2 also validates zero-size (raw_data_size == 0) sections, which v1 and the shipping digest loop skip. This only rejects a zero-size section whose data_addr points past the image -- malformed input that real PEs never produce (.bss uses PointerToRawData = 0, which passes) -- and matches the "don't start on malformed data" intent.

Quoted 4 lines

[1] https://lore.kernel.org/all/CAOs+rJXZ++7_o4L6LuT567FjdYsa=6NEki3qcFSRX+ehJxpoVg@mail.gmail.com/

Tested under KASAN (CONFIG_KASAN_GENERIC, x86-64) with a KUnit case that calls verify_pefile_signature() on real images: a signed-then-tampered

Same as your other patch - any reason not to include this KUnit case?

Quoted 23 lines

image (a section's data_addr set to pelen) is now rejected with -ELIBBAD in pefile_parse_binary(), before verify_pkcs7_signature() and with no KASAN splat, while an untampered signed image passes parse and reaches the signature check (-ENOKEY, as the test key is not trusted in this build). v1's KASAN repro -- a crafted context taken straight into the digest loop -- showed the slab-out-of-bounds this prevents.

 crypto/asymmetric_keys/verify_pefile.c | 9 +++++++++
 1 file changed, 9 insertions(+)
crypto/asymmetric_keys/verify_pefile.c +1 -0
index cec99db14..c447597f7 100644
@@ -30,6 +30,7 @@ static int pefile_parse_binary(const void *pebuf, unsigned int pelen,
        const struct data_dirent *dde;
        const struct section_header *sec;
        size_t cursor, datalen = pelen;
+       unsigned int loop;
 
        kenter("");
 

crypto/asymmetric_keys/verify_pefile.c static int pefile_parse_binary(const void *pebuf, unsigned int pelen, · line 120

Quoted 8 lines
crypto/asymmetric_keys/verify_pefile.c +5 -0
@@ -112,6 +113,14 @@ static int pefile_parse_binary(const void *pebuf, unsigned int pelen,
                return -ELIBBAD;
        ctx->secs = pebuf + cursor;
 
+       /* pefile_digest_pe_contents() hashes each section's raw data using
+        * these fields directly; reject a section whose data range falls
+        * outside the image so hashing never reads past the buffer.
+        */
+       for (loop = 0; loop < ctx->n_sections; loop++)

Looking at the code again: seems ctx->n_sections is user-controlled. What happens if it is 0?

Quoted 7 lines
crypto/asymmetric_keys/verify_pefile.c +3 -0
+               chkaddr(0, ctx->secs[loop].data_addr,
+                       ctx->secs[loop].raw_data_size);
+
        return 0;
 }
 
Quoted 5 lines

base-commit: d58772d8520c7ef247c4b95c9bd76d3a25da9ff5 --
2.53.0

Ignat

#4 lore raw

Re: [PATCH v2] crypto: asymmetric_keys - bound PE section ranges in pefile_parse_binary

Quoted 4 lines · Ignat Korchagin
Quoted 2 lines

Tested under KASAN (CONFIG_KASAN_GENERIC, x86-64) with a KUnit case that calls verify_pefile_signature() on real images: a signed-then-tampered

Same as your other patch - any reason not to include this KUnit case?

The reason I am not including my tests is that I use real signed EFI binaries (a valid one and a tampered one), ~88 KB each via xxd -i, which is a lot of binary to carry in-tree. For upstream I'll write a self-contained case that constructs a minimal PE in the test itself and asserts that pefile_parse_binary() rejects an out-of-range section, so there are no embedded blobs. I'll send it as a second patch in a v3 series, so the fix is not gated on the test.

Quoted 4 lines
Quoted 1 line

+ for (loop = 0; loop < ctx->n_sections; loop++)

Looking at the code again: seems ctx->n_sections is user-controlled. What happens if it is 0?

For this loop, nothing: n_sections is unsigned, so "loop < 0" is false on the first test and the body never runs -- no chkaddr() call and no ctx->secs[] dereference. So the patch itself adds no hazard for n_sections == 0.

But you have pointed at a real pre-existing problem one step further on. n_sections == 0 is not rejected anywhere -- pefile_parse_binary() only bounds it from above, against header_size -- and pefile_digest_pe_contents() then does:

	canon = kcalloc(ctx->n_sections, sizeof(unsigned), GFP_KERNEL);
	if (!canon)
		return -ENOMEM;
	...
	canon[0] = 0;

kcalloc(0, ...) returns ZERO_SIZE_PTR, which is non-NULL, so the !canon check passes and the unconditional "canon[0] = 0" writes through ZERO_SIZE_PTR. Because the digest runs only after verify_pkcs7_signature() succeeds, this has the same reachability as the read this patch fixes: a validly signed image with n_sections tampered to 0 (the PKCS#7 is unaffected) reaches it via kexec_file_load().

I confirmed it under KASAN. With a trusted key embedded, a signed image whose NumberOfSections I set to 0 (leaving the certificate table untouched, so verify_pkcs7_signature() still returns 0) faults in the digest step, while the untampered image verifies:

control (trusted, 7 sections): verify_pefile_signature() = 0

  BUG: kernel NULL pointer dereference, address: 0000000000000010
  #PF: supervisor write access in kernel mode
  Oops: 0002 [#1] SMP KASAN NOPTI
  RIP: 0010:verify_pefile_signature+0x5c3   (pefile_digest_pe_contents inlined)

Address 0x10 is ZERO_SIZE_PTR and error_code 0x2 is a write -- the canon[0] = 0 store -- so it is a write fault, reachable the same way as the out-of-bounds read this patch already addresses.

Since we are now centralising section-table sanity in pefile_parse_binary(), the natural fix is to reject n_sections == 0 there as well, e.g. by folding it into the existing bound:

	if (ctx->n_sections == 0 ||
	    ctx->n_sections > (ctx->header_size - cursor) / sizeof(*sec))
		return -ELIBBAD;

A PE with zero sections is malformed (a signed kernel/EFI image always has at least one), so rejecting it at parse is safe and closes the ZERO_SIZE_PTR write too. I will include that in v3 and cover it in the KUnit case.

Do you prefer it folded into this patch, or kept as a separate fix for the canon[0] write? Either way I will respin as v3 with the KUnit case once you let me know.

Thanks for the careful review, Ignat.

Fabrice

maintainer
#5 lore raw

Re: [PATCH v2] crypto: asymmetric_keys - bound PE section ranges in pefile_parse_binary

Sorry for delay on this,

Quoted 66 lines · Fabrice Derepas
Quoted 4 lines · Ignat Korchagin
Quoted 2 lines

Tested under KASAN (CONFIG_KASAN_GENERIC, x86-64) with a KUnit case that calls verify_pefile_signature() on real images: a signed-then-tampered

Same as your other patch - any reason not to include this KUnit case?

The reason I am not including my tests is that I use real signed EFI binaries (a valid one and a tampered one), ~88 KB each via xxd -i, which is a lot of binary to carry in-tree. For upstream I'll write a self-contained case that constructs a minimal PE in the test itself and asserts that pefile_parse_binary() rejects an out-of-range section, so there are no embedded blobs. I'll send it as a second patch in a v3 series, so the fix is not gated on the test.

Quoted 4 lines
Quoted 1 line

+ for (loop = 0; loop < ctx->n_sections; loop++)

Looking at the code again: seems ctx->n_sections is user-controlled. What happens if it is 0?

For this loop, nothing: n_sections is unsigned, so "loop < 0" is false on the first test and the body never runs -- no chkaddr() call and no ctx->secs[] dereference. So the patch itself adds no hazard for n_sections == 0.

But you have pointed at a real pre-existing problem one step further on. n_sections == 0 is not rejected anywhere -- pefile_parse_binary() only bounds it from above, against header_size -- and pefile_digest_pe_contents() then does:

        canon = kcalloc(ctx->n_sections, sizeof(unsigned), GFP_KERNEL);
        if (!canon)
                return -ENOMEM;
        ...
        canon[0] = 0;

kcalloc(0, ...) returns ZERO_SIZE_PTR, which is non-NULL, so the !canon check passes and the unconditional "canon[0] = 0" writes through ZERO_SIZE_PTR. Because the digest runs only after verify_pkcs7_signature() succeeds, this has the same reachability as the read this patch fixes: a validly signed image with n_sections tampered to 0 (the PKCS#7 is unaffected) reaches it via kexec_file_load().

I confirmed it under KASAN. With a trusted key embedded, a signed image whose NumberOfSections I set to 0 (leaving the certificate table untouched, so verify_pkcs7_signature() still returns 0) faults in the digest step, while the untampered image verifies:

control (trusted, 7 sections): verify_pefile_signature() = 0

  BUG: kernel NULL pointer dereference, address: 0000000000000010
  #PF: supervisor write access in kernel mode
  Oops: 0002 [#1] SMP KASAN NOPTI
  RIP: 0010:verify_pefile_signature+0x5c3   (pefile_digest_pe_contents inlined)

Address 0x10 is ZERO_SIZE_PTR and error_code 0x2 is a write -- the canon[0] = 0 store -- so it is a write fault, reachable the same way as the out-of-bounds read this patch already addresses.

Since we are now centralising section-table sanity in pefile_parse_binary(), the natural fix is to reject n_sections == 0 there as well, e.g. by folding it into the existing bound:

        if (ctx->n_sections == 0 ||
            ctx->n_sections > (ctx->header_size - cursor) / sizeof(*sec))
                return -ELIBBAD;
Quoted 6 lines

A PE with zero sections is malformed (a signed kernel/EFI image always has at least one), so rejecting it at parse is safe and closes the ZERO_SIZE_PTR write too. I will include that in v3 and cover it in the KUnit case.

Do you prefer it folded into this patch, or kept as a separate fix for the

Let's fold into this patch, since it is one place now.

Quoted 2 lines

canon[0] write? Either way I will respin as v3 with the KUnit case once you let me know.

Thank you.

Quoted 5 lines

Thanks for the careful review, Ignat.

Fabrice