bunpen: implement --bunpen-autodetect

This commit is contained in:
2024-08-28 11:35:58 +00:00
parent 38ee8be785
commit 35848ece02
2 changed files with 143 additions and 29 deletions

View File

@@ -5,6 +5,7 @@ use os;
export type error = !void;
export type cli_opts = struct {
autodetect: (void | autodetect),
// command to `exec` within the sandbox
cmd: []str,
// `--bunpen-debug`
@@ -18,6 +19,14 @@ export type cli_opts = struct {
run_paths: []str,
};
export type autodetect = enum {
EXISTING,
EXISTING_FILE,
EXISTING_FILE_OR_PARENT,
EXISTING_OR_PARENT,
PARENT,
};
export fn usage() void = {
fmt::println("bunpen: run a program within an environment where access to external resources (files, net) is restricted (i.e. sandbox)")!;
fmt::println("USAGE: bunpen [sandbox-arg ...] program [sandbox-arg|program-arg ...] [--] [program-arg ...]")!;
@@ -29,25 +38,11 @@ export fn usage() void = {
fmt::println(" show this message")!;
fmt::println(" --bunpen-debug[=n]")!;
fmt::println(" print debug messages to stderr")!;
fmt::println(" omit `n` for light debugging, or specify n=0/1/2/3 where higher = more verbose")!;
fmt::println(" omit `n` for light debugging, or specify n=0/1/2/3/4 where higher = more verbose")!;
fmt::println(" --bunpen-drop-shell")!;
fmt::println(" instead of running the program, drop into an interactive shell")!;
// fmt::println(" --bunpen-disable")!;
// fmt::println(" invoke the program directly, instead of inside a sandbox")!;
// fmt::println(" --bunpen-dry-run")!;
// fmt::println(" show what would be `exec`uted but do not perform any action")!;
// fmt::println(" --bunpen-method <bwrap|capshonly|pastaonly|landlock|none>")!;
// fmt::println(" use a specific sandboxer")!;
// fmt::println(" --bunpen-autodetect <existing|existingFile|existingFileOrParent|existingOrParent|parent>")!;
// fmt::println(" add files which appear later as CLI arguments into the sandbox")!;
// fmt::println(" --bunpen-cap <all|sys_admin|net_raw|net_admin|...>")!;
// fmt::println(" allow the sandboxed program to use the provided linux capability (both inside and outside the sandbox)")!;
// fmt::println(" special cap "all" to preserve all capabilities possible")!;
// fmt::println(" --bunpen-net-dev <iface>|all")!;
// fmt::println(" --bunpen-net-gateway <ip-address>")!;
// fmt::println(" --bunpen-dns <server>|host")!;
// fmt::println(" --bunpen-keep-namespace <all|cgroup|ipc|net|pid|uts>")!;
// fmt::println(" do not unshare the provided linux namespace")!;
fmt::println(" --bunpen-autodetect <existing|existingFile|existingFileOrParent|existingOrParent|parent>")!;
fmt::println(" add files which appear later as CLI arguments into the sandbox")!;
fmt::println(" --bunpen-keep-net")!;
fmt::println(" allow unrestricted access to the network")!;
fmt::println(" --bunpen-path <path>")!;
@@ -60,6 +55,20 @@ export fn usage() void = {
// fmt::println(" --bunpen-add-pwd")!;
// fmt::println(" shorthand for `--bunpen-path $PWD`")!;
// fmt::println("")!;
// fmt::println(" --bunpen-disable")!;
// fmt::println(" invoke the program directly, instead of inside a sandbox")!;
// fmt::println(" --bunpen-dry-run")!;
// fmt::println(" show what would be `exec`uted but do not perform any action")!;
// fmt::println(" --bunpen-method <bwrap|capshonly|pastaonly|landlock|none>")!;
// fmt::println(" use a specific sandboxer")!;
// fmt::println(" --bunpen-cap <all|sys_admin|net_raw|net_admin|...>")!;
// fmt::println(" allow the sandboxed program to use the provided linux capability (both inside and outside the sandbox)")!;
// fmt::println(" special cap "all" to preserve all capabilities possible")!;
// fmt::println(" --bunpen-net-dev <iface>|all")!;
// fmt::println(" --bunpen-net-gateway <ip-address>")!;
// fmt::println(" --bunpen-dns <server>|host")!;
// fmt::println(" --bunpen-keep-namespace <all|cgroup|ipc|net|pid|uts>")!;
// fmt::println(" do not unshare the provided linux namespace")!;
// fmt::println("the following environment variables are also considered and propagated to children:")!;
// fmt::println(" BUNPEN_DISABLE=1")!;
// fmt::println(" equivalent to `--bunpen-disable`")!;
@@ -72,7 +81,7 @@ export fn usage() void = {
};
export fn parse_args(args: []str) (cli_opts | error) = {
let parsed = cli_opts { ... };
let parsed = cli_opts { autodetect = void, ... };
for (let idx: size = 0; idx < len(args); idx += 1) {
let arg = args[idx];
@@ -81,11 +90,13 @@ export fn parse_args(args: []str) (cli_opts | error) = {
next = &args[idx+1];
};
switch (arg) {
case "--bunpen-autodetect" => idx += 1; parsed.autodetect = autodetect_fromstr(expect_arg("--bunpen-autodetect", next)?)?;
case "--bunpen-debug" => parsed.debug = 2;
case "--bunpen-debug=0" => parsed.debug = 0;
case "--bunpen-debug=1" => parsed.debug = 1;
case "--bunpen-debug=2" => parsed.debug = 2;
case "--bunpen-debug=3" => parsed.debug = 3;
case "--bunpen-debug=4" => parsed.debug = 4;
case "--bunpen-drop-shell" => parsed.drop_shell = true;
case "--bunpen-help" => parsed.help = true;
case "--bunpen-home-path" => idx += 1; append(parsed.home_paths, expect_arg("--bunpen-home-path", next)?);
@@ -105,3 +116,14 @@ fn expect_arg(name: str, value: nullable *str) (str | error) = {
case let v: *str => yield *v;
};
};
fn autodetect_fromstr(v: str) (autodetect | error) = {
return switch (v) {
case "existing" => yield autodetect::EXISTING;
case "existingFile" => yield autodetect::EXISTING_FILE;
case "existingFileOrParent" => yield autodetect::EXISTING_FILE_OR_PARENT;
case "existingOrParent" => yield autodetect::EXISTING_OR_PARENT;
case "parent" => yield autodetect::PARENT;
case => yield error;
};
};

View File

@@ -1,9 +1,12 @@
// vim: set shiftwidth=2 :
// ingest literal `cli_opts` into a more computer-friendly form
use fs;
use log;
use os;
use path;
use rt;
use rtext;
// the user requested to see help.
export type help = !void;
@@ -46,15 +49,6 @@ export fn ingest_cli_opts(opts: cli_opts) (cli_request | help) = {
//---- ingest `debug` ----//
req.debug = opts.debug;
//---- ingest `drop_shell` ----//
if (opts.drop_shell) {
// ignore the original command, and overwrite it with an interactive shell.
// TODO: respect the user's `$SHELL`.
req.exec_bin = "/bin/sh";
delete(req.exec_args[..]);
append(req.exec_args, "sh");
};
//---- ingest `help` ----//
if (opts.help) {
return help;
@@ -72,21 +66,43 @@ export fn ingest_cli_opts(opts: cli_opts) (cli_request | help) = {
//---- ingest `run_paths` ----//
ingest_paths(&req.paths, opts.run_paths, os::getenv("XDG_RUNTIME_DIR"));
//---- ingest `autodetect` (must be done after exec_args) ----//
match (opts.autodetect) {
case let method: autodetect =>
// N.B.: skip first arg, since that's the name of the executable and
// surely not an argument
ingest_autodetect(&req.paths, req.exec_args[1..], method);
case void => void;
};
//---- ingest `drop_shell` (must be done after autodetect) ----//
if (opts.drop_shell) {
// ignore the original command, and overwrite it with an interactive shell.
// TODO: respect the user's `$SHELL`.
req.exec_bin = "/bin/sh";
delete(req.exec_args[..]);
append(req.exec_args, "sh");
};
return req;
};
// convert each item in `from` to a path, relative to `base`, and append to `into`.
// if `allow_abs`, then paths with start with `/` are treated as absolute,
// instead of as relative to `base`.
fn ingest_paths(into: *[]path::buffer, from: []str, base: (str | void), allow_abs: bool = false) void = {
for (let path_str .. from) {
match (get_path(path_str, base, allow_abs)) {
case let p: path::buffer => append(into, p);
case let e: path::error =>
log::printfln("[translate_opts] omitting path {}: {}", path_str, path::strerror(e));
case missing_base => log::printfln("[translate_opts] omitting path {}: no base dir", path_str);
log::printfln("[config] omitting path {}: {}", path_str, path::strerror(e));
case let e: missing_base =>
log::printfln("[config] omitting path {}: {}", translate_strerror(e));
};
};
};
// path was specified in relative form, but the directory to base that on was omitted.
type missing_base = !void;
// `ingest_paths` helper: transforms `path_str` into an absolute path.
fn get_path(path_str: str, base: (str | void), allow_abs: bool) (path::buffer | path::error | missing_base) = {
@@ -97,3 +113,79 @@ fn get_path(path_str: str, base: (str | void), allow_abs: bool) (path::buffer |
case void => return missing_base;
};
};
// processes arguments from `consider` in the context of `--bunpen-autodetect METHOD`,
// and appends any extra paths wanted in the sandbox to `into` in response.
fn ingest_autodetect(into: *[]path::buffer, consider: []str, method: autodetect) void = {
let base = os::getcwd();
for (let path_str .. consider) {
match (get_path(path_str, base, true)) {
case let p: path::buffer => try_as_path(into, p, method);
case let e: path::error =>
log::printfln("[config/path] omitting path {}: {}", path_str, path::strerror(e));
case let e: missing_base =>
log::printfln("[configa/path] omitting path {}: {}", translate_strerror(e));
};
};
};
// consider the `pathbuf` in the context of the autodetect `method`, and append
// either that path, its parent, or neither.
fn try_as_path(into: *[]path::buffer, pathbuf: path::buffer, method: autodetect) void = {
let pathstr = path::string(&pathbuf);
switch (method) {
case autodetect::EXISTING =>
append_if_exists(into, [pathstr], true);
case autodetect::EXISTING_FILE =>
append_if_exists(into, [pathstr], false);
case autodetect::EXISTING_FILE_OR_PARENT =>
append_if_exists(into, [pathstr], false)
|| append_if_exists(into, [pathstr, ".."], true);
case autodetect::EXISTING_OR_PARENT =>
append_if_exists(into, [pathstr], true)
|| append_if_exists(into, [pathstr, ".."], true);
case autodetect::PARENT =>
append_if_exists(into, [pathstr, ".."], true);
};
};
// append the file encoded by `comps`, but only if it exists and is file-like.
// if it exists but as a directory, append it only so long as `dir_ok = true`.
fn append_if_exists(into: *[]path::buffer, comps: []str, dir_ok: bool) bool = {
let pathbuf = path::buffer { ... };
let do_append = match (deref_stat(&pathbuf, comps)) {
case let st: fs::filestat =>
yield (st.mode & rt::S_IFDIR) == 0 || dir_ok;
case let e: fs::error =>
log::printfln("[config/path/try] failed to deref/stat {}: {}", path::string(&pathbuf), fs::strerror(e));
yield false;
case let e: path::error =>
log::printfln("[config/path/try] failed to deref/stat: {}", path::strerror(e));
yield false;
};
if (do_append) {
log::printfln("[config/path] autodetected path: {}", path::string(&pathbuf));
append(into, pathbuf);
} else {
log::printfln("[config/path/try] not adding path: {}", path::string(&pathbuf));
};
return do_append;
};
// invoke `stat` on the path encoded by `comps`.
// if the path is a symlink, deref through the symlinks and stat the target.
fn deref_stat(pathbuf: *path::buffer, comps: []str) (fs::filestat | fs::error | path::error) = {
*pathbuf = path::init(comps...)?;
let pathstr = fs::realpath(os::cwd, path::string(pathbuf))?;
return fs::stat(os::cwd, pathstr);
};
fn translate_strerror(e: (missing_base | fs::error | path::error)) str = {
return match (e) {
case let e: missing_base => yield "non-absolute path, but unknown as to which base directory";
case let e: fs::error => yield fs::strerror(e);
case let e: path::error => yield path::strerror(e);
};
};