Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implemented seccomp notify integration tests #435

Merged
merged 4 commits into from
Oct 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/integration_test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test_log.log
26 changes: 24 additions & 2 deletions crates/integration_test/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions crates/integration_test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ flate2 = "1.0"
test_framework = { path = "../test_framework"}
anyhow = "1.0"
once_cell = "1.8.0"
oci-spec = { git = "https://github.com/containers/oci-spec-rs", rev = "3d5132a18c305be59d58187201429d8f0243b513" }
oci-spec = { git = "https://github.com/containers/oci-spec-rs", rev = "d6fb1e91742313cd0d0085937e2d6df5d4669720" }
which = "4.2.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_json = "1.0"
nix = "0.22.0"
libcontainer = { path = "../libcontainer" }
3 changes: 3 additions & 0 deletions crates/integration_test/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod utils;
use crate::tests::lifecycle::{ContainerCreate, ContainerLifecycle};
use crate::tests::linux_ns_itype::get_ns_itype_tests;
use crate::tests::pidfile::get_pidfile_test;
use crate::tests::seccomp_notify::get_seccomp_notify_test;
use crate::tests::tlb::get_tlb_test;
use crate::utils::support::set_runtime_path;
use anyhow::Result;
Expand Down Expand Up @@ -63,6 +64,7 @@ fn main() -> Result<()> {
let pidfile = get_pidfile_test();
let ns_itype = get_ns_itype_tests();
let cgroup_v1_pids = cgroups::pids::get_test_group();
let seccomp_notify = get_seccomp_notify_test();

tm.add_test_group(&cl);
tm.add_test_group(&cc);
Expand All @@ -72,6 +74,7 @@ fn main() -> Result<()> {
tm.add_test_group(&cgroup_v1_pids);

tm.add_cleanup(Box::new(cgroups::cleanup));
tm.add_test_group(&seccomp_notify);

if let Some(tests) = opts.tests {
let tests_to_run = parse_tests(&tests);
Expand Down
1 change: 1 addition & 0 deletions crates/integration_test/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ pub mod cgroups;
pub mod lifecycle;
pub mod linux_ns_itype;
pub mod pidfile;
pub mod seccomp_notify;
pub mod tlb;
127 changes: 127 additions & 0 deletions crates/integration_test/src/tests/seccomp_notify/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use crate::utils::{get_runtime_path, test_outside_container};
use anyhow::{anyhow, bail, Result};
use oci_spec::runtime::{
Arch, LinuxBuilder, LinuxSeccompAction, LinuxSeccompBuilder, LinuxSyscallBuilder, SpecBuilder,
};
use std::{
path::PathBuf,
sync::mpsc::{self, Receiver, Sender},
thread,
};
use test_framework::{Test, TestGroup, TestResult};

mod seccomp_agent;

const SECCOMP_LISTENER_PATH: &str = "/tmp/youki_seccomp_agent.unix";
const SECCOMP_METADATA: &str = "Hello World! This is an opaque seccomp metadata string";

fn get_seccomp_listener() -> PathBuf {
let seccomp_listener_path = PathBuf::from(SECCOMP_LISTENER_PATH);
// We will have to clean up leftover unix domain socket from previous runs.
if seccomp_listener_path.exists() {
std::fs::remove_file(&seccomp_listener_path)
.expect("failed to clean up existing seccomp listener");
}

seccomp_listener_path
}

fn test_seccomp_notify() -> Result<()> {
let seccomp_listener_path = get_seccomp_listener();
let seccomp_meta = String::from(SECCOMP_METADATA);
// Create a spec to include seccomp notify. We will need to have at least
// one syscall set to seccomp notify. We also need to set seccomp listener
// path and metadata.
let spec = SpecBuilder::default()
.linux(
LinuxBuilder::default()
.seccomp(
LinuxSeccompBuilder::default()
.default_action(LinuxSeccompAction::ScmpActAllow)
.architectures(vec![Arch::ScmpArchX86_64])
.listener_path(&seccomp_listener_path)
.listener_metadata(seccomp_meta)
.syscalls(vec![LinuxSyscallBuilder::default()
.names(vec![String::from("getcwd")])
.action(LinuxSeccompAction::ScmpActNotify)
.build()
.unwrap()])
.build()
.unwrap(),
)
.build()
.unwrap(),
)
.build()
.unwrap();

// two threads. One run container life cycle. Another one run seccomp agent...
let (sender, receiver): (
Sender<seccomp_agent::SeccompAgentResult>,
Receiver<seccomp_agent::SeccompAgentResult>,
) = mpsc::channel();
// We have to launch the seccomp agent before we launch the container.
// Otherwise, the container creation will be blocked on trying to send to
// the seccomp listener and never returns.
let child = thread::spawn(move || {
let res = seccomp_agent::recv_seccomp_listener(&seccomp_listener_path);
sender
.send(res)
.expect("failed to send seccomp agent result back to main thread");
});
if let TestResult::Failed(err) = test_outside_container(spec, &move |data| {
let (container_process_state, _) = receiver
.recv()
.expect("failed to receive from channel")
.expect("failed to receive from seccomp listener");

let state = match data.state {
Some(s) => s,
None => return TestResult::Failed(anyhow!("state command returned error")),
};

if state.id != container_process_state.state.id {
return TestResult::Failed(anyhow!("container id doesn't match"));
}

if state.pid.unwrap() != container_process_state.pid {
return TestResult::Failed(anyhow!("container process id doesn't match"));
}

if SECCOMP_METADATA != container_process_state.metadata {
return TestResult::Failed(anyhow!("seccomp listener metadata doesn't match"));
}

TestResult::Passed
}) {
bail!("failed to run test outside container: {:?}", err);
}

if let Err(err) = child.join() {
bail!("seccomp listener child thread fails: {:?}", err);
}

Ok(())
}

pub fn get_seccomp_notify_test<'a>() -> TestGroup<'a> {
let seccomp_notify_test = Test::new(
"seccomp_notify",
Box::new(|| {
let runtime = get_runtime_path();
// runc doesn't support seccomp notify yet
if runtime.ends_with("runc") {
return TestResult::Skipped;
}

match test_seccomp_notify() {
Ok(_) => TestResult::Passed,
Err(err) => TestResult::Failed(err),
}
}),
);
let mut tg = TestGroup::new("seccomp_notify");
tg.add(vec![Box::new(seccomp_notify_test)]);

tg
}
93 changes: 93 additions & 0 deletions crates/integration_test/src/tests/seccomp_notify/seccomp_agent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use anyhow::{bail, Context, Result};
use libcontainer::container::ContainerProcessState;
use nix::{
sys::{socket, uio},
unistd,
};
use std::{os::unix::prelude::RawFd, path::Path};

const DEFAULT_BUFFER_SIZE: usize = 4096;

pub type SeccompAgentResult = Result<(ContainerProcessState, RawFd)>;

// Receive information from seccomp notify listener. We will receive 2 items, 1
// container process state and 1 seccomp notify fd. This function will only
// receive one connection from the listener and will terminate all socket when
// returning, since we only expect at most 1 connection to the listener based on
// the spec.
pub fn recv_seccomp_listener(seccomp_listener: &Path) -> SeccompAgentResult {
let addr = socket::SockAddr::new_unix(seccomp_listener)?;
let socket = socket::socket(
socket::AddressFamily::Unix,
socket::SockType::Stream,
socket::SockFlag::empty(),
None,
)
.context("failed to create seccomp listener socket")?;
socket::bind(socket, &addr).context("failed to bind to seccomp listener socket")?;
// Force the backlog to be 1 so in the case of an error, only one connection
// from clients will be waiting.
socket::listen(socket, 1).context("failed to listen on seccomp listener")?;
let conn = match socket::accept(socket) {
Ok(conn) => conn,
Err(e) => {
let _ = unistd::close(socket);
bail!("failed to accept connection: {}", e);
}
};
let mut cmsgspace = nix::cmsg_space!([RawFd; 1]);
let mut buf = vec![0u8; DEFAULT_BUFFER_SIZE];
let iov = [uio::IoVec::from_mut_slice(&mut buf)];
let msg = match socket::recvmsg(
conn,
&iov,
Some(&mut cmsgspace),
socket::MsgFlags::MSG_CMSG_CLOEXEC,
) {
Ok(msg) => msg,
Err(e) => {
let _ = unistd::close(conn);
let _ = unistd::close(socket);
bail!("failed to receive message: {}", e);
}
};

// We received the message correctly here, so we can now safely close the socket and connection.
let _ = unistd::close(conn);
let _ = unistd::close(socket);

// We are expecting 1 SCM_RIGHTS message with 1 fd.
let cmsg = msg
.cmsgs()
.next()
.context("expecting at least 1 SCM_RIGHTS message")?;
let fd = match cmsg {
socket::ControlMessageOwned::ScmRights(fds) => {
if fds.len() != 1 {
bail!("expecting 1 fds, but received: {:?}", fds);
}

fds[0]
}
_ => {
bail!(
"expecting 1 SCM_RIGHTS message, but received {:?} instead",
cmsg
);
}
};

// We have to truncate the message to the correct size, so serde can
// deserialized the data correctly.
if msg.bytes >= DEFAULT_BUFFER_SIZE {
bail!("received more than the DEFAULT_BUFFER_SIZE");
}

buf.truncate(msg.bytes);

let container_process_state: libcontainer::container::ContainerProcessState =
serde_json::from_slice(&buf[..])
.context("failed to parse the received message as container process state")?;

Ok((container_process_state, fd))
}