Synced from monorepo Changes: - Workspace server: report `/ready` as failed with dwell on hub connect failure - Refresh OIDC token for the Grok agent in the shell - ACP terminal output recorder - Cross-platform provider auth commands in the shell - Default `/resume` to Grok sessions with a hint for hidden external sessions - Resume sessions by title with `--resume` - Limit app-builder archive size - Data-driven tag labels for slash commands - Doctor fixes for tmux - Custom provider gateways and subprocess environment policy in the shell - `/tutorial` — opt-in onboarding tour of Grok Build - Soft and required CLI version checks in the shell - Privacy banner env overrides survive live settings updates - Add remote flag to override the image-edit model - Return profile fields from auth info even when the access token is expired - Add edit control on queued prompt rows - Keep fail-closed policy when clearing orphans with no team - Setting to disable the Ctrl+Space/F8 voice shortcut - Pass `--raw` to pw-record so Linux dictation works on older PipeWire - Validate git URLs when adding marketplace entries - Stop shipping stale tool-doc parameter and tool names - Re-point dashboard attach after `/fork` only when the parent was attached - Surface Grok Computer media-generation results as file-path chunks - Clear web background-task tray on kill and keep the task description - Show privacy upsell banner in agent view until acted on - Add tools-server client callback surface - Protect persistent global hook sources Source-Revision: 95d84f443eddcbed6cbfd6eed22e2eafe6b3939d
363 lines
12 KiB
Rust
363 lines
12 KiB
Rust
//! Seccomp: child network filter (pre_exec) and process-wide namespace lockdown.
|
|
|
|
#[cfg(target_os = "linux")]
|
|
mod ns_lockdown {
|
|
use libc::sock_filter;
|
|
|
|
pub(super) const SECCOMP_RET_ALLOW: u32 = 0x7fff_0000;
|
|
pub(super) const SECCOMP_RET_ERRNO: u32 = 0x0005_0000;
|
|
pub(super) const EPERM_VAL: u32 = 1;
|
|
/// ENOSYS: libc treats clone3 as unavailable and falls back to legacy clone.
|
|
pub(super) const ENOSYS_VAL: u32 = libc::ENOSYS as u32;
|
|
#[cfg(target_arch = "x86_64")]
|
|
pub(super) const X32_SYSCALL_BIT: u32 = 0x4000_0000;
|
|
|
|
pub(super) const OFF_NR: u32 = 0;
|
|
pub(super) const OFF_ARCH: u32 = 4;
|
|
pub(super) const OFF_ARGS0_LO: u32 = 16; // LE low half of args[0]
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
pub(super) const EXPECTED_ARCH: u32 = 0xc000_003e; // AUDIT_ARCH_X86_64
|
|
#[cfg(target_arch = "aarch64")]
|
|
pub(super) const EXPECTED_ARCH: u32 = 0xc000_00b7; // AUDIT_ARCH_AARCH64
|
|
#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
|
|
pub(super) const EXPECTED_ARCH: u32 = 0;
|
|
|
|
pub(super) const CLONE_NAMESPACE_BITS: u32 = (libc::CLONE_NEWNS as u32)
|
|
| (libc::CLONE_NEWCGROUP as u32)
|
|
| (libc::CLONE_NEWUTS as u32)
|
|
| (libc::CLONE_NEWIPC as u32)
|
|
| (libc::CLONE_NEWUSER as u32)
|
|
| (libc::CLONE_NEWPID as u32)
|
|
| (libc::CLONE_NEWNET as u32)
|
|
| (libc::CLONE_NEWTIME as u32);
|
|
|
|
/// Linux `clone3` (arch-portable number; not always exported by libc).
|
|
pub(super) const SYS_CLONE3: u32 = 435;
|
|
|
|
fn stmt(code: u32, k: u32) -> sock_filter {
|
|
sock_filter {
|
|
code: code as u16,
|
|
jt: 0,
|
|
jf: 0,
|
|
k,
|
|
}
|
|
}
|
|
|
|
fn jump(code: u32, k: u32, jt: u8, jf: u8) -> sock_filter {
|
|
sock_filter {
|
|
code: code as u16,
|
|
jt,
|
|
jf,
|
|
k,
|
|
}
|
|
}
|
|
|
|
/// Classic BPF namespace lockdown.
|
|
///
|
|
/// - `unshare` / `setns` / legacy `clone(CLONE_NEW*)` → EPERM
|
|
/// - `clone3` → ENOSYS (flags live in a pointed-to struct classic BPF cannot
|
|
/// inspect; ENOSYS makes libc fall back to legacy clone for ordinary
|
|
/// spawn, while direct malicious clone3 cannot create namespaces)
|
|
pub fn build_namespace_lockdown_filter() -> Vec<sock_filter> {
|
|
use libc::{
|
|
BPF_ABS, BPF_JEQ, BPF_JMP, BPF_JSET, BPF_K, BPF_LD, BPF_RET, BPF_W, SYS_clone,
|
|
SYS_setns, SYS_unshare,
|
|
};
|
|
|
|
let mut f = Vec::with_capacity(22);
|
|
f.push(stmt(BPF_LD | BPF_W | BPF_ABS, OFF_ARCH));
|
|
f.push(jump(BPF_JMP | BPF_JEQ | BPF_K, EXPECTED_ARCH, 1, 0));
|
|
f.push(stmt(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM_VAL));
|
|
f.push(stmt(BPF_LD | BPF_W | BPF_ABS, OFF_NR));
|
|
#[cfg(target_arch = "x86_64")]
|
|
{
|
|
f.push(jump(BPF_JMP | BPF_JSET | BPF_K, X32_SYSCALL_BIT, 0, 1));
|
|
f.push(stmt(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM_VAL));
|
|
}
|
|
for sys in [SYS_unshare as u32, SYS_setns as u32] {
|
|
f.push(jump(BPF_JMP | BPF_JEQ | BPF_K, sys, 0, 1));
|
|
f.push(stmt(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM_VAL));
|
|
}
|
|
f.push(jump(BPF_JMP | BPF_JEQ | BPF_K, SYS_CLONE3, 0, 1));
|
|
f.push(stmt(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | ENOSYS_VAL));
|
|
f.push(jump(BPF_JMP | BPF_JEQ | BPF_K, SYS_clone as u32, 0, 3));
|
|
f.push(stmt(BPF_LD | BPF_W | BPF_ABS, OFF_ARGS0_LO));
|
|
f.push(jump(BPF_JMP | BPF_JSET | BPF_K, CLONE_NAMESPACE_BITS, 0, 1));
|
|
f.push(stmt(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM_VAL));
|
|
f.push(stmt(BPF_RET | BPF_K, SECCOMP_RET_ALLOW));
|
|
f
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn filter_jeq_immediates(filter: &[sock_filter]) -> Vec<u32> {
|
|
use libc::{BPF_JEQ, BPF_JMP, BPF_K};
|
|
let jeq = (BPF_JMP | BPF_JEQ | BPF_K) as u16;
|
|
filter
|
|
.iter()
|
|
.filter(|i| i.code == jeq)
|
|
.map(|i| i.k)
|
|
.collect()
|
|
}
|
|
|
|
pub fn install(filter: &mut [sock_filter]) -> std::io::Result<()> {
|
|
use libc::{
|
|
PR_SET_NO_NEW_PRIVS, SECCOMP_FILTER_FLAG_TSYNC, SECCOMP_SET_MODE_FILTER, SYS_seccomp,
|
|
prctl, sock_fprog,
|
|
};
|
|
|
|
let prog = sock_fprog {
|
|
len: filter.len() as u16,
|
|
filter: filter.as_mut_ptr(),
|
|
};
|
|
|
|
// SAFETY: standard NO_NEW_PRIVS before seccomp.
|
|
if unsafe { prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } != 0 {
|
|
return Err(std::io::Error::last_os_error());
|
|
}
|
|
|
|
// SAFETY: prog valid for the duration of the syscall.
|
|
// rc: 0 ok; >0 TSYNC failing TID; -1 errno.
|
|
let rc = unsafe {
|
|
libc::syscall(
|
|
SYS_seccomp,
|
|
SECCOMP_SET_MODE_FILTER as libc::c_long,
|
|
SECCOMP_FILTER_FLAG_TSYNC as libc::c_long,
|
|
&prog as *const sock_fprog as *const libc::c_void,
|
|
)
|
|
};
|
|
if rc == 0 {
|
|
return Ok(());
|
|
}
|
|
if rc > 0 {
|
|
return Err(std::io::Error::other(format!(
|
|
"seccomp TSYNC failed: thread {rc} could not install filter"
|
|
)));
|
|
}
|
|
Err(std::io::Error::last_os_error())
|
|
}
|
|
}
|
|
|
|
/// # Safety
|
|
/// After fork / before exec.
|
|
#[cfg(target_os = "linux")]
|
|
pub unsafe fn install_child_network_filter() -> std::io::Result<()> {
|
|
use libc::{
|
|
BPF_ABS, BPF_JEQ, BPF_JMP, BPF_K, BPF_LD, BPF_RET, BPF_W, PR_SET_NO_NEW_PRIVS,
|
|
PR_SET_SECCOMP, SECCOMP_MODE_FILTER, SYS_accept, SYS_accept4, SYS_bind, SYS_connect,
|
|
SYS_listen, SYS_sendmsg, SYS_sendto, prctl, sock_filter, sock_fprog,
|
|
};
|
|
|
|
const SECCOMP_RET_ALLOW: u32 = 0x7fff_0000;
|
|
const SECCOMP_RET_ERRNO: u32 = 0x0005_0000;
|
|
const EPERM_VAL: u32 = 1;
|
|
|
|
let blocked: &[i64] = &[
|
|
SYS_connect,
|
|
SYS_bind,
|
|
SYS_sendto,
|
|
SYS_sendmsg,
|
|
SYS_listen,
|
|
SYS_accept,
|
|
SYS_accept4,
|
|
];
|
|
let mut filter: Vec<sock_filter> = Vec::new();
|
|
filter.push(sock_filter {
|
|
code: (BPF_LD | BPF_W | BPF_ABS) as u16,
|
|
jt: 0,
|
|
jf: 0,
|
|
k: 0,
|
|
});
|
|
let n = blocked.len();
|
|
for (i, &sys) in blocked.iter().enumerate() {
|
|
let remaining = n - i - 1;
|
|
filter.push(sock_filter {
|
|
code: (BPF_JMP | BPF_JEQ | BPF_K) as u16,
|
|
jt: remaining as u8 + 1,
|
|
jf: 0,
|
|
k: sys as u32,
|
|
});
|
|
}
|
|
filter.push(sock_filter {
|
|
code: (BPF_RET | BPF_K) as u16,
|
|
jt: 0,
|
|
jf: 0,
|
|
k: SECCOMP_RET_ALLOW,
|
|
});
|
|
filter.push(sock_filter {
|
|
code: (BPF_RET | BPF_K) as u16,
|
|
jt: 0,
|
|
jf: 0,
|
|
k: SECCOMP_RET_ERRNO | EPERM_VAL,
|
|
});
|
|
let prog = sock_fprog {
|
|
len: filter.len() as u16,
|
|
filter: filter.as_mut_ptr(),
|
|
};
|
|
if unsafe { prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } != 0 {
|
|
return Err(std::io::Error::last_os_error());
|
|
}
|
|
if unsafe {
|
|
prctl(
|
|
PR_SET_SECCOMP,
|
|
SECCOMP_MODE_FILTER as libc::c_ulong,
|
|
&prog as *const _ as libc::c_ulong,
|
|
0,
|
|
0,
|
|
)
|
|
} != 0
|
|
{
|
|
return Err(std::io::Error::last_os_error());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Deny nested namespace creation on all threads (TSYNC).
|
|
/// Ordinary process creation uses legacy clone after clone3 returns ENOSYS.
|
|
///
|
|
/// # Safety
|
|
/// Process-wide; call after bwrap re-exec / at apply.
|
|
#[cfg(target_os = "linux")]
|
|
pub unsafe fn install_namespace_lockdown_filter() -> std::io::Result<()> {
|
|
let mut filter = ns_lockdown::build_namespace_lockdown_filter();
|
|
ns_lockdown::install(&mut filter)
|
|
}
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
pub unsafe fn install_child_network_filter() -> std::io::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
pub unsafe fn install_namespace_lockdown_filter() -> std::io::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(all(test, target_os = "linux"))]
|
|
mod tests {
|
|
use super::ns_lockdown::*;
|
|
use libc::{SYS_clone, SYS_setns, SYS_unshare, sock_filter};
|
|
|
|
/// Minimal classic-BPF interpreter over synthetic seccomp_data fields.
|
|
fn eval(filter: &[sock_filter], arch: u32, nr: u32, arg0_lo: u32) -> u32 {
|
|
use libc::{BPF_ABS, BPF_JEQ, BPF_JMP, BPF_JSET, BPF_K, BPF_LD, BPF_RET, BPF_W};
|
|
let mut pc = 0usize;
|
|
let mut a = 0u32;
|
|
for _ in 0..filter.len().saturating_mul(2) {
|
|
let insn = &filter[pc];
|
|
let op = insn.code as u32;
|
|
if op == (BPF_LD | BPF_W | BPF_ABS) {
|
|
a = match insn.k {
|
|
OFF_NR => nr,
|
|
OFF_ARCH => arch,
|
|
OFF_ARGS0_LO => arg0_lo,
|
|
_ => 0,
|
|
};
|
|
pc += 1;
|
|
} else if op == (BPF_JMP | BPF_JEQ | BPF_K) {
|
|
pc = if a == insn.k {
|
|
pc + 1 + insn.jt as usize
|
|
} else {
|
|
pc + 1 + insn.jf as usize
|
|
};
|
|
} else if op == (BPF_JMP | BPF_JSET | BPF_K) {
|
|
pc = if a & insn.k != 0 {
|
|
pc + 1 + insn.jt as usize
|
|
} else {
|
|
pc + 1 + insn.jf as usize
|
|
};
|
|
} else if op == (BPF_RET | BPF_K) {
|
|
return insn.k;
|
|
} else {
|
|
panic!("unsupported opcode {:#x} at {pc}", insn.code);
|
|
}
|
|
if pc >= filter.len() {
|
|
panic!("pc out of range");
|
|
}
|
|
}
|
|
panic!("filter did not RET");
|
|
}
|
|
|
|
fn is_allow(r: u32) -> bool {
|
|
r == SECCOMP_RET_ALLOW
|
|
}
|
|
fn is_eperm(r: u32) -> bool {
|
|
r == (SECCOMP_RET_ERRNO | EPERM_VAL)
|
|
}
|
|
fn is_enosys(r: u32) -> bool {
|
|
r == (SECCOMP_RET_ERRNO | ENOSYS_VAL)
|
|
}
|
|
|
|
#[test]
|
|
fn namespace_filter_targets_unshare_setns_clone3_and_clone() {
|
|
let f = build_namespace_lockdown_filter();
|
|
let jeqs = filter_jeq_immediates(&f);
|
|
assert!(jeqs.contains(&(SYS_unshare as u32)), "{jeqs:?}");
|
|
assert!(jeqs.contains(&(SYS_setns as u32)), "{jeqs:?}");
|
|
assert!(jeqs.contains(&SYS_CLONE3), "{jeqs:?}");
|
|
assert!(jeqs.contains(&(SYS_clone as u32)), "{jeqs:?}");
|
|
assert!(jeqs.contains(&EXPECTED_ARCH), "{jeqs:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn bpf_eval_ordinary_clone_allowed_namespace_clone_denied() {
|
|
let f = build_namespace_lockdown_filter();
|
|
// Ordinary clone/fork flags (no NEW*)
|
|
assert!(is_allow(eval(
|
|
&f,
|
|
EXPECTED_ARCH,
|
|
SYS_clone as u32,
|
|
0x11 /* SIGCHLD | CLONE_VM-ish low bits without NEW* */
|
|
)));
|
|
assert!(is_eperm(eval(
|
|
&f,
|
|
EXPECTED_ARCH,
|
|
SYS_clone as u32,
|
|
libc::CLONE_NEWUSER as u32
|
|
)));
|
|
assert!(is_eperm(eval(
|
|
&f,
|
|
EXPECTED_ARCH,
|
|
SYS_clone as u32,
|
|
libc::CLONE_NEWNS as u32
|
|
)));
|
|
}
|
|
|
|
#[test]
|
|
fn bpf_eval_clone3_enosys_unshare_setns_eperm_read_allowed() {
|
|
let f = build_namespace_lockdown_filter();
|
|
assert!(is_enosys(eval(&f, EXPECTED_ARCH, SYS_CLONE3, 0)));
|
|
assert!(is_eperm(eval(&f, EXPECTED_ARCH, SYS_unshare as u32, 0)));
|
|
assert!(is_eperm(eval(&f, EXPECTED_ARCH, SYS_setns as u32, 0)));
|
|
assert!(is_allow(eval(&f, EXPECTED_ARCH, 0, 0)));
|
|
}
|
|
|
|
#[test]
|
|
fn bpf_eval_wrong_arch_and_x32_denied() {
|
|
let f = build_namespace_lockdown_filter();
|
|
assert!(is_eperm(eval(&f, 0xdead_beef, SYS_clone as u32, 0)));
|
|
#[cfg(target_arch = "x86_64")]
|
|
{
|
|
// x32: nr has high bit set
|
|
assert!(is_eperm(eval(
|
|
&f,
|
|
EXPECTED_ARCH,
|
|
(SYS_unshare as u32) | X32_SYSCALL_BIT,
|
|
0
|
|
)));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn namespace_bits_cover_user_ns_and_mount_ns() {
|
|
assert_ne!(CLONE_NAMESPACE_BITS & (libc::CLONE_NEWUSER as u32), 0);
|
|
assert_ne!(CLONE_NAMESPACE_BITS & (libc::CLONE_NEWNS as u32), 0);
|
|
assert_ne!(CLONE_NAMESPACE_BITS & (libc::CLONE_NEWNET as u32), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn filter_ends_with_allow() {
|
|
let f = build_namespace_lockdown_filter();
|
|
assert_eq!(f.last().unwrap().k, SECCOMP_RET_ALLOW);
|
|
}
|
|
}
|