diff --git a/new_changelogs/20261029T083420Z-security-sandbox-policy-intersection.md b/new_changelogs/20261029T083420Z-security-sandbox-policy-intersection.md new file mode 100644 index 000000000..c1d6e0c91 --- /dev/null +++ b/new_changelogs/20261029T083420Z-security-sandbox-policy-intersection.md @@ -0,0 +1,3 @@ +- **Embedded and preload sandbox policies now intersect.** An allowing + in-process checker no longer bypasses a stricter shared-memory or file grant + list; an operation must satisfy every active containment layer. diff --git a/runtime/libaether_sandbox_preload.c b/runtime/libaether_sandbox_preload.c index 5c646380f..029b6634b 100644 --- a/runtime/libaether_sandbox_preload.c +++ b/runtime/libaether_sandbox_preload.c @@ -104,15 +104,16 @@ static int check_grant(const char* category, const char* resource) { match_target = resolved; } - // In-process levels take priority (embedded Python mode) + // An embedded policy and preload grants are independent boundaries. + // When both are active, the operation must satisfy both policies. if (aether_sandbox_check_resolved) { int result = aether_sandbox_check_resolved(category, match_target); - if (result >= 0) { - if (!result) log_deny(category, match_target); - return result; + if (result == 0) { + log_deny(category, match_target); + return 0; } } - // Fall back to file-based grants (LD_PRELOAD mode) + // Apply file/shared-memory grants as the second policy when active. if (!sandbox_active) return 1; for (int i = 0; i < grant_count; i++) { if (grants[i].cat[0] == '*' && grants[i].pat[0] == '*') return 1; diff --git a/tests/integration/sandbox_policy_intersection/harness.c b/tests/integration/sandbox_policy_intersection/harness.c new file mode 100644 index 000000000..d7548c2bd --- /dev/null +++ b/tests/integration/sandbox_policy_intersection/harness.c @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include + +typedef int (*aether_sandbox_check_fn)(const char*, const char*); + +static int allow_check(const char* category, const char* resource) { + (void)category; + (void)resource; + return 1; +} + +static int deny_check(const char* category, const char* resource) { + (void)category; + (void)resource; + return 0; +} + +static aether_sandbox_check_fn active_check = allow_check; + +int aether_sandbox_check_resolved(const char* category, const char* resource) { + return active_check(category, resource); +} + +int aether_sandbox_levels_here(void) { + return 1; +} + +int main(int argc, char** argv) { + if (argc != 3) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + active_check = strcmp(argv[1], "deny") == 0 + ? deny_check : allow_check; + + int fd = open(argv[2], O_RDONLY); + if (fd < 0) return errno == EACCES ? 3 : 4; + char buf[64]; + ssize_t n = read(fd, buf, sizeof buf); + close(fd); + if (n < 0) return 5; + if (write(STDOUT_FILENO, buf, (size_t)n) != n) return 6; + return 0; +} diff --git a/tests/integration/sandbox_policy_intersection/test_sandbox_policy_intersection.sh b/tests/integration/sandbox_policy_intersection/test_sandbox_policy_intersection.sh new file mode 100644 index 000000000..10c3e16a2 --- /dev/null +++ b/tests/integration/sandbox_policy_intersection/test_sandbox_policy_intersection.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# When both the embedded checker and preload grants are active, an operation +# must satisfy both policies. Neither policy may replace the other. + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +LIB="$ROOT/build/libaether_sandbox.so" + +case "$(uname -s 2>/dev/null)" in + Linux|FreeBSD) ;; + *) + echo " [SKIP] sandbox_policy_intersection: preload is Linux and FreeBSD only" + exit 0 ;; +esac +if [ ! -f "$LIB" ]; then + echo " [SKIP] sandbox_policy_intersection: preload library not built" + exit 0 +fi + +TMP="$(cd "$(mktemp -d)" && pwd -P)" +trap 'rm -rf "$TMP"' EXIT +if ! ${CC:-cc} -std=c11 -Wall -Wextra -Werror -rdynamic \ + "$SCRIPT_DIR/harness.c" -o "$TMP/harness"; then + echo " [FAIL] sandbox_policy_intersection: harness did not compile" + exit 1 +fi + +printf 'intersection sentinel\n' > "$TMP/data.txt" +printf 'exec:/not-used\n' > "$TMP/deny-read.grants" +printf 'fs_read:%s\n' "$TMP/data.txt" > "$TMP/allow-read.grants" +fail=0 + +run_case() { + label="$1" + checker="$2" + grants="$3" + expected="$4" + if LD_PRELOAD="$LIB" AETHER_SANDBOX_GRANTS="$grants" \ + AETHER_SANDBOX_LOG=none "$TMP/harness" "$checker" "$TMP/data.txt" \ + >"$TMP/out" 2>"$TMP/err"; then + got=allow + else + got=deny + fi + if [ "$got" = "$expected" ]; then + echo " [PASS] $label" + else + echo " [FAIL] $label: got $got, expected $expected" + fail=1 + fi +} + +run_case "preload denial survives an allowing embedded checker" \ + allow "$TMP/deny-read.grants" deny +run_case "embedded denial survives an allowing preload policy" \ + deny "$TMP/allow-read.grants" deny +run_case "both policies allowing permits the read" \ + allow "$TMP/allow-read.grants" allow + +echo "" +echo "sandbox_policy_intersection: $([ "$fail" -eq 0 ] && echo PASS || echo FAIL)" +[ "$fail" -eq 0 ]