grok-build-upstream-mirror/crates/codegen/xai-grok-sandbox/src/child_net.rs
grokkybara[bot] 69f0ba880a Synced from monorepo
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
2026-07-23 17:12:33 +00:00

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);
}
}