Skip to content

sys/vita: struct _reent in native ELF TLS, thread-exit reclaim, __cxa_thread_atexit - #115

Open
frangarcj wants to merge 8 commits into
vitafrom
next-native-tls
Open

sys/vita: struct _reent in native ELF TLS, thread-exit reclaim, __cxa_thread_atexit#115
frangarcj wants to merge 8 commits into
vitafrom
next-native-tls

Conversation

@frangarcj

@frangarcj frangarcj commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

What does this change?

struct _reent moves into native ELF TLS (--enable-newlib-reent-thread-local); the kernel-TLS-slot reent pool, __getreent() and __DYNAMIC_REENT__ go. What is left of the kernel slot (the two pointers the thread library needs) sits behind __VITA_NEWLIB_THREAD_SLOT__ in the new <sys/vita_thread.h>, default 1 (as before). A thread's TLS-resident newlib state is reclaimed on every exit path: explicitly through vita_exit_thread/vita_exit_delete_thread, and from a process-wide SCE_KERNEL_THREAD_EVENT_TYPE_EXIT handler registered at init (the same call SceLibc makes), with _reclaim_reent made idempotent under thread-local reent and kept away from the stdio cleanup hook, which closes every stream in the process. newlib provides __cxa_thread_atexit, running C++ thread_local destructors from that same exit path. Also fixed: <sys/param.h> reaching <sys/config.h> before <newlib.h>, which left hash.o with an unresolved _impure_ptr in the thread-local build.

Why is this change needed?

Phase B of vitasdk/.github#3. With --disable-tls, __thread compiles to emulated TLS that degrades to a single process-global slot inside a thread library; newlib carried a 256-entry table with a hard ceiling ending in __builtin_trap(); the hardware needs neither. Details, measurements and the review discussion are in the RFC.

Testing

  • Real PlayStation Vita hardware (3.60, 2026-08-30): newlib-tests, 88 tests, 82 pass, 0 failures, 6 skips. TLS group: initial values and isolation in raw threads and pthreads, multi-TU relocation, errno isolation, concurrent %f formatting, thread churn with heap bounds on every exit path (raw return, sceKernelExitDeleteThread, vita_exit_thread double reclaim, pthread return), C++ thread_local construction once per thread and destruction on pthread and raw-thread exit, a destructor calling snprintf/strtod over 84 threads with a flat heap.
  • Vita3K, on a build with a fix for the emulator's thread END handler (it never runs in the released emulator; PR to follow there): 88 tests, 0 failures, 26 consecutive full runs. Earlier runs that looked like emulator hangs were not: the guest had finished, but Vita3K does not exit on its own after sceKernelExitProcess in console mode and its file log lags by a 4 KiB buffer, so the end marker sometimes never reached the file before the runner's timeout. The log flush is part of the same Vita3K PR.
  • No regressions against the newlib 4.1 baseline of the suite.

Compatibility

Breaks the target ABI; every package is rebuilt at the series cut, per the RFC. Removed: __getreent, __errno, _impure_ptr, the fields of struct _reent; vitasdk_get_tls_data, vitasdk_get_pthread_data, vitasdk_delete_thread_reent remain while the switch is 1 and go with it. Ordinary errno and __thread code is source-compatible. Requires the toolchain built with --enable-tls and the vita-elf-create of vitasdk/vita-toolchain#294; libstdc++ must be configured with HAVE___CXA_THREAD_ATEXIT for the C++ part (vitasdk/buildscripts#185). RFC: vitasdk/.github#3.

Third-party material

None.

AI assistance

  • Claude Opus 5
  • Claude Sonnet 5
  • Claude Fable 5

Additional context

RFC: vitasdk/.github#3. Companions: vitasdk/pthread-embedded#24, vitasdk/vita-toolchain#294, vitasdk/buildscripts#185. Moving the switch needs a clean rebuild of newlib and pthread-embedded on an incremental tree; the buildscripts PR makes it part of both build identities.

Newlib's own reentrancy now lives in native TLS via
--enable-newlib-reent-thread-local, so drop __DYNAMIC_REENT__ and
GETREENT_PROVIDED along with the kernel-TLS-slot reent pool and
__getreent()/__getreent_for_thread().

The 0x89 kernel TLS slot stays, holding only the tls_data_ext and
pthread_data_ext pointers pthread-embedded still needs: pte_osThreadCreate
writes into a new thread's slot through sceKernelGetThreadTLSAddr(thid, ...)
before that thread starts, and another thread's TPIDRURO-relative block is
unreachable from outside it.

_exit_thread_common now reclaims the thread's own TLS-resident buffers with
_reclaim_reent(NULL) before its TLS block disappears, and no longer holds
_newlib_reent_mutex across exit_func, which normally never returns and would
leave the mutex owned by a dead thread.

