ANT-2026-AYWQW7P2 · openssl/openssl
use-after-free critical
CVE-2026-45447 GHSA-f684-cpcq-j565
Severity Claude high · Security research firm high · Maintainer critical
Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Calif.
ANT-2026-AYWQW7P2: PKCS7_verify frees caller-owned indata BIO when digestAlgorithms SET is empty
In PKCS7_dataInit(), when the attacker-supplied SignedData has an empty md_algs SET (DER 31 00), no BIO_f_md filter chain is built and the caller's indata BIO pointer is returned verbatim instead of a library-owned chain. PKCS7_verify()'s unconditional cleanup then calls BIO_free_all() on that pointer, freeing caller-owned memory. Any consumer following the documented SMIME_read_PKCS7 → PKCS7_verify → BIO_free(indata) pattern (including apps/smime.c) then performs a second BIO_free on the freed 128-byte struct bio_st. The second free writes to offset +88 (refcount decrement) and, if the chunk has been reclaimed with suitable contents, dereferences a vtable pointer at offset +8 for an attacker-influenced indirect call. The trigger is valid DER and reachable via an ordinary S/MIME email with a CA-issued certificate.
Target
Project: openssl
Location: crypto/pkcs7/pk7_smime.c:349-356 (sink); crypto/pkcs7/pk7_doit.c:304-306,403-406 (root cause)
Technical Details
ASAN: heap-use-after-free, WRITE of size 4 at freed heap address in CRYPTO_DOWN_REF. The empty-SET case leaves out == NULL in PKCS7_dataInit so it returns out = bio (the caller's pointer) directly; PKCS7_verify's cleanup assumes p7bio has a library-owned head and calls BIO_free_all(p7bio), freeing the caller's BIO. The caller's subsequent BIO_free(indata) then atomically decrements a->references at +88 in freed memory and, if ret hits 0, calls a->method->destroy(a) through a function pointer read from offset +8 of the reclaimed chunk.
Crash trace:
==NNN==ERROR: AddressSanitizer: heap-use-after-free on address 0x... at pc ...
WRITE of size 4 at 0x... thread T0
#0 ... in CRYPTO_DOWN_REF include/internal/refcount.h:64
#1 ... in BIO_free crypto/bio/bio_lib.c:126
#2 ... in smime_main apps/smime.c:732
freed by thread T0 here:
#3 ... in PKCS7_verify crypto/pkcs7/pk7_smime.c:356
previously allocated by thread T0 here:
#4 ... in multi_split crypto/asn1/asn_mime.c:678
Reproduction
- Craft a multipart/signed S/MIME message whose PKCS7 SignedData has md_algs = empty SET (DER 31 00), with a valid signer cert and SignerInfo.
- Deliver the message to the victim (email, gateway, or file fed to
openssl smime -verify). - Victim calls PKCS7_verify; PKCS7_dataInit returns the caller's indata; cleanup BIO_free_all frees it (first free).
- Victim's own BIO_free(indata) runs: CRYPTO_DOWN_REF writes 4 bytes at +88 of the freed/reclaimed chunk.
- If the slot was reclaimed with ref==1 and a fake BIO_METHOD* at +8, execution falls through to a->method->destroy(a) — attacker-controlled indirect call.
[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]
Suggested Fix
Either (1) in PKCS7_verify cleanup, add if (p7bio == indata) p7bio = NULL; before BIO_free_all(p7bio); or (2) in PKCS7_dataInit, when no digest filters were pushed and a caller bio was supplied, reject the input or push a BIO_f_null() so the returned chain always has a library-owned head.
Acknowledgement
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-AYWQW7P2.
Reference: ANT-2026-AYWQW7P2
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure
Triage and disclosure were performed by Calif.
- Verdict
- true positive
- Severity
- high
The change that resolved this finding.
diff --git a/crypto/pkcs7/pk7_smime.c b/crypto/pkcs7/pk7_smime.c
index 4bf26331c1a05..49129690deb96 100644
--- a/crypto/pkcs7/pk7_smime.c
+++ b/crypto/pkcs7/pk7_smime.c
@@ -221,6 +221,7 @@ int PKCS7_verify(PKCS7 *p7, const STACK_OF(X509) *certs, X509_STORE *store,
int i, j = 0, k, ret = 0;
BIO *p7bio = NULL;
BIO *tmpout = NULL;
+ BIO *next = NULL;
const PKCS7_CTX *p7_ctx;
if (p7 == NULL) {
@@ -351,9 +352,11 @@ int PKCS7_verify(PKCS7 *p7, const STACK_OF(X509) *certs, X509_STORE *store,
BIO_free(tmpout);
X509_STORE_CTX_free(cert_ctx);
OPENSSL_free(buf);
- if (indata != NULL)
- BIO_pop(p7bio);
- BIO_free_all(p7bio);
+ while (p7bio != NULL && p7bio != indata) {
+ next = BIO_pop(p7bio);
+ BIO_free(p7bio);
+ p7bio = next;
+ }
sk_X509_free(signers);
sk_X509_free(untrusted);
return ret;https://github.com/openssl/openssl/commit/f4129fbe3cde786d363510069fe297234f99be8f
Dates from discovery through public reveal.
- 2026-05-19 Reported to tracker
- 2026-05-28 Sent to maintainer
- 2026-05-28 Maintainer acknowledged
- 2026-06-08 Patch released
- 2026-08-17 Publicly revealed
SHA-3-512 hash:
c437365f3af15fb6db66d201e976b79c3cd9f89a37e15f897449b63d820ae951ecb1b647c56e7bc44774c928fbadf2437715bff69c2a687000563ff2260c9acc
Committed 2026-05-28 08:09 PT
Revealed 2026-08-17 10:47 PT
Verify (download preimage.json)
Show preimage JSON
{
"ant_id": "ANT-2026-AYWQW7P2",
"bug_class": "Use-After-Free",
"claude_severity": "high",
"commit_sha": null,
"created_at": "2026-05-20T01:49:21+00:00",
"description": "In PKCS7_dataInit(), when the attacker-supplied SignedData has an empty md_algs SET (DER `31 00`), no BIO_f_md filter chain is built and the caller's indata BIO pointer is returned verbatim instead of a library-owned chain. PKCS7_verify()'s unconditional cleanup then calls BIO_free_all() on that pointer, freeing caller-owned memory. Any consumer following the documented SMIME_read_PKCS7 → PKCS7_verify → BIO_free(indata) pattern (including apps/smime.c) then performs a second BIO_free on the freed 128-byte struct bio_st. The second free writes to offset +88 (refcount decrement) and, if the chunk has been reclaimed with suitable contents, dereferences a vtable pointer at offset +8 for an attacker-influenced indirect call. The trigger is valid DER and reachable via an ordinary S/MIME email with a CA-issued certificate.",
"discovered_at": "2026-03-28T00:00:00+00:00",
"location": "crypto/pkcs7/pk7_smime.c:349-356 (sink); crypto/pkcs7/pk7_doit.c:304-306,403-406 (root cause)",
"poc_sha256": null,
"preimage_version": 1,
"project": "openssl",
"reproduction": [
"1. Craft a multipart/signed S/MIME message whose PKCS7 SignedData has md_algs = empty SET (DER 31 00), with a valid signer cert and SignerInfo.",
"2. Deliver the message to the victim (email, gateway, or file fed to `openssl smime -verify`).",
"3. Victim calls PKCS7_verify; PKCS7_dataInit returns the caller's indata; cleanup BIO_free_all frees it (first free).",
"4. Victim's own BIO_free(indata) runs: CRYPTO_DOWN_REF writes 4 bytes at +88 of the freed/reclaimed chunk.",
"5. If the slot was reclaimed with ref==1 and a fake BIO_METHOD* at +8, execution falls through to a->method->destroy(a) — attacker-controlled indirect call."
],
"technical_details": "ASAN: heap-use-after-free, WRITE of size 4 at freed heap address in CRYPTO_DOWN_REF. The empty-SET case leaves `out == NULL` in PKCS7_dataInit so it returns `out = bio` (the caller's pointer) directly; PKCS7_verify's cleanup assumes p7bio has a library-owned head and calls BIO_free_all(p7bio), freeing the caller's BIO. The caller's subsequent BIO_free(indata) then atomically decrements `a->references` at +88 in freed memory and, if ret hits 0, calls `a->method->destroy(a)` through a function pointer read from offset +8 of the reclaimed chunk.",
"title": "PKCS7_verify frees caller-owned `indata` BIO when `digestAlgorithms` SET is empty",
"vendor_severity": "high"
}