use super::policy::{NetworkPolicy, SandboxPolicy};
use nix::libc;
pub fn apply_sandbox(policy: &SandboxPolicy) -> Result<(), String> {
set_no_new_privs()?;
if let Err(e) = apply_landlock(policy) {
eprintln!("localgpt-sandbox: landlock not applied: {}", e);
}
if policy.network == NetworkPolicy::Deny {
if let Err(e) = apply_seccomp_network_deny() {
eprintln!("localgpt-sandbox: seccomp not applied: {}", e);
}
}
Ok(())
}
fn set_no_new_privs() -> Result<(), String> {
let ret = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
if ret != 0 {
return Err(format!(
"PR_SET_NO_NEW_PRIVS failed: {}",
std::io::Error::last_os_error()
));
}
Ok(())
}
fn apply_landlock(policy: &SandboxPolicy) -> Result<(), String> {
use landlock::{
ABI, Access, AccessFs, PathBeneath, PathFd, Ruleset, RulesetAttr, RulesetCreatedAttr,
RulesetStatus,
};
let abi = ABI::V5;
let read_access = AccessFs::ReadFile | AccessFs::ReadDir | AccessFs::Execute;
let write_access = read_access
| AccessFs::WriteFile
| AccessFs::RemoveFile
| AccessFs::RemoveDir
| AccessFs::MakeReg
| AccessFs::MakeDir
| AccessFs::MakeSym;
let mut ruleset = Ruleset::default()
.handle_access(AccessFs::from_all(abi))
.map_err(|e| format!("Landlock ruleset creation: {}", e))?
.create()
.map_err(|e| format!("Landlock ruleset create: {}", e))?;
for path in &policy.read_only_paths {
if path.exists() {
if let Ok(fd) = PathFd::new(path) {
let _ = (&mut ruleset).add_rule(PathBeneath::new(fd, read_access));
}
}
}
if policy.workspace_path.exists() {
if let Ok(fd) = PathFd::new(&policy.workspace_path) {
let _ = (&mut ruleset).add_rule(PathBeneath::new(fd, write_access));
}
}
for path in &policy.extra_write_paths {
if path.exists() {
if let Ok(fd) = PathFd::new(path) {
let _ = (&mut ruleset).add_rule(PathBeneath::new(fd, write_access));
}
}
}
let status = ruleset
.restrict_self()
.map_err(|e| format!("Landlock restrict_self: {}", e))?;
match status.ruleset {
RulesetStatus::FullyEnforced => {}
RulesetStatus::PartiallyEnforced => {
eprintln!("localgpt-sandbox: Landlock partially enforced (ABI downgrade)");
}
RulesetStatus::NotEnforced => {
return Err("Landlock not enforced by kernel".to_string());
}
}
Ok(())
}
fn apply_seccomp_network_deny() -> Result<(), String> {
use seccompiler::{BpfProgram, SeccompAction, SeccompFilter, SeccompRule, TargetArch};
use std::collections::BTreeMap;
let denied_syscalls: Vec<i64> = vec![
libc::SYS_socket,
libc::SYS_connect,
libc::SYS_accept,
libc::SYS_accept4,
libc::SYS_bind,
libc::SYS_listen,
libc::SYS_sendto,
libc::SYS_sendmsg,
libc::SYS_sendmmsg,
libc::SYS_recvfrom,
libc::SYS_recvmsg,
libc::SYS_recvmmsg,
libc::SYS_ptrace,
];
let mut rules: BTreeMap<i64, Vec<SeccompRule>> = BTreeMap::new();
for syscall in denied_syscalls {
rules.insert(syscall, vec![SeccompRule::new(vec![]).unwrap()]);
}
let target_arch: TargetArch = std::env::consts::ARCH
.try_into()
.map_err(|e: seccompiler::BackendError| format!("seccomp unsupported arch: {}", e))?;
let filter = SeccompFilter::new(
rules,
SeccompAction::Allow, SeccompAction::Errno(libc::EPERM as u32), target_arch,
)
.map_err(|e| format!("seccomp filter creation: {}", e))?;
let bpf: BpfProgram = filter
.try_into()
.map_err(|e: seccompiler::BackendError| format!("seccomp BPF compilation: {}", e))?;
seccompiler::apply_filter(&bpf).map_err(|e| format!("seccomp apply_filter: {}", e))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_seccomp_syscall_list_is_valid() {
let syscalls = [
libc::SYS_socket,
libc::SYS_connect,
libc::SYS_accept,
libc::SYS_accept4,
libc::SYS_bind,
libc::SYS_listen,
libc::SYS_sendto,
libc::SYS_sendmsg,
libc::SYS_sendmmsg,
libc::SYS_recvfrom,
libc::SYS_recvmsg,
libc::SYS_recvmmsg,
libc::SYS_ptrace,
];
for syscall in syscalls {
assert!(syscall > 0, "Invalid syscall number: {}", syscall);
}
}
}