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, cmd: []str,
// `--bunpen-debug` // `--bunpen-debug`
debug: uint, debug: uint,
disable: bool,
drop_shell: bool, drop_shell: bool,
// `--bunpen-help` // `--bunpen-help`
help: bool, help: bool,
@@ -80,6 +81,13 @@ export fn usage() void = {
export fn parse_args(args: []str) (cli_opts | errors::invalid) = { export fn parse_args(args: []str) (cli_opts | errors::invalid) = {
let parsed = cli_opts { autodetect = void, ... }; 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) { for (let idx: size = 0; idx < len(args); idx += 1) {
let arg = args[idx]; let arg = args[idx];
let next: nullable *str = null; let next: nullable *str = null;

View File

@@ -15,44 +15,61 @@ use rt::ext;
export type help = void; export type help = void;
export type cli_request = struct { export type cli_request = struct {
// args to invoke the binary with. exec_params: exec_params,
// `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,
// what to keep in the restricted environment (paths, network, etc) // what to keep in the restricted environment (paths, network, etc)
resources: restrict::resources, resources: restrict::resources,
}; };
export fn ingest_cli_opts(opts: cli_opts) (cli_request | help) = { export type exec_params = struct {
let req = cli_request { ... }; // 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` ----// //---- ingest `cmd` ----//
if (len(os::args) > 0) { if (len(os::args) > 0) {
// forward argv0 // forward argv0
append(req.exec_args, os::args[0]); append(params.args, os::args[0]);
}; };
let saw_bin_path = false; let saw_bin_path = false;
for (let arg .. opts.cmd) { for (let arg .. opts.cmd) {
if (saw_bin_path) { if (saw_bin_path) {
append(req.exec_args, arg); append(params.args, arg);
} else { } else {
req.exec_bin = arg; params.bin = arg;
saw_bin_path = true; 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` ----// //---- ingest `help` ----//
if (opts.help) { if (opts.help) {
return 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` ----// //---- ingest `caps` ----//
req.resources.caps = restrict::cap_array_to_caps(opts.keep_caps); req.resources.caps = restrict::cap_array_to_caps(opts.keep_caps);
//---- ingest `home_paths` ----// //---- ingest `home_paths` ----//
ingest_paths(&req.resources.paths, opts.home_paths, os::getenv("HOME")); ingest_paths(&req.resources.paths, opts.home_paths, os::getenv("HOME"));
//---- ingest `keep_all_caps` ----// //---- ingest `keep_all_caps` ----//
@@ -71,12 +88,12 @@ export fn ingest_cli_opts(opts: cli_opts) (cli_request | help) = {
//---- ingest `run_paths` ----// //---- ingest `run_paths` ----//
ingest_paths(&req.resources.paths, opts.run_paths, os::getenv("XDG_RUNTIME_DIR")); 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) { match (opts.autodetect) {
case let method: autodetect => case let method: autodetect =>
// N.B.: skip first arg, since that's the name of the executable and // N.B.: skip first arg, since that's the name of the executable and
// surely not an argument // 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; case void => void;
}; };
@@ -84,9 +101,9 @@ export fn ingest_cli_opts(opts: cli_opts) (cli_request | help) = {
if (opts.drop_shell) { if (opts.drop_shell) {
// ignore the original command, and overwrite it with an interactive shell. // ignore the original command, and overwrite it with an interactive shell.
// TODO: respect the user's `$SHELL`. // TODO: respect the user's `$SHELL`.
req.exec_bin = "/bin/sh"; req.exec_params.bin = "/bin/sh";
delete(req.exec_args[..]); delete(req.exec_params.args[..]);
append(req.exec_args, "sh"); append(req.exec_params.args, "sh");
}; };
return req; 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: // 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 = { export fn main() void = {
// install my logger, but defaulted to no logging. // install my logger, but defaulted to no logging.
log::tree::install(tree::global); log::tree::install(tree::global);
@@ -36,29 +55,17 @@ export fn main() void = {
case let other: config::cli_opts => yield other; case let other: config::cli_opts => yield other;
}; };
// configure logging early let exec_params = match (config::ingest_cli_opts(opts)) {
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)) {
case config::help => case config::help =>
config::usage(); config::usage();
os::exit(0); 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 errors::ext::check("exec <user command>", do_exec(exec_params.bin, exec_params.args));
// 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));
}; };