Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 24 additions & 32 deletions src/core/dsl/builtins/fileutils.zig
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,6 @@ const Value = values.Value;
const BuiltinError = pathname.BuiltinError;
const ExecCtx = pathname.ExecCtx;

/// Hold a symlink target to the same keg/prefix boundary a write gets.
/// An absolute target is checked as-is; a relative one is resolved against the
/// link's parent directory (POSIX semantics) before checking, so `../lib/x`
/// from `<keg>/bin/y` is judged as `<keg>/lib/x` rather than rejected outright
/// for containing a `..`.
fn validateLinkTarget(ctx: ExecCtx, target: []const u8, link_path: []const u8) BuiltinError!void {
const parent = std.fs.path.dirname(link_path) orelse "/";
const resolved = std.fs.path.resolve(ctx.allocator, &.{ parent, target }) catch
return BuiltinError.OutOfMemory;
// Scratch only — the check consumes it, nothing downstream holds it.
defer ctx.allocator.free(resolved);
// Resolve the target's own parent chain, not just its spelling: a bottle
// can ship a directory symlink that a lexically in-keg target walks
// through. Same guard `rm_r`/`mkdir_p` use.
sandbox.validateWriteDir(ctx.io, resolved, ctx.cellar_path, ctx.malt_prefix) catch
return BuiltinError.PathSandboxViolation;
}

