From bc2823d622003bacbcaa6879d7dbd10297fc06d0 Mon Sep 17 00:00:00 2001 From: Colin Date: Mon, 2 Sep 2024 18:55:53 +0000 Subject: [PATCH] bunpen: better (still incomplete) capability boxing --- pkgs/additional/bunpen/config/autodetect.ha | 5 +- pkgs/additional/bunpen/config/capability.ha | 129 ------------- pkgs/additional/bunpen/config/cli.ha | 13 +- pkgs/additional/bunpen/main.ha | 11 +- pkgs/additional/bunpen/restrict/caps.ha | 32 ++- pkgs/additional/bunpen/rt/ext/capabilities.ha | 182 ++++++++++++++++++ 6 files changed, 230 insertions(+), 142 deletions(-) delete mode 100644 pkgs/additional/bunpen/config/capability.ha diff --git a/pkgs/additional/bunpen/config/autodetect.ha b/pkgs/additional/bunpen/config/autodetect.ha index aaa1360dd..402f9a840 100644 --- a/pkgs/additional/bunpen/config/autodetect.ha +++ b/pkgs/additional/bunpen/config/autodetect.ha @@ -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; }; }; diff --git a/pkgs/additional/bunpen/config/capability.ha b/pkgs/additional/bunpen/config/capability.ha deleted file mode 100644 index 41c56ee5d..000000000 --- a/pkgs/additional/bunpen/config/capability.ha +++ /dev/null @@ -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)); -}; diff --git a/pkgs/additional/bunpen/config/cli.ha b/pkgs/additional/bunpen/config/cli.ha index 6d538468b..079185b0f 100644 --- a/pkgs/additional/bunpen/config/cli.ha +++ b/pkgs/additional/bunpen/config/cli.ha @@ -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)?); }; }; diff --git a/pkgs/additional/bunpen/main.ha b/pkgs/additional/bunpen/main.ha index fa2bd023e..3de82c54b 100644 --- a/pkgs/additional/bunpen/main.ha +++ b/pkgs/additional/bunpen/main.ha @@ -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); diff --git a/pkgs/additional/bunpen/restrict/caps.ha b/pkgs/additional/bunpen/restrict/caps.ha index 99922264c..e1242c33e 100644 --- a/pkgs/additional/bunpen/restrict/caps.ha +++ b/pkgs/additional/bunpen/restrict/caps.ha @@ -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"); }; diff --git a/pkgs/additional/bunpen/rt/ext/capabilities.ha b/pkgs/additional/bunpen/rt/ext/capabilities.ha index 1aea046c3..f0363d182 100644 --- a/pkgs/additional/bunpen/rt/ext/capabilities.ha +++ b/pkgs/additional/bunpen/rt/ext/capabilities.ha @@ -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)); +};