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

REPORT

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

  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.

[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

SECURITY RESEARCH FIRM ANALYSIS

Triage and disclosure were performed by Calif.

Verdict
true positive
Severity
high
UPSTREAM FIX

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

TIMELINE

Dates from discovery through public reveal.

  1. 2026-05-19 Reported to tracker
  2. 2026-05-28 Sent to maintainer
  3. 2026-05-28 Maintainer acknowledged
  4. 2026-06-08 Patch released
  5. 2026-08-17 Publicly revealed
PROVENANCE

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"
}