bunpen: better (still incomplete) capability boxing

This commit is contained in:
2024-09-02 18:55:53 +00:00
parent 8b53f97c1c
commit bc2823d622
6 changed files with 230 additions and 142 deletions

View File

@@ -1,4 +1,5 @@
// vim: set shiftwidth=2 :
use errors;
export type autodetect = enum {
EXISTING,
@@ -8,14 +9,14 @@ export type autodetect = enum {
PARENT,
};
fn autodetect_fromstr(v: str) (autodetect | error) = {
fn autodetect_fromstr(v: str) (autodetect | errors::invalid) = {
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;
case => yield errors::invalid;
};
};

View File

@@ -1,129 +0,0 @@
// vim: set shiftwidth=2 :
use ascii;
use rt::ext;
use strings;
fn capability_fromstr(v: str) (rt::ext::cap | error) = {
// strip leading CAP_ and allow either form.
if (len(v) > 4 && ascii::strcasecmp(strings::sub(v, 0, 4), "CAP_") == 0)
v = strings::sub(v, 4);
if (ascii::strcasecmp(v, "AUDIT_CONTROL") == 0)
return rt::ext::cap::AUDIT_CONTROL;
if (ascii::strcasecmp(v, "AUDIT_READ") == 0)
return rt::ext::cap::AUDIT_READ;
if (ascii::strcasecmp(v, "AUDIT_WRITE") == 0)
return rt::ext::cap::AUDIT_WRITE;
if (ascii::strcasecmp(v, "BLOCK_SUSPEND") == 0)
return rt::ext::cap::BLOCK_SUSPEND;
if (ascii::strcasecmp(v, "BPF") == 0)
return rt::ext::cap::BPF;
if (ascii::strcasecmp(v, "CHECKPOINT_RESTORE") == 0)
return rt::ext::cap::CHECKPOINT_RESTORE;
if (ascii::strcasecmp(v, "CHOWN") == 0)
return rt::ext::cap::CHOWN;
if (ascii::strcasecmp(v, "DAC_OVERRIDE") == 0)
return rt::ext::cap::DAC_OVERRIDE;
if (ascii::strcasecmp(v, "DAC_READ_SEARCH") == 0)
return rt::ext::cap::DAC_READ_SEARCH;
if (ascii::strcasecmp(v, "FOWNER") == 0)
return rt::ext::cap::FOWNER;
if (ascii::strcasecmp(v, "FSETID") == 0)
return rt::ext::cap::FSETID;
if (ascii::strcasecmp(v, "IPC_LOCK") == 0)
return rt::ext::cap::IPC_LOCK;
if (ascii::strcasecmp(v, "IPC_OWNER") == 0)
return rt::ext::cap::IPC_OWNER;
if (ascii::strcasecmp(v, "KILL") == 0)
return rt::ext::cap::KILL;
if (ascii::strcasecmp(v, "LEASE") == 0)
return rt::ext::cap::LEASE;
if (ascii::strcasecmp(v, "LINUX_IMMUTABLE") == 0)
return rt::ext::cap::LINUX_IMMUTABLE;
if (ascii::strcasecmp(v, "MAC_ADMIN") == 0)
return rt::ext::cap::MAC_ADMIN;
if (ascii::strcasecmp(v, "MAC_OVERRIDE") == 0)
return rt::ext::cap::MAC_OVERRIDE;
if (ascii::strcasecmp(v, "MKNOD") == 0)
return rt::ext::cap::MKNOD;
if (ascii::strcasecmp(v, "NET_ADMIN") == 0)
return rt::ext::cap::NET_ADMIN;
if (ascii::strcasecmp(v, "NET_BIND_SERVICE") == 0)
return rt::ext::cap::NET_BIND_SERVICE;
if (ascii::strcasecmp(v, "NET_BROADCAST") == 0)
return rt::ext::cap::NET_BROADCAST;
if (ascii::strcasecmp(v, "NET_RAW") == 0)
return rt::ext::cap::NET_RAW;
if (ascii::strcasecmp(v, "PERFMON") == 0)
return rt::ext::cap::PERFMON;
if (ascii::strcasecmp(v, "SETFCAP") == 0)
return rt::ext::cap::SETFCAP;
if (ascii::strcasecmp(v, "SETGID") == 0)
return rt::ext::cap::SETGID;
if (ascii::strcasecmp(v, "SETPCAP") == 0)
return rt::ext::cap::SETPCAP;
if (ascii::strcasecmp(v, "SETUID") == 0)
return rt::ext::cap::SETUID;
if (ascii::strcasecmp(v, "SYS_ADMIN") == 0)
return rt::ext::cap::SYS_ADMIN;
if (ascii::strcasecmp(v, "SYS_BOOT") == 0)
return rt::ext::cap::SYS_BOOT;
if (ascii::strcasecmp(v, "SYS_CHROOT") == 0)
return rt::ext::cap::SYS_CHROOT;
if (ascii::strcasecmp(v, "SYS_MODULE") == 0)
return rt::ext::cap::SYS_MODULE;
if (ascii::strcasecmp(v, "SYS_NICE") == 0)
return rt::ext::cap::SYS_NICE;
if (ascii::strcasecmp(v, "SYS_PACCT") == 0)
return rt::ext::cap::SYS_PACCT;
if (ascii::strcasecmp(v, "SYS_PTRACE") == 0)
return rt::ext::cap::SYS_PTRACE;
if (ascii::strcasecmp(v, "SYS_RAWIO") == 0)
return rt::ext::cap::SYS_RAWIO;
if (ascii::strcasecmp(v, "SYS_RESOURCE") == 0)
return rt::ext::cap::SYS_RESOURCE;
if (ascii::strcasecmp(v, "SYS_TIME") == 0)
return rt::ext::cap::SYS_TIME;
if (ascii::strcasecmp(v, "SYS_TTY_CONFIG") == 0)
return rt::ext::cap::SYS_TTY_CONFIG;
if (ascii::strcasecmp(v, "SYSLOG") == 0)
return rt::ext::cap::SYSLOG;
if (ascii::strcasecmp(v, "WAKE_ALARM") == 0)
return rt::ext::cap::WAKE_ALARM;
return error;
};
// return true if `s` parses to `expect`
fn _parse_eq(s: str, expect: (rt::ext::cap | error)) bool = {
let got = capability_fromstr(s);
return match (expect) {
case let c: rt::ext::cap => yield match (got) {
case let c2: rt::ext::cap => yield c2 == c;
case => yield false;
};
case error => yield match (got) {
case error => yield true; // both errors
case => yield false;
};
};
};
@test fn cap_from_str_good() void = {
assert(_parse_eq("SYS_ADMIN", rt::ext::cap::SYS_ADMIN));
assert(_parse_eq("CAP_SYS_ADMIN", rt::ext::cap::SYS_ADMIN));
assert(_parse_eq("sys_admin", rt::ext::cap::SYS_ADMIN));
assert(_parse_eq("cap_sys_admin", rt::ext::cap::SYS_ADMIN));
assert(_parse_eq("CAP_sys_admin", rt::ext::cap::SYS_ADMIN));
assert(_parse_eq("cap_SYS_ADMIN", rt::ext::cap::SYS_ADMIN));
};
@test fn cap_from_str_bad() void = {
assert(_parse_eq("CAP_SYS_ADMIN_AND_MORE", error));
assert(_parse_eq("SYS_ADMIN_CAP", error));
assert(_parse_eq("SYS", error));
assert(_parse_eq("", error));
};

View File

@@ -1,10 +1,9 @@
// vim: set shiftwidth=2 :
use errors;
use fmt;
use os;
use rt::ext;
export type error = !void;
export type cli_opts = struct {
autodetect: (void | autodetect),
// command to `exec` within the sandbox
@@ -78,7 +77,7 @@ export fn usage() void = {
// fmt::println(" act as though the provided arg string appeared at the end of the CLI")!;
};
export fn parse_args(args: []str) (cli_opts | error) = {
export fn parse_args(args: []str) (cli_opts | errors::invalid) = {
let parsed = cli_opts { autodetect = void, ... };
for (let idx: size = 0; idx < len(args); idx += 1) {
@@ -110,17 +109,17 @@ export fn parse_args(args: []str) (cli_opts | error) = {
return parsed;
};
fn expect_arg(name: str, value: nullable *str) (str | error) = {
fn expect_arg(name: str, value: nullable *str) (str | errors::invalid) = {
return match (value) {
case null => yield error;
case null => yield errors::invalid;
case let v: *str => yield *v;
};
};
fn parse_caparg(into: *cli_opts, arg: str) (void | error) = {
fn parse_caparg(into: *cli_opts, arg: str) (void | errors::invalid) = {
if (arg == "all") {
into.keep_all_caps = true;
} else {
append(into.keep_caps, capability_fromstr(arg)?);
append(into.keep_caps, rt::ext::cap_fromstr(arg)?);
};
};

View File

@@ -1,5 +1,6 @@
// vim: set shiftwidth=2 :
use config;
use errors;
use errors::ext;
use log;
use log::tree;
@@ -30,7 +31,7 @@ export fn main() void = {
let name = os::args[0];
let opts = match (config::parse_args(os::args[1..])) {
case config::error =>
case errors::invalid =>
config::usage();
os::exit(1);
case let other: config::cli_opts => yield other;
@@ -46,10 +47,14 @@ export fn main() void = {
case let other: config::cli_request => yield other;
};
errors::ext::check("no_new_privs", rt::ext::no_new_privs());
restrict::namespace_restrict(&req.resources);
if (!req.resources.all_caps)
if (req.resources.all_caps) {
// TODO: this probably isn't what i want? i think this actually results in having no caps
log::printfln("not restricting capabilities");
} else {
restrict::capability_restrict(&req.resources);
};
errors::ext::check("no_new_privs", rt::ext::no_new_privs());
// 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);

View File

@@ -15,12 +15,42 @@ export fn caps_add(cs: *rt::ext::caps, cap: rt::ext::cap) void = {
*cs |= (1 << cap: u64);
};
fn caps_contains(cs: *rt::ext::caps, cap: rt::ext::cap) bool = {
return (*cs & (1 << cap: u64)) != 0;
};
export fn capability_restrict(what: *resources) void = {
// TODO: call PR_CAP_AMBIENT and PR_CAPBSET_DROP
errors::ext::check("capability", rt::ext::capset(
what.caps, // effective
what.caps, // permitted
what.caps, // inherited
));
// TODO: unless we acquire `CAP_SETPCAP`, all the `drop bounding` calls fail
for (let cap = rt::ext::CAP_FIRST; cap <= rt::ext::CAP_LAST; cap += 1) {
let cap_str = rt::ext::cap_tostring(cap);
if (caps_contains(&what.caps, cap)) {
log::printfln("[capability/restrict] raising {}", cap_str);
errors::ext::swallow(
"[capability] raise ambient {}",
rt::ext::cap_ambient_raise(cap),
cap_str,
);
} else {
log::printfln("[capability/restrict] lowering {}", cap_str);
// `swallow` when raising caps but `check` when lowering because messing
// up the latter means running with too many privs.
errors::ext::check(
"[capability] drop ambient {}",
rt::ext::cap_ambient_lower(cap),
cap_str,
);
errors::ext::check(
"[capability] drop bounding {}",
rt::ext::capbset_drop(cap),
cap_str,
);
};
};
log::println("capability restrictions activated");
};

View File

@@ -1,5 +1,8 @@
// vim: set shiftwidth=2 :
use ascii;
use errors;
use rt;
use strings;
export type cap = enum {
// In a system with the [_POSIX_CHOWN_RESTRICTED] option defined, this
@@ -258,6 +261,9 @@ export type cap = enum {
// Allow writing to ns_last_pid
CHECKPOINT_RESTORE = 40,
};
// iterate all caps via `for (let c = CAP_FIRST; c <= CAP_LAST; c += 1)`
export const CAP_FIRST: cap = cap::CHOWN;
export const CAP_LAST: cap = cap::CHECKPOINT_RESTORE;
// bitset of the above. cap::FOO corresponds to the *index* of a bit here.
// e.g. `let c: caps = (1 << cap::SYS_ADMIN)`
export type caps = u64;
@@ -342,3 +348,179 @@ export fn cap_ambient_lower(c: cap) (void | rt::errno) = {
export fn capbset_drop(c: cap) (void | rt::errno) = {
rt::prctl(rt::PR_CAPBSET_DROP, c: u64, 0, 0, 0)?;
};
export fn cap_tostring(c: cap) str = {
switch (c) {
case cap::AUDIT_CONTROL => return "CAP_AUDIT_CONTROL";
case cap::AUDIT_READ => return "CAP_AUDIT_READ";
case cap::AUDIT_WRITE => return "CAP_AUDIT_WRITE";
case cap::BLOCK_SUSPEND => return "CAP_BLOCK_SUSPEND";
case cap::BPF => return "CAP_BPF";
case cap::CHECKPOINT_RESTORE => return "CAP_CHECKPOINT_RESTORE";
case cap::CHOWN => return "CAP_CHOWN";
case cap::DAC_OVERRIDE => return "CAP_DAC_OVERRIDE";
case cap::DAC_READ_SEARCH => return "CAP_DAC_READ_SEARCH";
case cap::FOWNER => return "CAP_FOWNER";
case cap::FSETID => return "CAP_FSETID";
case cap::IPC_LOCK => return "CAP_IPC_LOCK";
case cap::IPC_OWNER => return "CAP_IPC_OWNER";
case cap::KILL => return "CAP_KILL";
case cap::LEASE => return "CAP_LEASE";
case cap::LINUX_IMMUTABLE => return "CAP_LINUX_IMMUTABLE";
case cap::MAC_ADMIN => return "CAP_MAC_ADMIN";
case cap::MAC_OVERRIDE => return "CAP_MAC_OVERRIDE";
case cap::MKNOD => return "CAP_MKNOD";
case cap::NET_ADMIN => return "CAP_NET_ADMIN";
case cap::NET_BIND_SERVICE => return "CAP_NET_BIND_SERVICE";
case cap::NET_BROADCAST => return "CAP_NET_BROADCAST";
case cap::NET_RAW => return "CAP_NET_RAW";
case cap::PERFMON => return "CAP_PERFMON";
case cap::SETFCAP => return "CAP_SETFCAP";
case cap::SETGID => return "CAP_SETGID";
case cap::SETPCAP => return "CAP_SETPCAP";
case cap::SETUID => return "CAP_SETUID";
case cap::SYSLOG => return "CAP_SYSLOG";
case cap::SYS_ADMIN => return "CAP_SYS_ADMIN";
case cap::SYS_BOOT => return "CAP_SYS_BOOT";
case cap::SYS_CHROOT => return "CAP_SYS_CHROOT";
case cap::SYS_MODULE => return "CAP_SYS_MODULE";
case cap::SYS_NICE => return "CAP_SYS_NICE";
case cap::SYS_PACCT => return "CAP_SYS_PACCT";
case cap::SYS_PTRACE => return "CAP_SYS_PTRACE";
case cap::SYS_RAWIO => return "CAP_SYS_RAWIO";
case cap::SYS_RESOURCE => return "CAP_SYS_RESOURCE";
case cap::SYS_TIME => return "CAP_SYS_TIME";
case cap::SYS_TTY_CONFIG => return "CAP_SYS_TTY_CONFIG";
case cap::WAKE_ALARM => return "CAP_WAKE_ALARM";
};
};
export fn cap_fromstr(v: str) (cap | errors::invalid) = {
// strip leading CAP_ and allow either form.
if (len(v) > 4 && ascii::strcasecmp(strings::sub(v, 0, 4), "CAP_") == 0)
v = strings::sub(v, 4);
if (ascii::strcasecmp(v, "AUDIT_CONTROL") == 0)
return cap::AUDIT_CONTROL;
if (ascii::strcasecmp(v, "AUDIT_READ") == 0)
return cap::AUDIT_READ;
if (ascii::strcasecmp(v, "AUDIT_WRITE") == 0)
return cap::AUDIT_WRITE;
if (ascii::strcasecmp(v, "BLOCK_SUSPEND") == 0)
return cap::BLOCK_SUSPEND;
if (ascii::strcasecmp(v, "BPF") == 0)
return cap::BPF;
if (ascii::strcasecmp(v, "CHECKPOINT_RESTORE") == 0)
return cap::CHECKPOINT_RESTORE;
if (ascii::strcasecmp(v, "CHOWN") == 0)
return cap::CHOWN;
if (ascii::strcasecmp(v, "DAC_OVERRIDE") == 0)
return cap::DAC_OVERRIDE;
if (ascii::strcasecmp(v, "DAC_READ_SEARCH") == 0)
return cap::DAC_READ_SEARCH;
if (ascii::strcasecmp(v, "FOWNER") == 0)
return cap::FOWNER;
if (ascii::strcasecmp(v, "FSETID") == 0)
return cap::FSETID;
if (ascii::strcasecmp(v, "IPC_LOCK") == 0)
return cap::IPC_LOCK;
if (ascii::strcasecmp(v, "IPC_OWNER") == 0)
return cap::IPC_OWNER;
if (ascii::strcasecmp(v, "KILL") == 0)
return cap::KILL;
if (ascii::strcasecmp(v, "LEASE") == 0)
return cap::LEASE;
if (ascii::strcasecmp(v, "LINUX_IMMUTABLE") == 0)
return cap::LINUX_IMMUTABLE;
if (ascii::strcasecmp(v, "MAC_ADMIN") == 0)
return cap::MAC_ADMIN;
if (ascii::strcasecmp(v, "MAC_OVERRIDE") == 0)
return cap::MAC_OVERRIDE;
if (ascii::strcasecmp(v, "MKNOD") == 0)
return cap::MKNOD;
if (ascii::strcasecmp(v, "NET_ADMIN") == 0)
return cap::NET_ADMIN;
if (ascii::strcasecmp(v, "NET_BIND_SERVICE") == 0)
return cap::NET_BIND_SERVICE;
if (ascii::strcasecmp(v, "NET_BROADCAST") == 0)
return cap::NET_BROADCAST;
if (ascii::strcasecmp(v, "NET_RAW") == 0)
return cap::NET_RAW;
if (ascii::strcasecmp(v, "PERFMON") == 0)
return cap::PERFMON;
if (ascii::strcasecmp(v, "SETFCAP") == 0)
return cap::SETFCAP;
if (ascii::strcasecmp(v, "SETGID") == 0)
return cap::SETGID;
if (ascii::strcasecmp(v, "SETPCAP") == 0)
return cap::SETPCAP;
if (ascii::strcasecmp(v, "SETUID") == 0)
return cap::SETUID;
if (ascii::strcasecmp(v, "SYS_ADMIN") == 0)
return cap::SYS_ADMIN;
if (ascii::strcasecmp(v, "SYS_BOOT") == 0)
return cap::SYS_BOOT;
if (ascii::strcasecmp(v, "SYS_CHROOT") == 0)
return cap::SYS_CHROOT;
if (ascii::strcasecmp(v, "SYS_MODULE") == 0)
return cap::SYS_MODULE;
if (ascii::strcasecmp(v, "SYS_NICE") == 0)
return cap::SYS_NICE;
if (ascii::strcasecmp(v, "SYS_PACCT") == 0)
return cap::SYS_PACCT;
if (ascii::strcasecmp(v, "SYS_PTRACE") == 0)
return cap::SYS_PTRACE;
if (ascii::strcasecmp(v, "SYS_RAWIO") == 0)
return cap::SYS_RAWIO;
if (ascii::strcasecmp(v, "SYS_RESOURCE") == 0)
return cap::SYS_RESOURCE;
if (ascii::strcasecmp(v, "SYS_TIME") == 0)
return cap::SYS_TIME;
if (ascii::strcasecmp(v, "SYS_TTY_CONFIG") == 0)
return cap::SYS_TTY_CONFIG;
if (ascii::strcasecmp(v, "SYSLOG") == 0)
return cap::SYSLOG;
if (ascii::strcasecmp(v, "WAKE_ALARM") == 0)
return cap::WAKE_ALARM;
return errors::invalid;
};
// return true if `s` parses to `expect`
fn _parse_eq(s: str, expect: (cap | errors::invalid)) bool = {
let got = cap_fromstr(s);
return match (expect) {
case let c: cap => yield match (got) {
case let c2: cap => yield c2 == c;
case => yield false;
};
case errors::invalid => yield match (got) {
case errors::invalid => yield true;
case => yield false;
};
};
};
@test fn cap_from_str_good() void = {
assert(_parse_eq("SYS_ADMIN", cap::SYS_ADMIN));
assert(_parse_eq("CAP_SYS_ADMIN", cap::SYS_ADMIN));
assert(_parse_eq("sys_admin", cap::SYS_ADMIN));
assert(_parse_eq("cap_sys_admin", cap::SYS_ADMIN));
assert(_parse_eq("CAP_sys_admin", cap::SYS_ADMIN));
assert(_parse_eq("cap_SYS_ADMIN", cap::SYS_ADMIN));
};
@test fn cap_from_str_bad() void = {
assert(_parse_eq("CAP_SYS_ADMIN_AND_MORE", errors::invalid));
assert(_parse_eq("SYS_ADMIN_CAP", errors::invalid));
assert(_parse_eq("SYS", errors::invalid));
assert(_parse_eq("", errors::invalid));
};
@test fn cap_roundtrip() void = {
assert(_parse_eq(cap_tostring(cap::SYS_ADMIN), cap::SYS_ADMIN));
};