diff --git a/src/apparmor.rs b/src/apparmor.rs new file mode 100644 index 0000000..2bd0203 --- /dev/null +++ b/src/apparmor.rs @@ -0,0 +1,28 @@ +//! AppArmor profile transition for the workload process. + +use std::io::Write; + +/// Stage an AppArmor profile transition that takes effect on the next `execve` +/// (the kernel's `aa_change_onexec` interface). +/// +/// The named profile must already be loaded in the kernel. Writing an un-loaded/unknown +/// profile name here will cause the next `execve` to fail with `-ENOENT`. +/// Must be called after `PR_SET_NO_NEW_PRIVS` and before `execvpe()`. +/// +/// The command must reach the kernel in a single `write(2)`, so it is formatted +/// into one buffer. Writes to the per-LSM attr node `/proc/self/attr/apparmor/exec` +/// (present on Linux 5.1+), and falls back to the pre-5.1 global node `/proc/self/attr/exec`. +pub fn change_onexec(profile: &str) -> std::io::Result<()> { + let cmd = format!("exec {profile}"); + let mut file = match std::fs::OpenOptions::new() + .write(true) + .open("/proc/self/attr/apparmor/exec") + { + Ok(file) => file, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => std::fs::OpenOptions::new() + .write(true) + .open("/proc/self/attr/exec")?, + Err(e) => return Err(e), + }; + file.write_all(cmd.as_bytes()) +} diff --git a/src/config.rs b/src/config.rs index cec014f..aa8368a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -86,6 +86,12 @@ pub struct ExecutableSpec { #[serde(default)] pub seccomp: Option, + /// An optional AppArmor profile name to transition to on `execve`. The named + /// profile must already be loaded in the kernel. Staged after + /// `PR_SET_NO_NEW_PRIVS`, before `execvpe()`. + #[serde(default)] + pub apparmor: Option, + /// An optional out-of-memory score adjustment value. pub oom_score_adj: Option, } diff --git a/src/lib.rs b/src/lib.rs index 8afabf8..73b7ee5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod apparmor; pub mod caps; pub mod cgroup; pub mod config; diff --git a/src/wrap.rs b/src/wrap.rs index ec92b84..1606520 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -725,6 +725,11 @@ impl ExecutableSpec { unsafe { filter.install()? }; } + if let Some(profile) = &self.apparmor { + crate::apparmor::change_onexec(profile) + .map_err(|e| anyhow!("failed to set AppArmor profile {profile:?}: {e}"))?; + } + // The Rust runtime ignores SIGPIPE (SIG_IGN) process-wide, and that // disposition is inherited across execve. Restore SIG_DFL so the // workload sees the standard broken-pipe behaviour, matching runc/crun.