Skip to documentation
SLOP

tiny.sandbox.bwrap

Reference tiny.sandbox bwrap

Defined in tiny.sandbox.

API (3)

Actions

Public operations.

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

Source

Called byCallsbwrapexecutetest sourcelib.sandbox.src.bwraptest: bubblewrap network policy has e...private sourcelib.sandbox.src.runexpectedAuditprivate sourcelib.sandbox.src.bwrapnetworkAuditbwrapaudit
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstest sourcelib.sandbox.src.bwraptest: bubblewrap availability probe e...test sourcelib.sandbox.src.bwraptest: bubblewrap required network iso...test sourcelib.sandbox.src.bwraptest: bubblewrap timeout removes a re...test sourcelib.sandbox.src.runtest: run isolates a connected caller...test sourcelib.sandbox.src.runtest: run requires a proven bubblewra...private sourcelib.sandbox.src.bwrapprobebwrapavailable
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsprivate sourcelib.sandbox.src.bwrapexecuteOrSkipprivate sourcelib.sandbox.src.bwrapprobetest sourcelib.sandbox.src.bwraptest: bubblewrap required network iso...test sourcelib.sandbox.src.bwraptest: bubblewrap timeout removes a re...runexecutebwrapauditprivate sourcelib.sandbox.src.bwrapbuildArgvprivate sourcelib.sandbox.src.bwrapdiffUpperprivate sourcelib.sandbox.src.bwraprealPathAllocprivate sourcelib.sandbox.src.bwraprealPathFileAlloc+2 morebwrapexecute
Static calls · unresolved targets: 4 · external targets: 13.

Source: lib/sandbox/src/bwrap.zig