/// rm — remove a file or array of files
pub fn rm(ctx: ExecCtx, _: ?Value, args: []const Value) BuiltinError!Value {
if (args.len == 0) return Value{ .nil = {} };
Expand All @@ -45,13 +27,13 @@ pub fn rm(ctx: ExecCtx, _: ?Value, args: []const Value) BuiltinError!Value {
.array => |items| {
for (items) |item| {
const path = item.asString(ctx.allocator) catch continue;
sandbox.validatePath(path, ctx.cellar_path, ctx.malt_prefix) catch continue;
sandbox.validateWriteDir(ctx.io, path, ctx.cellar_path, ctx.malt_prefix) catch continue;
std.Io.Dir.cwd().deleteFile(ctx.io, path) catch {};
}
},
else => {
const path = try args[0].asString(ctx.allocator);
sandbox.validatePath(path, ctx.cellar_path, ctx.malt_prefix) catch
sandbox.validateWriteDir(ctx.io, path, ctx.cellar_path, ctx.malt_prefix) catch
return BuiltinError.PathSandboxViolation;
std.Io.Dir.cwd().deleteFile(ctx.io, path) catch {};
},
Expand Down Expand Up @@ -230,7 +212,10 @@ pub fn lnS(ctx: ExecCtx, _: ?Value, args: []const Value) BuiltinError!Value {
// out of the keg for any later builtin (or the linker) to walk through.
// POSIX resolves a relative target against the link's own directory, so
// resolve it the same way and hold it to the same boundary as a write.
try validateLinkTarget(ctx, target, link_path);
sandbox.validateLinkTarget(ctx.allocator, ctx.io, target, link_path, ctx.cellar_path, ctx.malt_prefix) catch |e| switch (e) {
error.OutOfMemory => return BuiltinError.OutOfMemory,
error.PathSandboxViolation => return BuiltinError.PathSandboxViolation,
};

if (std.fs.path.dirname(link_path)) |parent| {
std.Io.Dir.cwd().createDirPath(ctx.io, parent) catch {};
Expand All @@ -248,33 +233,40 @@ pub fn lnSf(ctx: ExecCtx, _: ?Value, args: []const Value) BuiltinError!Value {
switch (args[0]) {
.array => |items| {
const dest_dir = args[1].asString(ctx.allocator) catch return Value{ .nil = {} };
sandbox.validatePath(dest_dir, ctx.cellar_path, ctx.malt_prefix) catch
sandbox.validateDirTarget(ctx.io, dest_dir, ctx.cellar_path, ctx.malt_prefix) catch
return BuiltinError.PathSandboxViolation;
std.Io.Dir.cwd().createDirPath(ctx.io, dest_dir) catch {};
for (items) |item| {
const target = item.asString(ctx.allocator) catch continue;
const base = std.fs.path.basename(target);
const link_path = std.fs.path.join(ctx.allocator, &.{ dest_dir, base }) catch continue;
std.Io.Dir.cwd().deleteFile(ctx.io, link_path) catch {};
std.Io.Dir.symLinkAbsolute(ctx.io, target, link_path, .{}) catch {};
defer ctx.allocator.free(link_path);
try forceSymlink(ctx, target, link_path);
}
},
else => {
const target = try args[0].asString(ctx.allocator);
const link_path = try args[1].asString(ctx.allocator);
sandbox.validatePath(link_path, ctx.cellar_path, ctx.malt_prefix) catch
return BuiltinError.PathSandboxViolation;

if (std.fs.path.dirname(link_path)) |parent| {
std.Io.Dir.cwd().createDirPath(ctx.io, parent) catch {};
}
std.Io.Dir.cwd().deleteFile(ctx.io, link_path) catch {};
std.Io.Dir.symLinkAbsolute(ctx.io, target, link_path, .{}) catch {};
try forceSymlink(ctx, target, link_path);
},
}
return Value{ .nil = {} };
}

fn forceSymlink(ctx: ExecCtx, target: []const u8, link_path: []const u8) BuiltinError!void {
sandbox.validateWriteDir(ctx.io, link_path, ctx.cellar_path, ctx.malt_prefix) catch
return BuiltinError.PathSandboxViolation;
sandbox.validateLinkTarget(ctx.allocator, ctx.io, target, link_path, ctx.cellar_path, ctx.malt_prefix) catch |e| switch (e) {
error.OutOfMemory => return BuiltinError.OutOfMemory,
error.PathSandboxViolation => return BuiltinError.PathSandboxViolation,
};
if (std.fs.path.dirname(link_path)) |parent| {
std.Io.Dir.cwd().createDirPath(ctx.io, parent) catch {};
}
std.Io.Dir.cwd().deleteFile(ctx.io, link_path) catch {};
std.Io.Dir.symLinkAbsolute(ctx.io, target, link_path, .{}) catch {};
}

const fs_test_io = std.Options.debug_io;

var scratch_seq: std.atomic.Value(u32) = .init(0);
Expand Down
18 changes: 13 additions & 5 deletions src/core/dsl/builtins/pathname.zig
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ pub const ExecCtx = struct {
pub fn mkpath(ctx: ExecCtx, receiver: ?Value, _: []const Value) BuiltinError!Value {
const path = try receiverPath(ctx.allocator, receiver);
if (path.len == 0) return Value{ .nil = {} };
sandbox.validateDirTarget(ctx.io, path, ctx.cellar_path, ctx.malt_prefix) catch
return BuiltinError.PathSandboxViolation;
std.Io.Dir.cwd().createDirPath(ctx.io, path) catch {};
return Value{ .nil = {} };
}
Expand Down Expand Up @@ -213,7 +215,7 @@ pub fn pkgetc(ctx: ExecCtx, receiver: ?Value, _: []const Value) BuiltinError!Val
pub fn unlink(ctx: ExecCtx, receiver: ?Value, _: []const Value) BuiltinError!Value {
const path = try receiverPath(ctx.allocator, receiver);
if (path.len == 0) return Value{ .nil = {} };
sandbox.validatePath(path, ctx.cellar_path, ctx.malt_prefix) catch
sandbox.validateWriteDir(ctx.io, path, ctx.cellar_path, ctx.malt_prefix) catch
return BuiltinError.PathSandboxViolation;
std.Io.Dir.cwd().deleteFile(ctx.io, path) catch {};
return Value{ .nil = {} };
Expand All @@ -228,6 +230,9 @@ pub fn unlink(ctx: ExecCtx, receiver: ?Value, _: []const Value) BuiltinError!Val
pub fn installSymlink(ctx: ExecCtx, receiver: ?Value, args: []const Value) BuiltinError!Value {
const dir = try receiverPath(ctx.allocator, receiver);
if (dir.len == 0) return Value{ .nil = {} };
sandbox.validateDirTarget(ctx.io, dir, ctx.cellar_path, ctx.malt_prefix) catch
return BuiltinError.PathSandboxViolation;
std.Io.Dir.cwd().createDirPath(ctx.io, dir) catch {};
for (args) |arg| try installSymlinkArg(ctx, dir, arg);
return Value{ .nil = {} };
}
Expand All @@ -251,8 +256,8 @@ fn installSymlinkArg(ctx: ExecCtx, dir: []const u8, arg: Value) BuiltinError!voi
}
}

/// Symlink `<dir>/<name>` → `<source>`, sandbox-validated on the link
/// path. Same non-raising fs contract as the rest of this module: a
/// Symlink `<dir>/<name>` → `<source>`, sandbox-validated on both paths.
/// Same non-raising fs contract as the rest of this module: a
/// failed mkdir/symlink surfaces downstream, not here.
fn linkInto(ctx: ExecCtx, dir: []const u8, source: []const u8, name: []const u8) BuiltinError!void {
if (source.len == 0 or name.len == 0) return;
Expand All @@ -261,10 +266,13 @@ fn linkInto(ctx: ExecCtx, dir: []const u8, source: []const u8, name: []const u8)
// Overflow means a pathological path — skip rather than truncate.
const link = std.fmt.bufPrint(&link_buf, "{s}/{s}", .{ dir, name }) catch return;

sandbox.validatePath(link, ctx.cellar_path, ctx.malt_prefix) catch
sandbox.validateWriteDir(ctx.io, link, ctx.cellar_path, ctx.malt_prefix) catch
return BuiltinError.PathSandboxViolation;
sandbox.validateLinkTarget(ctx.allocator, ctx.io, source, link, ctx.cellar_path, ctx.malt_prefix) catch |e| switch (e) {
error.OutOfMemory => return BuiltinError.OutOfMemory,
error.PathSandboxViolation => return BuiltinError.PathSandboxViolation,
};

std.Io.Dir.cwd().createDirPath(ctx.io, dir) catch {};
std.Io.Dir.cwd().deleteFile(ctx.io, link) catch {};
std.Io.Dir.symLinkAbsolute(ctx.io, source, link, .{}) catch {};
}
Expand Down
18 changes: 18 additions & 0 deletions src/core/dsl/sandbox.zig
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,24 @@ pub fn validateDirTarget(
try resolvedDirWithinBoundary(io, dir_path, cellar_path, malt_prefix);
}

/// Validate the object a new symlink will resolve to. Relative targets use the
/// link's parent directory, matching POSIX symlink semantics. Resolving the
/// target itself, including an existing final component, prevents a link from
/// becoming a doorway through another symlink that already leaves the prefix.
pub fn validateLinkTarget(
allocator: std.mem.Allocator,
io: std.Io,
target: []const u8,
link_path: []const u8,
cellar_path: []const u8,
malt_prefix: []const u8,
) (SandboxError || std.mem.Allocator.Error)!void {
const parent = std.fs.path.dirname(link_path) orelse "/";
const resolved = try std.fs.path.resolve(allocator, &.{ parent, target });
defer allocator.free(resolved);
try validateDirTarget(io, resolved, cellar_path, malt_prefix);
}

/// How `openTargetNoFollow` opens the leaf. `write` toggles WRONLY vs RDONLY
/// (chmod only needs a handle to `fchmod`); `create`/`truncate` map to
/// `O_CREAT`/`O_TRUNC`.
Expand Down
Loading
Loading