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.