Accesses to ptr->_errno become _REENT_ERRNO(ptr), which is where the field
lives once reent is thread-local.

Assisted-by: Claude Opus 5
<sys/param.h> reaches <sys/config.h> without going through <_ansi.h>, so
_WANT_REENT_THREAD_LOCAL is not defined when config.h looks for it, and the
include guard blocks a second look. <reent.h> then falls back to _impure_ptr,
which a thread-local build never defines: hash.o was left with an unresolved
_impure_ptr, breaking any link that pulls in dbm_open and friends.

Assisted-by: Claude Opus 5
With struct _reent in native TLS, the 256-entry table hosts nothing: it is a
directory of pointers to memory the thread library allocates itself, and the
kernel TLS slot it indexes is a kernel service the thread library can hold
directly.

__VITA_NEWLIB_THREAD_SLOT__ in the new <sys/vita_thread.h> selects who owns
it. At 1, the default, everything behaves as before. At 0, the table, the
mutex, the slot and vitasdk_get_tls_data/vitasdk_get_pthread_data/
vitasdk_delete_thread_reent are gone, and so is the 256-thread ceiling that
ended in __builtin_trap.

The value lives in the installed header rather than in a build flag because
newlib and everything built against it must agree on it, and only the header
guarantees that. Consumers that predate the header default to 1.

vita_exit_thread and vita_exit_delete_thread stay in both, and are now
declared: reclaiming a thread's own TLS-resident buffers before it exits is
the one piece of this only newlib can do.

Assisted-by: Claude Opus 5
__VITA_NEWLIB_THREAD_SLOT__ read as "there is a slot", which is true either
way -- the slot exists in both, what changes is who opens it.

Assisted-by: Claude Opus 5
Under --enable-newlib-reent-thread-local, _REENT_MP_FREELIST/_RESULT/
_P5S and _REENT_CVTBUF/_REENT_CLEANUP resolve to the calling thread's
own _tls_* globals regardless of the ptr argument, so a second call
after the first frees the same pointers again. The vita port is about
to call this twice on some threads (once explicitly on the thread's
own exit path, once from a process-wide EXIT event handler that also
catches threads that never call it), so leaving the freed pointers
dangling would double-free.

NULL them out right after freeing, under _REENT_THREAD_LOCAL only: the
non-thread-local path already guards reentry via ptr != _impure_ptr
and isn't part of this.

Assisted-by: Claude Sonnet 5
With struct _reent in native TLS, a thread's own newlib state (mprec
bigints, _cvtbuf) is only reclaimed today if it exits through
vita_exit_thread/vita_exit_delete_thread, which call
_reclaim_reent(NULL) in _exit_thread_common. A raw sceKernelCreateThread
thread that just returns from its entry function, or is exited/deleted
from outside, never goes through either, so it leaks that state
forever. The old kernel-TLS-slot reent pool didn't have this problem:
it reclaimed on slot reuse regardless of how a thread ended.

Register a process-wide SCE_KERNEL_THREAD_EVENT_TYPE_EXIT handler once
at newlib init, mirroring how SceLibc registers its own TLS destructor
handler (same 0x10027 process-wide mask, same guarantee that it runs
in the exiting thread's own context). The handler just calls
_reclaim_reent(NULL), which is now idempotent, so threads that already
reclaim explicitly via vita_exit_thread get it twice, harmlessly.

Registration failure is silent and non-fatal: without it, a thread
just leaks the way it always did before this handler existed.

Not unregistered in _free_vita_reent: its only caller is _exit(),
which calls sceKernelExitProcess() right after, tearing down every
kernel object the process owns.

Assisted-by: Claude Sonnet 5
_reclaim_reent ends by running _REENT_CLEANUP, which is _cleanup_r. With
the reent in native TLS the FILE list (__sglue) is global while that hook
is per thread, so _cleanup_r called from an exiting thread closes every
stream in the process and frees their buffers under the threads still
using them. The hook is only ever set by __sinit, as a "stdio is
initialised" mark for the calling thread; closing streams belongs to
exit(). Clear it before reclaiming, on both the explicit path and the
thread-exit handler.

Assisted-by: Claude Fable 5
libstdc++ has no thread-exit hook on newlib targets, so its libsupc++
fallback keeps thread_local destructors in a pthread key and only runs
them when the prebuilt library sees gthreads as active, which on a static
link needs libpthread whole-archive; raw kernel threads never get them.
Newlib on Vita does know when a thread exits now, so it can own this: a
per-thread list in TLS, run from the thread-exit path before the reent
is reclaimed, on every exit path including raw threads, and from atexit()
for the thread that ends the process.

libstdc++ only defers to libc when HAVE___CXA_THREAD_ATEXIT is defined at
its configure time, which the --with-newlib branch never checks; that
define is the toolchain side of this change.

Assisted-by: Claude Fable 5
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