Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/containment-sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,13 @@ should have both.
size limit, the sandbox stays active with zero grants rather than
running unrestricted.

- **An empty grant list starts the child with nothing allowed.**
`spawn_sandboxed` with no grants runs a compute-only worker: it can use
the pipes it was given, but no files, exec or network. Malformed lists
(odd length, a `:` in a category, CR or LF in a category or pattern) and
lists over the transport size limit are refused with `-1` and the
message "invalid grant list" or "grant list too large".

- **`execve` with an explicit environment keeps the sandbox controls.**
The preload layer re-adds `LD_PRELOAD` and the `AETHER_SANDBOX_*` /
`AETHER_CAPSICUM` entries it captured at startup, so the child stays
Expand Down
9 changes: 9 additions & 0 deletions new_changelogs/20261029T075739Z-fixed-sandbox-unique-shm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- **Sandbox grant transport is isolated and validated.**
Linux and FreeBSD now allocate an exclusive shared-memory object per
`spawn_sandboxed` call. An empty grant list starts the child with an active
sandbox and nothing allowed (a compute-only worker: the pipes it was given,
but no files, exec or network). Malformed lists, including CR/LF or `:`
injection into a category or pattern, and lists that exceed the transport
size (E2BIG) are refused with `-1` instead of being skipped or truncated,
and the diagnostic now says "invalid grant list" or "grant list too large"
rather than a shared-memory failure.
76 changes: 76 additions & 0 deletions runtime/sandbox/aether_sandbox_grants.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#ifndef AETHER_SANDBOX_GRANTS_H
#define AETHER_SANDBOX_GRANTS_H

#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>

#define AETHER_SANDBOX_GRANTS_SIZE 8192

extern int list_size(void*);
extern void* list_get_raw(void*, int);
extern const char* aether_string_data(const void*);

static int aether_sandbox_serialize_grants(void* grant_list, char* buf,
size_t buf_size, size_t* data_size) {
if (!grant_list || !buf || !data_size || buf_size == 0) {
errno = EINVAL;
return -1;
}

int n = list_size(grant_list);
if (n < 0 || n % 2 != 0) {
errno = EINVAL;
return -1;
}

size_t pos = 0;
buf[0] = '\0';
for (int i = 0; i < n; i += 2) {
void* category_item = list_get_raw(grant_list, i);
void* pattern_item = list_get_raw(grant_list, i + 1);
if (!category_item || !pattern_item) {
errno = EINVAL;
return -1;
}
const char* category = aether_string_data(category_item);
const char* pattern = aether_string_data(pattern_item);
if (strchr(category, ':') ||
strpbrk(category, "\r\n") || strpbrk(pattern, "\r\n")) {
errno = EINVAL;
return -1;
}

int written = snprintf(buf + pos, buf_size - pos, "%s:%s\n",
category, pattern);
if (written < 0 || (size_t)written >= buf_size - pos) {
errno = E2BIG;
return -1;
}
pos += (size_t)written;
}

*data_size = pos + 1;
return 0;
}

// Wrapper result codes for serialize_grants() in the spawn files, so the
// caller can say which step failed. All map to spawn_sandboxed's -1.
#define AETHER_SANDBOX_GRANTS_ERR_SHM (-1) // shared memory could not be created
#define AETHER_SANDBOX_GRANTS_ERR_INVALID (-2) // EINVAL from serialize_grants
#define AETHER_SANDBOX_GRANTS_ERR_TOOBIG (-3) // E2BIG from serialize_grants

static inline int aether_sandbox_grants_err_code(int err) {
return err == EINVAL ? AETHER_SANDBOX_GRANTS_ERR_INVALID
: err == E2BIG ? AETHER_SANDBOX_GRANTS_ERR_TOOBIG
: AETHER_SANDBOX_GRANTS_ERR_SHM;
}

static inline const char* aether_sandbox_grants_err_msg(int code) {
return code == AETHER_SANDBOX_GRANTS_ERR_INVALID ? "invalid grant list"
: code == AETHER_SANDBOX_GRANTS_ERR_TOOBIG ? "grant list too large"
: "cannot create shared memory for grants";
}

#endif
29 changes: 10 additions & 19 deletions runtime/sandbox/spawn_sandboxed_bsd.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 10 additions & 19 deletions runtime/sandbox/spawn_sandboxed_linux.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions tests/integration/sandbox_empty_grants/child.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions tests/integration/sandbox_empty_grants/harness.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/bin/sh
# An empty grant list starts the child under an active sandbox with nothing
# allowed: its file read, TCP connect and exec are all refused. The same child
# with matching grants is allowed all three, so the test shows confinement
# rather than a broken child.

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

case "$(uname -s 2>/dev/null)" in
Linux|FreeBSD) ;;
*)
echo " [SKIP] sandbox_empty_grants: spawn_sandboxed is Linux and FreeBSD only"
exit 0 ;;
esac
if [ ! -f "$ROOT/build/libaether.a" ] ||
[ ! -f "$ROOT/build/libaether_sandbox.so" ]; then
echo " [SKIP] sandbox_empty_grants: sandbox runtime not built"
exit 0
fi

TMP="$(cd "$(mktemp -d)" && pwd -P)"
trap 'rm -rf "$TMP"' EXIT

if ! ${CC:-cc} -std=c11 -D_DEFAULT_SOURCE -Wall -Wextra -Werror \
-o "$TMP/child" "$SCRIPT_DIR/child.c" 2>"$TMP/cc.err"; then
echo " [FAIL] sandbox_empty_grants: child did not compile"
sed 's/^/ /' "$TMP/cc.err" | head -8
exit 1
fi
if ! ${CC:-cc} -std=c11 -Wall -Wextra -Werror \
-o "$TMP/harness" "$SCRIPT_DIR/harness.c" \
"$ROOT/build/libaether.a" -lpthread -lm 2>"$TMP/cc.err"; then
if ! ${CC:-cc} -std=c11 -Wall -Wextra -Werror \
-o "$TMP/harness" "$SCRIPT_DIR/harness.c" \
"$ROOT/build/libaether.a" \
-lpthread -ldl -lm -lrt -lz -lssl -lcrypto 2>"$TMP/cc.err"; then
echo " [FAIL] sandbox_empty_grants: harness did not link"
sed 's/^/ /' "$TMP/cc.err" | head -8
exit 1
fi
fi

printf 'secret\n' > "$TMP/data.txt"

# SANDBOX_PRELOAD_OVERRIDE is for proving the test can fail: point it at an
# inert library and the child runs unconfined (see the commit message).
PRELOAD="${SANDBOX_PRELOAD_OVERRIDE:-$ROOT/build/libaether_sandbox.so}"
out="$(AETHER_SANDBOX_PRELOAD="$PRELOAD" AETHER_SANDBOX_LOG=none \
"$TMP/harness" "$TMP/child" "$TMP/data.txt" 2>&1 </dev/null)"
rc=$?
if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -q \
'^PASS: empty grant list confines the child'; then
echo " [PASS] empty grant list refuses read, connect and exec; matching grants allow them"
echo "PASS: sandbox_empty_grants"
exit 0
fi

echo " [FAIL] empty grant list did not confine the child, or the control was refused"
printf '%s\n' "$out" | sed 's/^/ /' | head -30
echo "FAIL: sandbox_empty_grants"
exit 1
Loading