Skip to content
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ This file records each release. The format follows [Keep a Changelog](https://ke
### Added

- A socket has the owner and the mode of its inode, as sockfs of Linux. A new socket has the file system user and group IDs of the process and the mode 0777. `fchown`, `fchmod` and `fchownat` with `AT_EMPTY_PATH` change them. After the release of the socket, a change of the owner gives `ENOENT`, as `sockfs_setattr` of Linux for CVE-2019-8912. LTP af_alg07 tests this (#541).
- AppArmor checks the capabilities, as capability.c of Linux. A capability that the profile does not allow gives `EPERM` and a record of the class `cap`, with the rules `audit` and `quiet` and the skip of a repeated record. Each check of a capability calls the `capable` hook of the security modules after the check of the capability module, also the checks in a user namespace and the checks of a file, as `ns_capable` and `capable_wrt_inode_uidgid` of Linux (#540).

### Fixed

- AppArmor finds the parent of a hat that has the name of a program, as `aa_split_fqname` of Linux. In `P///bin/x` the parent is `P` and the hat is `/bin/x`. Before, the parent was `P/`, so a `change_hat` from that hat to a sibling hat gave `ECHILD` (#540).

## [0.5.25] - 2026-11-13

Expand Down
1 change: 0 additions & 1 deletion core/rk-syscall/src/ns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,6 @@ pub(crate) fn may_join_mnt_ns(cred: &Cred, ns: &MountNamespace) -> Result<(), Er
if !cred.ns_capable(ns.owner(), cap::SYS_ADMIN)
|| !cred.has_cap(cap::SYS_CHROOT)
|| !cred.has_cap(cap::SYS_ADMIN)
|| !rk_vfs::security::capable(cred, cap::SYS_ADMIN, 0)
{
return Err(Errno::EPERM);
}
Expand Down
10 changes: 2 additions & 8 deletions core/rk-syscall/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,7 @@ fn new_cgroup_ns(process: &Process, cred: &Cred) -> Result<(), Errno> {
/// `EPERM` without the capabilities.
fn may_install(cred: &Cred, owner: &UserNs) -> Result<(), Errno> {
let cap = rk_vfs::cred::cap::SYS_ADMIN;
if !cred.ns_capable(owner, cap)
|| !cred.has_cap(cap)
|| !rk_vfs::security::capable(cred, cap, 0)
{
if !cred.ns_capable(owner, cap) || !cred.has_cap(cap) {
return Err(Errno::EPERM);
}
Ok(())
Expand All @@ -334,10 +331,7 @@ fn may_install(cred: &Cred, owner: &UserNs) -> Result<(), Errno> {
/// `EPERM` without the capabilities.
fn may_join_cgroup_ns(old: &Cred, cred: &Cred, ns: u32) -> Result<(), Errno> {
let cap = rk_vfs::cred::cap::SYS_ADMIN;
if !old.ns_capable(&crate::ns::cgroup_owner(ns), cap)
|| !old.ns_capable(&cred.user_ns, cap)
|| !rk_vfs::security::capable(old, cap, 0)
{
if !old.ns_capable(&crate::ns::cgroup_owner(ns), cap) || !old.ns_capable(&cred.user_ns, cap) {
return Err(Errno::EPERM);
}
Ok(())
Expand Down
12 changes: 7 additions & 5 deletions fs/rk-vfs/src/cred.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,19 +288,21 @@ impl Cred {
/// The process has the capability in the first user namespace, and no security module
/// denies it, as `capable` and `security_capable` of Linux.
pub fn capable(&self, cap: u32) -> bool {
self.ns_capable(user_ns::init(), cap) && crate::security::capable(self, cap, 0)
self.ns_capable(user_ns::init(), cap)
}

/// The process has the capability in its own user namespace: the capability is in its
/// effective set, as `ns_capable(current_user_ns(), cap)` of Linux.
/// effective set, and no security module denies it, as `ns_capable(current_user_ns(), cap)`
/// of Linux.
pub fn has_cap(&self, cap: u32) -> bool {
self.cap_effective & cap::bit(cap) != 0
self.cap_effective & cap::bit(cap) != 0 && crate::security::capable_lsm(self, cap, 0)
}

/// The process has the capability in the user namespace `ns`, as `ns_capable` and
/// `cap_capable` of Linux. The capabilities apply in the namespace of the process and in the
/// namespaces below it. The owner of a namespace has all the capabilities in it, if the
/// process is in the parent of the namespace.
/// process is in the parent of the namespace. After this check of the capability module, the
/// other security modules check the capability, as `security_capable` of Linux.
pub fn ns_capable(&self, ns: &UserNs, cap: u32) -> bool {
let mut ns = ns;
loop {
Expand All @@ -312,7 +314,7 @@ impl Cred {
}
let Some(parent) = ns.parent() else { return false };
if **parent == *self.user_ns && ns.owner() == self.euid {
return true;
return crate::security::capable_lsm(self, cap, 0);
}
ns = parent;
}
Expand Down
10 changes: 10 additions & 0 deletions fs/rk-vfs/src/security/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,16 @@ pub fn capable(cred: &Cred, cap: u32, opts: u32) -> bool {
call_int_hook!(capable, cred, cap, opts).is_ok()
}

/// `security_capable` after the capability module: the other active modules check the
/// capability `cap` of the task with `cred`. The caller has done the check of the capability
/// module, `cap_capable` of Linux, in the user namespace of the request.
pub fn capable_lsm(cred: &Cred, cap: u32, opts: u32) -> bool {
modules()
.iter()
.filter(|m| m.info.id.id != id::CAPABILITY)
.all(|m| m.info.hooks.capable(cred, cap, opts).is_ok())
}

/// `security_capget`.
pub fn capget(target: &Task<'_>) -> Result<()> {
call_int_hook!(capget, target)
Expand Down
143 changes: 143 additions & 0 deletions sec/rk-lsm-apparmor/src/capability.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//! The mediation of the capabilities, as capability.c of Linux.
//!
//! A confined task has a capability only if each profile of its label allows it. The check
//! comes after the check of the capability module, so a task without the capability in its
//! effective set gets no record. A denial gives `EPERM` and a record of the class `cap`:
//!
//! ```text
//! apparmor="DENIED" operation="capable" class="cap" profile="P" pid=PID comm="COMM"
//! capability=13 capname="net_raw"
//! ```

use crate::audit::{self, Kind};
use crate::domain;
use crate::policy::{Mode, Profile};
use alloc::format;
use alloc::sync::Arc;
use rk_abi::Errno;
use rk_frame::sync::SpinLock;
use rk_vfs::cred::Cred;
use rk_vfs::inode::Result;

/// `CAP_OPT_NOAUDIT`: the caller asks for no record.
pub const CAP_OPT_NOAUDIT: u32 = 1 << 1;

/// The last profile with a record of a denial and its capabilities, as the `audit_cache` of
/// Linux. A second denial of the same capability for the same profile gives no record. Linux
/// has one cache for each CPU. This kernel has one.
static CACHE: SpinLock<(usize, u64)> = SpinLock::new((0, 0));

/// The name of the capability `cap` in the record, as `capability_names` of Linux.
fn cap_name(cap: u32) -> &'static str {
crate::features::CAPS_MASK.split(' ').nth(cap as usize).unwrap_or("?")
}

/// The bit of the capability `cap` in a set of the policy.
fn bit(cap: u32) -> u64 {
1u64.checked_shl(cap).unwrap_or(0)
}

/// Logs the check of the capability `cap` for the profile `p` with the result `error`, as
/// `audit_caps` of Linux.
///
/// # Errors
///
/// Gives `error`, except for a denial that complain mode allows.
fn audit_caps(
p: &Arc<Profile>,
cap: u32,
info: Option<&'static str>,
error: Option<Errno>,
) -> Result<()> {
let rules = p.rules.as_deref();
let audit_all = rules.is_some_and(|r| r.audit);
let caps = rules.map(|r| r.caps).unwrap_or_default();
let kind = match error {
// An allowed capability has a record only if the policy audits it.
None if !audit_all && caps.audit & bit(cap) == 0 => return Ok(()),
None => Kind::Audit,
Some(_) if p.mode == Mode::Kill => Kind::Killed,
// A rule with `quiet` hides the denial, also in complain mode, as on Linux.
Some(e) if caps.quiet & bit(cap) != 0 && !audit_all => return Err(e),
Some(_) => domain::mode_kind(Some(p)),
};
// The same denial again gives no record.
{
let mut cache = CACHE.lock();
let key = Arc::as_ptr(p) as usize;
if cache.0 == key && cache.1 & bit(cap) != 0 {
// As on Linux, a repeated denial does not kill the task again.
let kind = if p.mode == Mode::Complain { Kind::Allowed } else { Kind::Denied };
return domain::outcome(kind, error);
}
if cache.0 != key {
*cache = (key, 0);
}
cache.1 |= bit(cap);
}
let fq = p.fqname();
let d = audit::Data {
op: Some("capable"),
class: Some("cap"),
info,
error: error.map_or(0, |e| -i32::from(e.get())),
profile: Some(&fq),
name: None,
};
let mut tail = format!(" capability={cap} capname=");
audit::untrusted(&mut tail, cap_name(cap).as_bytes());
audit::log(kind, &d, &tail);
domain::outcome(kind, error)
}

/// The check of the capability `cap` for the profile `p`, `profile_capable` of Linux.
///
/// # Errors
///
/// Gives `EPERM` if the profile does not allow the capability, except in complain mode.
fn profile_capable(p: &Arc<Profile>, cap: u32, opts: u32) -> Result<()> {
let caps = p.rules.as_ref().map(|r| r.caps).unwrap_or_default();
let error = (caps.allow & bit(cap) == 0).then_some(Errno::EPERM);
let mut info = None;
if opts & CAP_OPT_NOAUDIT != 0 {
if p.mode != Mode::Complain {
return error.map_or(Ok(()), Err);
}
// Complain mode logs the request, but tells that it is optional.
info = Some("optional: no audit");
}
audit_caps(p, cap, info, error)
}

/// `apparmor_capable` and `aa_capable` of Linux: the task with `cred` uses the capability
/// `cap`. Each profile of the label checks it.
///
/// # Errors
///
/// Gives the error of the last profile that denies the capability.
pub fn capable(cred: &Cred, cap: u32, opts: u32) -> Result<()> {
if domain::label_id(cred) == 0 {
return Ok(());
}
let mut result = Ok(());
for p in domain::label_of(cred).iter().filter(|p| p.mode != Mode::Unconfined) {
if let Err(e) = profile_capable(p, cap, opts) {
result = Err(e);
}
}
result
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn a_capability_has_the_name_of_linux() {
assert_eq!(cap_name(0), "chown");
assert_eq!(cap_name(13), "net_raw");
assert_eq!(cap_name(21), "sys_admin");
assert_eq!(cap_name(40), "checkpoint_restore");
assert_eq!(bit(64), 0);
}
}
2 changes: 1 addition & 1 deletion sec/rk-lsm-apparmor/src/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ impl Value {
}

/// The capabilities, `AA_SFS_CAPS_MASK`.
const CAPS_MASK: &str = "chown dac_override dac_read_search fowner fsetid kill setgid setuid \
pub(crate) const CAPS_MASK: &str = "chown dac_override dac_read_search fowner fsetid kill setgid setuid \
setpcap linux_immutable net_bind_service net_broadcast net_admin net_raw ipc_lock ipc_owner \
sys_module sys_rawio sys_chroot sys_ptrace sys_pacct sys_admin sys_boot sys_nice sys_resource \
sys_time sys_tty_config mknod lease audit_write audit_control setfcap mac_override mac_admin \
Expand Down
8 changes: 7 additions & 1 deletion sec/rk-lsm-apparmor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
//! unpack of the binary policy ([`unpack`]) with its DFA tables ([`dfa`]), the policy namespace
//! that holds the profiles ([`policy`]), the audit records ([`audit`]), the label of a task
//! with its change at an exec ([`domain`]), the changes that a task asks for with
//! `change_hat` and `change_profile` ([`task`]) and the checks of the open files ([`file`]).
//! `change_hat` and `change_profile` ([`task`]), the checks of the files ([`file`]) and the
//! checks of the capabilities ([`capability`]).
//!
//! The module is active when the command line has `apparmor=1`, or when `lsm=` names it. With
//! `apparmor=0` it is not active. This is the value 0 of the old
Expand All @@ -19,6 +20,7 @@
extern crate alloc;

pub mod audit;
pub mod capability;
pub mod dfa;
pub mod domain;
pub mod features;
Expand Down Expand Up @@ -46,6 +48,10 @@ use rk_vfs::security::{
struct AppArmor;

impl SecurityHooks for AppArmor {
fn capable(&self, cred: &Cred, cap: u32, opts: u32) -> Result<()> {
capability::capable(cred, cap, opts)
}

fn bprm_creds_for_exec(&self, bprm: &mut Bprm<'_>) -> Result<()> {
domain::bprm_creds_for_exec(bprm)
}
Expand Down
25 changes: 23 additions & 2 deletions sec/rk-lsm-apparmor/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,26 @@ impl Profile {

/// The name without the names of the parents, `base.name` of Linux.
pub fn base_name(&self) -> &str {
self.name.rfind("//").map_or(&self.name, |i| &self.name[i + 2..])
last_sep(&self.name).map_or(&self.name, |i| &self.name[i + 2..])
}
}

/// The name of the parent of the profile `name`.
fn parent_of(name: &str) -> Option<&str> {
name.rfind("//").map(|i| &name[..i])
last_sep(name).map(|i| &name[..i])
}

/// The position of the last `//` that separates the names of a profile and its parent. As
/// `aa_split_fqname` of Linux, the search goes from the start, so in `P///bin/x` the parent is `P`
/// and the hat is `/bin/x`, which is the name of a program.
fn last_sep(name: &str) -> Option<usize> {
let mut at = 0;
let mut last = None;
while let Some(i) = name[at..].find("//") {
last = Some(at + i);
at += i + 2;
}
last
}

/// A policy namespace, `struct aa_ns` of Linux.
Expand Down Expand Up @@ -494,6 +507,14 @@ mod tests {
ns.profiles().iter().map(|p| alloc::format!("{} ({})", p.name, p.mode.name())).collect()
}

#[test]
fn a_hat_with_the_name_of_a_program_has_the_right_parent() {
assert_eq!(parent_of("/bin/w///bin/x"), Some("/bin/w"));
assert_eq!(parent_of("a//b//c"), Some("a//b"));
assert_eq!(parent_of("/bin/w"), None);
assert_eq!(&"/bin/w///bin/x"[last_sep("/bin/w///bin/x").unwrap() + 2..], "/bin/x");
}

#[test]
fn root_may_change_the_policy_and_a_user_may_not() {
let root = Cred::root();
Expand Down
2 changes: 2 additions & 0 deletions sec/rk-lsm-apparmor/src/unpack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,8 @@ mod tests {
let tmp = allow(&t.file, class::FILE, b"/tmp/a/b", 0);
assert_eq!(tmp & (perm::READ | perm::WRITE), perm::READ | perm::WRITE);
assert_eq!(allow(&t.file, class::FILE, b"/etc/passwd", 0) & perm::READ, 0);
// The profile has no capability rule.
assert_eq!(t.caps, Caps::default());
}

/// A small policy: the profile `rk_min` in complain mode, with no rules.
Expand Down
10 changes: 8 additions & 2 deletions tests/user/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -15840,7 +15840,8 @@ static int aa_write(const char *path, const char *data, size_t len, int flags)
* With the argument "exec", it checks that it has no label for the next exec. With the argument
* "deny", it checks that rk_test in enforce mode denies an open, a lock, a stat, a change of the
* attributes, a truncate to the size argv[2], a link, a rename, a create, a remove and an exec
* with EACCES, and gives 0 or the number of the first check that failed. It does not read its
* with EACCES, and chroot and sethostname with EPERM, because the rules allow no capability. It
* gives 0 or the number of the first check that failed. It does not read its
* label, because the rules do not allow it. The names are in /etc, which has no rule.
*/
static int aa_label_helper(int argc, char **argv)
Expand Down Expand Up @@ -15879,6 +15880,10 @@ static int aa_label_helper(int argc, char **argv)
return 14;
if (unlink("/etc/rk_test.aa") == 0 || errno != EACCES)
return 15;
if (chroot("/") == 0 || errno != EPERM)
return 16;
if (sethostname("rk-aa", 5) == 0 || errno != EPERM)
return 17;
execv("/tmp/rk-sub", args);
return errno == EACCES ? 0 : 5;
}
Expand Down Expand Up @@ -16206,7 +16211,7 @@ static void test_apparmor(void)
execv("/usr/bin/rk-test", five);
_exit(127);
}
check("apparmor: in enforce mode, an open, a lock, a path operation and an exec that the rules do not allow give EACCES", pid > 0 && waitpid(pid, &status, 0) == pid && WIFEXITED(status) && WEXITSTATUS(status) == 0);
check("apparmor: in enforce mode, an open, a lock, a path operation and an exec that the rules do not allow give EACCES, and a capability gives EPERM", pid > 0 && waitpid(pid, &status, 0) == pid && WIFEXITED(status) && WEXITSTATUS(status) == 0);
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
printf("init: the AppArmor enforce checks failed at the check %d\n", WIFEXITED(status) ? WEXITSTATUS(status) : -1);
pid = fork();
Expand All @@ -16224,6 +16229,7 @@ static void test_apparmor(void)
check("apparmor: the kernel log has the denials, as on Linux", n > 0 && strstr(buf, "apparmor=\"DENIED\" operation=\"open\" class=\"file\" profile=\"rk_test\" name=\"/etc/rk_test.aa\" pid=") != NULL && strstr(buf, "apparmor=\"DENIED\" operation=\"file_lock\" class=\"file\" profile=\"rk_test\" name=\"/tmp/rk-aa-w\" pid=") != NULL && strstr(buf, "apparmor=\"DENIED\" operation=\"exec\" class=\"file\" profile=\"rk_test\" name=\"/tmp/rk-sub\" pid=") != NULL);
check("apparmor: the kernel log has the denials of the path operations, as on Linux", strstr(buf, "operation=\"getattr\"") == NULL && strstr(buf, "apparmor=\"DENIED\" operation=\"chmod\" class=\"file\" profile=\"rk_test\" name=\"/etc/rk_test.aa\" pid=") != NULL && strstr(buf, "operation=\"truncate\" class=\"file\" profile=\"rk_test\" name=\"/etc/rk_test.aa\" pid=") != NULL && strstr(buf, "operation=\"rename_src\" class=\"file\" profile=\"rk_test\" name=\"/etc/rk_test.aa\" pid=") != NULL && strstr(buf, "operation=\"unlink\" class=\"file\" profile=\"rk_test\" name=\"/etc/rk_test.aa\" pid=") != NULL);
check("apparmor: the kernel log has the denials of the link and the creates, as on Linux", strstr(buf, "operation=\"link\" class=\"file\" profile=\"rk_test\" name=\"/etc/rk-aa-l\" pid=") != NULL && strstr(buf, " requested_mask=\"l\" denied_mask=\"l\" fsuid=0 ouid=0 target=\"/etc/rk_test.aa\"") != NULL && strstr(buf, "operation=\"mkdir\" class=\"file\" profile=\"rk_test\" name=\"/etc/rk-aa-d/\" pid=") != NULL && strstr(buf, "operation=\"rmdir\" class=\"file\" profile=\"rk_test\" name=\"/usr/bin/\" pid=") != NULL && strstr(buf, "operation=\"symlink\" class=\"file\" profile=\"rk_test\" name=\"/etc/rk-aa-s\" pid=") != NULL && strstr(buf, "operation=\"mknod\" class=\"file\" profile=\"rk_test\" name=\"/etc/rk-aa-n\" pid=") != NULL);
check("apparmor: the kernel log has the denials of the capabilities, as on Linux", strstr(buf, "apparmor=\"DENIED\" operation=\"capable\" class=\"cap\" profile=\"rk_test\" pid=") != NULL && strstr(buf, " capability=18 capname=\"sys_chroot\"") != NULL && strstr(buf, " capability=21 capname=\"sys_admin\"") != NULL);
check("apparmor: the kernel log has the missing profile in enforce mode", strstr(buf, "apparmor=\"DENIED\" operation=\"change_profile\" class=\"file\" info=\"label not found\" error=-2 profile=\"rk_test\" name=\"rk_none\" pid=") != NULL);
}
check("apparmor: a remove of rk_test removes its children", aa_write("/sys/kernel/security/apparmor/.remove", "rk_test", 7, 0) == 0 && aa_read("/sys/kernel/security/apparmor/profiles", buf, 64) == 0);
Expand Down
Loading