From e9e1bb54f007ab1501a066574fd149f51b8e24f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl=20H=C3=B6rberg?= Date: Tue, 16 Dec 2025 15:32:14 +0100 Subject: [PATCH] Fix GC finalization cycle in SNI callback implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SNI callback storage was causing finalization cycle warnings because contexts directly referenced callback boxes through instance variables, while the callbacks themselves could return other contexts, creating circular dependencies that the GC couldn't safely finalize. This fix moves callback box storage from instance variables to a class-level hash keyed by context handle. This breaks the finalization cycle because contexts no longer directly reference the callbacks through their object graph - they only hold primitive handle pointers. When a context is finalized, it looks up and explicitly frees its callback box from the class storage, ensuring proper cleanup without circular dependencies. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/stdlib/openssl_sni.cr | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/stdlib/openssl_sni.cr b/src/stdlib/openssl_sni.cr index 8f0c1adb6b..6f12e7ba94 100644 --- a/src/stdlib/openssl_sni.cr +++ b/src/stdlib/openssl_sni.cr @@ -21,8 +21,9 @@ lib LibSSL end class OpenSSL::SSL::Context::Server - # Stores the SNI callback Box to prevent garbage collection - @sni_callback_box : Pointer(Void)? + # Class-level storage for SNI callback boxes to prevent garbage collection + # while avoiding finalization cycles. Keyed by the SSL context handle pointer. + @@sni_callback_boxes = {} of Pointer(Void) => Pointer(Void) # Sets a Server Name Indication (SNI) callback. # The callback receives the hostname from the client and should return @@ -75,7 +76,9 @@ class OpenSSL::SSL::Context::Server # Box the callback to pass to C callback_box = Box.box(block) - @sni_callback_box = callback_box + + # Store in class-level hash to prevent GC while avoiding finalization cycles + @@sni_callback_boxes[@handle.as(Pointer(Void))] = callback_box # Set the callback using SSL_CTX_callback_ctrl LibSSL.ssl_ctx_callback_ctrl(@handle, LibSSL::SSL_CTRL_SET_TLSEXT_SERVERNAME_CB, c_callback.unsafe_as(Proc(Void))) @@ -83,4 +86,14 @@ class OpenSSL::SSL::Context::Server # Set the arg that will be passed to the callback LibSSL.ssl_ctx_ctrl(@handle, LibSSL::SSL_CTRL_SET_TLSEXT_SERVERNAME_ARG, 0, callback_box) end + + def finalize + # Free the callback box from class-level storage + handle_key = @handle.as(Pointer(Void)) + if callback_box = @@sni_callback_boxes.delete(handle_key) + GC.free(callback_box) + end + # Call the parent class finalizer to clean up OpenSSL resources + super + end end