Using AISLE to Find Vulnerabilities in Amazon's Crypto Stack: AWS-LC and s2n-tls
Author
Joshua Rogers
Date Published

In January, we published details about the 12 vulnerabilities which AISLE's AI analyzer discovered in the OpenSSL codebase, including the technical details of a CVSS3.0-defined critical severity CVE. Following that publication, online discussion drifted towards the obvious question: "if OpenSSL can have so many vulnerabilities which can be discovered by AI, what else is hidden in other critical cryptographic libraries?"
This time around, we decided to take a look at Amazon's crypto and TLS stack: AWS-LC, and s2n-tls. AWS-LC is Amazon's recommended cryptographic library for use across all services, tools, and applications, including s2n-tls, AWS KMS, S3 TLS termination and object encryption. It is also used in a plethora of open source projects such as HAProxy, Python, and NGINX, which are all moving towards those libraries for performance reasons.
Using our autonomous analyzer, we reported a total of 13 issues: 8 in AWS-LC and 5 in s2n-tls. Two of the issues in AWS-LC had CVEs assigned (CVE-2026-3336 and CVE-2026-3337), one of the issues in s2n-tls had a GHSA assigned (GHSA-5whh-4q9j-7v28), and the remaining 10 were fixed as valid bugs, even though they did not exactly hit the bar for assigning CVEs for them. While reporting these issues, Amazon also discovered and fixed a third vulnerability, CVE-2026-3338.
Upon receiving our reports and fixing these issues, Amazon released their own advisory on their security bulletin and issued the following statement:
“AWS acknowledges the findings reported by Joshua Rogers and the AISLE team across AWS-LC and s2n-tls. All identified issues have been addressed, and fixes have already been applied to the affected open source libraries. No AWS services were impacted. No action is required for customers that are using AWS cloud services and whose application code is not integrated directly with these libraries. Customers whose code is integrated directly with these libraries should upgrade to the latest releases of AWS-LC, s2n-tls, and optional utilities including aws-kms-tls-auth. We appreciate Joshua's responsible engagement with AWS Security throughout the disclosure process and look forward to continued collaboration to protect our customers and the industry.”
Just like our findings in OpenSSL, the problems we discovered were a mix of logic bugs along with the other classic problems seen in these types of codebases: memory management issues, divide-by-zeros, type confusions, use-after-frees/double-frees, and so on.
In the remainder of this post, we'll outline the vulnerabilities and summarize the remaining bugs.
Vulnerabilities
AWS-LC: PKCS7_verify only chain-validates the last signer (CVE-2026-3336)
In crypto/pkcs7/pkcs7.c's PKCS7_verify(), the code did the following:
C1/* crypto/pkcs7/pkcs7.c */2 for (size_t k = 0; k < sk_X509_num(signers); k++) {3 X509 *signer = sk_X509_value(signers, k);4 if (!X509_STORE_CTX_init(cert_ctx, store, signer, untrusted)) {5 OPENSSL_PUT_ERROR(PKCS7, ERR_R_X509_LIB);6 goto out;7 }8 if (!X509_STORE_CTX_set_default(cert_ctx, "smime_sign")) {9 goto out;10 }11 X509_STORE_CTX_set0_crls(cert_ctx, p7->d.sign->crl);12 }13 // NOTE: unlike most of our functions, |X509_verify_cert| can return <= 014 if (X509_verify_cert(cert_ctx) <= 0) {15#if !defined(BORINGSSL_UNSAFE_FUZZER_MODE)16 // For fuzz testing, we do not want to bail out early.17 OPENSSL_PUT_ERROR(PKCS7, PKCS7_R_CERTIFICATE_VERIFY_ERROR);18 goto out;19#endif20 }
Can you spot the issue? That if (X509_verify_cert(cert_ctx) <= 0) { block should have been inside the loop, verifying each certificate's signer in the chain. Instead, it only checked that the last signer was valid, even though signature verification should be performed per-signer.
A crafted PKCS#7 with multiple signers can therefore pass overall verification as long as the last signer chains to trust, even if earlier signers failed chain validation.
AWS-LC: AES-CCM non-constant-time tag verification (CVE-2026-3337)
In crypto/fipsmodule/cipher/e_aesccm.c, the handling of decryption of AES-CCM messages (AEAD mode combining CTR encryption with CBC-MAC authentication) compared the computed authentication tag to the caller-provided tag using OPENSSL_memcmp(), which is typically early-exit -- i.e. non-constant timing. The affected code path was for messages handled via the EVP_CIPHER API (OpenSSL's high-level symmetric cipher interface and encoding).
C1/* crypto/fipsmodule/cipher/e_aesccm.c */2 // Validate the tag and invalidate the output if it doesn't match.3 if (OPENSSL_memcmp(cipher_ctx->tag, computed_tag, cipher_ctx->M)) {4 OPENSSL_cleanse(out, len);5 return -1;6 }
Since OPENSSL_memcmp() exits early, a side-channel/timing attack could be used to leak the authentication tag of the cipher.
The fix was to use a constant-time compare (CRYPTO_memcmp()) so the comparison cost does not depend on the first mismatching byte.
s2n-tls: PrefixedList attacker-controlled overallocation (GHSA-5whh-4q9j-7v28)
In bindings/rust/aws-kms-tls-auth/src/prefixed_list.rs's PrefixedList::decode_from(), the on-wire length prefix is a byte length, but it was used directly as the element count for Vec::with_capacity. That means an attacker can drive large capacity reservations during parsing (up to 65,535 elements for a u16-prefixed list). If T is larger than a byte, this amplifies handshake-time memory usage and can be used for resource-exhaustion DoS in code paths that decode attacker-controlled lists (e.g. TLS PSK-related parsing).
JavaScript1/* bindings/rust/aws-kms-tls-auth/src/prefixed_list.rs */2 let length_usize = length as usize; // byte length on wire3 let mut list: Vec<T> = Vec::with_capacity(length_usize); // treated as element count
The core issue is the eager reservation based on attacker-controlled input; the decoder already enforces byte bounds, so this reservation is avoidable and can be weaponized for memory pressure.
Other Issues
The bugs Amazon classified as "bug" rather than "vulnerability" are still the sort of things you do not want sitting in a crypto/TLS library, especially in the long tail of applications that integrate these libraries in unusual ways. The issues we’ve summarized below are the types of things people miss in manual review because they live in error paths, uncommon digest choices, length-to-allocation assumptions, and "this pointer is valid as long as..." lifetime expectations that are rarely written down. Our research shows that AI analyzers aren’t subject to the same limitations.
AWS-LC
DH_compute_key_hashed OOB write with XOF digests
In crypto/fipsmodule/dh/dh.c's DH_compute_key_hashed(), an output-length variable was passed down into digest finalization without being initialized. For XOF digests (for example SHAKE), that length is treated as an input size, so the digest final step can attempt a large write into the caller buffer before the function later errors out.
HKDF divide-by-zero DoS (XOF / zero-sized digest)
In crypto/fipsmodule/evp/p_hkdf.c's pkey_hkdf_derive(), the code accepted digests where EVP_MD_size(md) can be 0 (notably XOF-style digests in this tree). That zero digest length later flows into the "number of blocks" math in HKDF expand logic, triggering a divide-by-zero crash (process DoS).
KEM derive type confusion (wired to HKDF derive)
In crypto/fipsmodule/evp/p_kem.c's EVP_PKEY_kem_pkey_meth(), the method table sets the KEM derive callback to pkey_hkdf_derive. A KEM EVP_PKEY_CTX stores a different ctx->data layout than HKDF expects, so calling EVP_PKEY_derive() on a KEM context can misinterpret memory and crash.
DSA keygen UAF/double-free on error path
In crypto/evp_extra/p_dsa.c's pkey_dsa_keygen(), ownership of the freshly allocated DSA * was transferred into the output EVP_PKEY before a later fallible step. If that later step failed, the error path freed the original DSA * while the EVP_PKEY still held the pointer, setting up UAF/double-free on subsequent use or cleanup.
EVP_PKEY_asn1_find_str 1-byte OOB read
In crypto/evp_extra/evp_asn1.c's EVP_PKEY_asn1_find_str(), the code computed a comparison length as 1 + min(name_len, pem_str_len) and passed it to OPENSSL_strncasecmp. If the caller supplies a non-NUL-terminated buffer where len equals the buffer size, that +1 can make the compare read one byte past the caller-provided bound.
PKCS#8 PBE unbounded memory allocation
In crypto/pkcs8/pkcs8.c's pkcs8_pbe_decrypt(), the code allocated OPENSSL_malloc(in_len) where in_len comes from the ASN.1 ciphertext OCTET STRING length. The only meaningful size guard happened after the allocation, so a crafted blob can force a huge allocation attempt and trigger OOM/termination (memory DoS).
s2n-tls
QUIC handshake framing desync (stale header reuse)
In tls/s2n_quic_support.c, s2n_recv_quic_post_handshake_message() (via s2n_quic_read_handshake_message()) can read the 4-byte post-handshake message header from a buffer that was not rewritten on the subsequent call. If a peer sends more than one post-handshake QUIC message, the second receive may reuse stale header bytes already present in the stuffer, desynchronizing header/payload framing. This causes the next post-handshake message to fail parsing and typically results in the connection being closed.
DHE server overallocation / extra DH work (missing bound on peer key length)
In crypto/s2n_dhe.c's s2n_dh_compute_shared_secret_as_server(), the code used the peer-provided Yc_length directly as the byte length for BN_bin2bn() and subsequent DH operations. Without bounding Yc_length to the negotiated group size, a client can force oversized allocations and extra bignum work per handshake (resource DoS).
Under-aligned allocations in Rust memory callbacks
In bindings/rust/extended/s2n-tls/src/init.rs's mem allocator callbacks, the alignment constant was set to size_of::<usize>(), which can be weaker than what C malloc promises for arbitrary object types. If the allocator honors that weaker alignment, C code can receive pointers that violate alignment assumptions, leading to undefined behavior and potential crashes on some platforms.
Rust FFI lifetime issue in Fingerprint builder
In bindings/rust/extended/s2n-tls/src/fingerprint.rs's Builder::build(), the Rust wrapper allowed constructing a Fingerprint that stores a raw pointer to a ClientHello inside C without tying the returned object's lifetime to the ClientHello. That lets safe Rust drop ClientHello while C still holds the pointer, so later fingerprint operations can dereference freed memory (UAF/UB).
Conclusion
AISLE started 2026 by publicizing our stress-test of the world's default crypto library, resulting in 12 CVEs in OpenSSL. Now, we're continuing to showcase our product's impact in the real world by identifying, reporting, and fixing vulnerabilities before they can be abused.
If you're interested in learning how AISLE can help you get to zero vulnerabilities in your environment, book a demo.
Appendix
The following table is a reference of each reported issue, and a link to the patch.
Title | Link |
|---|---|
PrefixedList attacker-controlled overallocation | https://github.com/aws/s2n-tls/security/advisories/GHSA-5whh-4q9j-7v28 |
PKCS7_verify Certificate Chain Validation Bypass | https://github.com/aws/aws-lc/security/advisories/GHSA-cfwj-9wp5-wqvp |
AES-CCM Non-Constant-Time Tag Verification | https://github.com/aws/aws-lc/security/advisories/GHSA-frmv-5gcm-jwxh |
Under-aligned allocations in Rust memory callbacks | |
DHE server overallocation / extra DH work (missing bound on peer key length) | |
Rust FFI lifetime issue in Fingerprint builder | |
QUIC handshake framing desync / DoS (stale header reuse) | |
DSA Keygen UAF/Double-Free | |
EVP_PKEY_asn1_find_str 1-byte OOB Read | |
PKCS#8 PBE Unbounded Memory Allocation | |
DH_compute_key_hashed OOB Write with XOF Digests | |
HKDF Divide-by-Zero DoS | |
KEM Derive Type Confusion |