bunpen: implement BUNPEN_DISABLE=1 env var to bypass sandboxing

This commit is contained in:
2024-09-03 00:27:14 +00:00
parent 5ae12272bd
commit f3b9369783
3 changed files with 69 additions and 37 deletions

View File

@@ -10,6 +10,7 @@ export type cli_opts = struct {
cmd: []str,
// `--bunpen-debug`
debug: uint,
disable: bool,
drop_shell: bool,
// `--bunpen-help`
help: bool,
@@ -80,6 +81,13 @@ export fn usage() void = {
export fn parse_args(args: []str) (cli_opts | errors::invalid) = {
let parsed = cli_opts { autodetect = void, ... };
let dis = match (os::getenv("BUNPEN_DISABLE")) {
case let d: str => yield d;
case void => yield "";
};
if (dis != "" && dis != "0")
parsed.disable = true;
for (let idx: size = 0; idx < len(args); idx += 1) {
let arg = args[idx];
let next: nullable *str = null;

View File

@@ -15,44 +15,61 @@ use rt::ext;
export type help = void;
export type cli_request = struct {
// args to invoke the binary with.
// `exec_args[0]` is, by convention, the name of the executable,
// relevant for multi-call binaries like `busybox`.
exec_args: []str,
// path to the binary to be exec'd inside the sandbox.
// if the user requested `--bunpen-drop-shell`, this will be their shell (e.g. /bin/sh).
exec_bin: str,
exec_params: exec_params,
// what to keep in the restricted environment (paths, network, etc)
resources: restrict::resources,
};
export fn ingest_cli_opts(opts: cli_opts) (cli_request | help) = {
let req = cli_request { ... };
export type exec_params = struct {
// args to invoke the binary with.
// `args[0]` is, by convention, the name of the executable,
// relevant for multi-call binaries like `busybox`.
args: []str,
// path to the binary to be exec'd inside the sandbox.
// if the user requested `--bunpen-drop-shell`, this will be their shell (e.g. /bin/sh), assuming `BUNPEN_DISABLE=1` wasn't specified.
bin: str,
};
fn cli_opts_get_exec_params(opts: cli_opts) exec_params = {
let params = exec_params { ... };
//---- ingest `cmd` ----//
if (len(os::args) > 0) {
// forward argv0
append(req.exec_args, os::args[0]);
append(params.args, os::args[0]);
};
let saw_bin_path = false;
for (let arg .. opts.cmd) {
if (saw_bin_path) {
append(req.exec_args, arg);
append(params.args, arg);
} else {
req.exec_bin = arg;
params.bin = arg;
saw_bin_path = true;
};
};
return params;
};
export fn ingest_cli_opts(opts: cli_opts) (cli_request | exec_params | help) = {
let req = cli_request { ... };
//---- ingest `help` ----//
if (opts.help) {
return help;
};
//---- ingest opts.cmd ----//
req.exec_params = cli_opts_get_exec_params(opts);
//---- ingest `disable` ----//
if (opts.disable)
return req.exec_params;
//---- ingest `caps` ----//
req.resources.caps = restrict::cap_array_to_caps(opts.keep_caps);
//---- ingest `home_paths` ----//
ingest_paths(&req.resources.paths, opts.home_paths, os::getenv("HOME"));
//---- ingest `keep_all_caps` ----//
@@ -71,12 +88,12 @@ export fn ingest_cli_opts(opts: cli_opts) (cli_request | help) = {
//---- ingest `run_paths` ----//
ingest_paths(&req.resources.paths, opts.run_paths, os::getenv("XDG_RUNTIME_DIR"));
//---- ingest `autodetect` (must be done after exec_args) ----//
//---- ingest `autodetect` (must be done after exec_params) ----//
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_paths(&req.resources.paths, req.exec_args[1..], os::getcwd(), true, method);
ingest_paths(&req.resources.paths, req.exec_params.args[1..], os::getcwd(), true, method);
case void => void;
};
@@ -84,9 +101,9 @@ export fn ingest_cli_opts(opts: cli_opts) (cli_request | help) = {
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");
req.exec_params.bin = "/bin/sh";
delete(req.exec_params.args[..]);
append(req.exec_params.args, "sh");
};
return req;

View File

@@ -25,6 +25,25 @@ fn do_exec(path: str, args: []str) (os::exec::error | void) = {
// work if you don't care about that:
};
fn prepare_env(req: config::cli_request) config::exec_params = {
{
let argv_str = strings::join(" ", os::args[1..]...);
defer free(argv_str);
log::printfln("invoked with: {}", argv_str);
};
// set no_new_privs early. this is a flag which prevents us from gaining privs
// via SUID/SGID executables, which we never intend to do.
errors::ext::check("no_new_privs", rt::ext::no_new_privs());
restrict::namespace_restrict(&req.resources);
restrict::capability_restrict(&req.resources);
// XXX: landlock prevents other sandboxers like `bwrap` from executing,
// because it forbids all future `mount` syscalls. so don't landlock.
// restrict::landlock_restrict(&req.resources);
return req.exec_params;
};
export fn main() void = {
// install my logger, but defaulted to no logging.
log::tree::install(tree::global);
@@ -36,29 +55,17 @@ export fn main() void = {
case let other: config::cli_opts => yield other;
};
// configure logging early
log::tree::set_level(tree::global, opts.debug);
{
let argv_str = strings::join(" ", os::args[1..]...);
defer free(argv_str);
log::printfln("invoked with: {}", argv_str);
};
let req = match (config::ingest_cli_opts(opts)) {
let exec_params = match (config::ingest_cli_opts(opts)) {
case config::help =>
config::usage();
os::exit(0);
case let other: config::cli_request => yield other;
case let p: config::exec_params => yield p; // run without sandboxing (BUNPEN_DISABLE=1)
case let req: config::cli_request =>
// configure logging early
log::tree::set_level(tree::global, opts.debug);
yield prepare_env(req);
};
// set no_new_privs early. this is a flag which prevents us from gaining privs
// via SUID/SGID executables, which we never intend to do.
errors::ext::check("no_new_privs", rt::ext::no_new_privs());
restrict::namespace_restrict(&req.resources);
restrict::capability_restrict(&req.resources);
// XXX: landlock prevents other sandboxers like `bwrap` from executing,
// because it forbids all future `mount` syscalls. so don't landlock.
// restrict::landlock_restrict(&req.resources);
errors::ext::check("exec <user command>", do_exec(req.exec_bin, req.exec_args));
errors::ext::check("exec <user command>", do_exec(exec_params.bin, exec_params.args));
};