tiny.sandbox.bwrap
Defined in tiny.sandbox.
API (3)
Actions
Public operations.
Source
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.
tiny.sandbox.bwrap.audit[function] atlib/sandbox/src/bwrap.zig:108lib.sandbox.src.bwrap.buildArgv[function] — private source atlib/sandbox/src/bwrap.zig:222in nearest public ownertiny.sandbox.bwraplib.sandbox.src.bwrap.diffUpper[function] — private source atlib/sandbox/src/bwrap.zig:290in nearest public ownertiny.sandbox.bwraplib.sandbox.src.bwrap.realPathAlloc[function] — private source atlib/sandbox/src/bwrap.zig:282in nearest public ownertiny.sandbox.bwraplib.sandbox.src.bwrap.realPathFileAlloc[function] — private source atlib/sandbox/src/bwrap.zig:286in nearest public ownertiny.sandbox.bwraplib.sandbox.src.bwrap.repairWorkDir[function] — private source atlib/sandbox/src/bwrap.zig:327in nearest public ownertiny.sandbox.bwraptiny.sandbox.scan.capturePaths[function] atlib/sandbox/src/scan.zig:64
Audit
| Definitions | 4 |
|---|---|
| Public names | 4 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |