Skip to documentation
SLOP

tiny.sandbox.confined

Reference tiny.sandbox confined

Defined in tiny.sandbox.

API (11)

Actions

Public operations.

Types and contracts

Public types and contracts.

Values and defaults

Public values and defaults.

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

Source

Called byCallstest sourcelib.sandbox.src.confinedtest: confined arguments expose only ...test sourcelib.sandbox.src.confinedtest: confined private command path b...private sourcelib.sandbox.src.confinedargumentsprivate sourcelib.sandbox.src.confinedvalidateconfinedlaunch
Static calls · unresolved targets: 0 · external targets: 2.
Called byCallsprivate sourcelib.sandbox.src.confinedvalidateprivate sourcelib.sandbox.src.confinedtextconfinedvalidateEnvironment
Static calls · unresolved targets: 0 · external targets: 0.

Source: lib/sandbox/src/confined.zig

zig
const std = @import("std");pub const arguments_bytes_max = 64 * 1024;pub const runtime_paths_max = 128;pub const environment_max = 32;pub const environment_bytes_max = 64 * 1024;pub const Variable = struct {    name: []const u8,    value: []const u8,};pub const Command = struct {    name: []const u8,    path: []const u8,};/// Describes the inputs, output directory, command arguments, and environment/// for a single Bubblewrap launch. Calling `launch` uses this description to/// construct argument strings and standard input configuration bytes, but does/// not run the command itself.////// Both `source` and `output` must be absolute paths. When configured,/// Bubblewrap binds `source` read-only at `/workspace`, binds `output` writable/// at `/output`, and sets `/workspace` as the working directory. The plan also/// accepts between 1 and 128 top-level `/nix/store` entries mounted read-only/// at their host paths. Validation verifies string formatting for these Nix/// store paths without checking host file existence or dependency closures./// Standard configuration adds `/proc`, `/dev`, and a temporary `/tmp` mount,/// so child visibility is not restricted to declared read-only inputs.////// Execution clears inherited environment variables and sets only explicitly/// declared variables. The plan requests unshared Linux namespaces, a new/// process session, and parent-death termination so the command dies with the/// parent process.////// String slices in `Plan` are borrowed from the caller. The returned `Launch`/// copies paths and environment variables into its standard input buffer, but/// borrows the command argument strings directly. Callers must keep those/// argument strings alive as long as the resulting `Launch` is in use.pub const Plan = struct {    source: []const u8,    output: []const u8,    runtime: []const []const u8,    environment: []const Variable,    argv: []const []const u8,    commands: []const Command = &.{},};pub const Launch = struct {    argv: []const []const u8,    stdin: []const u8,    pub fn deinit(self: *Launch, allocator: std.mem.Allocator) void {        allocator.free(self.argv);        allocator.free(self.stdin);        self.* = undefined;    }};/// Builds the command arguments and standard input configuration buffer for/// Bubblewrap, keeping child arguments separate from Bubblewrap option parsing.////// The resulting argument array begins with `launcher`, `--args`, `0`, and/// `--`, followed by the child command arguments. Configuration options pass as/// NUL-separated bytes over standard input capped at 64 KiB, ensuring child/// arguments matching Bubblewrap flag syntax are never interpreted as launcher/// options.////// Before allocating the argument array, structural validation confirms that/// `source` and `output` are absolute, runtime Nix store entries number from 1/// to 128, environment variables satisfy size limits, and command arguments/// contain between 1 and 319 valid strings. Later serialization steps check/// path text formatting and verify that total option bytes fit within the 64/// KiB limit. When serialization or allocation fails, the function frees any/// partial allocations and returns the error.////// The returned `Launch` owns the argument array and the serialized option/// bytes, both freed by calling `deinit` with `allocator`. Individual argument/// strings for the launcher and child command remain borrowed from the caller/// and must outlive the `Launch` instance.pub fn launch(allocator: std.mem.Allocator, launcher: []const u8, plan: Plan) !Launch {    try validate(plan);    const argv = try allocator.alloc([]const u8, plan.argv.len + 4);    errdefer allocator.free(argv);    @memcpy(argv[0..4], &[_][]const u8{ launcher, "--args", "0", "--" });    @memcpy(argv[4..], plan.argv);    return .{ .argv = argv, .stdin = try arguments(allocator, plan) };}fn arguments(allocator: std.mem.Allocator, plan: Plan) ![]u8 {    try validate(plan);    var bytes: [arguments_bytes_max]u8 = undefined;    var writer = std.Io.Writer.fixed(&bytes);    try append(&writer, &.{ "--unshare-all", "--die-with-parent", "--new-session", "--clearenv" });    for (plan.runtime) |path| try append(&writer, &.{ "--ro-bind", path, path });    try append(&writer, &.{        "--proc",    "/proc",      "--dev",      "/dev",   "--tmpfs",   "/tmp",        "--ro-bind", plan.source,  "/workspace", "--bind", plan.output, "/output",        "--chdir",   "/workspace",    });    if (plan.commands.len != 0) {        try append(&writer, &.{ "--dir", "/tmp/tiny-bin" });        for (plan.commands) |command| {            const destination = try std.fmt.allocPrint(allocator, "/tmp/tiny-bin/{s}", .{command.name});            defer allocator.free(destination);            try append(&writer, &.{ "--ro-bind", command.path, destination });        }    }    for (plan.environment) |variable| {        try append(&writer, &.{ "--setenv", variable.name, variable.value });    }    return allocator.dupe(u8, writer.buffered());}fn append(writer: *std.Io.Writer, values: []const []const u8) !void {    for (values) |value| {        try text(value);        try writer.writeAll(value);        try writer.writeByte(0);    }}fn validate(plan: Plan) !void {    if (!std.fs.path.isAbsolute(plan.source) or !std.fs.path.isAbsolute(plan.output)) {        return error.InvalidConfinementPath;    }    if (plan.runtime.len == 0 or plan.runtime.len > runtime_paths_max) {        return error.ConfinementRuntimeCapacityExceeded;    }    try validateEnvironment(plan.environment);    if (plan.argv.len == 0 or plan.argv.len > 319) return error.InvalidConfinementArguments;    for (plan.argv) |argument| try text(argument);    for (plan.runtime) |path| {        if (!std.mem.startsWith(u8, path, "/nix/store/") or            std.mem.indexOfScalar(u8, path[11..], '/') != null or path.len < 45)        {            return error.InvalidConfinementRuntime;        }    }    if (plan.commands.len > 16) return error.ConfinementRuntimeCapacityExceeded;    for (plan.commands, 0..) |command, index| {        if (command.name.len == 0 or !std.ascii.isAlphabetic(command.name[0]))            return error.InvalidConfinementCommand;        for (command.name) |byte| {            if (!std.ascii.isAlphanumeric(byte) and byte != '-' and byte != '_')                return error.InvalidConfinementCommand;        }        if (!std.fs.path.isAbsolute(command.path)) return error.InvalidConfinementCommand;        for (plan.commands[0..index]) |prior| {            if (std.mem.eql(u8, prior.name, command.name)) return error.InvalidConfinementCommand;        }    }}/// Validates declared environment variables against structural and encoding/// constraints before a caller copies them for launch.////// The slice must contain at most 32 variables, with the combined length of all/// name and value bytes capped at 64 KiB. Exceeding either boundary returns/// `error.ConfinementEnvironmentCapacityExceeded`. Variable names must not be/// empty, contain `=`, or repeat across the slice, or validation fails with/// `error.InvalidConfinementEnvironment`. Any NUL byte or invalid UTF-8/// sequence in a name or value returns `error.InvalidConfinementText`.////// Because the 64 KiB check measures only raw name and value bytes without/// option framing overhead, a valid environment is not guaranteed to fit inside/// the final launch option buffer.pub fn validateEnvironment(environment: []const Variable) !void {    if (environment.len > environment_max) return error.ConfinementEnvironmentCapacityExceeded;    var total: usize = 0;    for (environment, 0..) |variable, index| {        try text(variable.name);        try text(variable.value);        total = std.math.add(usize, total, variable.name.len) catch            return error.ConfinementEnvironmentCapacityExceeded;        total = std.math.add(usize, total, variable.value.len) catch            return error.ConfinementEnvironmentCapacityExceeded;        if (total > environment_bytes_max) return error.ConfinementEnvironmentCapacityExceeded;        if (variable.name.len == 0 or std.mem.indexOfScalar(u8, variable.name, '=') != null) {            return error.InvalidConfinementEnvironment;        }        for (environment[0..index]) |prior| {            if (std.mem.eql(u8, prior.name, variable.name)) {                return error.InvalidConfinementEnvironment;            }        }    }}fn text(value: []const u8) !void {    if (std.mem.indexOfScalar(u8, value, 0) != null or !std.unicode.utf8ValidateSlice(value)) {        return error.InvalidConfinementText;    }}test "confined arguments expose only declared runtime and separate captured source" {    const runtime = "/nix/store/00000000000000000000000000000000-runtime";    var result = try launch(std.testing.allocator, "/usr/bin/bwrap", .{        .source = "/retained/source",        .output = "/retained/output",        .runtime = &.{runtime},        .environment = &.{.{ .name = "LANG", .value = "C" }},        .argv = &.{ runtime ++ "/bin/check", "literal\n$()", "" },    });    defer result.deinit(std.testing.allocator);    const bytes = result.stdin;    try std.testing.expect(std.mem.indexOf(u8, bytes, "--clearenv\x00") != null);    try std.testing.expect(std.mem.indexOf(u8, bytes, "--share-net") == null);    try std.testing.expect(        std.mem.indexOf(u8, bytes, "--ro-bind\x00/retained/source\x00/workspace") != null,    );    try std.testing.expectEqualStrings("literal\n$()", result.argv[result.argv.len - 2]);    try std.testing.expectEqualStrings("", result.argv[result.argv.len - 1]);}test "confined private command path binds only named executables" {    const runtime = "/nix/store/00000000000000000000000000000000-runtime";    var result = try launch(std.testing.allocator, "/usr/bin/bwrap", .{        .source = "/retained/source",        .output = "/retained/output",        .runtime = &.{runtime},        .environment = &.{.{ .name = "PATH", .value = "/tmp/tiny-bin" }},        .argv = &.{runtime ++ "/bin/check"},        .commands = &.{.{ .name = "sh", .path = runtime ++ "/bin/sh" }},    });    defer result.deinit(std.testing.allocator);    try std.testing.expect(std.mem.indexOf(u8, result.stdin, "--dir\x00/tmp/tiny-bin\x00") != null);    try std.testing.expect(std.mem.indexOf(u8, result.stdin, "--ro-bind\x00" ++ runtime ++ "/bin/sh\x00/tmp/tiny-bin/sh\x00") != null);    try std.testing.expect(std.mem.indexOf(u8, result.stdin, "/tmp/tiny-bin/date") == null);    try std.testing.expectError(error.InvalidConfinementCommand, launch(std.testing.allocator, "/usr/bin/bwrap", .{        .source = "/retained/source",        .output = "/retained/output",        .runtime = &.{runtime},        .environment = &.{},        .argv = &.{runtime ++ "/bin/check"},        .commands = &.{.{ .name = "../date", .path = runtime ++ "/bin/date" }},    }));}

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

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

Audit

Definitions12
Public names12
Members12
Version26.7.0
Revisiondaab053ee433