Skip to content

Seed static TLS in the threads that already run - #11

Open
buyemune-sys wants to merge 2 commits into
pg83:mainfrom
buyemune-sys:fix/static-tls-running-threads
Open

buyemune-sys wants to merge 2 commits into
pg83:mainfrom
buyemune-sys:fix/static-tls-running-threads

Conversation

@buyemune-sys

@buyemune-sys buyemune-sys commented Oct 17, 2026 •

Copy link
Copy Markdown

The problem

A module whose TLS is placed in the static window (the pad) gets an initialized copy of its template only in the thread that called dlopen and, through musl's __copy_tls, in threads created after the load. Threads that were already running see zeroes instead of the initial values.

The README describes this as an initial-exec-only restriction. In fact every module that fits is placed in the window (Loader::allocateStaticTls, lib/elf_loader.cpp:1540), so ordinary general-dynamic modules (the -fPIC default) are affected too:

                            glibc   solo main
IE module, earlier thread:   42        0
GD module, earlier thread:   42        0

glibc initializes the block in every live thread in this situation (_dl_init_static_tls). For an application server that loads plugins on demand, after its worker threads already exist, this is the normal case.

Also: the README says the arena is 16 KiB, while SOLO_TLS_PAD in the code is 1 MiB (lib/musl_tls.c:49).

The fix

After registering the module, soloTlsRegister (lib/musl_tls.c:183) walks musl's thread ring under __tl_lock and seeds the block at the same thread-pointer offset in each thread. All of it runs under __inhibit_ptc(), as musl's own dlopen does. The reason: pthread_create allocates and copies a new thread's TLS before adding the thread to the list, so a thread created concurrently with the registration would otherwise miss both the template and the seeding. The real __inhibit_ptc/__acquire_ptc already get linked in from src/thread/lock_ptc.c.

README: the restriction is dropped and the arena size is corrected to 1 MiB.

Test

The existing test in tst/smoke.cpp locked in the old behavior (pre-existing thread saw %d instead of zeroed TLS). Now the "parked" thread, created before the IE module loads, runs the full glibc_ie_selfcheck: the .tdata template, zeroed .tbss, the buffer, alignment, and a write.

  • on main (new test only): pre-existing thread initial-exec selfcheck failed: 1;
  • with the fix: ./build test passes, and standalone IE/GD examples read 42 in every thread.

buyemune-sys and others added 2 commits October 20, 2026 15:42
Security updates replaced libapparmor1 4.0.1really4.0.1-0ubuntu0.24.04.7
with .9 and libexpat1 2.6.1-2ubuntu0.4 with ubuntu0.6, and the pool kept
only the new files, so the cold-cache Fetch x86_64 job answers 404 and
every x86-64 job behind it is skipped. Both pins move to the versions
noble-updates ships now; their SHA256s match the archive's Packages index.
The other twelve pins are still served.
A module placed in the static TLS window got its template copied only
into the loading thread and, through musl's __copy_tls, into threads
created afterwards. Threads already running read zeroes instead of the
module's initial values — for initial-exec variables and, since every
module that fits is placed in the window, for general-dynamic ones too.
glibc seeds those threads on dlopen (_dl_init_static_tls), so a glibc
library loaded after its users' threads exist, a common case for an
application server loading plugins on demand, misbehaved only here.

soloTlsRegister now seeds the block in every other thread on musl's
thread list. It runs with thread creation inhibited, as musl's own
dlopen does: pthread_create copies a new thread's TLS before the thread
joins the list, and a thread racing the registration would otherwise
miss both the template and the seeding.

The smoke test's parked thread now runs the full initial-exec selfcheck
instead of expecting zeroes, and the README drops the restriction and
names the arena's actual size, 1 MiB rather than 16 KiB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@buyemune-sys
buyemune-sys force-pushed the fix/static-tls-running-threads branch from d933b19 to a722c8a Compare October 20, 2026 08:11
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