Skip to main content

libcontainer/seccomp/
mod.rs

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    // We don't support notify as default action. After the seccomp filter is
121    // created with notify, the container process will have to communicate the
122    // returned fd to another process. Therefore, we need the write syscall or
123    // otherwise, the write syscall will be block by the seccomp filter causing
124    // the container process to hang. `runc` also disallow notify as default
125    // action.
126    // Note: read and close syscall are also used, because if we can
127    // successfully write fd to another process, the other process can choose to
128    // handle read/close syscall and allow read and close to proceed as
129    // expected.
130    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    // The SCMP_FLTATR_CTL_NNP controls if the seccomp load function will set
187    // the new privilege bit automatically in prctl. Normally this is a good
188    // thing, but for us we need better control. Based on the spec, if OCI
189    // runtime spec doesn't set the no new privileges in Process, we should not
190    // set it here.  If the seccomp load operation fails without enough
191    // privilege, so be it. To prevent this automatic behavior, we unset the
192    // value here.
193    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                // When the action is the same as the default action, the rule is redundant. We can
201                // skip this here to avoid failing when we add the rules.
202                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                        // If we failed to resolve the syscall by name, likely the kernel
214                        // doeesn't support this syscall. So it is safe to skip...
215                        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                    // libseccomp allows multiple argument comparisons in a single rule,
224                    // but each syscall argument can only be compared once per rule.
225                    // When multiple comparisons target the same argument index,
226                    // we follow runc's behavior and add each condition as a separate rule.
227                    // Ref: libseccomp seccomp_rule_add(3)
228                    // https://github.com/seccomp/libseccomp/blob/9d7a3cd937e7841ece62ac19f0f06aafd0fdaaa9/doc/man/man3/seccomp_rule_add.3#L137
229                    // Ref: runc seccomp_linux.go
230                    // https://github.com/opencontainers/runc/blob/4b97e12fccdfca981a296d9ef82df5f3ae95e288/libcontainer/seccomp/seccomp_linux.go#L327
231                    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    // In order to use the SECCOMP_SET_MODE_FILTER operation, either the calling
301    // thread must have the CAP_SYS_ADMIN capability in its user namespace, or
302    // the thread must already have the no_new_privs bit set.
303    // Ref: https://man7.org/linux/man-pages/man2/seccomp.2.html
304    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        // Note: seccomp profile is really hard to write unit test for. First,
344        // we can't really test default error or kill action, since rust test
345        // actually relies on certain syscalls. Second, some of the syscall will
346        // not return errorno. These syscalls will just send an abort signal or
347        // even just segfaults.  Here we choose to use `getcwd` syscall for
348        // testing, since it will correctly return an error under seccomp rule.
349        // This is more of a sanity check.
350
351        // Here, we choose an error that getcwd call would never return on its own, so
352        // we can make sure that getcwd failed because of seccomp rule.
353        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        // We know linux and seccomp exist, so let's just unwrap.
400        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}