From 1b27855abf0ec315156d137dfc275c93fbf32dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adil=20Alperen=20=C3=87iftci?= Date: Thu, 29 Oct 2026 12:26:59 +0300 Subject: [PATCH] runtime: keep sandbox checker thread-local MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I reproduced one thread replacing another thread's in-process sandbox checker while their permission stacks stayed thread-local. Store the checker in TLS and let the preload bridge query it through a weak getter, which keeps standalone preload use valid. Add a synchronized two-thread regression test for policy isolation. Signed-off-by: Adil Alperen Çiftci --- compiler/codegen/codegen.c | 2 +- ...curity-sandbox-checker-thread-isolation.md | 6 ++ runtime/aether_sandbox.c | 8 ++- runtime/aether_sandbox.h | 6 +- runtime/libaether_sandbox_preload.c | 12 ++-- .../integration/sandbox_checker_tls/harness.c | 65 +++++++++++++++++++ .../test_sandbox_checker_tls.sh | 41 ++++++++++++ 7 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 new_changelogs/20261029T092300Z-security-sandbox-checker-thread-isolation.md create mode 100644 tests/integration/sandbox_checker_tls/harness.c create mode 100755 tests/integration/sandbox_checker_tls/test_sandbox_checker_tls.sh diff --git a/compiler/codegen/codegen.c b/compiler/codegen/codegen.c index 9c8afa129..4f32fe367 100644 --- a/compiler/codegen/codegen.c +++ b/compiler/codegen/codegen.c @@ -9570,7 +9570,7 @@ void generate_program(CodeGenerator* gen, ASTNode* program) { if (has_sandbox) { // Sandbox bridge: connects compiler-generated context stack to runtime checks. print_line(gen, "typedef int (*aether_sandbox_check_fn)(const char*, const char*);"); - print_line(gen, "extern aether_sandbox_check_fn _aether_sandbox_checker;"); + print_line(gen, "extern AETHER_TLS_SHARED aether_sandbox_check_fn _aether_sandbox_checker;"); print_line(gen, "extern int list_size(void*);"); print_line(gen, "extern void* list_get_raw(void*, int);"); print_line(gen, "#include \"aether_sandbox_match.h\""); diff --git a/new_changelogs/20261029T092300Z-security-sandbox-checker-thread-isolation.md b/new_changelogs/20261029T092300Z-security-sandbox-checker-thread-isolation.md new file mode 100644 index 000000000..17889be28 --- /dev/null +++ b/new_changelogs/20261029T092300Z-security-sandbox-checker-thread-isolation.md @@ -0,0 +1,6 @@ +- **Sandbox policies no longer cross thread boundaries.** The in-process + checker was process-global while generated permission stacks were + thread-local, so one thread entering or leaving a sandbox block could + replace or clear another thread's checker. The checker is now thread-local, + and the preload layer reads it through a weak getter that remains safe when + no embedded runtime is present. diff --git a/runtime/aether_sandbox.c b/runtime/aether_sandbox.c index c06c20c7f..4bf0b23ce 100644 --- a/runtime/aether_sandbox.c +++ b/runtime/aether_sandbox.c @@ -6,8 +6,12 @@ #include #include "aether_sandbox_path.h" -// Default: no sandbox checker installed = everything allowed -aether_sandbox_check_fn _aether_sandbox_checker = 0; +// Default: no sandbox checker installed in this thread = everything allowed +AETHER_TLS_SHARED aether_sandbox_check_fn _aether_sandbox_checker = 0; + +aether_sandbox_check_fn aether_sandbox_get_checker(void) { + return _aether_sandbox_checker; +} // The checker sees an fs path where it leads (aether_sandbox_path.h). A path // that cannot be resolved is refused, not matched as written. diff --git a/runtime/aether_sandbox.h b/runtime/aether_sandbox.h index 1a1bcf909..34eb25532 100644 --- a/runtime/aether_sandbox.h +++ b/runtime/aether_sandbox.h @@ -11,6 +11,7 @@ #define AETHER_SANDBOX_H #include "aether_sandbox_match.h" /* aether_net_resource, the shared matcher */ +#include "utils/aether_compiler.h" #ifdef AETHER_HAS_SANDBOX @@ -20,8 +21,9 @@ // Returns: 1 = allowed, 0 = denied typedef int (*aether_sandbox_check_fn)(const char* category, const char* resource); -// Global: set by compiler-generated code when inside a sandbox block -extern aether_sandbox_check_fn _aether_sandbox_checker; +// Per-thread: set by compiler-generated code when inside a sandbox block +extern AETHER_TLS_SHARED aether_sandbox_check_fn _aether_sandbox_checker; +aether_sandbox_check_fn aether_sandbox_get_checker(void); // The checker, with an fs path resolved to where it leads first (".." and // symlinks), so a grant for "/box/*" cannot be left through "/box/../x" or a diff --git a/runtime/libaether_sandbox_preload.c b/runtime/libaether_sandbox_preload.c index bbb4aaede..6c1e445a1 100644 --- a/runtime/libaether_sandbox_preload.c +++ b/runtime/libaether_sandbox_preload.c @@ -62,7 +62,8 @@ static void* (*real_dlopen)(const char*, int) = NULL; // Check the in-process Aether sandbox checker first (embedded mode), // then fall back to file-based grants (LD_PRELOAD mode) typedef int (*aether_sandbox_check_fn)(const char*, const char*); -extern aether_sandbox_check_fn _aether_sandbox_checker __attribute__((weak)); +extern aether_sandbox_check_fn aether_sandbox_get_checker(void) + __attribute__((weak)); static void log_deny(const char* category, const char* resource) { if (log_mode == LOG_NONE) return; @@ -81,6 +82,9 @@ static void log_deny(const char* category, const char* resource) { } static int check_grant(const char* category, const char* resource) { + aether_sandbox_check_fn checker = + aether_sandbox_get_checker ? aether_sandbox_get_checker() : NULL; + // For fs_* categories, resolve the path BEFORE matching so a grant // for `/tmp/*` can't be subverted with `/tmp/../etc/shadow` or a // symlink under /tmp. tcp/env/exec resources stay as-is — realpath @@ -89,7 +93,7 @@ static int check_grant(const char* category, const char* resource) { const char* match_target = resource; if (category && strncmp(category, "fs_", 3) == 0 && resource) { if (!aether_sandbox_canon_path(resource, resolved, sizeof(resolved))) { - if ((&_aether_sandbox_checker && _aether_sandbox_checker) || sandbox_active) { + if (checker || sandbox_active) { log_deny(category, resource); return 0; } @@ -99,8 +103,8 @@ static int check_grant(const char* category, const char* resource) { } // In-process checker takes priority (embedded Python mode) - if (&_aether_sandbox_checker && _aether_sandbox_checker) { - int result = _aether_sandbox_checker(category, match_target); + if (checker) { + int result = checker(category, match_target); if (!result) log_deny(category, match_target); return result; } diff --git a/tests/integration/sandbox_checker_tls/harness.c b/tests/integration/sandbox_checker_tls/harness.c new file mode 100644 index 000000000..78ebd879a --- /dev/null +++ b/tests/integration/sandbox_checker_tls/harness.c @@ -0,0 +1,65 @@ +#include +#include + +#include "aether_sandbox.h" + +static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t changed = PTHREAD_COND_INITIALIZER; +static int worker_ready; +static int main_checked; +static int worker_result = -1; + +static int deny_all(const char* category, const char* resource) { + (void)category; + (void)resource; + return 0; +} + +static int allow_all(const char* category, const char* resource) { + (void)category; + (void)resource; + return 1; +} + +static void* worker(void* unused) { + (void)unused; + _aether_sandbox_checker = allow_all; + + pthread_mutex_lock(&lock); + worker_ready = 1; + pthread_cond_signal(&changed); + while (!main_checked) pthread_cond_wait(&changed, &lock); + pthread_mutex_unlock(&lock); + + worker_result = aether_sandbox_check("env", "WORKER_VALUE"); + _aether_sandbox_checker = NULL; + return NULL; +} + +int main(void) { + _aether_sandbox_checker = deny_all; + pthread_t thread; + if (pthread_create(&thread, NULL, worker, NULL) != 0) return 2; + + pthread_mutex_lock(&lock); + while (!worker_ready) pthread_cond_wait(&changed, &lock); + pthread_mutex_unlock(&lock); + + int main_result = aether_sandbox_check("env", "MAIN_VALUE"); + + pthread_mutex_lock(&lock); + main_checked = 1; + pthread_cond_signal(&changed); + pthread_mutex_unlock(&lock); + + pthread_join(thread, NULL); + _aether_sandbox_checker = NULL; + + if (main_result != 0 || worker_result != 1) { + fprintf(stderr, "FAIL checker state crossed threads: main=%d worker=%d\n", + main_result, worker_result); + return 1; + } + printf("PASS: sandbox checker state is thread-local\n"); + return 0; +} diff --git a/tests/integration/sandbox_checker_tls/test_sandbox_checker_tls.sh b/tests/integration/sandbox_checker_tls/test_sandbox_checker_tls.sh new file mode 100755 index 000000000..d44bf7211 --- /dev/null +++ b/tests/integration/sandbox_checker_tls/test_sandbox_checker_tls.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# Each thread installs and restores its own in-process sandbox checker. One +# thread leaving an enforce block must not disable another thread's policy. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +case "$(uname -s 2>/dev/null)" in + Linux|FreeBSD|Darwin) ;; + *) + echo " [SKIP] sandbox_checker_tls: POSIX thread harness" + exit 0 ;; +esac +if [ ! -f "$ROOT/build/libaether.a" ]; then + echo " [SKIP] sandbox_checker_tls: build/libaether.a not built" + exit 0 +fi + +TMP="$(cd "$(mktemp -d)" && pwd -P)" +trap 'rm -rf "$TMP"' EXIT +if ! ${CC:-cc} -std=c11 -Wall -Wextra -Werror -DAETHER_HAS_SANDBOX \ + -I"$ROOT/runtime" -o "$TMP/harness" "$SCRIPT_DIR/harness.c" \ + "$ROOT/build/libaether.a" -lpthread -lm 2>"$TMP/cc.err"; then + echo " [FAIL] sandbox_checker_tls: harness did not compile" + sed 's/^/ /' "$TMP/cc.err" | head -10 + exit 1 +fi + +out="$($TMP/harness 2>&1)" +rc=$? +if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -q \ + '^PASS: sandbox checker state is thread-local$'; then + echo " [PASS] sandbox checker state is thread-local" + echo "PASS: sandbox_checker_tls" + exit 0 +fi + +echo " [FAIL] sandbox checker state crossed threads" +printf '%s\n' "$out" | sed 's/^/ /' | head -10 +echo "FAIL: sandbox_checker_tls" +exit 1