1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::time::SystemTime;
4
5use serde::{Deserialize, Serialize};
6
7use crate::error::PolicyError;
8
9#[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 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
61pub enum FsIsolation {
62 #[default]
63 None,
64 OverlayFs,
65 BranchFs,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
70pub enum BranchAction {
71 #[default]
72 Commit,
73 Abort,
74 Keep,
75}
76
77#[derive(Clone, Serialize, Deserialize)]
79pub struct Policy {
80 pub fs_writable: Vec<PathBuf>,
82 pub fs_readable: Vec<PathBuf>,
83 pub fs_denied: Vec<PathBuf>,
84
85 pub deny_syscalls: Option<Vec<String>>,
87 pub allow_syscalls: Option<Vec<String>>,
88
89 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 pub isolate_ipc: bool,
98 pub isolate_signals: bool,
99 pub isolate_pids: bool,
100
101 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 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 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 pub chroot: Option<PathBuf>,
127 pub clean_env: bool,
128 pub env: HashMap<String, String>,
129 pub close_fds: bool,
130
131 pub gpu_devices: Option<Vec<u32>>,
133
134 pub cpu_cores: Option<Vec<u32>>,
136 pub num_cpus: Option<u32>,
137 pub port_remap: bool,
138
139 pub uid: Option<u32>,
141
142 #[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#[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 if self.deny_syscalls.is_some() && self.allow_syscalls.is_some() {
433 return Err(PolicyError::MutuallyExclusiveSyscalls);
434 }
435
436 if let Some(cpu) = self.max_cpu {
438 if cpu == 0 || cpu > 100 {
439 return Err(PolicyError::InvalidCpuPercent(cpu));
440 }
441 }
442
443 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}