From 91c63be5d4e41d44e0746ef533af4a0b1967d6b3 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Wed, 9 Sep 2026 21:03:01 +0200 Subject: [PATCH] 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 Signed-off-by: Martin Vogel --- src/cli/config_edit_path.c | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/cli/config_edit_path.c b/src/cli/config_edit_path.c index ec26a5534..2217f3855 100644 --- a/src/cli/config_edit_path.c +++ b/src/cli/config_edit_path.c @@ -11,6 +11,7 @@ #include #include +#include #include #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) {