zig
const std = @import("std");const builtin = @import("builtin");const sys = @import("sys");const audit_data = @import("audit.zig");const change = @import("change.zig");const command = @import("command.zig");const cwd_data = @import("cwd.zig");const layer_data = @import("layer.zig");const plan_data = @import("plan.zig");const resolve = @import("resolve.zig");const result_data = @import("result.zig");const scan = @import("scan.zig");const Allocator = std.mem.Allocator;const fs_io = sys.fs.debugIo();const workspace_path = "/workspace";pub fn available(allocator: Allocator, scratch: sys.fs.Dir) bool {    if (comptime builtin.os.tag != .linux) return false;    return probe(allocator, scratch) catch false;}fn probe(allocator: Allocator, scratch: sys.fs.Dir) !bool {    var source = try layer_data.create(allocator, .{        .scratch = scratch,        .prefix = "bwrap-probe-source",        .max_file_bytes = 1024,    });    defer source.deinit();    try source.root.createDirPath(fs_io, "sub");    try source.root.writeFile(fs_io, .{ .sub_path = "sub/input.txt", .data = "input" });    try source.root.writeFile(fs_io, .{ .sub_path = "a.txt", .data = "old" });    try source.root.writeFile(fs_io, .{ .sub_path = "gone.txt", .data = "remove" });    const host_path = try probeHostPathAlloc(allocator);    defer allocator.free(host_path);    std.Io.Dir.deleteFileAbsolute(fs_io, host_path) catch {};    defer std.Io.Dir.deleteFileAbsolute(fs_io, host_path) catch {};    const command_text = try std.fmt.allocPrint(        allocator,        "test \"$(pwd)\" = /workspace/sub && cat input.txt > output.txt && printf changed > ../a.txt && printf created > ../new.txt && rm ../gone.txt && printf isolated > {s}",        .{host_path},    );    defer allocator.free(command_text);    const argv = [_][]const u8{ "bash", "-lc", command_text };    var result = execute(allocator, .{        .scratch = scratch,        .source = source.root,        .argv = argv[0..],        .cwd = "sub",        .runner = .bubblewrap_overlay,        .prefix = "bwrap-probe",        .timeout_ms = 2000,        .stdout_limit = 4096,        .stderr_limit = 4096,        .max_file_bytes = 1024,    }) catch return false;    defer result.deinit();    if (result.status.exitCode() != 0) return false;    if (result.audit.runner != .bubblewrap_overlay) return false;    if (result.changes.find("a.txt")) |entry| {        if (entry.operation != .put) return false;    } else return false;    if (result.changes.find("new.txt")) |entry| {        if (entry.operation != .put) return false;    } else return false;    if (result.changes.find("gone.txt")) |entry| {        if (entry.operation != .delete) return false;    } else return false;    if (result.changes.find("sub/output.txt")) |entry| {        if (entry.operation != .put) return false;    } else return false;    if (!try resultFileEquals(allocator, &result, "a.txt", "changed")) return false;    if (!try resultFileEquals(allocator, &result, "sub/output.txt", "input")) return false;    if (!try sourceFileEquals(allocator, source.root, "a.txt", "old")) return false;    if (!try sourceFileEquals(allocator, source.root, "gone.txt", "remove")) return false;    if (source.root.statFile(fs_io, "new.txt", .{})) |_| return false else |err| switch (err) {        error.FileNotFound => {},        else => return err,    }    if (std.Io.Dir.cwd().statFile(fs_io, host_path, .{})) |_| return false else |err| switch (err) {        error.FileNotFound => {},        else => return err,    }    return true;}fn probeHostPathAlloc(allocator: Allocator) Allocator.Error![]u8 {    const pid = sys.process.currentProcessId() catch 0;    const now = sys.time.nanoTimestamp();    return try std.fmt.allocPrint(allocator, "/tmp/tiny-sandbox-bwrap-probe-{x}-{x}", .{ pid, now });}fn resultFileEquals(allocator: Allocator, result: *const result_data.Result, path: []const u8, expected: []const u8) !bool {    const actual = try result.readFileAlloc(allocator, path, expected.len + 1);    defer allocator.free(actual);    return std.mem.eql(u8, actual, expected);}fn sourceFileEquals(allocator: Allocator, source: sys.fs.Dir, path: []const u8, expected: []const u8) !bool {    const actual = try source.readFileAlloc(fs_io, path, allocator, .limited(expected.len + 1));    defer allocator.free(actual);    return std.mem.eql(u8, actual, expected);}pub fn audit(plan: plan_data.Plan) audit_data.Audit {    return .{        .runner = .bubblewrap_overlay,        .filesystem = .overlay,        .process = .bubblewrap_namespace,        .network = networkAudit(plan),        .environment = if (plan.environ_map == null) .inherited else .replaced,    };}fn networkAudit(plan: plan_data.Plan) audit_data.Network {    if (comptime builtin.os.tag != .linux) return .inherited;    if (plan.policy.prefersNetworkIsolation()) return .bubblewrap_namespace;    return .inherited;}pub fn execute(allocator: Allocator, plan: plan_data.Plan) !result_data.Result {    if (plan.argv.len == 0) return error.EmptyArgv;    try cwd_data.validate(plan.cwd);    var layer = try layer_data.create(allocator, .{        .scratch = plan.scratch,        .prefix = plan.prefix,        .max_file_bytes = plan.max_file_bytes,    });    errdefer {        repairWorkDir(allocator, plan.scratch, layer.path);        layer.deinit();    }    var container = try plan.scratch.openDir(fs_io, layer.path, .{ .iterate = true });    defer container.close(fs_io);    try container.createDir(fs_io, "lower", .default_dir);    try container.createDir(fs_io, "work", .default_dir);    var lower_dir: sys.fs.Dir = if (plan.source) |source| source else try container.openDir(fs_io, "lower", .{ .iterate = true });    defer if (plan.source == null) lower_dir.close(fs_io);    if (!std.mem.eql(u8, plan.cwd, ".")) {        var opened_cwd = try lower_dir.openDir(fs_io, plan.cwd, .{});        defer opened_cwd.close(fs_io);        try cwd_data.ensureInside(lower_dir, opened_cwd);    }    var before = try scan.capturePaths(lower_dir, allocator);    defer before.deinit();    const lower_path = try realPathAlloc(allocator, lower_dir);    defer allocator.free(lower_path);    const upper_path = try realPathAlloc(allocator, layer.root);    defer allocator.free(upper_path);    const work_path = try realPathFileAlloc(allocator, container, "work");    defer allocator.free(work_path);    var bwrap_argv = try buildArgv(allocator, plan, lower_path, upper_path, work_path);    defer bwrap_argv.deinit();    var executed = try command.run(allocator, .{        .argv = bwrap_argv.items.items,        .cwd = .inherit,        .stdout_limit = plan.stdout_limit,        .stderr_limit = plan.stderr_limit,        .timeout_ms = plan.timeout_ms,        .interrupt = plan.interrupt,        .descriptor_policy = .isolated,    });    errdefer executed.deinit(allocator);    repairWorkDir(allocator, plan.scratch, layer.path);    var changes = try diffUpper(allocator, &before, layer.root);    errdefer changes.deinit();    const stdout = executed.stdout;    const stderr = executed.stderr;    executed.stdout = &.{};    executed.stderr = &.{};    return .{        .allocator = allocator,        .layer = layer,        .status = .{ .term = executed.term },        .stdout = stdout,        .stderr = stderr,        .changes = changes,        .audit = audit(plan),    };}const Argv = struct {    allocator: Allocator,    items: std.ArrayList([]const u8) = .empty,    owned: std.ArrayList([]u8) = .empty,    fn init(allocator: Allocator) Argv {        return .{ .allocator = allocator };    }    fn deinit(self: *Argv) void {        for (self.owned.items) |item| self.allocator.free(item);        self.owned.deinit(self.allocator);        self.items.deinit(self.allocator);        self.* = undefined;    }    fn append(self: *Argv, value: []const u8) Allocator.Error!void {        try self.items.append(self.allocator, value);    }    fn appendOwned(self: *Argv, value: []u8) Allocator.Error!void {        errdefer self.allocator.free(value);        try self.owned.append(self.allocator, value);        try self.items.append(self.allocator, value);    }};fn buildArgv(allocator: Allocator, plan: plan_data.Plan, lower_path: []const u8, upper_path: []const u8, work_path: []const u8) !Argv {    var argv = Argv.init(allocator);    errdefer argv.deinit();    try argv.append("bwrap");    try argv.append("--unshare-all");    if (networkAudit(plan) == .inherited) try argv.append("--share-net");    try argv.append("--die-with-parent");    try argv.append("--new-session");    try argv.append("--proc");    try argv.append("/proc");    try argv.append("--dev");    try argv.append("/dev");    try argv.append("--tmpfs");    try argv.append("/tmp");    try appendReadOnlyBind(&argv, "/usr");    try appendReadOnlyTryBind(&argv, "/bin");    try appendReadOnlyTryBind(&argv, "/lib");    try appendReadOnlyTryBind(&argv, "/lib64");    try appendReadOnlyTryBind(&argv, "/etc");    try argv.append("--dir");    try argv.append(workspace_path);    try argv.append("--overlay-src");    try argv.append(lower_path);    try argv.append("--overlay");    try argv.append(upper_path);    try argv.append(work_path);    try argv.append(workspace_path);    try argv.append("--chdir");    if (std.mem.eql(u8, plan.cwd, ".")) {        try argv.append(workspace_path);    } else {        try argv.appendOwned(try std.fmt.allocPrint(allocator, "{s}/{s}", .{ workspace_path, plan.cwd }));    }    if (plan.environ_map) |env| {        try argv.append("--clearenv");        var iterator = env.iterator();        while (iterator.next()) |entry| {            try argv.append("--setenv");            try argv.append(entry.key_ptr.*);            try argv.append(entry.value_ptr.*);        }    }    try argv.append("--");    for (plan.argv) |arg| try argv.append(arg);    return argv;}fn appendReadOnlyBind(argv: *Argv, path: []const u8) Allocator.Error!void {    try argv.append("--ro-bind");    try argv.append(path);    try argv.append(path);}fn appendReadOnlyTryBind(argv: *Argv, path: []const u8) Allocator.Error!void {    try argv.append("--ro-bind-try");    try argv.append(path);    try argv.append(path);}fn realPathAlloc(allocator: Allocator, dir: sys.fs.Dir) ![:0]u8 {    return try resolve.dirPathAllocZ(allocator, dir);}fn realPathFileAlloc(allocator: Allocator, dir: sys.fs.Dir, path: []const u8) ![:0]u8 {    return try dir.realPathFileAlloc(fs_io, path, allocator);}fn diffUpper(allocator: Allocator, lower: *const scan.Snapshot, upper: sys.fs.Dir) !change.Set {    var upper_snapshot = try scan.capturePaths(upper, allocator);    defer upper_snapshot.deinit();    var changes = change.Set.init(allocator);    errdefer changes.deinit();    for (upper_snapshot.entries.items) |entry| {        if (entry.kind == .other and isWhiteout(upper, entry.path)) {            try appendDeletes(&changes, lower, entry.path);            continue;        }        if (entry.kind == .directory and lower.find(entry.path) != null) continue;        try changes.append(.put, entry);    }    return changes;}fn isWhiteout(dir: sys.fs.Dir, path: []const u8) bool {    const stat = dir.statFile(fs_io, path, .{}) catch return false;    return stat.kind == .character_device or stat.kind == .whiteout;}fn appendDeletes(changes: *change.Set, lower: *const scan.Snapshot, path: []const u8) !void {    for (lower.entries.items) |entry| {        if (!isDeletedPath(path, entry.path)) continue;        if (changes.find(entry.path) != null) continue;        try changes.append(.delete, entry);    }}fn isDeletedPath(path: []const u8, candidate: []const u8) bool {    if (std.mem.eql(u8, path, candidate)) return true;    if (!std.mem.startsWith(u8, candidate, path)) return false;    if (candidate.len <= path.len) return false;    return candidate[path.len] == std.fs.path.sep;}fn repairWorkDir(allocator: Allocator, scratch: sys.fs.Dir, layer_path: []const u8) void {    const path = std.fmt.allocPrint(allocator, "{s}/work/work", .{layer_path}) catch return;    defer allocator.free(path);    scratch.setFilePermissions(fs_io, path, .default_dir, .{}) catch {};}fn executeOrSkip(allocator: Allocator, plan: plan_data.Plan) !result_data.Result {    return execute(allocator, plan) catch |err| switch (err) {        error.FileNotFound => return error.SkipZigTest,        else => return err,    };}const NetworkWitness = struct {    tcp: sys.net.Server,    dns: sys.net.Socket,    fn init() !NetworkWitness {        const loopback = try sys.net.Address.parseIp4("127.0.0.1", 0);        var tcp = try loopback.listen(.{            .backlog = 4,            .close_on_exec = true,        });        errdefer tcp.deinit();        const dns = try sys.net.udpDatagramSocket(.{ .close_on_exec = true });        errdefer sys.net.close(dns);        try sys.net.bindIp4(dns, sys.net.ip4Address(.{ 127, 0, 0, 1 }, 0));        return .{ .tcp = tcp, .dns = dns };    }    fn deinit(self: *NetworkWitness) void {        self.tcp.deinit();        sys.net.close(self.dns);        self.* = undefined;    }    fn tcpPort(self: *const NetworkWitness) u16 {        return self.tcp.listen_address.getPort();    }    fn dnsPort(self: *const NetworkWitness) !u16 {        return try sys.net.socketPort(self.dns);    }    fn expectTraffic(self: *NetworkWitness) !void {        try std.testing.expect(try sys.net.pollReadable(self.tcp.socket, 1000));        var accepted = try self.tcp.accept();        defer accepted.stream.close();        var connection: [1]u8 = undefined;        try std.testing.expectEqual(@as(usize, 1), try accepted.stream.read(&connection));        try std.testing.expectEqual(@as(u8, 'C'), connection[0]);        try std.testing.expect(try sys.net.pollReadable(self.dns, 1000));        var query: [512]u8 = undefined;        const received = try sys.net.recvFromIpAddress(self.dns, &query, 0);        try std.testing.expect(received.bytes >= 12);        try std.testing.expectEqualSlices(u8, &.{ 0x12, 0x34 }, query[0..2]);    }    fn expectNoTraffic(self: *const NetworkWitness, timeout_ms: i32) !void {        try std.testing.expect(!try sys.net.pollReadable(self.tcp.socket, timeout_ms));        try std.testing.expect(!try sys.net.pollReadable(self.dns, timeout_ms));    }};fn networkProbeCommandAlloc(    allocator: Allocator,    witness: *const NetworkWitness,) ![]u8 {    return try std.fmt.allocPrint(allocator,        \\tcp=0        \\exec 3<>/dev/tcp/127.0.0.1/{d} || tcp=$?        \\if test "$tcp" -eq 0; then printf C >&3; fi        \\dns=0        \\dns_query='\x12\x34\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00'        \\printf "$dns_query" > /dev/udp/127.0.0.1/{d} || dns=$?        \\printf ran > ran.txt        \\test "$tcp" -eq 0 && test "$dns" -eq 0    , .{ witness.tcpPort(), try witness.dnsPort() });}fn descendantCommandAlloc(allocator: Allocator, port: u16) ![]u8 {    return try std.fmt.allocPrint(allocator,        \\exec 3<>/dev/tcp/127.0.0.1/{d}        \\printf R >&3        \\exec 3>&-        \\exec 1>&- 2>&-        \\/usr/bin/setsid bash -c '        \\trap "" TERM HUP        \\exec 4<>/dev/tcp/127.0.0.1/{d}        \\printf D >&4        \\IFS= read -r -n 1 gate <&4        \\' &        \\wait    , .{ port, port });}fn expectTcpByte(server: *sys.net.Server, expected: u8) !void {    try std.testing.expect(try sys.net.pollReadable(server.socket, 1500));    var accepted = try server.accept();    defer accepted.stream.close();    var bytes: [1]u8 = undefined;    try std.testing.expectEqual(@as(usize, 1), try accepted.stream.read(&bytes));    try std.testing.expectEqual(expected, bytes[0]);}fn expectArgv(expected: []const []const u8, actual: []const []const u8) !void {    std.debug.assert(expected.len <= 64);    std.debug.assert(actual.len <= 64);    try std.testing.expectEqual(expected.len, actual.len);    for (expected, actual) |expected_arg, actual_arg| {        try std.testing.expectEqualStrings(expected_arg, actual_arg);    }}const inherited_argv = [_][]const u8{    "bwrap",         "--unshare-all", "--share-net",   "--die-with-parent",    "--new-session", "--proc",        "/proc",         "--dev",    "/dev",          "--tmpfs",       "/tmp",          "--ro-bind",    "/usr",          "/usr",          "--ro-bind-try", "/bin",    "/bin",          "--ro-bind-try", "/lib",          "/lib",    "--ro-bind-try", "/lib64",        "/lib64",        "--ro-bind-try",    "/etc",          "/etc",          "--dir",         "/workspace",    "--overlay-src", "lower",         "--overlay",     "upper",    "work",          "/workspace",    "--chdir",       "/workspace",    "--",            "bash",          "-c",            ":",};const isolated_argv = [_][]const u8{    "bwrap",         "--unshare-all", "--die-with-parent", "--new-session",    "--proc",        "/proc",         "--dev",             "/dev",    "--tmpfs",       "/tmp",          "--ro-bind",         "/usr",    "/usr",          "--ro-bind-try", "/bin",              "/bin",    "--ro-bind-try", "/lib",          "/lib",              "--ro-bind-try",    "/lib64",        "/lib64",        "--ro-bind-try",     "/etc",    "/etc",          "--dir",         "/workspace",        "--overlay-src",    "lower",         "--overlay",     "upper",             "work",    "/workspace",    "--chdir",       "/workspace",        "--",    "bash",          "-c",            ":",};test "bubblewrap availability probe exercises overlay cwd and namespace behavior" {    if (comptime builtin.os.tag != .linux) return error.SkipZigTest;    var scratch = std.testing.tmpDir(.{});    defer scratch.cleanup();    if (!available(std.testing.allocator, scratch.dir)) return error.SkipZigTest;}test "bubblewrap network policy has exact argv and audit" {    if (comptime builtin.os.tag != .linux) return error.SkipZigTest;    var scratch = std.testing.tmpDir(.{});    defer scratch.cleanup();    const command_argv = [_][]const u8{ "bash", "-c", ":" };    const base_plan = plan_data.Plan{        .scratch = scratch.dir,        .argv = &command_argv,        .runner = .bubblewrap_overlay,    };    var inherited = try buildArgv(std.testing.allocator, base_plan, "lower", "upper", "work");    defer inherited.deinit();    try expectArgv(&inherited_argv, inherited.items.items);    try std.testing.expectEqual(audit_data.Network.inherited, audit(base_plan).network);    var required_plan = base_plan;    required_plan.policy.network = .require_isolated;    var required = try buildArgv(std.testing.allocator, required_plan, "lower", "upper", "work");    defer required.deinit();    try expectArgv(&isolated_argv, required.items.items);    try std.testing.expectEqual(        audit_data.Network.bubblewrap_namespace,        audit(required_plan).network,    );    var preferred_plan = base_plan;    preferred_plan.policy.network = .prefer_isolated;    var preferred = try buildArgv(std.testing.allocator, preferred_plan, "lower", "upper", "work");    defer preferred.deinit();    try expectArgv(&isolated_argv, preferred.items.items);}test "bubblewrap required network isolation blocks host connections and DNS" {    if (comptime builtin.os.tag != .linux) return error.SkipZigTest;    var scratch = std.testing.tmpDir(.{});    defer scratch.cleanup();    if (!available(std.testing.allocator, scratch.dir)) return error.SkipZigTest;    var witness = NetworkWitness.init() catch |err| switch (err) {        error.UnsupportedPlatform => return error.SkipZigTest,        else => return err,    };    defer witness.deinit();    const command_text = try networkProbeCommandAlloc(std.testing.allocator, &witness);    defer std.testing.allocator.free(command_text);    const command_argv = [_][]const u8{ "bash", "-c", command_text };    {        var inherited = try execute(std.testing.allocator, .{            .scratch = scratch.dir,            .argv = &command_argv,            .runner = .bubblewrap_overlay,            .prefix = "bwrap-network-inherited",            .max_file_bytes = 1024,        });        defer inherited.deinit();        try std.testing.expectEqual(@as(i64, 0), inherited.status.exitCode().?);        try std.testing.expectEqual(audit_data.Network.inherited, inherited.audit.network);        try std.testing.expect(try resultFileEquals(            std.testing.allocator,            &inherited,            "ran.txt",            "ran",        ));        try witness.expectTraffic();    }    var isolated = try execute(std.testing.allocator, .{        .scratch = scratch.dir,        .argv = &command_argv,        .runner = .bubblewrap_overlay,        .policy = .{ .network = .require_isolated },        .prefix = "bwrap-network-isolated",        .max_file_bytes = 1024,    });    defer isolated.deinit();    try std.testing.expect(isolated.status.exitCode().? != 0);    try std.testing.expectEqual(        audit_data.Network.bubblewrap_namespace,        isolated.audit.network,    );    try std.testing.expect(try resultFileEquals(        std.testing.allocator,        &isolated,        "ran.txt",        "ran",    ));    try witness.expectNoTraffic(100);}test "bubblewrap timeout removes a resistant setsid descendant and disposable state" {    if (comptime builtin.os.tag != .linux) return error.SkipZigTest;    if (!sys.fs.absolutePathExists("/usr/bin/setsid")) return error.SkipZigTest;    var scratch = std.testing.tmpDir(.{});    defer scratch.cleanup();    if (!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 = 2,        .close_on_exec = true,    });    defer listener.deinit();    const command_text = try descendantCommandAlloc(        std.testing.allocator,        listener.listen_address.getPort(),    );    defer std.testing.allocator.free(command_text);    const command_argv = [_][]const u8{ "bash", "-c", command_text };    try std.testing.expectError(error.Timeout, execute(std.testing.allocator, .{        .scratch = scratch.dir,        .argv = &command_argv,        .runner = .bubblewrap_overlay,        .prefix = "bwrap-descendant-cleanup",        .timeout_ms = 1000,        .max_file_bytes = 1024,    }));    try expectTcpByte(&listener, 'R');    try std.testing.expect(try sys.net.pollReadable(listener.socket, 1500));    var descendant = try listener.accept();    defer descendant.stream.close();    var byte: [1]u8 = undefined;    try std.testing.expectEqual(@as(usize, 1), try descendant.stream.read(&byte));    try std.testing.expectEqual(@as(u8, 'D'), byte[0]);    try std.testing.expect(try sys.net.pollReadable(descendant.stream.handle, 1500));    try std.testing.expectEqual(@as(usize, 0), try descendant.stream.read(&byte));    var after = try scan.capturePaths(scratch.dir, std.testing.allocator);    defer after.deinit();    try std.testing.expectEqual(@as(usize, 0), after.entries.items.len);}test "bubblewrap overlay records puts deletes and preserves source" {    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 argv = [_][]const u8{ "bash", "-c", "printf changed > a.txt; rm gone.txt; printf new > new.txt" };    var result = try executeOrSkip(std.testing.allocator, .{        .scratch = scratch.dir,        .source = source.dir,        .argv = argv[0..],        .runner = .bubblewrap_overlay,        .prefix = "bwrap-test",        .max_file_bytes = 1024,    });    defer result.deinit();    try std.testing.expectEqual(audit_data.Runner.bubblewrap_overlay, result.audit.runner);    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 changed = try result.readFileAlloc(std.testing.allocator, "a.txt", 1024);    defer std.testing.allocator.free(changed);    try std.testing.expectEqualStrings("changed", changed);    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);}test "bubblewrap overlay tolerates lower files larger than the byte cap" {    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 = "0123456789abcdef0123456789abcdef" });    try source.dir.writeFile(fs_io, .{ .sub_path = "a.txt", .data = "old" });    const argv = [_][]const u8{ "bash", "-c", "printf changed > a.txt" };    var result = try executeOrSkip(std.testing.allocator, .{        .scratch = scratch.dir,        .source = source.dir,        .argv = argv[0..],        .runner = .bubblewrap_overlay,        .prefix = "bwrap-oversize-lower",        .max_file_bytes = 16,    });    defer result.deinit();    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.expect(result.changes.find("huge.bin") == null);    const changed = try result.readFileAlloc(std.testing.allocator, "a.txt", 1024);    defer std.testing.allocator.free(changed);    try std.testing.expectEqualStrings("changed", changed);}test "bubblewrap overlay records generated files larger than the byte cap" {    var source = std.testing.tmpDir(.{});    defer source.cleanup();    var scratch = std.testing.tmpDir(.{});    defer scratch.cleanup();    try source.dir.writeFile(fs_io, .{ .sub_path = "seed.txt", .data = "seed" });    const argv = [_][]const u8{ "bash", "-c", "printf '0123456789abcdef0123456789abcdef' > big.out" };    var result = try executeOrSkip(std.testing.allocator, .{        .scratch = scratch.dir,        .source = source.dir,        .argv = argv[0..],        .runner = .bubblewrap_overlay,        .prefix = "bwrap-oversize-generated",        .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("big.out").?.operation);    const generated = try result.readFileAlloc(std.testing.allocator, "big.out", 1024);    defer std.testing.allocator.free(generated);    try std.testing.expectEqualStrings("0123456789abcdef0123456789abcdef", generated);}test "bubblewrap overlay keeps absolute tmp writes out of the host" {    var scratch = std.testing.tmpDir(.{});    defer scratch.cleanup();    const host_path = "/tmp/tiny-sandbox-bwrap-host-write";    std.Io.Dir.deleteFileAbsolute(fs_io, host_path) catch {};    defer std.Io.Dir.deleteFileAbsolute(fs_io, host_path) catch {};    const argv = [_][]const u8{ "bash", "-c", "printf isolated > /tmp/tiny-sandbox-bwrap-host-write" };    var result = try executeOrSkip(std.testing.allocator, .{        .scratch = scratch.dir,        .argv = argv[0..],        .runner = .bubblewrap_overlay,        .prefix = "bwrap-host-write",        .max_file_bytes = 1024,    });    defer result.deinit();    try std.testing.expectEqual(@as(i64, 0), result.status.exitCode().?);    try std.testing.expectError(error.FileNotFound, std.Io.Dir.cwd().statFile(fs_io, host_path, .{}));}test "bubblewrap overlay expands directory whiteout deletes" {    var source = std.testing.tmpDir(.{});    defer source.cleanup();    var scratch = std.testing.tmpDir(.{});    defer scratch.cleanup();    try source.dir.createDirPath(fs_io, "dir");    try source.dir.writeFile(fs_io, .{ .sub_path = "dir/nested.txt", .data = "nested" });    const argv = [_][]const u8{ "bash", "-c", "rm -r dir" };    var result = try executeOrSkip(std.testing.allocator, .{        .scratch = scratch.dir,        .source = source.dir,        .argv = argv[0..],        .runner = .bubblewrap_overlay,        .prefix = "bwrap-dir-delete",        .max_file_bytes = 1024,    });    defer result.deinit();    try std.testing.expectEqual(change.Operation.delete, result.changes.find("dir").?.operation);    try std.testing.expectEqual(change.Operation.delete, result.changes.find("dir/nested.txt").?.operation);}

Source: lib/sandbox/src/root.zig:30

zig
pub const bwrap = @import("bwrap.zig");

Complete call list for bwrap.execute

7 direct calls.

Audit

Definitions4
Public names4
Members0
Version26.7.0
Revisiondaab053ee433