1use std::collections::HashSet;
2use std::num::TryFromIntError;
3use std::os::unix::io;
4
5use libseccomp::{
6 ScmpAction, ScmpArch, ScmpArgCompare, ScmpCompareOp, ScmpFilterContext, ScmpSyscall,
7};
8use oci_spec::runtime::{
9 Arch, LinuxSeccomp, LinuxSeccompAction, LinuxSeccompFilterFlag, LinuxSeccompOperator,
10};
11
12#[derive(Debug, thiserror::Error)]
13pub enum SeccompError {
14 #[error("failed to translate trace action due to failed to convert errno {errno} into i16")]
15 TraceAction { source: TryFromIntError, errno: i32 },
16 #[error("SCMP_ACT_NOTIFY cannot be used as default action")]
17 NotifyAsDefaultAction,
18 #[error("SCMP_ACT_NOTIFY cannot be used for the write syscall")]
19 NotifyWriteSyscall,
20 #[error("failed to add arch to seccomp")]
21 AddArch {
22 source: libseccomp::error::SeccompError,
23 arch: Arch,
24 },
25 #[error("failed to load seccomp context")]
26 LoadContext {
27 source: libseccomp::error::SeccompError,
28 },
29 #[error("failed to get seccomp notify id")]
30 GetNotifyId {
31 source: libseccomp::error::SeccompError,
32 },
33 #[error("failed to add rule to seccomp")]
34 AddRule {
35 source: libseccomp::error::SeccompError,
36 },
37 #[error("failed to create new seccomp filter")]
38 NewFilter {
39 source: libseccomp::error::SeccompError,
40 default: LinuxSeccompAction,
41 },
42 #[error("failed to set filter flag")]
43 SetFilterFlag {
44 source: libseccomp::error::SeccompError,
45 flag: LinuxSeccompFilterFlag,
46 },
47 #[error("failed to set SCMP_FLTATR_CTL_NNP")]
48 SetCtlNnp {
49 source: libseccomp::error::SeccompError,
50 },
51}
52
53type Result<T> = std::result::Result<T, SeccompError>;
54
55fn translate_arch(arch: Arch) -> ScmpArch {
56 match arch {
57 Arch::ScmpArchNative => ScmpArch::Native,
58 Arch::ScmpArchX86 => ScmpArch::X86,
59 Arch::ScmpArchX86_64 => ScmpArch::X8664,
60 Arch::ScmpArchX32 => ScmpArch::X32,
61 Arch::ScmpArchArm => ScmpArch::Arm,
62 Arch::ScmpArchAarch64 => ScmpArch::Aarch64,
63 Arch::ScmpArchMips => ScmpArch::Mips,
64 Arch::ScmpArchMips64 => ScmpArch::Mips64,
65 Arch::ScmpArchMips64n32 => ScmpArch::Mips64N32,
66 Arch::ScmpArchMipsel => ScmpArch::Mipsel,
67 Arch::ScmpArchMipsel64 => ScmpArch::Mipsel64,
68 Arch::ScmpArchMipsel64n32 => ScmpArch::Mipsel64N32,
69 Arch::ScmpArchPpc => ScmpArch::Ppc,
70 Arch::ScmpArchPpc64 => ScmpArch::Ppc64,
71 Arch::ScmpArchPpc64le => ScmpArch::Ppc64Le,
72 Arch::ScmpArchS390 => ScmpArch::S390,
73 Arch::ScmpArchS390x => ScmpArch::S390X,
74 Arch::ScmpArchRiscv64 => ScmpArch::Riscv64,
75 Arch::ScmpArchParisc => ScmpArch::Parisc,
76 Arch::ScmpArchParisc64 => ScmpArch::Parisc64,
77 Arch::ScmpArchLoongarch64 => ScmpArch::Loongarch64,
78 Arch::ScmpArchM68k => ScmpArch::M68k,
79 Arch::ScmpArchSh => ScmpArch::Sh,
80 Arch::ScmpArchSheb => ScmpArch::Sheb,
81 }
82}
83
84fn translate_action(action: LinuxSeccompAction, errno: Option<u32>) -> Result<ScmpAction> {
85 tracing::trace!(?action, ?errno, "translating action");
86 let errno = errno.map(|e| e as i32).unwrap_or(libc::EPERM);
87 let action = match action {
88 LinuxSeccompAction::ScmpActKill => ScmpAction::KillThread,
89 LinuxSeccompAction::ScmpActTrap => ScmpAction::Trap,
90 LinuxSeccompAction::ScmpActErrno => ScmpAction::Errno(errno),
91 LinuxSeccompAction::ScmpActTrace => ScmpAction::Trace(
92 errno
93 .try_into()
94 .map_err(|err| SeccompError::TraceAction { source: err, errno })?,
95 ),
96 LinuxSeccompAction::ScmpActAllow => ScmpAction::Allow,
97 LinuxSeccompAction::ScmpActKillProcess => ScmpAction::KillProcess,
98 LinuxSeccompAction::ScmpActNotify => ScmpAction::Notify,
99 LinuxSeccompAction::ScmpActLog => ScmpAction::Log,
100 LinuxSeccompAction::ScmpActKillThread => ScmpAction::KillThread,
101 };
102
103 tracing::trace!(?action, "translated action");
104 Ok(action)
105}
106
107fn translate_op(op: LinuxSeccompOperator, datum_b: Option<u64>) -> ScmpCompareOp {
108 match op {
109 LinuxSeccompOperator::ScmpCmpNe => ScmpCompareOp::NotEqual,
110 LinuxSeccompOperator::ScmpCmpLt => ScmpCompareOp::Less,
111 LinuxSeccompOperator::ScmpCmpLe => ScmpCompareOp::LessOrEqual,
112 LinuxSeccompOperator::ScmpCmpEq => ScmpCompareOp::Equal,
113 LinuxSeccompOperator::ScmpCmpGe => ScmpCompareOp::GreaterEqual,
114 LinuxSeccompOperator::ScmpCmpGt => ScmpCompareOp::Greater,
115 LinuxSeccompOperator::ScmpCmpMaskedEq => ScmpCompareOp::MaskedEqual(datum_b.unwrap_or(0)),
116 }
117}
118
119fn check_seccomp(seccomp: &LinuxSeccomp) -> Result<()> {
120 if seccomp.default_action() == LinuxSeccompAction::ScmpActNotify {
131 return Err(SeccompError::NotifyAsDefaultAction);
132 }
133
134 if let Some(syscalls) = seccomp.syscalls() {
135 for syscall in syscalls {
136 if syscall.action() == LinuxSeccompAction::ScmpActNotify {
137 for name in syscall.names() {
138 if name == "write" {
139 return Err(SeccompError::NotifyWriteSyscall);
140 }
141 }
142 }
143 }
144 }
145
146 Ok(())
147}
148
149#[tracing::instrument(level = "trace", skip(seccomp))]
150pub fn initialize_seccomp(seccomp: &LinuxSeccomp) -> Result<Option<io::RawFd>> {
151 check_seccomp(seccomp)?;
152
153 tracing::trace!(default_action = ?seccomp.default_action(), errno = ?seccomp.default_errno_ret(), "initializing seccomp");
154 let default_action = translate_action(seccomp.default_action(), seccomp.default_errno_ret())?;
155 let mut ctx =
156 ScmpFilterContext::new(default_action).map_err(|err| SeccompError::NewFilter {
157 source: err,
158 default: seccomp.default_action(),
159 })?;
160
161 if let Some(flags) = seccomp.flags() {
162 for flag in flags {
163 match flag {
164 LinuxSeccompFilterFlag::SeccompFilterFlagLog => ctx.set_ctl_log(true),
165 LinuxSeccompFilterFlag::SeccompFilterFlagTsync => ctx.set_ctl_tsync(true),
166 LinuxSeccompFilterFlag::SeccompFilterFlagSpecAllow => ctx.set_ctl_ssb(true),
167 LinuxSeccompFilterFlag::SeccompFilterFlagWaitKillableRecv => {
168 ctx.set_ctl_waitkill(true)
169 }
170 }
171 .map_err(|err| SeccompError::SetFilterFlag {
172 source: err,
173 flag: *flag,
174 })?;
175 }
176 }
177
178 if let Some(architectures) = seccomp.architectures() {
179 for &arch in architectures {
180 tracing::trace!(?arch, "adding architecture");
181 ctx.add_arch(translate_arch(arch))
182 .map_err(|err| SeccompError::AddArch { source: err, arch })?;
183 }
184 }
185
186 ctx.set_ctl_nnp(false)
194 .map_err(|err| SeccompError::SetCtlNnp { source: err })?;
195
196 if let Some(syscalls) = seccomp.syscalls() {
197 for syscall in syscalls {
198 let action = translate_action(syscall.action(), syscall.errno_ret())?;
199 if action == default_action {
200 tracing::warn!(
203 "detect a seccomp action that is the same as the default action: {:?}",
204 syscall
205 );
206 continue;
207 }
208
209 for name in syscall.names() {
210 let sc = match ScmpSyscall::from_name(name) {
211 Ok(x) => x,
212 Err(_) => {
213 tracing::warn!(
216 "failed to resolve syscall, likely kernel doesn't support this. {:?}",
217 name
218 );
219 continue;
220 }
221 };
222 match syscall.args() {
223 Some(args) => {
232 let mut comparators = Vec::<ScmpArgCompare>::with_capacity(args.len());
233 let mut seen = HashSet::new();
234 let mut has_duplicate_index = false;
235
236 for arg in args {
237 let index = arg.index() as u32;
238 let comparator = ScmpArgCompare::new(
239 index,
240 translate_op(arg.op(), arg.value_two()),
241 arg.value(),
242 );
243 if !seen.insert(index) {
244 has_duplicate_index = true;
245 }
246 comparators.push(comparator);
247 }
248
249 if has_duplicate_index {
250 for comparator in &comparators {
251 tracing::trace!(
252 ?name,
253 ?action,
254 ?comparator,
255 "add seccomp conditional rule separately"
256 );
257 ctx.add_rule_conditional(action, sc, std::slice::from_ref(comparator))
258 .map_err(|err| {
259 tracing::error!(
260 "failed to add seccomp action: {:?}. Comparator: {:?} Syscall: {name}",
261 &action,
262 comparator,
263 );
264 SeccompError::AddRule { source: err }
265 })?;
266 }
267 } else {
268 tracing::trace!(
269 ?name,
270 ?action,
271 ?comparators,
272 "add seccomp conditional rule"
273 );
274 ctx.add_rule_conditional(action, sc, &comparators)
275 .map_err(|err| {
276 tracing::error!(
277 "failed to add seccomp action: {:?}. Comparators: {:?} Syscall: {name}",
278 &action,
279 comparators,
280 );
281 SeccompError::AddRule { source: err }
282 })?;
283 }
284 }
285 None => {
286 tracing::trace!(?name, ?action, "add seccomp rule");
287 ctx.add_rule(action, sc).map_err(|err| {
288 tracing::error!(
289 "failed to add seccomp rule: {:?}. Syscall: {name}",
290 &sc
291 );
292 SeccompError::AddRule { source: err }
293 })?;
294 }
295 }
296 }
297 }
298 }
299
300 ctx.load()
305 .map_err(|err| SeccompError::LoadContext { source: err })?;
306
307 let fd = if is_notify(seccomp) {
308 Some(
309 ctx.get_notify_fd()
310 .map_err(|err| SeccompError::GetNotifyId { source: err })?,
311 )
312 } else {
313 None
314 };
315
316 Ok(fd)
317}
318
319pub fn is_notify(seccomp: &LinuxSeccomp) -> bool {
320 seccomp
321 .syscalls()
322 .iter()
323 .flatten()
324 .any(|syscall| syscall.action() == LinuxSeccompAction::ScmpActNotify)
325}
326
327#[cfg(test)]
328mod tests {
329 use std::path;
330
331 use anyhow::{Context, Result};
332 use oci_spec::runtime::{
333 Arch, LinuxSeccompArgBuilder, LinuxSeccompBuilder, LinuxSyscallBuilder,
334 };
335 use serial_test::serial;
336
337 use super::*;
338 use crate::test_utils::{self, TestCallbackError};
339
340 #[test]
341 #[serial]
342 fn test_basic() -> Result<()> {
343 let expect_error = libc::EAGAIN;
354
355 let syscall = LinuxSyscallBuilder::default()
356 .names(vec![String::from("getcwd")])
357 .action(LinuxSeccompAction::ScmpActErrno)
358 .errno_ret(expect_error as u32)
359 .build()?;
360 let seccomp_profile = LinuxSeccompBuilder::default()
361 .default_action(LinuxSeccompAction::ScmpActAllow)
362 .architectures(vec![Arch::ScmpArchNative])
363 .syscalls(vec![syscall])
364 .build()?;
365
366 test_utils::test_in_child_process(|| {
367 let _ = prctl::set_no_new_privileges(true);
368 initialize_seccomp(&seccomp_profile).expect("failed to initialize seccomp");
369 let ret = nix::unistd::getcwd();
370 if ret.is_ok() {
371 Err(TestCallbackError::Custom(
372 "getcwd didn't error out as seccomp profile specified".to_string(),
373 ))?;
374 }
375
376 if let Some(errno) = ret.err() {
377 if errno != nix::errno::Errno::from_raw(expect_error) {
378 Err(TestCallbackError::Custom(format!(
379 "getcwd failed but we didn't get the expected error from seccomp profile: {}",
380 errno
381 )))?;
382 }
383 }
384
385 Ok(())
386 })?;
387
388 Ok(())
389 }
390
391 #[test]
392 #[serial]
393 fn test_moby() -> Result<()> {
394 let fixture_path =
395 path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/seccomp/fixture/config.json");
396 let spec = oci_spec::runtime::Spec::load(fixture_path)
397 .context("Failed to load test spec for seccomp")?;
398
399 let seccomp_profile = spec.linux().as_ref().unwrap().seccomp().as_ref().unwrap();
401 test_utils::test_in_child_process(|| {
402 let _ = prctl::set_no_new_privileges(true);
403 initialize_seccomp(seccomp_profile).expect("failed to initialize seccomp");
404
405 Ok(())
406 })?;
407
408 Ok(())
409 }
410
411 #[test]
412 #[serial]
413 fn test_seccomp_notify() -> Result<()> {
414 let syscall = LinuxSyscallBuilder::default()
415 .names(vec![String::from("getcwd")])
416 .action(LinuxSeccompAction::ScmpActNotify)
417 .build()?;
418 let seccomp_profile = LinuxSeccompBuilder::default()
419 .default_action(LinuxSeccompAction::ScmpActAllow)
420 .architectures(vec![Arch::ScmpArchNative])
421 .syscalls(vec![syscall])
422 .build()?;
423 test_utils::test_in_child_process(|| {
424 let _ = prctl::set_no_new_privileges(true);
425 let fd =
426 initialize_seccomp(&seccomp_profile).expect("failed to initialize seccomp profile");
427 if fd.is_none() {
428 Err(TestCallbackError::Custom(
429 "failed to get a seccomp notify fd with notify seccomp profile".to_string(),
430 ))?;
431 }
432
433 Ok(())
434 })?;
435
436 Ok(())
437 }
438
439 #[test]
440 #[serial]
441 fn test_seccomp_conditional_rule_multiple_distinct_args() -> Result<()> {
442 let syscall = LinuxSyscallBuilder::default()
443 .names(vec![String::from("socket")])
444 .action(LinuxSeccompAction::ScmpActErrno)
445 .errno_ret(libc::EAGAIN as u32)
446 .args(vec![
447 LinuxSeccompArgBuilder::default()
448 .index(0_usize)
449 .value(libc::AF_INET as u64)
450 .op(LinuxSeccompOperator::ScmpCmpEq)
451 .build()?,
452 LinuxSeccompArgBuilder::default()
453 .index(1_usize)
454 .value(libc::SOCK_STREAM as u64)
455 .op(LinuxSeccompOperator::ScmpCmpEq)
456 .build()?,
457 ])
458 .build()?;
459
460 let seccomp_profile = LinuxSeccompBuilder::default()
461 .default_action(LinuxSeccompAction::ScmpActAllow)
462 .architectures(vec![Arch::ScmpArchNative])
463 .syscalls(vec![syscall])
464 .build()?;
465
466 test_utils::test_in_child_process(|| {
467 let _ = prctl::set_no_new_privileges(true);
468 initialize_seccomp(&seccomp_profile).expect("failed to initialize seccomp");
469 Ok(())
470 })?;
471
472 Ok(())
473 }
474
475 #[test]
476 #[serial]
477 fn test_seccomp_conditional_rule_duplicate_arg_index() -> Result<()> {
478 let syscall = LinuxSyscallBuilder::default()
479 .names(vec![String::from("socket")])
480 .action(LinuxSeccompAction::ScmpActErrno)
481 .errno_ret(libc::EAGAIN as u32)
482 .args(vec![
483 LinuxSeccompArgBuilder::default()
484 .index(0_usize)
485 .value(libc::AF_INET as u64)
486 .op(LinuxSeccompOperator::ScmpCmpEq)
487 .build()?,
488 LinuxSeccompArgBuilder::default()
489 .index(0_usize)
490 .value(libc::AF_UNIX as u64)
491 .op(LinuxSeccompOperator::ScmpCmpNe)
492 .build()?,
493 ])
494 .build()?;
495
496 let seccomp_profile = LinuxSeccompBuilder::default()
497 .default_action(LinuxSeccompAction::ScmpActAllow)
498 .architectures(vec![Arch::ScmpArchNative])
499 .syscalls(vec![syscall])
500 .build()?;
501
502 test_utils::test_in_child_process(|| {
503 let _ = prctl::set_no_new_privileges(true);
504 initialize_seccomp(&seccomp_profile).expect("failed to initialize seccomp");
505 Ok(())
506 })?;
507
508 Ok(())
509 }
510
511 #[test]
512 #[serial]
513 fn test_seccomp_multiple_syscall_entries_for_same_name() -> Result<()> {
514 let rule1 = LinuxSyscallBuilder::default()
515 .names(vec!["socket".into()])
516 .action(LinuxSeccompAction::ScmpActErrno)
517 .errno_ret(libc::EAGAIN as u32)
518 .args(vec![
519 LinuxSeccompArgBuilder::default()
520 .index(0_usize)
521 .value(libc::AF_NETLINK as u64)
522 .op(LinuxSeccompOperator::ScmpCmpEq)
523 .build()?,
524 LinuxSeccompArgBuilder::default()
525 .index(2_usize)
526 .value(libc::NETLINK_AUDIT as u64)
527 .op(LinuxSeccompOperator::ScmpCmpNe)
528 .build()?,
529 ])
530 .build()?;
531
532 let rule2 = LinuxSyscallBuilder::default()
533 .names(vec!["socket".into()])
534 .action(LinuxSeccompAction::ScmpActErrno)
535 .errno_ret(libc::EAGAIN as u32)
536 .args(vec![
537 LinuxSeccompArgBuilder::default()
538 .index(0_usize)
539 .value(libc::AF_INET as u64)
540 .op(LinuxSeccompOperator::ScmpCmpNe)
541 .build()?,
542 ])
543 .build()?;
544
545 let profile = LinuxSeccompBuilder::default()
546 .default_action(LinuxSeccompAction::ScmpActAllow)
547 .architectures(vec![Arch::ScmpArchNative])
548 .syscalls(vec![rule1, rule2])
549 .build()?;
550
551 test_utils::test_in_child_process(|| {
552 let _ = prctl::set_no_new_privileges(true);
553 initialize_seccomp(&profile).expect("failed to initialize seccomp");
554
555 Ok(())
556 })?;
557
558 Ok(())
559 }
560}