From c1066cceec9a23b1085e04a2f708dcac141d350e Mon Sep 17 00:00:00 2001 From: Adil Alperen Ciftci Date: Thu, 29 Oct 2026 12:54:28 +0300 Subject: [PATCH] contrib.host.python: restore nested map context Preserve the previous shared-map token around nested run_sandboxed_with_map calls and restore it when the inner call returns. Reject an over-depth entry before activating or freezing the new map context. Add an embedded CPython regression covering the inner map, restored outer map, and final token cleanup. Signed-off-by: Adil Alperen Ciftci --- contrib/host/python/README.md | 8 +- contrib/host/python/aether_host_python.c | 7 +- ...9T101500Z-fixed-python-host-map-context.md | 4 + .../test_host_python_map_context.sh | 143 ++++++++++++++++++ 4 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 new_changelogs/20261029T101500Z-fixed-python-host-map-context.md create mode 100755 tests/integration/host_python_map_context/test_host_python_map_context.sh diff --git a/contrib/host/python/README.md b/contrib/host/python/README.md index b1921e046..861b5340a 100644 --- a/contrib/host/python/README.md +++ b/contrib/host/python/README.md @@ -127,7 +127,7 @@ main() { ## Testing -Three tests exercise this host, each covering a different facet: +Four tests exercise this host, each covering a different facet: - **Namespace / FFI**: [`tests/integration/namespace_python/`](../../../tests/integration/namespace_python/), @@ -154,6 +154,12 @@ Three tests exercise this host, each covering a different facet: the write landed and the frozen input is untampered. SKIPs when the Python dev package isn't installed. +- **Nested shared-map context**: + [`tests/integration/host_python_map_context/`](../../../tests/integration/host_python_map_context/) + enters `run_sandboxed_with_map` recursively through the embedded CPython + process. It verifies that the inner run sees only its map, the outer map is + restored afterward, and no token remains active after the outer return. + - **Build smoke**: [`tests/scripts/contrib_build.sh`](../../../tests/scripts/contrib_build.sh), run by `make contrib`. It probes for Python via `python3-config` and, diff --git a/contrib/host/python/aether_host_python.c b/contrib/host/python/aether_host_python.c index ba3a66598..581fa59a6 100644 --- a/contrib/host/python/aether_host_python.c +++ b/contrib/host/python/aether_host_python.c @@ -264,17 +264,18 @@ int python_run_sandboxed_with_map(void* perms, const char* code, uint64_t map_to if (python_init() != 0) return -1; register_map_module(); + if (aether_sandbox_depth() >= AETHER_SANDBOX_MAX_LEVELS) return -1; + // Freeze inputs and set active token extern void aether_shared_map_freeze_inputs_by_token(uint64_t); aether_shared_map_freeze_inputs_by_token(map_token); + uint64_t previous_map_token = py_current_map_token; py_current_map_token = map_token; // Inject convenience imports so user code just calls aether_map_get/put const char* preamble = "from _aether_map import aether_map_get, aether_map_put\n"; - if (aether_sandbox_depth() >= AETHER_SANDBOX_MAX_LEVELS) return -1; - // The grants as a level of this thread's, in the runtime (#2955) aether_sandbox_push(perms); @@ -284,7 +285,7 @@ int python_run_sandboxed_with_map(void* perms, const char* code, uint64_t map_to // Restore aether_sandbox_pop(); - py_current_map_token = 0; + py_current_map_token = previous_map_token; return result; } diff --git a/new_changelogs/20261029T101500Z-fixed-python-host-map-context.md b/new_changelogs/20261029T101500Z-fixed-python-host-map-context.md new file mode 100644 index 000000000..ae5226690 --- /dev/null +++ b/new_changelogs/20261029T101500Z-fixed-python-host-map-context.md @@ -0,0 +1,4 @@ +- **The embedded Python host now restores nested shared-map contexts.** + `python.run_sandboxed_with_map` preserves the caller's map token across a + nested hosted run and still clears the token when the outermost run returns. + A rejected over-depth call no longer replaces the active map context. diff --git a/tests/integration/host_python_map_context/test_host_python_map_context.sh b/tests/integration/host_python_map_context/test_host_python_map_context.sh new file mode 100755 index 000000000..b0ca1638c --- /dev/null +++ b/tests/integration/host_python_map_context/test_host_python_map_context.sh @@ -0,0 +1,143 @@ +#!/bin/sh +# The in-process Python host keeps the shared-map token scoped to each +# run_sandboxed_with_map call. A nested run must restore the outer token, and +# the outer return must leave no token active. +set -u + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +case "$(uname -s 2>/dev/null)" in + MINGW*|MSYS*|CYGWIN*|Windows_NT) + echo " [SKIP] host_python_map_context on Windows" + exit 0 + ;; +esac + +if ! command -v python3-config >/dev/null 2>&1 || ! command -v python3 >/dev/null 2>&1; then + echo " [SKIP] host_python_map_context: python3 development files unavailable" + exit 0 +fi + +PY_CFLAGS="$(python3-config --embed --cflags 2>/dev/null || python3-config --cflags 2>/dev/null)" +PY_LIBS="$(python3-config --embed --ldflags 2>/dev/null || python3-config --ldflags 2>/dev/null)" +PY_SONAME="$(python3 -c 'import sysconfig; print(sysconfig.get_config_var("INSTSONAME") or "")' 2>/dev/null)" +if [ -z "$PY_CFLAGS" ] || [ -z "$PY_LIBS" ] || [ -z "$PY_SONAME" ]; then + echo " [SKIP] host_python_map_context: libpython build metadata unavailable" + exit 0 +fi +if [ ! -f "$ROOT/build/libaether.a" ]; then + echo " [SKIP] host_python_map_context: libaether.a not built" + exit 0 +fi +export AETHER_PYTHON_SONAME="$PY_SONAME" + +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +if ! gcc -o "$TMPDIR/test_python_map_context" \ + "$ROOT/contrib/host/python/aether_host_python.c" \ + "$ROOT/runtime/aether_sandbox.c" \ + "$ROOT/runtime/aether_shared_map.c" \ + $PY_CFLAGS -I"$ROOT" -I"$ROOT/runtime" \ + -DAETHER_HAS_PYTHON -DAETHER_HAS_SANDBOX -rdynamic \ + -L"$ROOT/build" -laether $PY_LIBS -ldl -lm -lrt -lpthread \ + -Wno-discarded-qualifiers \ + -xc - 2>"$TMPDIR/cc.log" <<'CEOF' +#include +#include +#include +#include + +#include "contrib/host/python/aether_host_python.h" +#include "runtime/aether_shared_map.h" + +extern void *list_new(void); +extern void list_free(void *); + +void aether_args_init(int argc, char **argv) +{ + (void)argc; + (void)argv; +} + +void *_aether_ctx_stack[64]; +int _aether_ctx_depth = 0; + +static const char *shown(const char *value) +{ + return value ? value : "(null)"; +} + +int main(void) +{ + uint64_t outer_token = 0; + uint64_t inner_token = 0; + AetherSharedMap *outer = aether_shared_map_new(&outer_token); + AetherSharedMap *inner = aether_shared_map_new(&inner_token); + void *perms = list_new(); + char code[4096]; + + if (!outer || !inner || !perms) + { + fprintf(stderr, "FAIL setup\n"); + return 1; + } + + aether_shared_map_put(outer, "scope", "outer"); + aether_shared_map_put(inner, "scope", "inner"); + + int written = snprintf( + code, sizeof(code), + "import ctypes\n" + "nested = ctypes.PyDLL(None).python_run_sandboxed_with_map\n" + "nested.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint64]\n" + "nested.restype = ctypes.c_int\n" + "assert aether_map_get('scope') == 'outer'\n" + "rc = nested(None, b\"assert aether_map_get('scope') == 'inner'\\naether_map_put('inner_result', 'ok')\\n\", %llu)\n" + "assert rc == 0\n" + "restored = aether_map_get('scope')\n" + "aether_map_put('outer_result', 'missing' if restored is None else restored)\n", + (unsigned long long)inner_token); + if (written < 0 || (size_t)written >= sizeof(code)) + { + fprintf(stderr, "FAIL generated Python source\n"); + return 1; + } + + int run_rc = python_run_sandboxed_with_map(perms, code, outer_token); + const char *inner_result = aether_shared_map_get(inner, "inner_result"); + const char *outer_result = aether_shared_map_get(outer, "outer_result"); + int cleared_rc = python_run( + "assert aether_map_get('scope') is None, 'map token survived outer return'"); + + printf("run=%d inner=%s outer=%s cleared=%d\n", + run_rc, shown(inner_result), shown(outer_result), cleared_rc); + + int ok = run_rc == 0 && inner_result && outer_result && + cleared_rc == 0 && strcmp(inner_result, "ok") == 0 && + strcmp(outer_result, "outer") == 0; + + aether_shared_map_revoke_token(inner_token); + aether_shared_map_revoke_token(outer_token); + aether_shared_map_free(inner); + aether_shared_map_free(outer); + python_finalize(); + list_free(perms); + + if (!ok) + { + fprintf(stderr, "FAIL Python host did not restore the outer map context\n"); + return 1; + } + puts("PASS Python host restored and cleared nested map contexts"); + return 0; +} +CEOF +then + echo " [SKIP] host_python_map_context: harness compile/link failed" + head -12 "$TMPDIR/cc.log" | sed 's/^/ /' + exit 0 +fi + +"$TMPDIR/test_python_map_context"