tiny.sandbox.run
Defined in tiny.sandbox.
API (3)
Actions
Public operations.
execute: Executes a run plan within a disposable layer directory, returning aResultthat owns captured standard streams, the detected change set, anAuditrecord, and the layer directory handle.
Types and contracts
Public types and contracts.
Result: Holds the resources and captured outputs of an execution, owning the standard output and standard error buffers, the recorded change set, the configurationAudit, and the layer directory.Status
Source
Source: lib/sandbox/src/root.zig:42
zig
pub const run = @import("run.zig");Source: lib/sandbox/src/run.zig
zig
const std = @import("std");const builtin = @import("builtin");const sys = @import("sys");const audit_data = @import("audit.zig");const bwrap = @import("bwrap.zig");const copied = @import("copied.zig");const change = @import("change.zig");const macos = @import("macos.zig");const plan_data = @import("plan.zig");const result_data = @import("result.zig");const windows = @import("windows.zig");const Allocator = std.mem.Allocator;const fs_io = sys.fs.debugIo();pub const Status = @import("command.zig").Status;pub const Result = result_data.Result;/// Executes a run plan within a disposable layer directory, returning a/// `Result` that owns captured standard streams, the detected change set, an/// `Audit` record, and the layer directory handle.////// Before launching the child process, `execute` checks the selected runner/// capabilities against the policy requested in `plan`, returning an error if a/// required setting is unsupported. A child process that exits with a nonzero/// status code produces a successful `Result` carrying that status. The error/// union instead covers refused policies along with host setup, execution, and/// data collection failures.////// Callers inspect changes to the layer through the change set. Each entry can/// describe a file, directory, or symbolic link, recording either a path to/// create or replace using the `put` operation or a path to remove using the/// `delete` operation. The library reads the source to prepare the layer and/// reports changes, but it does not automatically apply changes back to the/// source. An unconfined child may independently write external paths on the/// host.pub fn execute(allocator: Allocator, plan: plan_data.Plan) !Result { try plan.policy.validate(expectedAudit(plan)); return switch (plan.runner) { .staging => executeStaging(allocator, plan), .bubblewrap_overlay => bwrap.execute(allocator, plan), .macos_staging => macos.execute(allocator, plan), .windows_staging => windows.execute(allocator, plan), };}fn expectedAudit(plan: plan_data.Plan) audit_data.Audit { return switch (plan.runner) { .staging => stagingAudit(plan), .bubblewrap_overlay => bwrap.audit(plan), .macos_staging => macos.audit(plan), .windows_staging => windows.audit(plan), };}fn stagingAudit(plan: plan_data.Plan) audit_data.Audit { return .{ .environment = if (plan.environ_map == null) .inherited else .replaced };}fn executeStaging(allocator: Allocator, plan: plan_data.Plan) !Result { return try copied.execute(allocator, plan, stagingAudit(plan));}fn scratchEntryCount(dir: sys.fs.Dir, allocator: Allocator) !usize { var opened = try dir.openDir(fs_io, ".", .{ .iterate = true }); defer opened.close(fs_io); var walker = try opened.walk(allocator); defer walker.deinit(); var count: usize = 0; while (try walker.next(fs_io)) |_| count += 1; return count;}fn expectSocketByte(socket: sys.net.Socket, expected: u8) !void { try std.testing.expect(try sys.net.pollReadable(socket, 1000)); var byte: [1]u8 = undefined; try std.testing.expectEqual( @as(usize, 1), try sys.net.recv(socket, &byte, 0), ); try std.testing.expectEqual(expected, byte[0]);}const ShellCommand = struct { argv: [3][]const u8, fn slice(self: *const ShellCommand) []const []const u8 { return self.argv[0..]; }};fn hostShellCommand(posix: []const u8, windows_command: []const u8) ShellCommand { if (comptime builtin.os.tag == .windows) return .{ .argv = .{ "cmd.exe", "/C", windows_command } }; return .{ .argv = .{ "sh", "-c", posix } };}test "run executes in disposable layer and records file changes" { var source = std.testing.tmpDir(.{}); defer source.cleanup(); var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); try source.dir.writeFile(fs_io, .{ .sub_path = "a.txt", .data = "old" }); try source.dir.writeFile(fs_io, .{ .sub_path = "gone.txt", .data = "remove" }); const command = hostShellCommand( "printf hello; printf changed > a.txt; printf new > new.txt; rm gone.txt", "echo hello&&echo changed>a.txt&&echo new>new.txt&&del gone.txt", ); var result = try execute(std.testing.allocator, .{ .scratch = scratch.dir, .source = source.dir, .argv = command.slice(), .prefix = "run-test", .max_file_bytes = 1024, }); defer result.deinit(); try std.testing.expect(std.mem.startsWith(u8, result.stdout, "hello")); try std.testing.expectEqual(@as(i64, 0), result.status.exitCode().?); try std.testing.expectEqual(change.Operation.put, result.changes.find("a.txt").?.operation); try std.testing.expectEqual(change.Operation.put, result.changes.find("new.txt").?.operation); try std.testing.expectEqual(change.Operation.delete, result.changes.find("gone.txt").?.operation); const staged = try result.readFileAlloc(std.testing.allocator, "a.txt", 1024); defer std.testing.allocator.free(staged); try std.testing.expectEqualStrings("changed", staged); const original = try source.dir.readFileAlloc(fs_io, "a.txt", std.testing.allocator, .limited(1024)); defer std.testing.allocator.free(original); try std.testing.expectEqualStrings("old", original); const gone = try source.dir.readFileAlloc(fs_io, "gone.txt", std.testing.allocator, .limited(1024)); defer std.testing.allocator.free(gone); try std.testing.expectEqualStrings("remove", gone); try std.testing.expectError(error.FileNotFound, source.dir.statFile(fs_io, "new.txt", .{}));}test "run records generated symlink as filesystem delta" { if (comptime builtin.os.tag == .windows) return error.SkipZigTest; var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); const command = hostShellCommand( "printf target > target.txt; ln -s target.txt link", "", ); var result = execute(std.testing.allocator, .{ .scratch = scratch.dir, .argv = command.slice(), .prefix = "run-link", .max_file_bytes = 1024, }) catch |err| switch (err) { error.AccessDenied, error.PermissionDenied => return error.SkipZigTest, else => return err, }; defer result.deinit(); const link = result.changes.find("link").?; try std.testing.expectEqual(change.Operation.put, link.operation); try std.testing.expectEqual(change.Kind.sym_link, link.entry.kind); try std.testing.expectEqualStrings("target.txt", link.entry.target);}test "run returns nonzero status without discarding filesystem changes" { var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); const command = hostShellCommand( "printf err >&2; printf kept > kept.txt; exit 7", "echo err 1>&2&&echo kept>kept.txt&&exit /B 7", ); var result = try execute(std.testing.allocator, .{ .scratch = scratch.dir, .argv = command.slice(), .prefix = "nonzero-test", .max_file_bytes = 1024, }); defer result.deinit(); try std.testing.expectEqual(@as(i64, 7), result.status.exitCode().?); try std.testing.expect(std.mem.startsWith(u8, result.stderr, "err")); try std.testing.expectEqual(change.Operation.put, result.changes.find("kept.txt").?.operation);}test "run honors relative command cwd inside disposable layer" { var source = std.testing.tmpDir(.{}); defer source.cleanup(); var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); try source.dir.createDirPath(fs_io, "sub"); try source.dir.writeFile(fs_io, .{ .sub_path = "sub/input.txt", .data = "input" }); const command = hostShellCommand( "cat input.txt > output.txt", "type input.txt > output.txt", ); var result = try execute(std.testing.allocator, .{ .scratch = scratch.dir, .source = source.dir, .argv = command.slice(), .cwd = "sub", .prefix = "cwd-test", .max_file_bytes = 1024, }); defer result.deinit(); try std.testing.expectEqual(change.Operation.put, result.changes.find("sub/output.txt").?.operation); const output = try result.readFileAlloc(std.testing.allocator, "sub/output.txt", 1024); defer std.testing.allocator.free(output); try std.testing.expectEqualStrings("input", output);}test "run rejects cwd paths outside the disposable layer" { var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); const argv = [_][]const u8{ "bash", "-c", ":" }; try std.testing.expectError(error.AbsoluteCwd, execute(std.testing.allocator, .{ .scratch = scratch.dir, .argv = argv[0..], .cwd = "/tmp", .prefix = "cwd-absolute", })); try std.testing.expectError(error.CwdEscapesLayer, execute(std.testing.allocator, .{ .scratch = scratch.dir, .argv = argv[0..], .cwd = "../outside", .prefix = "cwd-parent", })); try std.testing.expectEqual(@as(usize, 0), try scratchEntryCount(scratch.dir, std.testing.allocator));}test "run rejects cwd symlinks outside the disposable layer" { var source = std.testing.tmpDir(.{}); defer source.cleanup(); var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); source.dir.symLink(fs_io, "/tmp", "outside", .{ .is_directory = true }) catch |err| switch (err) { error.AccessDenied, error.PermissionDenied => return error.SkipZigTest, else => return err, }; const argv = [_][]const u8{ "bash", "-c", ":" }; try std.testing.expectError(error.CwdEscapesLayer, execute(std.testing.allocator, .{ .scratch = scratch.dir, .source = source.dir, .argv = argv[0..], .cwd = "outside", .prefix = "cwd-symlink", })); try std.testing.expectEqual(@as(usize, 0), try scratchEntryCount(scratch.dir, std.testing.allocator));}test "run can execute a materialized relative program" { if (comptime !sys.fs.FilePermissions.has_executable_bit) return error.SkipZigTest; var source = std.testing.tmpDir(.{}); defer source.cleanup(); var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); try source.dir.writeFile(fs_io, .{ .sub_path = "tool.sh", .data = "#!/bin/sh\nprintf script > script.out\n" }); try source.dir.setFilePermissions(fs_io, "tool.sh", .executable_file, .{}); const argv = [_][]const u8{"./tool.sh"}; var result = try execute(std.testing.allocator, .{ .scratch = scratch.dir, .source = source.dir, .argv = argv[0..], .prefix = "relative-program", .max_file_bytes = 1024, }); defer result.deinit(); try std.testing.expectEqual(@as(i64, 0), result.status.exitCode().?); try std.testing.expectEqual(change.Operation.put, result.changes.find("script.out").?.operation);}test "run can replace the child environment" { var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); var env = sys.process.Environ.Map.init(std.testing.allocator); defer env.deinit(); try env.put("SANDBOX_ENV_TEST", "present"); try env.put("HOME", "/sandbox-home"); const command = hostShellCommand( "printf '%s:%s' \"$SANDBOX_ENV_TEST\" \"$HOME\"", "echo %SANDBOX_ENV_TEST%:%HOME%", ); var result = try execute(std.testing.allocator, .{ .scratch = scratch.dir, .argv = command.slice(), .environ_map = &env, .prefix = "env-test", .max_file_bytes = 1024, }); defer result.deinit(); try std.testing.expect(std.mem.startsWith(u8, result.stdout, "present:/sandbox-home")); try std.testing.expectEqual(audit_data.Environment.replaced, result.audit.environment);}test "run rejects unmet process isolation before command execution" { var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); const argv = [_][]const u8{ "bash", "-c", "printf ran > touched.txt" }; try std.testing.expectError(error.UnmetProcessIsolation, execute(std.testing.allocator, .{ .scratch = scratch.dir, .argv = argv[0..], .policy = .{ .process = .require_isolated }, .prefix = "policy-process", .max_file_bytes = 1024, })); try std.testing.expectEqual(@as(usize, 0), try scratchEntryCount(scratch.dir, std.testing.allocator));}test "run rejects unmet network isolation before command execution" { var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); const argv = [_][]const u8{ "bash", "-c", "printf ran > touched.txt" }; try std.testing.expectError(error.UnmetNetworkIsolation, execute(std.testing.allocator, .{ .scratch = scratch.dir, .argv = argv[0..], .policy = .{ .network = .require_isolated }, .prefix = "policy-network", .max_file_bytes = 1024, })); try std.testing.expectEqual( @as(usize, 0), try scratchEntryCount(scratch.dir, std.testing.allocator), );}test "run requires a proven bubblewrap network namespace" { var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); const argv = [_][]const u8{ "bash", "-c", "printf ran > touched.txt" }; const plan = plan_data.Plan{ .scratch = scratch.dir, .argv = &argv, .runner = .bubblewrap_overlay, .policy = .{ .network = .require_isolated }, .prefix = "policy-network-bwrap", .max_file_bytes = 1024, }; if (comptime builtin.os.tag != .linux) { try std.testing.expectError( error.UnmetNetworkIsolation, execute(std.testing.allocator, plan), ); return; } if (!bwrap.available(std.testing.allocator, scratch.dir)) return error.SkipZigTest; var result = try execute(std.testing.allocator, plan); defer result.deinit(); try std.testing.expectEqual(@as(i64, 0), result.status.exitCode().?); try std.testing.expectEqual( audit_data.Network.bubblewrap_namespace, result.audit.network, ); try std.testing.expectEqual( change.Operation.put, result.changes.find("touched.txt").?.operation, );}test "run isolates a connected caller descriptor without mutating its owner" { if (comptime builtin.os.tag != .linux) return error.SkipZigTest; var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); if (!bwrap.available(std.testing.allocator, scratch.dir)) return error.SkipZigTest; const loopback = try sys.net.Address.parseIp4("127.0.0.1", 0); var listener = try loopback.listen(.{ .backlog = 1, .close_on_exec = true, }); defer listener.deinit(); const client = try sys.net.tcpStreamSocket(.{ .close_on_exec = false }); defer sys.net.close(client); try sys.fd.setInheritOnExec(client); try sys.net.connect( client, listener.listen_address.socketAddress(), listener.listen_address.socketAddressLen(), ); var peer = try listener.accept(); defer peer.stream.close(); try std.testing.expect(client > 2); try std.testing.expect(!try sys.fd.closeOnExec(client)); try std.testing.expect(try sys.fd.closeOnExec(listener.socket)); try std.testing.expect(try sys.fd.closeOnExec(peer.stream.handle)); const script = try std.fmt.allocPrint( std.testing.allocator, "printf C 2>/dev/null >&{d} || true; printf ran > ran.txt", .{client}, ); defer std.testing.allocator.free(script); const argv = [_][]const u8{ "bash", "-c", script }; { var anchor = try execute(std.testing.allocator, .{ .scratch = scratch.dir, .argv = &argv, .prefix = "run-descriptor-anchor", .max_file_bytes = 1024, }); defer anchor.deinit(); try std.testing.expectEqual(@as(i64, 0), anchor.status.exitCode().?); try expectSocketByte(peer.stream.handle, 'C'); try std.testing.expect(!try sys.net.pollReadable(peer.stream.handle, 0)); } var isolated = try execute(std.testing.allocator, .{ .scratch = scratch.dir, .argv = &argv, .runner = .bubblewrap_overlay, .policy = .{ .network = .require_isolated }, .prefix = "run-descriptor-isolated", .max_file_bytes = 1024, }); defer isolated.deinit(); try std.testing.expectEqual(@as(i64, 0), isolated.status.exitCode().?); try std.testing.expectEqual( audit_data.Network.bubblewrap_namespace, isolated.audit.network, ); const ran = try isolated.readFileAlloc(std.testing.allocator, "ran.txt", 4); defer std.testing.allocator.free(ran); try std.testing.expectEqualStrings("ran", ran); try std.testing.expect(!try sys.net.pollReadable(peer.stream.handle, 100)); try std.testing.expect(sys.fd.isOpen(client)); try std.testing.expect(!try sys.fd.closeOnExec(client)); try std.testing.expectEqual( @as(usize, 1), try sys.net.sendNoSignal(client, "P"), ); try expectSocketByte(peer.stream.handle, 'P'); try peer.stream.writeAll("Q"); try expectSocketByte(client, 'Q');}test "run cleans disposable layer after output limit failure" { var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); const command = hostShellCommand( "printf abcdef", "echo abcdef", ); try std.testing.expectError(error.StdoutStreamTooLong, execute(std.testing.allocator, .{ .scratch = scratch.dir, .argv = command.slice(), .stdout_limit = 2, .stderr_limit = 1024, .prefix = "limit-test", })); try std.testing.expectEqual(@as(usize, 0), try scratchEntryCount(scratch.dir, std.testing.allocator));}test "run cleans writes one level above the materialized root" { var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); const command = hostShellCommand( "printf outside > ../outside.txt", "echo outside>..\\outside.txt", ); var result = try execute(std.testing.allocator, .{ .scratch = scratch.dir, .argv = command.slice(), .prefix = "parent-write", .max_file_bytes = 1024, }); try std.testing.expect(result.changes.find("outside.txt") == null); result.deinit(); try std.testing.expectEqual(@as(usize, 0), try scratchEntryCount(scratch.dir, std.testing.allocator));}test "run cleans disposable layer after timeout" { var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); const command = hostShellCommand( "sleep 2", "ping -n 3 127.0.0.1 >NUL", ); try std.testing.expectError(error.Timeout, execute(std.testing.allocator, .{ .scratch = scratch.dir, .argv = command.slice(), .timeout_ms = 1, .prefix = "timeout-test", })); try std.testing.expectEqual(@as(usize, 0), try scratchEntryCount(scratch.dir, std.testing.allocator));}test "run skips oversized source files instead of failing staging" { var source = std.testing.tmpDir(.{}); defer source.cleanup(); var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); try source.dir.writeFile(fs_io, .{ .sub_path = "huge.bin", .data = "0123456789abcdef" }); try source.dir.writeFile(fs_io, .{ .sub_path = "small.txt", .data = "ok" }); const command = hostShellCommand( "cat small.txt > echoed.txt", "type small.txt > echoed.txt", ); var result = try execute(std.testing.allocator, .{ .scratch = scratch.dir, .source = source.dir, .argv = command.slice(), .prefix = "skip-oversize", .max_file_bytes = 8, }); defer result.deinit(); try std.testing.expectEqual(@as(i64, 0), result.status.exitCode().?); try std.testing.expectEqual(change.Operation.put, result.changes.find("echoed.txt").?.operation); const echoed = try result.readFileAlloc(std.testing.allocator, "echoed.txt", 1024); defer std.testing.allocator.free(echoed); try std.testing.expect(std.mem.startsWith(u8, echoed, "ok"));}test "run records generated files regardless of the source file limit" { var scratch = std.testing.tmpDir(.{}); defer scratch.cleanup(); const command = hostShellCommand( "printf abcdef > huge.txt", "echo abcdef>huge.txt", ); var result = try execute(std.testing.allocator, .{ .scratch = scratch.dir, .argv = command.slice(), .prefix = "size-test", .max_file_bytes = 2, }); defer result.deinit(); try std.testing.expectEqual(change.Operation.put, result.changes.find("huge.txt").?.operation); const generated = try result.readFileAlloc(std.testing.allocator, "huge.txt", 1024); defer std.testing.allocator.free(generated); try std.testing.expect(std.mem.startsWith(u8, generated, "abcdef"));}Audit
| Definitions | 1 |
|---|---|
| Public names | 1 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |