Skip to content

Keep a certificate alive while one of its times is in use - #110

Merged
quinnj-bot[bot] merged 1 commit into
mainfrom
claude/asn1-time-owner
Oct 29, 2026
Merged

quinnj-bot[bot] merged 1 commit into
mainfrom
claude/asn1-time-owner

Conversation

@quinnj-bot

@quinnj-bot quinnj-bot Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

cert.time_not_before and cert.time_not_after return an Asn1Time that points into the certificate (X509_getm_notBefore/notAfter), with no reference to the certificate. When the certificate is collected first, the time points at freed memory:

t = X509Certificate(pem).time_not_after   # or get_peer_certificate(stream).time_not_after
GC.gc()
String(t)   # reads the freed certificate

Separately, free(t) on such a time freed the certificate's own ASN1_TIME, which X509_free later frees again.

Fix

  • Asn1Time gets an owner field. The two getters set it to a new X509Certificate wrapper holding its own reference (X509_up_ref). The certificate then stays alive while the time is in use, even if the original wrapper is collected or freed explicitly.
  • free on such a time drops that reference and leaves the time to the certificate.
  • Dates.adjust(cert.time_not_after, Year(1)) still changes the certificate in place. The tests and test/http_helpers.jl rely on that.
  • With the extra field, the struct is no longer passed to C as a bare pointer. The five ccalls that took Asn1Time now declare Ptr{Cvoid} and use a Base.unsafe_convert method; ccall keeps the object rooted during the call.

Verification

  • New testset CertificateTimeKeepsCertificate covers:
    • the time of a dropped certificate after GC.gc(true), 20 rounds;
    • the time after free(cert);
    • free(time) followed by reading the certificate's time;
    • adjusting through the time.
  • On main, the suite with only the new test aborts: signal 6, the double free. With the fix, all 24 checks pass.
  • Repro script: printing X509Certificate(pem).time_not_after after a collection.
    • main: wrong 200 of 200 times with MallocScribble=1, and 500 of 500 without it while other certificates are parsed.
    • This branch: always right.
  • Full suite on Julia 1.12 -t 4: passes.

Self-review

  • Layout change: code outside the package that passes OpenSSL.Asn1Time by value in its own ccall would now pass a two-field struct. A GitHub code search for Asn1Time in Julia code finds only copies of this package. Every ccall in the package that took it is converted.
  • Asn1Time(ptr) (no owner, no finalizer) behaves as before. Owned times (Asn1Time(), Asn1Time(::Int), Asn1Time(::DateTime)) still have their finalizer, and free frees them as before. The existing Asn1Time testset, which frees twice, passes.
  • A view is never C_NULL: the getters throw on a NULL time. adjust on a freed view allocates a new time, as before.
  • Each getter call costs two small allocations and an atomic X509_up_ref. These are not hot paths.

🤖 Generated with Claude Code

`cert.time_not_before` and `cert.time_not_after` return an Asn1Time that
points into the certificate (X509_getm_notBefore/notAfter), but it held no
reference to it. Once the certificate was collected, the time read freed
memory: `X509Certificate(pem).time_not_after` printed after a collection
failed every time (200 of 200), and freeing such a time freed the
certificate's own, which the certificate freed again later (the test
process aborts on main).

Asn1Time gets an `owner` field. The getters set it to a certificate
wrapper with its own reference (X509_up_ref), so neither collecting nor
explicitly freeing the original certificate frees the time, and `free` on
such a time drops that reference instead of freeing the time. Adjusting
it still changes the certificate's time, as the tests and helpers rely on.
With the extra field the struct no longer passes to C as a bare pointer,
so the ccalls take `Ptr{Cvoid}` through `unsafe_convert`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.08%. Comparing base (14a9842) to head (71fa07f).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #110      +/-   ##
==========================================
+ Coverage   83.07%   83.08%   +0.01%     
==========================================
  Files           2        2              
  Lines        1341     1342       +1     
==========================================
+ Hits         1114     1115       +1     
  Misses        227      227              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@quinnj-bot
quinnj-bot Bot merged commit 00707a7 into main Oct 29, 2026
14 checks passed
@quinnj-bot
quinnj-bot Bot deleted the claude/asn1-time-owner branch October 29, 2026 23:52
@quinnj-bot quinnj-bot Bot mentioned this pull request Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant