Posted to tcl by stevel at Wed Jul 29 01:38:55 GMT 2026view pretty
What the problem was
TclTLS freed an OpenSSL data structure at exit while TLS channels were still
using it.
Concretely, in TlsLibInit() the extension registered a Tcl exit handler:
Tcl_CreateExitHandler(TlsLibShutdown, NULL);
and TlsLibShutdown() BIO_cleanup() BIO_meth_free(BioMethods). BioMethods
is the custom BIO_METHOD the vtable of function pointers (read, write, ctrl,
destroy) that every TclTLS BIO points at to route OpenSSL's I/O through Tcl
channels.
The fatal assumption was that exit handlers run last. They don't. In Tcl 9's
generic/tclEvent.c, both exit paths run exit handlers first and close channels
afterwards:
- Tcl_Exit() InvokeExitHandlers() (line 981) FinalizeThread() (996)
TclFinalizeIOSubsystem() (1413)
- Tcl_Finalize() InvokeExitHandlers() (1201) Tcl_FinalizeThread() (1223)
same
So on exit:
1. TlsLibShutdown frees the vtable.
2. TclFinalizeIOSubsystem then walks every channel still open and closes it.
3. Closing your live XMPP socket ran TlsClose2Proc TlsCloseProc, which
called into the BIO and BIO_ctrl() dereferences b->method->ctrl to dispatch.
That vtable was freed memory. Jump through a garbage pointer segfault.
That's why it only happened on exit, and only with a connection still up:
jabberlib's XMPP socket was still open, so there was a live BIO pointing at a
dead vtable. Close everything first and there's nothing left to trip over.
It's also heap-dependent if the freed block hadn't been reused yet, the
stale pointers still read as valid and the process exited cleanly, which is
exactly why it looked intermittent.