Skip to documentation
SLOP

tiny.sandbox.run

Reference tiny.sandbox run

Defined in tiny.sandbox.

API (3)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.sandboxrun
Static calls · unresolved targets: unknown · external targets: unknown.

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

Definitions1
Public names1
Members0
Version26.7.0
Revisiondaab053ee433