Skip to main content

sandlock_core/
policy.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::time::SystemTime;
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::PolicyError;
8
9/// A byte size value.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub struct ByteSize(pub u64);
12
13impl ByteSize {
14    pub fn bytes(n: u64) -> Self {
15        ByteSize(n)
16    }
17
18    pub fn kib(n: u64) -> Self {
19        ByteSize(n * 1024)
20    }
21
22    pub fn mib(n: u64) -> Self {
23        ByteSize(n * 1024 * 1024)
24    }
25
26    pub fn gib(n: u64) -> Self {
27        ByteSize(n * 1024 * 1024 * 1024)
28    }
29
30    pub fn parse(s: &str) -> Result<Self, PolicyError> {
31        let s = s.trim();
32        if s.is_empty() {
33            return Err(PolicyError::Invalid("empty byte size string".into()));
34        }
35
36        // Check for suffix
37        let last = s.chars().last().unwrap();
38        if last.is_ascii_alphabetic() {
39            let (num_str, suffix) = s.split_at(s.len() - 1);
40            let n: u64 = num_str
41                .trim()
42                .parse()
43                .map_err(|_| PolicyError::Invalid(format!("invalid byte size: {}", s)))?;
44            match suffix.to_ascii_uppercase().as_str() {
45                "K" => Ok(ByteSize::kib(n)),
46                "M" => Ok(ByteSize::mib(n)),
47                "G" => Ok(ByteSize::gib(n)),
48                other => Err(PolicyError::Invalid(format!("unknown byte size suffix: {}", other))),
49            }
50        } else {
51            let n: u64 = s
52                .parse()
53                .map_err(|_| PolicyError::Invalid(format!("invalid byte size: {}", s)))?;
54            Ok(ByteSize(n))
55        }
56    }
57}
58
59/// Filesystem isolation mode.
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
61pub enum FsIsolation {
62    #[default]
63    None,
64    OverlayFs,
65    BranchFs,
66}
67
68/// Action to take on branch exit.
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
70pub enum BranchAction {
71    #[default]
72    Commit,
73    Abort,
74    Keep,
75}
76
77/// Sandbox policy configuration.
78#[derive(Clone, Serialize, Deserialize)]
79pub struct Policy {
80    // Filesystem access
81    pub fs_writable: Vec<PathBuf>,
82    pub fs_readable: Vec<PathBuf>,
83    pub fs_denied: Vec<PathBuf>,
84
85    // Syscall filtering
86    pub deny_syscalls: Option<Vec<String>>,
87    pub allow_syscalls: Option<Vec<String>>,
88
89    // Network
90    pub net_allow_hosts: Vec<String>,
91    pub net_bind: Vec<u16>,
92    pub net_connect: Vec<u16>,
93    pub no_raw_sockets: bool,
94    pub no_udp: bool,
95
96    // Namespace isolation
97    pub isolate_ipc: bool,
98    pub isolate_signals: bool,
99    pub isolate_pids: bool,
100
101    // Resource limits
102    pub max_memory: Option<ByteSize>,
103    pub max_processes: u32,
104    pub max_open_files: Option<u32>,
105    pub max_cpu: Option<u8>,
106
107    // Reproducibility
108    pub random_seed: Option<u64>,
109    pub time_start: Option<SystemTime>,
110    pub no_randomize_memory: bool,
111    pub no_huge_pages: bool,
112    pub no_coredump: bool,
113    pub deterministic_dirs: bool,
114    pub hostname: Option<String>,
115
116    // Filesystem branch
117    pub fs_isolation: FsIsolation,
118    pub workdir: Option<PathBuf>,
119    pub cwd: Option<PathBuf>,
120    pub fs_storage: Option<PathBuf>,
121    pub max_disk: Option<ByteSize>,
122    pub on_exit: BranchAction,
123    pub on_error: BranchAction,
124
125    // Environment
126    pub chroot: Option<PathBuf>,
127    pub clean_env: bool,
128    pub env: HashMap<String, String>,
129    pub close_fds: bool,
130
131    // Devices
132    pub gpu_devices: Option<Vec<u32>>,
133
134    // CPU
135    pub cpu_cores: Option<Vec<u32>>,
136    pub num_cpus: Option<u32>,
137    pub port_remap: bool,
138
139    // User namespace
140    pub uid: Option<u32>,
141
142    // Dynamic policy callback
143    #[serde(skip)]
144    pub policy_fn: Option<crate::policy_fn::PolicyCallback>,
145}
146
147impl std::fmt::Debug for Policy {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.debug_struct("Policy")
150            .field("fs_readable", &self.fs_readable)
151            .field("fs_writable", &self.fs_writable)
152            .field("max_memory", &self.max_memory)
153            .field("max_processes", &self.max_processes)
154            .field("policy_fn", &self.policy_fn.as_ref().map(|_| "<callback>"))
155            .finish_non_exhaustive()
156    }
157}
158
159impl Policy {
160    pub fn builder() -> PolicyBuilder {
161        PolicyBuilder::default()
162    }
163}
164
165/// Fluent builder for `Policy`.
166#[derive(Default)]
167pub struct PolicyBuilder {
168    fs_writable: Vec<PathBuf>,
169    fs_readable: Vec<PathBuf>,
170    fs_denied: Vec<PathBuf>,
171
172    deny_syscalls: Option<Vec<String>>,
173    allow_syscalls: Option<Vec<String>>,
174
175    net_allow_hosts: Vec<String>,
176    net_bind: Vec<u16>,
177    net_connect: Vec<u16>,
178    no_raw_sockets: Option<bool>,
179    no_udp: bool,
180
181    isolate_ipc: bool,
182    isolate_signals: bool,
183    isolate_pids: bool,
184
185    max_memory: Option<ByteSize>,
186    max_processes: Option<u32>,
187    max_open_files: Option<u32>,
188    max_cpu: Option<u8>,
189
190    random_seed: Option<u64>,
191    time_start: Option<SystemTime>,
192    no_randomize_memory: bool,
193    no_huge_pages: bool,
194    no_coredump: bool,
195    deterministic_dirs: bool,
196    hostname: Option<String>,
197
198    fs_isolation: Option<FsIsolation>,
199    workdir: Option<PathBuf>,
200    cwd: Option<PathBuf>,
201    fs_storage: Option<PathBuf>,
202    max_disk: Option<ByteSize>,
203    on_exit: Option<BranchAction>,
204    on_error: Option<BranchAction>,
205
206    chroot: Option<PathBuf>,
207    clean_env: bool,
208    env: HashMap<String, String>,
209    close_fds: Option<bool>,
210
211    gpu_devices: Option<Vec<u32>>,
212
213    cpu_cores: Option<Vec<u32>>,
214    num_cpus: Option<u32>,
215    port_remap: bool,
216
217    uid: Option<u32>,
218    policy_fn: Option<crate::policy_fn::PolicyCallback>,
219}
220
221impl PolicyBuilder {
222    pub fn fs_write(mut self, path: impl Into<PathBuf>) -> Self {
223        self.fs_writable.push(path.into());
224        self
225    }
226
227    pub fn fs_read(mut self, path: impl Into<PathBuf>) -> Self {
228        self.fs_readable.push(path.into());
229        self
230    }
231
232    pub fn fs_deny(mut self, path: impl Into<PathBuf>) -> Self {
233        self.fs_denied.push(path.into());
234        self
235    }
236
237    pub fn deny_syscalls(mut self, calls: Vec<String>) -> Self {
238        self.deny_syscalls = Some(calls);
239        self
240    }
241
242    pub fn allow_syscalls(mut self, calls: Vec<String>) -> Self {
243        self.allow_syscalls = Some(calls);
244        self
245    }
246
247    pub fn net_allow_host(mut self, host: impl Into<String>) -> Self {
248        self.net_allow_hosts.push(host.into());
249        self
250    }
251
252    pub fn net_bind_port(mut self, port: u16) -> Self {
253        self.net_bind.push(port);
254        self
255    }
256
257    pub fn net_connect_port(mut self, port: u16) -> Self {
258        self.net_connect.push(port);
259        self
260    }
261
262    pub fn no_raw_sockets(mut self, v: bool) -> Self {
263        self.no_raw_sockets = Some(v);
264        self
265    }
266
267    pub fn no_udp(mut self, v: bool) -> Self {
268        self.no_udp = v;
269        self
270    }
271
272    pub fn isolate_ipc(mut self, v: bool) -> Self {
273        self.isolate_ipc = v;
274        self
275    }
276
277    pub fn isolate_signals(mut self, v: bool) -> Self {
278        self.isolate_signals = v;
279        self
280    }
281
282    pub fn isolate_pids(mut self, v: bool) -> Self {
283        self.isolate_pids = v;
284        self
285    }
286
287    pub fn max_memory(mut self, size: ByteSize) -> Self {
288        self.max_memory = Some(size);
289        self
290    }
291
292    pub fn max_processes(mut self, n: u32) -> Self {
293        self.max_processes = Some(n);
294        self
295    }
296
297    pub fn max_open_files(mut self, n: u32) -> Self {
298        self.max_open_files = Some(n);
299        self
300    }
301
302    pub fn max_cpu(mut self, pct: u8) -> Self {
303        self.max_cpu = Some(pct);
304        self
305    }
306
307    pub fn random_seed(mut self, seed: u64) -> Self {
308        self.random_seed = Some(seed);
309        self
310    }
311
312    pub fn time_start(mut self, t: SystemTime) -> Self {
313        self.time_start = Some(t);
314        self
315    }
316
317    pub fn no_randomize_memory(mut self, v: bool) -> Self {
318        self.no_randomize_memory = v;
319        self
320    }
321
322    pub fn no_huge_pages(mut self, v: bool) -> Self {
323        self.no_huge_pages = v;
324        self
325    }
326
327    pub fn no_coredump(mut self, v: bool) -> Self {
328        self.no_coredump = v;
329        self
330    }
331
332    pub fn deterministic_dirs(mut self, v: bool) -> Self {
333        self.deterministic_dirs = v;
334        self
335    }
336
337    pub fn hostname(mut self, name: impl Into<String>) -> Self {
338        self.hostname = Some(name.into());
339        self
340    }
341
342    pub fn fs_isolation(mut self, iso: FsIsolation) -> Self {
343        self.fs_isolation = Some(iso);
344        self
345    }
346
347    pub fn workdir(mut self, path: impl Into<PathBuf>) -> Self {
348        self.workdir = Some(path.into());
349        self
350    }
351
352    pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
353        self.cwd = Some(path.into());
354        self
355    }
356
357    pub fn fs_storage(mut self, path: impl Into<PathBuf>) -> Self {
358        self.fs_storage = Some(path.into());
359        self
360    }
361
362    pub fn max_disk(mut self, size: ByteSize) -> Self {
363        self.max_disk = Some(size);
364        self
365    }
366
367    pub fn on_exit(mut self, action: BranchAction) -> Self {
368        self.on_exit = Some(action);
369        self
370    }
371
372    pub fn on_error(mut self, action: BranchAction) -> Self {
373        self.on_error = Some(action);
374        self
375    }
376
377    pub fn chroot(mut self, path: impl Into<PathBuf>) -> Self {
378        self.chroot = Some(path.into());
379        self
380    }
381
382    pub fn clean_env(mut self, v: bool) -> Self {
383        self.clean_env = v;
384        self
385    }
386
387    pub fn env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
388        self.env.insert(key.into(), value.into());
389        self
390    }
391
392    pub fn close_fds(mut self, v: bool) -> Self {
393        self.close_fds = Some(v);
394        self
395    }
396
397    pub fn gpu_devices(mut self, devices: Vec<u32>) -> Self {
398        self.gpu_devices = Some(devices);
399        self
400    }
401
402    pub fn cpu_cores(mut self, cores: Vec<u32>) -> Self {
403        self.cpu_cores = Some(cores);
404        self
405    }
406
407    pub fn num_cpus(mut self, n: u32) -> Self {
408        self.num_cpus = Some(n);
409        self
410    }
411
412    pub fn port_remap(mut self, v: bool) -> Self {
413        self.port_remap = v;
414        self
415    }
416
417    pub fn policy_fn(
418        mut self,
419        f: impl Fn(crate::policy_fn::SyscallEvent, &mut crate::policy_fn::PolicyContext) -> crate::policy_fn::Verdict + Send + Sync + 'static,
420    ) -> Self {
421        self.policy_fn = Some(std::sync::Arc::new(f));
422        self
423    }
424
425    pub fn uid(mut self, id: u32) -> Self {
426        self.uid = Some(id);
427        self
428    }
429
430    pub fn build(self) -> Result<Policy, PolicyError> {
431        // Validate: deny_syscalls and allow_syscalls are mutually exclusive
432        if self.deny_syscalls.is_some() && self.allow_syscalls.is_some() {
433            return Err(PolicyError::MutuallyExclusiveSyscalls);
434        }
435
436        // Validate: max_cpu must be 1-100
437        if let Some(cpu) = self.max_cpu {
438            if cpu == 0 || cpu > 100 {
439                return Err(PolicyError::InvalidCpuPercent(cpu));
440            }
441        }
442
443        // Validate: fs_isolation != None requires workdir
444        let fs_isolation = self.fs_isolation.unwrap_or_default();
445        if fs_isolation != FsIsolation::None && self.workdir.is_none() {
446            return Err(PolicyError::FsIsolationRequiresWorkdir);
447        }
448
449        Ok(Policy {
450            fs_writable: self.fs_writable,
451            fs_readable: self.fs_readable,
452            fs_denied: self.fs_denied,
453            deny_syscalls: self.deny_syscalls,
454            allow_syscalls: self.allow_syscalls,
455            net_allow_hosts: self.net_allow_hosts,
456            net_bind: self.net_bind,
457            net_connect: self.net_connect,
458            no_raw_sockets: self.no_raw_sockets.unwrap_or(true),
459            no_udp: self.no_udp,
460            isolate_ipc: self.isolate_ipc,
461            isolate_signals: self.isolate_signals,
462            isolate_pids: self.isolate_pids,
463            max_memory: self.max_memory,
464            max_processes: self.max_processes.unwrap_or(64),
465            max_open_files: self.max_open_files,
466            max_cpu: self.max_cpu,
467            random_seed: self.random_seed,
468            time_start: self.time_start,
469            no_randomize_memory: self.no_randomize_memory,
470            no_huge_pages: self.no_huge_pages,
471            no_coredump: self.no_coredump,
472            deterministic_dirs: self.deterministic_dirs,
473            hostname: self.hostname,
474            fs_isolation,
475            workdir: self.workdir,
476            cwd: self.cwd,
477            fs_storage: self.fs_storage,
478            max_disk: self.max_disk,
479            on_exit: self.on_exit.unwrap_or_default(),
480            on_error: self.on_error.unwrap_or_default(),
481            chroot: self.chroot,
482            clean_env: self.clean_env,
483            env: self.env,
484            close_fds: self.close_fds.unwrap_or(true),
485            gpu_devices: self.gpu_devices,
486            cpu_cores: self.cpu_cores,
487            num_cpus: self.num_cpus,
488            port_remap: self.port_remap,
489            uid: self.uid,
490            policy_fn: self.policy_fn,
491        })
492    }
493}