fix(cli): keep config follow-roots off the static TLS block (#2110)

#1954's config-symlink follow support stored its opted-in roots in a
_Thread_local char[4][4096] -- 16 KiB in the binary's static TLS block.
On glibc/x86-64 that block is carved out of every thread's stack, including
the parent-death watchdog thread, created with a deliberately tiny 64 KiB
stack (PARENT_WATCHDOG_STACK_SIZE, main.c). The extra 16 KiB pushed that
thread's pthread_create past the static-TLS + TCB + guard budget, so it
failed with EINVAL: client_start_parent_watchdog() returned false and the
stdio server exited before answering initialize. The parent-death watchdog
regression test (#406/#407, scripts/test.sh Step 5) went red on every Linux
x86-64 leg while macOS/arm64 stayed green -- its TLS is not stack-carved.

Move the buffer off the static TLS block: keep a _Thread_local POINTER
(8 bytes) and calloc the 16 KiB lazily on first add, freeing it in
follow_clear(). Per-thread isolation and the exact API/semantics are
unchanged; the watchdog thread's static-TLS budget is restored. main.c and
the watchdog path are untouched.

Evidence (local, Linux x86-64 gcc under QEMU + Linux arm64):
- PT_TLS MemSiz 59,889 B -> 43,505 B (-16,384 B, exactly the array).
- parent-watchdog test 10/10 red -> 10/10 green; red again on revert.
- origin/main 10/10 green on the same env (regression is PR-specific).
- config editor + cli suites: 522/522 ASan/UBSan (macOS),
  523/523 ASan/LeakSanitizer (Linux arm64), no leak.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin VogelandClaude Fable 5.1 committed 2026-09-09 21:03:01 +02:00
1 parent a3c24a71b4
commit 91c63be5d4
1 file changed
+19 -1
+19 -1
View File
@@ -11,6 +11,7 @@
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef _WIN32
@@ -66,7 +67,16 @@ void cbm_config_edit_target_close(cbm_config_edit_target_t *target) {
#ifndef _WIN32
static CBM_TLS char edit_path_roots[EDIT_PATH_MAX_ROOTS][CBM_CONFIG_EDIT_PATH_MAX];
/* The opted-in follow-roots are per-thread (test isolation), but the storage
* is a heap buffer behind a thread-local POINTER, not a thread-local array.
* A `_Thread_local char[4][4096]` sits in the binary's static TLS block, and
* glibc carves that block out of every thread's stack allocation — including
* the parent-death watchdog's deliberately tiny 64 KiB stack (main.c). The
* 16 KiB array pushed the watchdog's pthread_create over the static-TLS +
* guard budget, so it failed with EINVAL on glibc/x86-64 (green on macOS,
* whose TLS is not stack-carved): #2110 CI. The pointer keeps the static TLS
* block at 8 bytes; the buffer is allocated on first add and freed by clear. */
static CBM_TLS char (*edit_path_roots)[CBM_CONFIG_EDIT_PATH_MAX] = NULL;
static CBM_TLS size_t edit_path_root_count = 0U;
#ifdef CBM_CONFIG_EDIT_PATH_ENABLE_TEST_API
@@ -101,6 +111,12 @@ int cbm_config_edit_path_follow_add_root(const char *root) {
if (!cbm_canonical_path(root, canonical, sizeof(canonical))) {
return -1;
}
if (!edit_path_roots) {
edit_path_roots = calloc(EDIT_PATH_MAX_ROOTS, sizeof(*edit_path_roots));
if (!edit_path_roots) {
return -1;
}
}
if (edit_path_copy(canonical, edit_path_roots[edit_path_root_count],
sizeof(edit_path_roots[0])) != 0) {
return -1;
@@ -111,6 +127,8 @@ int cbm_config_edit_path_follow_add_root(const char *root) {
void cbm_config_edit_path_follow_clear(void) {
edit_path_root_count = 0U;
free(edit_path_roots);
edit_path_roots = NULL;
}
static int edit_path_under_root(const char *canonical) {