diff --git a/pkgs/additional/bunpen/config/cli.ha b/pkgs/additional/bunpen/config/cli.ha index c8e4349c7..3561d776b 100644 --- a/pkgs/additional/bunpen/config/cli.ha +++ b/pkgs/additional/bunpen/config/cli.ha @@ -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 ")!; - // fmt::println(" use a specific sandboxer")!; - // fmt::println(" --bunpen-autodetect ")!; - // fmt::println(" add files which appear later as CLI arguments into the sandbox")!; - // fmt::println(" --bunpen-cap ")!; - // 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 |all")!; - // fmt::println(" --bunpen-net-gateway ")!; - // fmt::println(" --bunpen-dns |host")!; - // fmt::println(" --bunpen-keep-namespace ")!; - // fmt::println(" do not unshare the provided linux namespace")!; + fmt::println(" --bunpen-autodetect ")!; + 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 ")!; @@ -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 ")!; + // fmt::println(" use a specific sandboxer")!; + // fmt::println(" --bunpen-cap ")!; + // 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 |all")!; + // fmt::println(" --bunpen-net-gateway ")!; + // fmt::println(" --bunpen-dns |host")!; + // fmt::println(" --bunpen-keep-namespace ")!; + // 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; + }; +}; diff --git a/pkgs/additional/bunpen/config/translate_opts.ha b/pkgs/additional/bunpen/config/translate_opts.ha index 36b63358f..2febee03b 100644 --- a/pkgs/additional/bunpen/config/translate_opts.ha +++ b/pkgs/additional/bunpen/config/translate_opts.ha @@ -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); + }; +};