mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 06:12:08 +00:00
Compare commits
15 Commits
claude/fix
...
dylan/spaw
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bf145fe38 | ||
|
|
0a9b852a64 | ||
|
|
05be0f7117 | ||
|
|
e2d1d9f899 | ||
|
|
9db1d80e50 | ||
|
|
8228f0d459 | ||
|
|
572a96ce0d | ||
|
|
3ac8b1ff85 | ||
|
|
96292141b3 | ||
|
|
783fcfaa81 | ||
|
|
6b3df67dce | ||
|
|
4d5ceecd0f | ||
|
|
b58b9f15b5 | ||
|
|
c387e08cfe | ||
|
|
51967f2889 |
171
packages/bun-types/bun.d.ts
vendored
171
packages/bun-types/bun.d.ts
vendored
@@ -6326,6 +6326,177 @@ declare module "bun" {
|
||||
* @default undefined (no limit)
|
||||
*/
|
||||
maxBuffer?: number;
|
||||
|
||||
/**
|
||||
* Process sandboxing options.
|
||||
*
|
||||
* You can specify options for multiple sandbox technologies - options
|
||||
* for unavailable technologies are silently ignored. This allows writing
|
||||
* cross-platform code that sandboxes on all supported platforms.
|
||||
*
|
||||
* Currently supports:
|
||||
* - **seccomp**: Linux syscall filtering via BPF
|
||||
* - **seatbelt**: macOS sandbox via SBPL profiles
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Cross-platform sandboxing
|
||||
* const proc = Bun.spawn({
|
||||
* cmd: ["./untrusted-program"],
|
||||
* sandbox: {
|
||||
* seccomp: bpfFilter, // Linux
|
||||
* seatbelt: ` // macOS
|
||||
* (version 1)
|
||||
* (allow default)
|
||||
* (deny network*)
|
||||
* `,
|
||||
* },
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
sandbox?: {
|
||||
/**
|
||||
* seccomp-BPF filter (Linux only).
|
||||
*
|
||||
* A compiled BPF program that restricts which system calls the
|
||||
* subprocess can make. Silently ignored on non-Linux platforms.
|
||||
*
|
||||
* The buffer must contain valid BPF bytecode. Each BPF instruction
|
||||
* is 8 bytes:
|
||||
* - 2 bytes: opcode
|
||||
* - 1 byte: jump-true offset
|
||||
* - 1 byte: jump-false offset
|
||||
* - 4 bytes: k value
|
||||
*
|
||||
* **Important**: Your BPF program MUST check the architecture
|
||||
* (`seccomp_data.arch`) before checking syscall numbers, as syscall
|
||||
* numbers differ between architectures (x86_64, arm64, etc.).
|
||||
*
|
||||
* The filter is applied after `fork()` but before `execve()`, and
|
||||
* persists across `execve()`. Child processes inherit the filter.
|
||||
*
|
||||
* Bun automatically calls `prctl(PR_SET_NO_NEW_PRIVS, 1)` before
|
||||
* applying the filter.
|
||||
*
|
||||
* If the filter kills the process (via `SECCOMP_RET_KILL`), the
|
||||
* subprocess will have `exitCode: null` and `signalCode: "SIGSYS"`.
|
||||
*
|
||||
* @see https://man7.org/linux/man-pages/man2/seccomp.2.html
|
||||
* @platform linux
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Simple format: raw BPF filter
|
||||
* Bun.spawn({
|
||||
* cmd: ["program"],
|
||||
* sandbox: { seccomp: bpfFilter }
|
||||
* });
|
||||
*
|
||||
* // Object format with flags
|
||||
* Bun.spawn({
|
||||
* cmd: ["program"],
|
||||
* sandbox: {
|
||||
* seccomp: {
|
||||
* filter: bpfFilter,
|
||||
* flags: ["LOG", "SPEC_ALLOW"]
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
seccomp?:
|
||||
| ArrayBufferView
|
||||
| ArrayBuffer
|
||||
| {
|
||||
/** BPF filter bytecode. Each instruction is 8 bytes. */
|
||||
filter: ArrayBufferView | ArrayBuffer;
|
||||
/**
|
||||
* Seccomp filter flags. Can be a single flag or array of flags.
|
||||
*
|
||||
* - `"LOG"` - Log all filter actions except ALLOW to audit log
|
||||
* - `"SPEC_ALLOW"` - Disable Speculative Store Bypass mitigation
|
||||
*/
|
||||
flags?: "LOG" | "SPEC_ALLOW" | ("LOG" | "SPEC_ALLOW")[];
|
||||
};
|
||||
|
||||
/**
|
||||
* macOS Seatbelt sandbox profile (SBPL format).
|
||||
*
|
||||
* Silently ignored on non-macOS platforms.
|
||||
*
|
||||
* The sandbox is applied after `fork()` but before `exec()` using
|
||||
* Apple's `sandbox_init()` API. Once applied, the sandbox cannot
|
||||
* be removed or weakened. Child processes inherit the sandbox.
|
||||
*
|
||||
* Common operations to allow/deny:
|
||||
* - `file-read*`, `file-write*` - File system access
|
||||
* - `network-outbound`, `network-inbound` - Network access
|
||||
* - `process-exec`, `process-fork` - Process creation
|
||||
* - `sysctl-read`, `sysctl-write` - System information
|
||||
*
|
||||
* Can be specified as:
|
||||
* - A string containing an inline SBPL profile
|
||||
* - An object with `profile` (SBPL string) and optional `parameters`
|
||||
* - An object with `namedProfile` referencing a system profile
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Inline SBPL profile (string)
|
||||
* const proc = Bun.spawn({
|
||||
* cmd: ["./my-script.sh"],
|
||||
* sandbox: {
|
||||
* seatbelt: `
|
||||
* (version 1)
|
||||
* (deny default)
|
||||
* (allow file-read*)
|
||||
* (allow process-exec)
|
||||
* (allow process-fork)
|
||||
* `
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // SBPL profile with parameters
|
||||
* const proc2 = Bun.spawn({
|
||||
* cmd: ["./my-script.sh"],
|
||||
* sandbox: {
|
||||
* seatbelt: {
|
||||
* profile: `
|
||||
* (version 1)
|
||||
* (deny default)
|
||||
* (allow file-read* (subpath (param "ALLOWED_PATH")))
|
||||
* `,
|
||||
* parameters: {
|
||||
* ALLOWED_PATH: "/tmp/safe-directory"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Named system profile
|
||||
* const proc3 = Bun.spawn({
|
||||
* cmd: ["./my-script.sh"],
|
||||
* sandbox: {
|
||||
* seatbelt: { namedProfile: "quicklookd" }
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @see https://reverse.put.as/wp-content/uploads/2011/09/Apple-Sandbox-Guide-v1.0.pdf
|
||||
* @platform darwin
|
||||
*/
|
||||
seatbelt?:
|
||||
| string
|
||||
| {
|
||||
/** Inline SBPL profile string */
|
||||
profile: string;
|
||||
/** Optional parameters passed to the SBPL profile via the `param()` function */
|
||||
parameters?: Record<string, string>;
|
||||
}
|
||||
| {
|
||||
/** Named profile (e.g., "quicklookd", "pfd") */
|
||||
namedProfile: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface SpawnSyncOptions<In extends Writable, Out extends Readable, Err extends Readable>
|
||||
|
||||
@@ -340,6 +340,16 @@ const shouldValidateLeakSan = test => {
|
||||
return !(skipsForLeaksan.includes(test) || skipsForLeaksan.includes("test/" + test));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether a test expects spawned child processes to crash (e.g., seccomp tests).
|
||||
* For these tests, we disable core dumps to avoid CI failures from expected crashes.
|
||||
* @param {string} test
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const expectsSpawnedProcessCrash = test => {
|
||||
return test.endsWith("spawn-sandbox.test.ts");
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} testPath
|
||||
* @returns {string[]}
|
||||
@@ -603,6 +613,11 @@ async function runTests() {
|
||||
// prettier-ignore
|
||||
env.LSAN_OPTIONS = `malloc_context_size=100:print_suppressions=0:suppressions=${process.cwd()}/test/leaksan.supp`;
|
||||
}
|
||||
if (expectsSpawnedProcessCrash(testPath)) {
|
||||
// Tests that spawn processes expected to crash (e.g., seccomp sandbox tests)
|
||||
// should not generate core dumps
|
||||
env.ASAN_OPTIONS = "allow_user_segv_handler=1:disable_coredump=1";
|
||||
}
|
||||
return runTest(title, async () => {
|
||||
const { ok, error, stdout, crashes } = await spawnBun(execPath, {
|
||||
cwd: cwd,
|
||||
@@ -1350,9 +1365,27 @@ async function spawnBunTest(execPath, testPath, opts = { cwd }) {
|
||||
// prettier-ignore
|
||||
env.LSAN_OPTIONS = `malloc_context_size=100:print_suppressions=0:suppressions=${process.cwd()}/test/leaksan.supp`;
|
||||
}
|
||||
// For tests that spawn processes expected to crash, disable core dumps
|
||||
const disableCoreDumps = expectsSpawnedProcessCrash(relative(cwd, absPath));
|
||||
const isAsanBuild = basename(execPath).includes("asan");
|
||||
if (disableCoreDumps && isAsanBuild) {
|
||||
// ASAN builds: use ASAN_OPTIONS to disable core dumps
|
||||
env.ASAN_OPTIONS = "allow_user_segv_handler=1:disable_coredump=1";
|
||||
}
|
||||
|
||||
const { ok, error, stdout, crashes } = await spawnBun(execPath, {
|
||||
args: isReallyTest ? testArgs : [...args, absPath],
|
||||
// Build the command args
|
||||
let spawnArgs = isReallyTest ? testArgs : [...args, absPath];
|
||||
let spawnExecPath = execPath;
|
||||
|
||||
// Non-ASAN Linux builds: wrap with ulimit -c 0 to disable core dumps
|
||||
if (disableCoreDumps && isLinux && !isAsanBuild) {
|
||||
const originalCmd = [execPath, ...spawnArgs].map(a => `"${a.replace(/"/g, '\\"')}"`).join(" ");
|
||||
spawnExecPath = "/bin/sh";
|
||||
spawnArgs = ["-c", `ulimit -c 0 && exec ${originalCmd}`];
|
||||
}
|
||||
|
||||
const { ok, error, stdout, crashes } = await spawnBun(spawnExecPath, {
|
||||
args: spawnArgs,
|
||||
cwd: opts["cwd"],
|
||||
timeout: isReallyTest ? timeout : 30_000,
|
||||
env,
|
||||
|
||||
@@ -157,6 +157,7 @@ pub fn spawnMaybeSync(
|
||||
var terminal_info: ?Terminal.CreateResult = null;
|
||||
var existing_terminal: ?*Terminal = null; // Existing terminal passed by user
|
||||
var terminal_js_value: jsc.JSValue = .zero;
|
||||
var sandbox: bun.spawn.SpawnOptions.Sandbox = .{};
|
||||
defer {
|
||||
if (abort_signal) |signal| {
|
||||
signal.unref();
|
||||
@@ -420,6 +421,166 @@ pub fn spawnMaybeSync(
|
||||
stdio[2] = .{ .fd = slave_fd };
|
||||
}
|
||||
}
|
||||
|
||||
// Parse sandbox option
|
||||
if (try args.getTruthy(globalThis, "sandbox")) |sandbox_val| {
|
||||
// Parse sandbox.seccomp (seccomp BPF filter) - only on Linux, ignored on other platforms
|
||||
if (comptime Environment.isLinux) {
|
||||
if (try sandbox_val.getTruthy(globalThis, "seccomp")) |seccomp_val| {
|
||||
// Check if it's an ArrayBuffer/TypedArray (simple format) or object (extended format)
|
||||
if (seccomp_val.asArrayBuffer(globalThis)) |array_buffer| {
|
||||
// Simple format: just the filter bytes
|
||||
const slice = array_buffer.slice();
|
||||
if (slice.len > 0) {
|
||||
if (slice.len % 8 != 0) {
|
||||
return globalThis.throwInvalidArguments(
|
||||
"sandbox.seccomp filter must be a multiple of 8 bytes (each BPF instruction is 8 bytes)",
|
||||
.{},
|
||||
);
|
||||
}
|
||||
sandbox.linux.seccomp_filter = try allocator.dupe(u8, slice);
|
||||
}
|
||||
} else if (seccomp_val.isObject()) {
|
||||
// Extended format: { filter: ArrayBuffer, flags?: string | string[] }
|
||||
if (try seccomp_val.getTruthy(globalThis, "filter")) |filter_val| {
|
||||
if (filter_val.asArrayBuffer(globalThis)) |array_buffer| {
|
||||
const slice = array_buffer.slice();
|
||||
if (slice.len > 0) {
|
||||
if (slice.len % 8 != 0) {
|
||||
return globalThis.throwInvalidArguments(
|
||||
"sandbox.seccomp.filter must be a multiple of 8 bytes (each BPF instruction is 8 bytes)",
|
||||
.{},
|
||||
);
|
||||
}
|
||||
sandbox.linux.seccomp_filter = try allocator.dupe(u8, slice);
|
||||
}
|
||||
} else {
|
||||
return globalThis.throwInvalidArgumentType(
|
||||
"spawn",
|
||||
"sandbox.seccomp.filter",
|
||||
"ArrayBuffer or TypedArray",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return globalThis.throwInvalidArguments(
|
||||
"sandbox.seccomp object must have 'filter' property",
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
// Parse optional flags (string or array of strings)
|
||||
if (try seccomp_val.get(globalThis, "flags")) |flags_val| {
|
||||
if (!flags_val.isUndefinedOrNull()) {
|
||||
var flags: u32 = 0;
|
||||
if (flags_val.isString()) {
|
||||
// Single flag: "LOG"
|
||||
const flag = try SeccompFlag.fromJS(globalThis, flags_val) orelse {
|
||||
return globalThis.throwInvalidArguments(
|
||||
"Unknown seccomp flag. Expected: LOG or SPEC_ALLOW",
|
||||
.{},
|
||||
);
|
||||
};
|
||||
flags = @intFromEnum(flag);
|
||||
} else if (flags_val.jsType().isArray()) {
|
||||
// Array of flags: ["LOG", "SPEC_ALLOW"]
|
||||
var iter = try flags_val.arrayIterator(globalThis);
|
||||
while (try iter.next()) |item| {
|
||||
const flag = try SeccompFlag.fromJS(globalThis, item) orelse {
|
||||
return globalThis.throwInvalidArguments(
|
||||
"Unknown seccomp flag. Expected: LOG or SPEC_ALLOW",
|
||||
.{},
|
||||
);
|
||||
};
|
||||
flags |= @intFromEnum(flag);
|
||||
}
|
||||
} else {
|
||||
return globalThis.throwInvalidArgumentType(
|
||||
"spawn",
|
||||
"sandbox.seccomp.flags",
|
||||
"string or array of strings",
|
||||
);
|
||||
}
|
||||
sandbox.linux.seccomp_flags = flags;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return globalThis.throwInvalidArgumentType(
|
||||
"spawn",
|
||||
"sandbox.seccomp",
|
||||
"ArrayBuffer, TypedArray, or object",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse sandbox.seatbelt (macOS SBPL profile) - only on macOS, ignored on other platforms
|
||||
if (comptime Environment.isMac) {
|
||||
if (try sandbox_val.getTruthy(globalThis, "seatbelt")) |seatbelt_val| {
|
||||
if (seatbelt_val.isString()) {
|
||||
const slice = seatbelt_val.toSlice(globalThis, allocator) catch {
|
||||
return globalThis.throwInvalidArgumentType("spawn", "sandbox.seatbelt", "string");
|
||||
};
|
||||
if (slice.len > 0) {
|
||||
sandbox.darwin.profile = try allocator.dupeZ(u8, slice.slice());
|
||||
}
|
||||
} else if (seatbelt_val.isObject()) {
|
||||
// Object format: { profile: "...", parameters?: {...} } or { namedProfile: "..." }
|
||||
if (try seatbelt_val.get(globalThis, "namedProfile")) |named_val| {
|
||||
if ((try seatbelt_val.get(globalThis, "profile")) != null) {
|
||||
return globalThis.throwInvalidArguments("sandbox.seatbelt cannot have both 'profile' and 'namedProfile'", .{});
|
||||
}
|
||||
const slice = named_val.toSlice(globalThis, allocator) catch {
|
||||
return globalThis.throwInvalidArgumentType("spawn", "sandbox.seatbelt.namedProfile", "string");
|
||||
};
|
||||
if (slice.len > 0) {
|
||||
sandbox.darwin.profile = try allocator.dupeZ(u8, slice.slice());
|
||||
sandbox.darwin.flags = 1; // SANDBOX_NAMED
|
||||
}
|
||||
} else if (try seatbelt_val.get(globalThis, "profile")) |profile_val| {
|
||||
const slice = profile_val.toSlice(globalThis, allocator) catch {
|
||||
return globalThis.throwInvalidArgumentType("spawn", "sandbox.seatbelt.profile", "string");
|
||||
};
|
||||
if (slice.len > 0) {
|
||||
sandbox.darwin.profile = try allocator.dupeZ(u8, slice.slice());
|
||||
}
|
||||
|
||||
// Parse optional parameters
|
||||
if (try seatbelt_val.getTruthy(globalThis, "parameters")) |params_val| {
|
||||
const params_obj = params_val.getObject() orelse {
|
||||
return globalThis.throwInvalidArgumentType("spawn", "sandbox.seatbelt.parameters", "object");
|
||||
};
|
||||
|
||||
var params_iter = try jsc.JSPropertyIterator(.{ .skip_empty_name = false, .include_value = true }).init(globalThis, params_obj);
|
||||
defer params_iter.deinit();
|
||||
|
||||
// Format: ["KEY1", "VALUE1", "KEY2", "VALUE2", ..., null]
|
||||
var params_list = try std.ArrayList(?[*:0]const u8).initCapacity(allocator, params_iter.len * 2 + 1);
|
||||
|
||||
while (try params_iter.next()) |key| {
|
||||
const value = params_iter.value;
|
||||
if (value.isUndefined()) continue;
|
||||
|
||||
const value_str = try value.toBunString(globalThis);
|
||||
defer value_str.deref();
|
||||
|
||||
params_list.appendAssumeCapacity((try key.toOwnedSliceZ(allocator)).ptr);
|
||||
params_list.appendAssumeCapacity((try value_str.toOwnedSliceZ(allocator)).ptr);
|
||||
}
|
||||
|
||||
params_list.appendAssumeCapacity(null);
|
||||
sandbox.darwin.parameters = @ptrCast(params_list.items.ptr);
|
||||
}
|
||||
} else {
|
||||
// Object doesn't have either 'profile' or 'namedProfile'
|
||||
return globalThis.throwInvalidArgumentType("spawn", "sandbox.seatbelt", "object with 'profile' or 'namedProfile'");
|
||||
}
|
||||
} else {
|
||||
return globalThis.throwInvalidArgumentType("spawn", "sandbox.seatbelt", "string or object");
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Parse sandbox.appContainer (Windows sandbox options)
|
||||
}
|
||||
} else {
|
||||
try getArgv(globalThis, cmd_value, PATH, cwd, &argv0, allocator, &argv);
|
||||
}
|
||||
@@ -578,6 +739,9 @@ pub fn spawnMaybeSync(
|
||||
break :blk -1;
|
||||
} else {},
|
||||
|
||||
// Platform-specific sandbox configuration
|
||||
.sandbox = sandbox,
|
||||
|
||||
.windows = if (Environment.isWindows) .{
|
||||
.hide_window = windows_hide,
|
||||
.verbatim_arguments = windows_verbatim_arguments,
|
||||
@@ -1099,6 +1263,22 @@ pub fn appendEnvpFromJS(globalThis: *jsc.JSGlobalObject, object: *jsc.JSObject,
|
||||
const log = Output.scoped(.Subprocess, .hidden);
|
||||
extern "C" const BUN_DEFAULT_PATH_FOR_SPAWN: [*:0]const u8;
|
||||
|
||||
/// Seccomp filter flags for Linux sandboxing.
|
||||
/// These correspond to SECCOMP_FILTER_FLAG_* constants.
|
||||
pub const SeccompFlag = enum(u32) {
|
||||
LOG = 0x2, // SECCOMP_FILTER_FLAG_LOG
|
||||
SPEC_ALLOW = 0x4, // SECCOMP_FILTER_FLAG_SPEC_ALLOW
|
||||
// NEW_LISTENER = 0x8, // SECCOMP_FILTER_FLAG_NEW_LISTENER
|
||||
|
||||
const Map = bun.ComptimeStringMap(SeccompFlag, .{
|
||||
.{ "LOG", .LOG },
|
||||
.{ "SPEC_ALLOW", .SPEC_ALLOW },
|
||||
// .{ "NEW_LISTENER", .NEW_LISTENER },
|
||||
});
|
||||
|
||||
pub const fromJS = Map.fromJS;
|
||||
};
|
||||
|
||||
const IPC = @import("../../ipc.zig");
|
||||
const Terminal = @import("./Terminal.zig");
|
||||
const std = @import("std");
|
||||
|
||||
@@ -996,6 +996,8 @@ pub const PosixSpawnOptions = struct {
|
||||
no_sigpipe: bool = true,
|
||||
/// PTY slave fd for controlling terminal setup (-1 if not using PTY).
|
||||
pty_slave_fd: i32 = -1,
|
||||
/// Platform-specific sandbox configuration.
|
||||
sandbox: Sandbox = .{},
|
||||
|
||||
pub const Stdio = union(enum) {
|
||||
path: []const u8,
|
||||
@@ -1011,6 +1013,56 @@ pub const PosixSpawnOptions = struct {
|
||||
pub fn deinit(_: *const PosixSpawnOptions) void {
|
||||
// no-op
|
||||
}
|
||||
|
||||
/// Sandbox configuration for process spawning.
|
||||
/// Contains platform-specific sandboxing options for both Linux (seccomp) and macOS (seatbelt).
|
||||
pub const Sandbox = struct {
|
||||
/// Linux sandbox options.
|
||||
linux: Linux = .{},
|
||||
/// macOS sandbox options.
|
||||
darwin: Darwin = .{},
|
||||
|
||||
pub const Linux = struct {
|
||||
/// Seccomp BPF filter bytecode.
|
||||
/// Must be a multiple of 8 bytes (sizeof(struct sock_filter)).
|
||||
seccomp_filter: ?[]const u8 = null,
|
||||
/// Seccomp filter flags (SECCOMP_FILTER_FLAG_*).
|
||||
/// Only used when seccomp_filter is set.
|
||||
seccomp_flags: u32 = 0,
|
||||
};
|
||||
|
||||
pub const Darwin = struct {
|
||||
/// Sandbox profile (SBPL string or named profile).
|
||||
/// Applied after fork() but before exec() using sandbox_init().
|
||||
profile: ?[:0]const u8 = null,
|
||||
/// Sandbox flags: 0 = inline SBPL string, 1 = named profile from /usr/share/sandbox
|
||||
flags: u64 = 0,
|
||||
/// Sandbox parameters: array of key-value pairs for the param() function in SBPL.
|
||||
/// Format: ["KEY1", "VALUE1", "KEY2", "VALUE2", ..., null]
|
||||
parameters: ?[*:null]const ?[*:0]const u8 = null,
|
||||
};
|
||||
|
||||
/// C-compatible layout for FFI with posix_spawn_bun.
|
||||
pub const Extern = extern struct {
|
||||
linux_seccomp_filter: ?[*]const u8 = null,
|
||||
linux_seccomp_filter_len: usize = 0,
|
||||
linux_seccomp_flags: u32 = 0,
|
||||
darwin_profile: ?[*:0]const u8 = null,
|
||||
darwin_flags: u64 = 0,
|
||||
darwin_parameters: ?[*:null]const ?[*:0]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn toExtern(self: Sandbox) Extern {
|
||||
return .{
|
||||
.linux_seccomp_filter = if (self.linux.seccomp_filter) |s| s.ptr else null,
|
||||
.linux_seccomp_filter_len = if (self.linux.seccomp_filter) |s| s.len else 0,
|
||||
.linux_seccomp_flags = self.linux.seccomp_flags,
|
||||
.darwin_profile = if (self.darwin.profile) |s| s.ptr else null,
|
||||
.darwin_flags = self.darwin.flags,
|
||||
.darwin_parameters = self.darwin.parameters,
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
pub const WindowsSpawnResult = struct {
|
||||
@@ -1066,6 +1118,8 @@ pub const WindowsSpawnOptions = struct {
|
||||
can_block_entire_thread_to_reduce_cpu_usage_in_fast_path: bool = false,
|
||||
/// PTY not supported on Windows - this is a void placeholder for struct compatibility
|
||||
pty_slave_fd: void = {},
|
||||
sandbox: Sandbox = .{},
|
||||
|
||||
pub const WindowsOptions = struct {
|
||||
verbatim_arguments: bool = false,
|
||||
hide_window: bool = true,
|
||||
@@ -1096,6 +1150,8 @@ pub const WindowsSpawnOptions = struct {
|
||||
stdio.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
pub const Sandbox = struct {};
|
||||
};
|
||||
|
||||
pub const PosixSpawnResult = struct {
|
||||
@@ -1264,6 +1320,9 @@ pub fn spawnProcessPosix(
|
||||
// Pass PTY slave fd to attr for controlling terminal setup
|
||||
attr.pty_slave_fd = options.pty_slave_fd;
|
||||
|
||||
// Pass platform-specific sandbox settings to attr
|
||||
attr.sandbox = options.sandbox;
|
||||
|
||||
if (options.cwd.len > 0) {
|
||||
try actions.chdir(options.cwd);
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ pub const BunSpawn = struct {
|
||||
pty_slave_fd: i32 = -1,
|
||||
flags: u16 = 0,
|
||||
reset_signals: bool = false,
|
||||
sandbox: bun.spawn.SpawnOptions.Sandbox = .{},
|
||||
|
||||
pub fn init() !Attr {
|
||||
return Attr{};
|
||||
@@ -270,6 +271,7 @@ pub const PosixSpawn = struct {
|
||||
detached: bool = false,
|
||||
actions: ActionsList = .{},
|
||||
pty_slave_fd: i32 = -1,
|
||||
sandbox: SpawnOptions.Sandbox.Extern = .{},
|
||||
|
||||
const ActionsList = extern struct {
|
||||
ptr: ?[*]const BunSpawn.Action = null,
|
||||
@@ -324,14 +326,17 @@ pub const PosixSpawn = struct {
|
||||
) Maybe(pid_t) {
|
||||
const pty_slave_fd = if (attr) |a| a.pty_slave_fd else -1;
|
||||
const detached = if (attr) |a| a.detached else false;
|
||||
const sandbox = if (attr) |a| a.sandbox else SpawnOptions.Sandbox{};
|
||||
|
||||
// Use posix_spawn_bun when:
|
||||
// - Linux: always (uses vfork which is fast and safe)
|
||||
// - macOS: only for PTY spawns (pty_slave_fd >= 0) because PTY setup requires
|
||||
// - macOS: for PTY spawns (pty_slave_fd >= 0) because PTY setup requires
|
||||
// setsid() + ioctl(TIOCSCTTY) before exec, which system posix_spawn can't do.
|
||||
// For non-PTY spawns on macOS, we use system posix_spawn which is safer
|
||||
// - macOS: for sandbox spawns (sandbox.darwin_profile != null) because sandbox_init()
|
||||
// must be called after fork() but before exec(), which system posix_spawn can't do.
|
||||
// For non-PTY, non-sandbox spawns on macOS, we use system posix_spawn which is safer
|
||||
// (Apple's posix_spawn uses a kernel fast-path that avoids fork() entirely).
|
||||
const use_bun_spawn = Environment.isLinux or (Environment.isMac and pty_slave_fd >= 0);
|
||||
const use_bun_spawn = Environment.isLinux or (Environment.isMac and (pty_slave_fd >= 0 or sandbox.darwin.profile != null));
|
||||
|
||||
if (use_bun_spawn) {
|
||||
return BunSpawnRequest.spawn(
|
||||
@@ -347,6 +352,7 @@ pub const PosixSpawn = struct {
|
||||
.chdir_buf = if (actions) |a| a.chdir_buf else null,
|
||||
.detached = detached,
|
||||
.pty_slave_fd = pty_slave_fd,
|
||||
.sandbox = sandbox.toExtern(),
|
||||
},
|
||||
argv,
|
||||
envp,
|
||||
|
||||
@@ -15,6 +15,24 @@
|
||||
|
||||
#if OS(LINUX)
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <linux/seccomp.h>
|
||||
#include <linux/filter.h>
|
||||
#endif
|
||||
|
||||
#if OS(DARWIN)
|
||||
#include <sandbox.h>
|
||||
|
||||
// Private function used by Chrome, Firefox, Nix, etc.
|
||||
// Apple officially deprecates sandbox_init() but continues to support it.
|
||||
extern "C" int sandbox_init_with_parameters(
|
||||
const char* profile,
|
||||
uint64_t flags,
|
||||
const char* const parameters[],
|
||||
char** errorbuf);
|
||||
|
||||
// Sandbox flags (from sandbox.h, but not always exposed)
|
||||
#define BUN_SANDBOX_NAMED 0x0001
|
||||
#endif
|
||||
|
||||
extern char** environ;
|
||||
@@ -99,6 +117,17 @@ typedef struct bun_spawn_request_t {
|
||||
bool detached;
|
||||
bun_spawn_file_action_list_t actions;
|
||||
int pty_slave_fd; // -1 if not using PTY, otherwise the slave fd to set as controlling terminal
|
||||
// Linux seccomp BPF filter (Linux only)
|
||||
const void* linux_seccomp_filter; // Pointer to BPF bytecode (array of sock_filter structs)
|
||||
size_t linux_seccomp_filter_len; // Length in bytes (must be multiple of 8)
|
||||
uint32_t linux_seccomp_flags; // SECCOMP_FILTER_FLAG_* flags
|
||||
// macOS sandbox profile (SBPL string or named profile)
|
||||
const char* darwin_profile;
|
||||
// macOS sandbox flags: 0 = SANDBOX_STRING (inline SBPL), 1 = SANDBOX_NAMED
|
||||
uint64_t darwin_flags;
|
||||
// macOS sandbox parameters: null-terminated array of "KEY\0VALUE\0" pairs, ending with empty string
|
||||
// Format: ["KEY1", "VALUE1", "KEY2", "VALUE2", ..., NULL]
|
||||
const char* const* darwin_parameters;
|
||||
} bun_spawn_request_t;
|
||||
|
||||
// Raw exit syscall that doesn't go through libc.
|
||||
@@ -284,6 +313,54 @@ extern "C" ssize_t posix_spawn_bun(
|
||||
// Close all fds > current_max_fd, preferring cloexec if available
|
||||
closeRangeOrLoop(current_max_fd + 1, INT_MAX, true);
|
||||
|
||||
#if OS(LINUX)
|
||||
// Apply seccomp filter if provided (Linux only)
|
||||
if (request->linux_seccomp_filter && request->linux_seccomp_filter_len > 0) {
|
||||
// Validate filter length (must be multiple of sizeof(sock_filter) = 8)
|
||||
if (request->linux_seccomp_filter_len % sizeof(struct sock_filter) != 0) {
|
||||
errno = EINVAL;
|
||||
return childFailed();
|
||||
}
|
||||
|
||||
// Set no_new_privs (required for seccomp without CAP_SYS_ADMIN)
|
||||
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) {
|
||||
return childFailed();
|
||||
}
|
||||
|
||||
// Construct sock_fprog
|
||||
struct sock_fprog prog = {
|
||||
.len = static_cast<unsigned short>(request->linux_seccomp_filter_len / sizeof(struct sock_filter)),
|
||||
.filter = static_cast<struct sock_filter*>(const_cast<void*>(request->linux_seccomp_filter)),
|
||||
};
|
||||
|
||||
if (syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, request->linux_seccomp_flags, &prog) == -1) {
|
||||
return childFailed();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if OS(DARWIN)
|
||||
// Apply macOS sandbox profile if provided
|
||||
// The sandbox is applied before execve and persists across exec.
|
||||
// Child processes inherit the sandbox - they cannot remove or weaken it.
|
||||
if (request->darwin_profile && request->darwin_profile[0] != '\0') {
|
||||
char* errorbuf = nullptr;
|
||||
// darwin_flags: 0 = inline SBPL string, BUN_SANDBOX_NAMED = named profile from /usr/share/sandbox
|
||||
// darwin_parameters: array of key-value pairs for the param() function in SBPL
|
||||
if (sandbox_init_with_parameters(
|
||||
request->darwin_profile,
|
||||
request->darwin_flags,
|
||||
request->darwin_parameters,
|
||||
&errorbuf)
|
||||
!= 0) {
|
||||
// sandbox_init failed - child process will exit immediately via childFailed()
|
||||
// No need to free errorbuf since we're about to _exit()
|
||||
errno = EINVAL;
|
||||
return childFailed();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (execve(path, argv, envp) == -1) {
|
||||
return childFailed();
|
||||
}
|
||||
|
||||
871
test/js/bun/spawn/spawn-sandbox.test.ts
Normal file
871
test/js/bun/spawn/spawn-sandbox.test.ts
Normal file
@@ -0,0 +1,871 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { bunEnv, tempDir } from "harness";
|
||||
|
||||
const isLinux = process.platform === "linux";
|
||||
const isMac = process.platform === "darwin";
|
||||
|
||||
// BPF instruction that returns SECCOMP_RET_ALLOW (0x7fff0000)
|
||||
// struct sock_filter { __u16 code; __u8 jt; __u8 jf; __u32 k; }
|
||||
// BPF_RET | BPF_K = 0x0006, jt=0, jf=0, k=0x7fff0000
|
||||
// Little-endian bytes: [0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f]
|
||||
const ALLOW_ALL_FILTER = new Uint8Array([0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f]);
|
||||
|
||||
// BPF instruction that returns SECCOMP_RET_KILL_PROCESS (0x80000000)
|
||||
// This will kill the process on any syscall
|
||||
const KILL_ALL_FILTER = new Uint8Array([0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80]);
|
||||
|
||||
// BPF filter that blocks write() syscall with EPERM
|
||||
// This allows the process to continue but write() calls will fail with "Operation not permitted"
|
||||
//
|
||||
// BPF program logic:
|
||||
// 0: Load arch from seccomp_data (offset 4)
|
||||
// 1: If arch matches expected, continue; else jump to kill
|
||||
// 2: Load syscall number (offset 0)
|
||||
// 3: If syscall == write, jump to block with EPERM
|
||||
// 4: Allow all other syscalls
|
||||
// 5: Return ERRNO | EPERM (0x00050001)
|
||||
// 6: Kill process (for wrong architecture)
|
||||
//
|
||||
// Architecture-specific values:
|
||||
// x86_64: AUDIT_ARCH = 0xc000003e, write syscall = 1
|
||||
// aarch64: AUDIT_ARCH = 0xc00000b7, write syscall = 64
|
||||
//
|
||||
// prettier-ignore
|
||||
const BLOCK_WRITE_FILTER_X86_64 = new Uint8Array([
|
||||
// struct sock_filter { __u16 code; __u8 jt; __u8 jf; __u32 k; }
|
||||
// Byte order: [code_lo, code_hi, jt, jf, k_0, k_1, k_2, k_3]
|
||||
//
|
||||
// Instruction 0: Load architecture (BPF_LD | BPF_W | BPF_ABS, offset=4)
|
||||
0x20, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
|
||||
// Instruction 1: If arch == x86_64 (0xc000003e), continue (jt=0); else jump to instruction 6 (jf=4)
|
||||
0x15, 0x00, 0x00, 0x04, 0x3e, 0x00, 0x00, 0xc0,
|
||||
// Instruction 2: Load syscall number (offset=0)
|
||||
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Instruction 3: If syscall == 1 (write), jump to instruction 5 (jt=1); else continue (jf=0)
|
||||
0x15, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
// Instruction 4: Allow (SECCOMP_RET_ALLOW = 0x7fff0000)
|
||||
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f,
|
||||
// Instruction 5: Block with EPERM (SECCOMP_RET_ERRNO | 1 = 0x00050001)
|
||||
0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x05, 0x00,
|
||||
// Instruction 6: Kill (SECCOMP_RET_KILL_PROCESS = 0x80000000) for wrong arch
|
||||
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
|
||||
]);
|
||||
|
||||
// prettier-ignore
|
||||
const BLOCK_WRITE_FILTER_AARCH64 = new Uint8Array([
|
||||
// Instruction 0: Load architecture (BPF_LD | BPF_W | BPF_ABS, offset=4)
|
||||
0x20, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
|
||||
// Instruction 1: If arch == aarch64 (0xc00000b7), continue (jt=0); else jump to instruction 6 (jf=4)
|
||||
0x15, 0x00, 0x00, 0x04, 0xb7, 0x00, 0x00, 0xc0,
|
||||
// Instruction 2: Load syscall number (offset=0)
|
||||
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// Instruction 3: If syscall == 64 (write on aarch64), jump to instruction 5 (jt=1); else continue (jf=0)
|
||||
0x15, 0x00, 0x01, 0x00, 0x40, 0x00, 0x00, 0x00,
|
||||
// Instruction 4: Allow (SECCOMP_RET_ALLOW = 0x7fff0000)
|
||||
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x7f,
|
||||
// Instruction 5: Block with EPERM (SECCOMP_RET_ERRNO | 1 = 0x00050001)
|
||||
0x06, 0x00, 0x00, 0x00, 0x01, 0x00, 0x05, 0x00,
|
||||
// Instruction 6: Kill (SECCOMP_RET_KILL_PROCESS = 0x80000000) for wrong arch
|
||||
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
|
||||
]);
|
||||
|
||||
// Select the appropriate filter based on current architecture
|
||||
const BLOCK_WRITE_FILTER = process.arch === "arm64" ? BLOCK_WRITE_FILTER_AARCH64 : BLOCK_WRITE_FILTER_X86_64;
|
||||
|
||||
// SBPL profile that allows everything (proper SBPL syntax)
|
||||
const ALLOW_ALL_PROFILE = `(version 1)
|
||||
(allow default)`;
|
||||
|
||||
// SBPL profile that denies network access
|
||||
const DENY_NETWORK_PROFILE = `(version 1)
|
||||
(allow default)
|
||||
(deny network*)`;
|
||||
|
||||
// SBPL profile that denies file writes
|
||||
const DENY_FILE_WRITE_PROFILE = `(version 1)
|
||||
(allow default)
|
||||
(deny file-write*)`;
|
||||
|
||||
describe("spawn sandbox (Linux)", () => {
|
||||
test.if(!isLinux)("sandbox.seccomp is silently ignored on non-Linux", async () => {
|
||||
// On non-Linux, sandbox.seccomp should be silently ignored
|
||||
// This allows users to specify all platform options at once
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "test"],
|
||||
sandbox: { seccomp: ALLOW_ALL_FILTER },
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("test\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isLinux)("rejects filter with invalid length (not multiple of 8)", () => {
|
||||
expect(() => {
|
||||
Bun.spawn({
|
||||
cmd: ["echo", "test"],
|
||||
sandbox: { seccomp: new Uint8Array([0x06, 0x00, 0x00]) }, // 3 bytes, not multiple of 8
|
||||
});
|
||||
}).toThrow("multiple of 8 bytes");
|
||||
});
|
||||
|
||||
test.if(isLinux)("rejects non-ArrayBuffer sandbox.seccomp value", () => {
|
||||
expect(() => {
|
||||
Bun.spawn({
|
||||
cmd: ["echo", "test"],
|
||||
sandbox: { seccomp: "not a buffer" as unknown as Uint8Array },
|
||||
});
|
||||
}).toThrow("ArrayBuffer, TypedArray, or object");
|
||||
});
|
||||
|
||||
test.if(isLinux)("accepts empty filter (no-op)", async () => {
|
||||
// Empty filter should be treated as no filter
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "hello"],
|
||||
sandbox: { seccomp: new Uint8Array(0) },
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("hello\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isLinux)("spawn with allow-all filter works with echo", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "sandboxed"],
|
||||
sandbox: { seccomp: ALLOW_ALL_FILTER },
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("sandboxed\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isLinux)("spawnSync with allow-all filter works with echo", () => {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: ["echo", "sync-sandboxed"],
|
||||
sandbox: { seccomp: ALLOW_ALL_FILTER },
|
||||
});
|
||||
|
||||
expect(result.stdout.toString()).toBe("sync-sandboxed\n");
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isLinux)("spawnSync with kill-all filter terminates process with SIGSYS", () => {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: ["/bin/true"],
|
||||
env: bunEnv,
|
||||
sandbox: { seccomp: KILL_ALL_FILTER },
|
||||
});
|
||||
|
||||
// Process is killed before it can do anything
|
||||
expect(result.stdout.toString()).toBe("");
|
||||
|
||||
// When killed by signal, exitCode is null and signalCode contains the signal name
|
||||
expect(result.exitCode).toBeNull();
|
||||
expect(result.signalCode).toBe("SIGSYS");
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
test.if(isLinux)("spawn with kill-all filter terminates process with SIGSYS", async () => {
|
||||
// The process should be killed immediately when it tries to make any syscall.
|
||||
// seccomp sends SIGSYS (signal 31) to terminate the process.
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["/bin/true"],
|
||||
env: bunEnv,
|
||||
sandbox: { seccomp: KILL_ALL_FILTER },
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
await proc.exited;
|
||||
const stdout = await proc.stdout.text();
|
||||
const stderr = await proc.stderr.text();
|
||||
|
||||
// Process is killed before it can output anything
|
||||
expect(stdout).toBe("");
|
||||
expect(stderr).toBe("");
|
||||
|
||||
// When killed by signal, exitCode is null and signalCode contains the signal name.
|
||||
// SIGSYS indicates seccomp blocked a syscall.
|
||||
expect(proc.exitCode).toBeNull();
|
||||
expect(proc.signalCode).toBe("SIGSYS");
|
||||
});
|
||||
|
||||
test.if(isLinux)("accepts ArrayBuffer", async () => {
|
||||
const buffer = ALLOW_ALL_FILTER.buffer.slice(
|
||||
ALLOW_ALL_FILTER.byteOffset,
|
||||
ALLOW_ALL_FILTER.byteOffset + ALLOW_ALL_FILTER.byteLength,
|
||||
);
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "arraybuffer"],
|
||||
sandbox: { seccomp: buffer },
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("arraybuffer\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isLinux)("accepts different TypedArray types", async () => {
|
||||
// Test with Uint16Array (same bytes, different view)
|
||||
const uint16View = new Uint16Array(ALLOW_ALL_FILTER.buffer);
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "uint16"],
|
||||
sandbox: { seccomp: uint16View },
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("uint16\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isLinux)("filter that blocks write() causes echo to fail", async () => {
|
||||
// echo uses write() to output to stdout
|
||||
// With write() blocked, echo should fail
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "this should not appear"],
|
||||
sandbox: { seccomp: BLOCK_WRITE_FILTER },
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
const stdout = await proc.stdout.text();
|
||||
|
||||
// echo should fail to write and exit with error, or produce no output
|
||||
// echo exits with 1 when write fails
|
||||
expect(stdout).toBe("");
|
||||
expect(exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test.if(isLinux)("filter that blocks write() allows /bin/true to succeed", async () => {
|
||||
// /bin/true doesn't write anything, it just exits with 0
|
||||
// So blocking write() shouldn't affect it
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["/bin/true"],
|
||||
sandbox: { seccomp: BLOCK_WRITE_FILTER },
|
||||
});
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isLinux)("filter that blocks write() causes /bin/false to exit non-zero", async () => {
|
||||
// /bin/false doesn't write anything, it just exits with 1
|
||||
// So blocking write() shouldn't affect it - it should still exit 1
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["/bin/false"],
|
||||
sandbox: { seccomp: BLOCK_WRITE_FILTER },
|
||||
});
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
expect(exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test.if(isLinux)("spawnSync filter that blocks write() causes echo to fail", () => {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: ["echo", "this should not appear"],
|
||||
sandbox: { seccomp: BLOCK_WRITE_FILTER },
|
||||
});
|
||||
|
||||
// echo should fail to write and exit with 1
|
||||
expect(result.stdout.toString()).toBe("");
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
// Seccomp flags tests
|
||||
test.if(isLinux)("seccomp object format with filter property works", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "object-format"],
|
||||
sandbox: {
|
||||
seccomp: {
|
||||
filter: ALLOW_ALL_FILTER,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("object-format\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isLinux)("seccomp single flag as string", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "with-log-flag"],
|
||||
sandbox: {
|
||||
seccomp: {
|
||||
filter: ALLOW_ALL_FILTER,
|
||||
flags: "LOG",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("with-log-flag\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isLinux)("seccomp single flag in array", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "spec-allow"],
|
||||
sandbox: {
|
||||
seccomp: {
|
||||
filter: ALLOW_ALL_FILTER,
|
||||
flags: ["SPEC_ALLOW"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("spec-allow\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isLinux)("seccomp multiple flags", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "multiple-flags"],
|
||||
sandbox: {
|
||||
seccomp: {
|
||||
filter: ALLOW_ALL_FILTER,
|
||||
flags: ["LOG", "SPEC_ALLOW"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("multiple-flags\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isLinux)("seccomp spawnSync with flags", () => {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: ["echo", "sync-with-flags"],
|
||||
sandbox: {
|
||||
seccomp: {
|
||||
filter: ALLOW_ALL_FILTER,
|
||||
flags: "LOG",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.stdout.toString()).toBe("sync-with-flags\n");
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isLinux)("seccomp object format requires filter property", () => {
|
||||
expect(() => {
|
||||
Bun.spawn({
|
||||
cmd: ["echo", "test"],
|
||||
sandbox: {
|
||||
seccomp: { flags: "LOG" } as unknown as Uint8Array,
|
||||
},
|
||||
});
|
||||
}).toThrow("filter");
|
||||
});
|
||||
|
||||
test.if(isLinux)("seccomp rejects unknown flag", () => {
|
||||
expect(() => {
|
||||
Bun.spawn({
|
||||
cmd: ["echo", "test"],
|
||||
sandbox: {
|
||||
seccomp: {
|
||||
filter: ALLOW_ALL_FILTER,
|
||||
flags: "INVALID_FLAG" as "LOG",
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrow("Unknown seccomp flag");
|
||||
});
|
||||
|
||||
test.if(isLinux)("seccomp rejects non-string flag in array", () => {
|
||||
expect(() => {
|
||||
Bun.spawn({
|
||||
cmd: ["echo", "test"],
|
||||
sandbox: {
|
||||
seccomp: {
|
||||
filter: ALLOW_ALL_FILTER,
|
||||
flags: ["LOG", 123] as unknown as ("LOG" | "SPEC_ALLOW")[],
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrow("Unknown seccomp flag");
|
||||
});
|
||||
|
||||
test.if(isLinux)("seccomp object format filter must be ArrayBuffer or TypedArray", () => {
|
||||
expect(() => {
|
||||
Bun.spawn({
|
||||
cmd: ["echo", "test"],
|
||||
sandbox: {
|
||||
seccomp: {
|
||||
filter: "not a buffer" as unknown as Uint8Array,
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrow("ArrayBuffer or TypedArray");
|
||||
});
|
||||
|
||||
test.if(isLinux)("seccomp object format with empty flags array", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "empty-flags"],
|
||||
sandbox: {
|
||||
seccomp: {
|
||||
filter: ALLOW_ALL_FILTER,
|
||||
flags: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("empty-flags\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("spawn sandbox (macOS)", () => {
|
||||
test.if(!isMac)("sandbox.seatbelt is silently ignored on non-macOS", async () => {
|
||||
// On non-macOS, sandbox.seatbelt should be silently ignored
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "test"],
|
||||
sandbox: { seatbelt: ALLOW_ALL_PROFILE },
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("test\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isMac)("non-string sandbox.seatbelt value throws", () => {
|
||||
// When sandbox.seatbelt is not a string or object, the spawn itself throws
|
||||
expect(() => {
|
||||
Bun.spawn({
|
||||
cmd: ["echo", "test"],
|
||||
sandbox: { seatbelt: 12345 as unknown as string },
|
||||
});
|
||||
}).toThrow("Expected sandbox.seatbelt to be a string or object");
|
||||
});
|
||||
|
||||
test.if(isMac)("spawn with allow-all profile works with echo", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "sandboxed"],
|
||||
sandbox: { seatbelt: ALLOW_ALL_PROFILE },
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("sandboxed\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isMac)("spawnSync with allow-all profile works with echo", () => {
|
||||
const result = Bun.spawnSync({
|
||||
cmd: ["echo", "sync-sandboxed"],
|
||||
sandbox: { seatbelt: ALLOW_ALL_PROFILE },
|
||||
});
|
||||
|
||||
expect(result.stdout.toString()).toBe("sync-sandboxed\n");
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isMac)("spawn with deny-network profile allows echo", async () => {
|
||||
// echo doesn't use network, so it should work
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "no-network"],
|
||||
sandbox: { seatbelt: DENY_NETWORK_PROFILE },
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("no-network\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isMac)("spawn with deny-file-write profile allows /usr/bin/true", async () => {
|
||||
// /usr/bin/true doesn't write files, so it should work
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["/usr/bin/true"],
|
||||
sandbox: { seatbelt: DENY_FILE_WRITE_PROFILE },
|
||||
});
|
||||
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isMac)("spawnSync with deny-file-write profile allows /usr/bin/false", () => {
|
||||
// /usr/bin/false doesn't write files, it just exits with 1
|
||||
const result = Bun.spawnSync({
|
||||
cmd: ["/usr/bin/false"],
|
||||
sandbox: { seatbelt: DENY_FILE_WRITE_PROFILE },
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test("can specify both seccomp and seatbelt sandbox options", async () => {
|
||||
// This should work on all platforms - each platform uses its own option
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "cross-platform"],
|
||||
sandbox: {
|
||||
seccomp: ALLOW_ALL_FILTER,
|
||||
seatbelt: ALLOW_ALL_PROFILE,
|
||||
},
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("cross-platform\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isMac)("invalid SBPL profile causes spawn to fail", () => {
|
||||
// An invalid sandbox profile causes the child to fail immediately with EINVAL
|
||||
expect(() => {
|
||||
Bun.spawn({
|
||||
cmd: ["/usr/bin/true"],
|
||||
sandbox: { seatbelt: "(invalid sbpl garbage)" },
|
||||
});
|
||||
}).toThrow("EINVAL: invalid argument, posix_spawn");
|
||||
});
|
||||
|
||||
test.if(isMac)("object format with profile property works", async () => {
|
||||
// Test the object format { profile: "..." }
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "object-format"],
|
||||
sandbox: { seatbelt: { profile: ALLOW_ALL_PROFILE } },
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("object-format\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isMac)("object format with profile and parameters works", async () => {
|
||||
// SBPL profile that uses param() function to get a value
|
||||
// This profile allows everything but demonstrates parameter passing
|
||||
const profileWithParams = `(version 1)
|
||||
(allow default)
|
||||
(deny file-write* (subpath (param "BLOCKED_PATH")))`;
|
||||
|
||||
// Test the object format with parameters
|
||||
// We block writes to /nonexistent which shouldn't affect /usr/bin/true
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["/usr/bin/true"],
|
||||
sandbox: {
|
||||
seatbelt: {
|
||||
profile: profileWithParams,
|
||||
parameters: { BLOCKED_PATH: "/nonexistent" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isMac)("spawnSync with profile and parameters works", () => {
|
||||
const profileWithParams = `(version 1)
|
||||
(allow default)
|
||||
(deny file-write* (subpath (param "BLOCKED_PATH")))`;
|
||||
|
||||
const result = Bun.spawnSync({
|
||||
cmd: ["/usr/bin/true"],
|
||||
sandbox: {
|
||||
seatbelt: {
|
||||
profile: profileWithParams,
|
||||
parameters: { BLOCKED_PATH: "/nonexistent" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isMac)("specifying both profile and namedProfile throws error", () => {
|
||||
expect(() => {
|
||||
Bun.spawn({
|
||||
cmd: ["/usr/bin/true"],
|
||||
sandbox: {
|
||||
seatbelt: {
|
||||
profile: ALLOW_ALL_PROFILE,
|
||||
namedProfile: "pfd",
|
||||
} as unknown as string,
|
||||
},
|
||||
});
|
||||
}).toThrow("sandbox.seatbelt cannot have both 'profile' and 'namedProfile'");
|
||||
});
|
||||
|
||||
test.if(isMac)("empty parameters object is allowed", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["echo", "empty-params"],
|
||||
sandbox: {
|
||||
seatbelt: {
|
||||
profile: ALLOW_ALL_PROFILE,
|
||||
parameters: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const stdout = await proc.stdout.text();
|
||||
expect(stdout).toBe("empty-params\n");
|
||||
expect(await proc.exited).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isMac)("seatbelt object without profile or namedProfile throws error", () => {
|
||||
expect(() => {
|
||||
Bun.spawn({
|
||||
cmd: ["/usr/bin/true"],
|
||||
sandbox: {
|
||||
seatbelt: { parameters: { KEY: "VALUE" } } as unknown as string,
|
||||
},
|
||||
});
|
||||
}).toThrow("Expected sandbox.seatbelt to be a object with 'profile' or 'namedProfile'");
|
||||
});
|
||||
|
||||
test.if(isMac)("namedProfile with non-existent profile throws EINVAL", () => {
|
||||
// A non-existent named profile should fail with EINVAL
|
||||
expect(() => {
|
||||
Bun.spawn({
|
||||
cmd: ["/usr/bin/true"],
|
||||
sandbox: {
|
||||
seatbelt: { namedProfile: "this-profile-does-not-exist-12345" },
|
||||
},
|
||||
});
|
||||
}).toThrow("EINVAL: invalid argument, posix_spawn");
|
||||
});
|
||||
|
||||
test.if(isMac)("namedProfile restricts process execution", () => {
|
||||
// First verify ls works without sandbox
|
||||
const withoutSandbox = Bun.spawnSync({
|
||||
cmd: ["/bin/ls", "/"],
|
||||
});
|
||||
expect(withoutSandbox.stdout.toString()).toContain("usr");
|
||||
expect(withoutSandbox.exitCode).toBe(0);
|
||||
|
||||
// The "pfd" (packet filter daemon) profile only allows executing /usr/libexec/pfd
|
||||
// So /bin/ls should fail with EPERM (Operation not permitted)
|
||||
// The sandbox blocks execve, which causes spawn to throw
|
||||
expect(() => {
|
||||
Bun.spawnSync({
|
||||
cmd: ["/bin/ls", "/"],
|
||||
sandbox: {
|
||||
seatbelt: { namedProfile: "pfd" },
|
||||
},
|
||||
});
|
||||
}).toThrow("EPERM: operation not permitted, posix_spawn");
|
||||
});
|
||||
|
||||
test.if(isMac)("namedProfile restricts file writes during execution", () => {
|
||||
using dir = tempDir("named-profile-test", {});
|
||||
const testFile = `${dir}/test.txt`;
|
||||
|
||||
// First verify file write works without sandbox
|
||||
const withoutSandbox = Bun.spawnSync({
|
||||
cmd: ["/bin/sh", "-c", `echo hello > "${testFile}" && rm "${testFile}"`],
|
||||
});
|
||||
expect(withoutSandbox.exitCode).toBe(0);
|
||||
|
||||
// The "quicklookd" profile allows exec and file reads, but blocks file writes
|
||||
// The process spawns successfully, but the write operation fails
|
||||
const withSandbox = Bun.spawnSync({
|
||||
cmd: ["/bin/sh", "-c", `echo hello > "${testFile}"`],
|
||||
sandbox: {
|
||||
seatbelt: { namedProfile: "quicklookd" },
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// The shell runs but write fails with "Operation not permitted", exit code 1
|
||||
expect(withSandbox.stderr.toString()).toContain("Operation not permitted");
|
||||
expect(withSandbox.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test.if(isMac)("SBPL parameters successfully block specified path", () => {
|
||||
// Create a temporary directory
|
||||
using dir = tempDir("sbpl-param-test", {});
|
||||
const testFile = `${dir}/test.txt`;
|
||||
|
||||
// SBPL profile that blocks writes to the path specified by parameter
|
||||
const profileWithParams = `(version 1)
|
||||
(allow default)
|
||||
(deny file-write* (subpath (param "BLOCKED_PATH")))`;
|
||||
|
||||
// First verify write works without sandbox
|
||||
const withoutSandbox = Bun.spawnSync({
|
||||
cmd: ["/bin/sh", "-c", `echo hello > "${testFile}" && rm "${testFile}"`],
|
||||
});
|
||||
expect(withoutSandbox.exitCode).toBe(0);
|
||||
|
||||
// With sandbox blocking the temp directory - should fail
|
||||
const withSandboxBlocking = Bun.spawnSync({
|
||||
cmd: ["/bin/sh", "-c", `echo hello > "${testFile}"`],
|
||||
sandbox: {
|
||||
seatbelt: {
|
||||
profile: profileWithParams,
|
||||
parameters: { BLOCKED_PATH: String(dir) },
|
||||
},
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
expect(withSandboxBlocking.stderr.toString()).toContain("Operation not permitted");
|
||||
expect(withSandboxBlocking.exitCode).toBe(1);
|
||||
|
||||
// With sandbox blocking a different path - should succeed
|
||||
const withSandboxAllowing = Bun.spawnSync({
|
||||
cmd: ["/bin/sh", "-c", `echo hello > "${testFile}" && rm "${testFile}"`],
|
||||
sandbox: {
|
||||
seatbelt: {
|
||||
profile: profileWithParams,
|
||||
parameters: { BLOCKED_PATH: "/nonexistent" },
|
||||
},
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
expect(withSandboxAllowing.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isMac)("SBPL profile with multiple parameters", () => {
|
||||
using dir1 = tempDir("sbpl-multi-param-1", {});
|
||||
using dir2 = tempDir("sbpl-multi-param-2", {});
|
||||
const testFile1 = `${dir1}/test.txt`;
|
||||
const testFile2 = `${dir2}/test.txt`;
|
||||
|
||||
// SBPL profile that blocks writes to two different paths via parameters
|
||||
const profileWithMultipleParams = `(version 1)
|
||||
(allow default)
|
||||
(deny file-write* (subpath (param "BLOCKED_PATH_1")))
|
||||
(deny file-write* (subpath (param "BLOCKED_PATH_2")))`;
|
||||
|
||||
// Both paths blocked - writes to dir1 should fail
|
||||
const result1 = Bun.spawnSync({
|
||||
cmd: ["/bin/sh", "-c", `echo hello > "${testFile1}"`],
|
||||
sandbox: {
|
||||
seatbelt: {
|
||||
profile: profileWithMultipleParams,
|
||||
parameters: {
|
||||
BLOCKED_PATH_1: String(dir1),
|
||||
BLOCKED_PATH_2: String(dir2),
|
||||
},
|
||||
},
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
expect(result1.stderr.toString()).toContain("Operation not permitted");
|
||||
expect(result1.exitCode).toBe(1);
|
||||
|
||||
// Both paths blocked - writes to dir2 should also fail
|
||||
const result2 = Bun.spawnSync({
|
||||
cmd: ["/bin/sh", "-c", `echo hello > "${testFile2}"`],
|
||||
sandbox: {
|
||||
seatbelt: {
|
||||
profile: profileWithMultipleParams,
|
||||
parameters: {
|
||||
BLOCKED_PATH_1: String(dir1),
|
||||
BLOCKED_PATH_2: String(dir2),
|
||||
},
|
||||
},
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
expect(result2.stderr.toString()).toContain("Operation not permitted");
|
||||
expect(result2.exitCode).toBe(1);
|
||||
|
||||
// Only dir1 blocked - writes to dir2 should succeed
|
||||
const result3 = Bun.spawnSync({
|
||||
cmd: ["/bin/sh", "-c", `echo hello > "${testFile2}" && cat "${testFile2}" && rm "${testFile2}"`],
|
||||
sandbox: {
|
||||
seatbelt: {
|
||||
profile: profileWithMultipleParams,
|
||||
parameters: {
|
||||
BLOCKED_PATH_1: String(dir1),
|
||||
BLOCKED_PATH_2: "/nonexistent",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result3.stdout.toString()).toBe("hello\n");
|
||||
expect(result3.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isMac)("async spawn with namedProfile blocking file writes", async () => {
|
||||
using dir = tempDir("async-named-profile-test", {});
|
||||
const testFile = `${dir}/test.txt`;
|
||||
|
||||
// First verify write works without sandbox
|
||||
await using procWithout = Bun.spawn({
|
||||
cmd: ["/bin/sh", "-c", `echo hello > "${testFile}" && rm "${testFile}"`],
|
||||
});
|
||||
expect(await procWithout.exited).toBe(0);
|
||||
|
||||
// The "quicklookd" profile blocks file writes
|
||||
await using procWith = Bun.spawn({
|
||||
cmd: ["/bin/sh", "-c", `echo hello > "${testFile}"`],
|
||||
sandbox: {
|
||||
seatbelt: { namedProfile: "quicklookd" },
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const stderr = await procWith.stderr.text();
|
||||
const exitCode = await procWith.exited;
|
||||
|
||||
expect(stderr).toContain("Operation not permitted");
|
||||
expect(exitCode).toBe(1);
|
||||
});
|
||||
|
||||
test.if(isMac)("async spawn with SBPL profile blocking writes", async () => {
|
||||
using dir = tempDir("async-sbpl-test", {});
|
||||
const testFile = `${dir}/test.txt`;
|
||||
|
||||
const profileWithParams = `(version 1)
|
||||
(allow default)
|
||||
(deny file-write* (subpath (param "BLOCKED_PATH")))`;
|
||||
|
||||
// Blocked write
|
||||
await using procBlocked = Bun.spawn({
|
||||
cmd: ["/bin/sh", "-c", `echo blocked > "${testFile}"`],
|
||||
sandbox: {
|
||||
seatbelt: {
|
||||
profile: profileWithParams,
|
||||
parameters: { BLOCKED_PATH: String(dir) },
|
||||
},
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const stderrBlocked = await procBlocked.stderr.text();
|
||||
const exitCodeBlocked = await procBlocked.exited;
|
||||
|
||||
expect(stderrBlocked).toContain("Operation not permitted");
|
||||
expect(exitCodeBlocked).toBe(1);
|
||||
|
||||
// Allowed write (different path blocked)
|
||||
await using procAllowed = Bun.spawn({
|
||||
cmd: ["/bin/sh", "-c", `echo allowed > "${testFile}" && cat "${testFile}" && rm "${testFile}"`],
|
||||
sandbox: {
|
||||
seatbelt: {
|
||||
profile: profileWithParams,
|
||||
parameters: { BLOCKED_PATH: "/nonexistent" },
|
||||
},
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const stdoutAllowed = await procAllowed.stdout.text();
|
||||
const exitCodeAllowed = await procAllowed.exited;
|
||||
|
||||
expect(stdoutAllowed).toBe("allowed\n");
|
||||
expect(exitCodeAllowed).toBe(0);
|
||||
});
|
||||
|
||||
test.if(isMac)("async spawn with deny-file-write profile blocks writes", async () => {
|
||||
using dir = tempDir("async-deny-write-test", {});
|
||||
const testFile = `${dir}/test.txt`;
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: ["/bin/sh", "-c", `echo test > "${testFile}"`],
|
||||
sandbox: { seatbelt: DENY_FILE_WRITE_PROFILE },
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const stderr = await proc.stderr.text();
|
||||
const exitCode = await proc.exited;
|
||||
|
||||
expect(stderr).toContain("Operation not permitted");
|
||||
expect(exitCode).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user