fix: segfault in certs codec aborting chalked docker builds - #766
Open
maximilianhurl wants to merge 3 commits into
Open
fix: segfault in certs codec aborting chalked docker builds#766maximilianhurl wants to merge 3 commits into
maximilianhurl wants to merge 3 commits into
Conversation
extract_cert_data() passed the result of X509_get_pubkey() straight to EVP_PKEY_base_id() without a NULL check. X509_get_pubkey() returns NULL for a certificate whose public key will not parse, which is exactly what d2i_X509_bio() produces when the codec is pointed at arbitrary binary content -- as prep_postexec does when it subscans / with the certs codec and reaches a statically linked Go binary. The crash killed the chalk-wrapped docker build; chalk then logged "retrying without chalk" and rebuilt unwrapped, so the job stayed green while the pushed image carried no chalk mark. That is invisible in CI and shows up downstream as missing build/push records and deployments. Reproduced by mutating a DER certificate and driving it through the parse loop under ASan+UBSan: the original crashes within 20k mutations, this survives 160k across 8 seeds. The same run surfaced a heap overflow in BIO_all(), which built its buffer with strndup() -- stopping at the first NUL -- while `total` counted every byte read, so later indices ran past the allocation. Also fixes, in the same function: - EC certificates lost all X.509 metadata after Serial. The public key was encoded with PKCS1, an RSA-only structure, so for EC keys the encoder emitted nothing and a NULL landed mid-key_value, truncating both cleanup_key_value() and cstringArrayToSeq() on the Nim side. SubjectPublicKeyInfo covers every key type. - Leaks: the X509, EVP_PKEY, BIGNUM and encoder context were never released, cleanup_key_value() never freed the arrays themselves, and subject_short/issuer_short were never freed at all. A 119-certificate CA bundle leaked 771KB; it now scans clean under LeakSanitizer. - The serial came from OpenSSL but was released with free(). - Unchecked returns from OBJ_nid2ln/OBJ_nid2sn, ASN1_STRING_length of 0 (which read data[-1] and wrote into a zero-byte allocation), and convert_ASN1TIME, which returned -1 even on success and left its buffer uninitialised on early failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Chalked Docker builds have been dying at
prep_postexecand silently falling back to unchalked:The retry succeeds and pushes, so CI stays green while the image carries no chalk mark. Downstream that looks like missing build/push records and no deployment correlation — the symptom that led here.
This is not new and not tied to a release: it reproduces on
1.1.4-dev,1.2.0-devand1.2.0. The same repo on the same chalk version produces both outcomes (47 chalked / 69 not), which is what pointed at input-dependent memory corruption rather than a regression.Cause
prep_postexecsubscans/withscan_codecs = ["certs"]. Withcerts.scan_no_extensiondefaulting to true, every extensionless file — including a statically linked Go binary — is fed toextract_cert_data(), which falls back tod2i_X509_bio()when the PEM read fails. On arbitrary binary content that occasionally yields a structurally valid but malformed certificate.src/utils/certs.c:172-174:X509_get_pubkey()returns NULL when the public key will not parse. Unchecked.How it was found
DER-encode a real certificate, flip 1–16 random bytes, drive it through the actual parse loop under ASan+UBSan:
Crashes within 20,000 mutations on the first seed. The same run flagged a second defect at
certs.c:126.Changes
X509_get_pubkey()NULL check — the crash. The record is treated as "not a certificate" instead of dereferencing.BIO_all()made binary-safe — it built its buffer withstrndup(), which stops at the first NUL, whiletotalcounted every byte read; later indices then ran past the allocation. Nowcalloc+memcpywith bounded trailing writes.SubjectPublicKeyInfoinstead ofPKCS1— PKCS1 is RSA-only, so for EC keys the encoder emitted nothing and a NULL landed mid-key_value, truncating bothcleanup_key_value()andcstringArrayToSeq()on the Nim side. Every field afterSerialwas silently lost for EC certificates (39 of 119 in a stock CA bundle).X509,EVP_PKEY,BIGNUMand encoder context were never released;cleanup_key_value()freed the strings but not the arrays;subject_short/issuer_shortwere never freed at all. Also anOPENSSL_free/freemismatch on the serial.OBJ_nid2ln/OBJ_nid2sn,ASN1_STRING_lengthof 0 (readdata[-1], wrote into a zero-byte allocation), andconvert_ASN1TIME, which returned-1even on success and left its buffer uninitialised on early failure.Verification
Built with
-fsanitize=address,undefinedagainst system OpenSSL 3 (one local substitution for thecrypto/x509.hinternal include).🤖 Generated with Claude Code