diff --git a/CHANGELOG.md b/CHANGELOG.md index 679d73a..f4bc6ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ This file records each release. The format follows [Keep a Changelog](https://ke ### Added - The `AF_ALG` sockets with the type `skcipher`, as crypto/algif_skcipher.c of Linux, for `ecb(aes)`, `cbc(aes)`, `ctr(aes)` and `xts(aes)` of `rk-crypto`. A `sendmsg` gives the operation with `ALG_SET_OP` and the IV with `ALG_SET_IV` in control messages, and adds the data to the queue. A `recv` encrypts or decrypts the data of the queue in blocks. With `MSG_MORE`, the operation continues in the next `send`, and the IV goes from one `recv` to the next. libcryptsetup uses these sockets to open a LUKS2 key slot without dm-crypt (#541). +- The `getdents` call of x86_64, with the records of the old format `linux_dirent`, as `filldir` of Linux. The `readdir` test of the AppArmor regression tests uses it (#540). +- AppArmor checks the path operations, as the path hooks of lsm.c of Linux: a create, a `mkdir`, a `mknod`, a symbolic link, a hard link, a rename, an `unlink`, an `rmdir`, a truncate, a `chmod`, a `chown` and a `stat`. The VFS has the hooks `path_*`, `inode_getattr` and `file_truncate` of Linux, at the places where Linux calls them (#540). ## [0.5.24] - 2026-11-12 diff --git a/core/rk-syscall/src/fs.rs b/core/rk-syscall/src/fs.rs index c2c985d..9093635 100644 --- a/core/rk-syscall/src/fs.rs +++ b/core/rk-syscall/src/fs.rs @@ -764,7 +764,12 @@ pub(crate) fn lseek(cx: &Cx<'_>, fd: u64, offset: u64, whence: u64) -> SysResult } pub(crate) fn fstat(cx: &Cx<'_>, fd: u64, buf: u64) -> SysResult { - let meta = cx.file(fd)?.metadata()?; + let file = cx.file(fd)?; + // The LSM check of `vfs_getattr` of Linux. + if let Some(at) = file.location() { + rk_vfs::security::inode_getattr(at)?; + } + let meta = file.metadata()?; cx.write_val(buf, &stat_of(&meta))?; Ok(0) } @@ -784,6 +789,10 @@ fn lookup( flags: u32, ) -> Result<(Metadata, Option, u64), Errno> { let (meta, inode, at) = lookup_at(cx, dirfd, path, flags)?; + // The LSM check of `vfs_getattr` of Linux. + if let Some(at) = &at { + rk_vfs::security::inode_getattr(at)?; + } Ok((meta, inode, at.map_or(0, |at| at.mount.id()))) } @@ -844,6 +853,10 @@ pub(crate) fn statx( return Err(Errno::EINVAL); } let (meta, inode, at) = lookup_at(cx, dirfd, path, flags)?; + // The LSM check of `vfs_getattr` of Linux. + if let Some(at) = &at { + rk_vfs::security::inode_getattr(at)?; + } let mnt_id = at.as_ref().map_or(0, |at| at.mount.id()); let mut s = Statx::new_zeroed(); // The unique ID if the caller asks for it, else the ID of mountinfo, as `vfs_statx_path` of @@ -995,6 +1008,38 @@ pub(crate) fn getdents64(cx: &Cx<'_>, fd: u64, buf: u64, count: u64) -> SysResul Ok(n as u64) } +/// The `getdents` call of x86_64, which gives the records of the old format `linux_dirent`. A +/// record of `linux_dirent64` has the same length, so the records are changed in place: the name +/// moves to offset 18, and the type goes in the last byte of the record, as `filldir` of Linux. +#[cfg(target_arch = "x86_64")] +pub(crate) fn getdents(cx: &Cx<'_>, fd: u64, buf: u64, count: u64) -> SysResult { + let file = cx.file(fd)?; + let mut data = vec![0u8; (count as usize).min(CHUNK)]; + let n = file.read_dir(&mut data)?; + to_old_dirents(&mut data[..n]); + cx.write_bytes(buf, &data[..n])?; + Ok(n as u64) +} + +/// Changes the `linux_dirent64` records in `data` to `linux_dirent` records in place. +#[cfg(target_arch = "x86_64")] +fn to_old_dirents(data: &mut [u8]) { + let mut at = 0; + while at + 19 < data.len() { + let reclen = usize::from(u16::from_ne_bytes([data[at + 16], data[at + 17]])); + if reclen < 20 || at + reclen > data.len() { + break; + } + let rec = &mut data[at..at + reclen]; + let kind = rec[18]; + let len = rec[19..].iter().position(|&b| b == 0).unwrap_or(reclen - 20); + rec.copy_within(19..19 + len, 18); + rec[18 + len..].fill(0); + rec[reclen - 1] = kind; + at += reclen; + } +} + pub(crate) fn ioctl(cx: &Cx<'_>, fd: u64, cmd: u64, arg: u64) -> SysResult { let cmd = cmd as u32; // As `security_file_ioctl` in the `ioctl` call of Linux. @@ -1627,6 +1672,14 @@ fn change_attr( attr: &SetAttr, change: Change, ) -> SysResult { + // The LSM checks come before `notify_change`, as `chmod_common` and `chown_common` of Linux. + if let Some(at) = at { + if change == Change::Owner { + rk_vfs::security::path_chown(at, attr.uid, attr.gid)?; + } else if let Some(mode) = attr.mode { + rk_vfs::security::path_chmod(at, mode)?; + } + } rk_vfs::inode::check_set_attr(&**inode, attr)?; let meta = inode.metadata(); // A symbolic link has no mode of its own, as `notify_change` of Linux. A path through a @@ -1813,6 +1866,7 @@ pub(crate) fn truncate(cx: &Cx<'_>, path: u64, size: u64) -> SysResult { // `get_write_access` and `break_lease` in `vfs_truncate` of Linux. let _access = rk_vfs::file::WriteAccess::new(at.inode())?; rk_vfs::lease::break_lease(at.inode(), rk_abi::native::open::O_WRONLY)?; + rk_vfs::security::path_truncate(&at)?; fsize_check(cx, &at.inode().metadata(), size)?; let mode = fs.cred().write_kill(&at.inode().metadata()); let attr = SetAttr { size: Some(size), mode, ..SetAttr::default() }; diff --git a/core/rk-syscall/src/table.rs b/core/rk-syscall/src/table.rs index b01a9ac..e31c050 100644 --- a/core/rk-syscall/src/table.rs +++ b/core/rk-syscall/src/table.rs @@ -352,6 +352,8 @@ fn handle(cx: &mut Cx<'_>, n: u32, a: [u64; 6]) -> SysResult { Ok(u64::from(cx.vnr(cx.thread.tid()))) } nr::GETDENTS64 => fs::getdents64(cx, a[0], a[1], a[2]), + #[cfg(target_arch = "x86_64")] + nr::GETDENTS => fs::getdents(cx, a[0], a[1], a[2]), nr::CLOCK_GETTIME => info::clock_gettime(cx, a[0], a[1]), nr::CLOCK_SETTIME => info::clock_settime(cx, a[0], a[1]), nr::CLOCK_ADJTIME => info::clock_adjtime(cx, a[0], a[1]), diff --git a/fs/rk-vfs/src/file.rs b/fs/rk-vfs/src/file.rs index 3005725..2a6c22e 100644 --- a/fs/rk-vfs/src/file.rs +++ b/fs/rk-vfs/src/file.rs @@ -1550,6 +1550,8 @@ impl File { let mode = crate::cred::current().write_kill(&inode.metadata()); let attr = SetAttr { size: Some(size), mode, ..SetAttr::default() }; check_set_attr(&**inode, &attr)?; + // The LSM check comes before the truncate, as `do_ftruncate` of Linux. + crate::security::file_truncate(self)?; crate::mapped::extend(inode, size); inode.set_attr(&attr)?; crate::mapped::changed(inode, size, u64::MAX); diff --git a/fs/rk-vfs/src/fs.rs b/fs/rk-vfs/src/fs.rs index 70356c4..9f995d7 100644 --- a/fs/rk-vfs/src/fs.rs +++ b/fs/rk-vfs/src/fs.rs @@ -301,6 +301,9 @@ impl FsContext { if parent.dir.dentry.is_dead() { return Err(Errno::ENOENT); } + // `may_o_create` of Linux asks the LSM for a `mknod` first. + let new_mode = mode::S_IFREG | (perm & mode::PERM_MASK & !self.umask); + crate::security::path_mknod(&parent.dir, &parent.name, new_mode, 0)?; self.may_change(&parent.dir)?; check_immutable(&**parent.dir.inode())?; let dir = parent.dir.inode(); @@ -431,11 +434,51 @@ impl FsContext { file.set_nonotify(); return Err(e); } + } + // The truncate comes after the checks of the open, as `handle_truncate` in `do_open` of + // Linux. + if let Err(e) = self.open_truncate(&file, flags, created) { + // The open did not complete, so the close gives no event. + file.set_nonotify(); + return Err(e); + } + if !file.flags().is_path() + && let Some(at) = file.location() + { crate::notify::open(at, false); } Ok(file) } + /// The truncate of an open of a regular file with `O_TRUNC` for a write. The truncate + /// clears the set-ID bits, as `handle_truncate` of Linux. + fn open_truncate(&self, file: &File, flags: u32, created: bool) -> Result<()> { + let write = matches!(flags & o::O_ACCMODE, o::O_WRONLY | o::O_RDWR); + if !write || flags & o::O_TRUNC == 0 || file.flags().is_path() { + return Ok(()); + } + let Some(at) = file.location() else { + return Ok(()); + }; + let meta = at.inode().metadata(); + if meta.file_type() != Some(FileType::Regular) { + return Ok(()); + } + // As `do_open` of Linux, a new file has no truncate, so it has no LSM check and no + // event of it. + if !created { + crate::security::file_truncate(file)?; + } + let mode = if created { None } else { self.cred.write_kill(&meta) }; + let attr = SetAttr { size: Some(0), mode, ..SetAttr::default() }; + at.inode().set_attr(&attr)?; + crate::mapped::changed(at.inode(), 0, u64::MAX); + if !created { + crate::notify::change(Some(at), at.inode(), &attr); + } + Ok(()) + } + /// The part of `open_inode` that makes the open file. fn open_file(&self, at: Location, flags: u32, created: bool) -> Result> { let meta = at.inode().metadata(); @@ -513,22 +556,7 @@ impl FsContext { // The open breaks the leases that conflict, as `break_lease` in // `do_dentry_open` of Linux. The file counts as open during the wait, so a // holder cannot downgrade to a read lease while an open for a write waits. - let finish = || -> Result<()> { - crate::lease::break_lease(at.inode(), flags)?; - if write && flags & o::O_TRUNC != 0 { - // The truncate clears the set-ID bits, as `handle_truncate` of Linux. - let mode = if created { None } else { self.cred.write_kill(&meta) }; - let attr = SetAttr { size: Some(0), mode, ..SetAttr::default() }; - at.inode().set_attr(&attr)?; - crate::mapped::changed(at.inode(), 0, u64::MAX); - // As `do_open` of Linux, a new file has no truncate and no event of it. - if !created { - crate::notify::change(Some(&at), at.inode(), &attr); - } - } - Ok(()) - }; - if let Err(e) = finish() { + if let Err(e) = crate::lease::break_lease(at.inode(), flags) { // The open did not complete, so the close gives no event. file.set_nonotify(); return Err(e); @@ -571,15 +599,16 @@ impl FsContext { if absent(&r) { Ok(false) } else { r.map(|_| true) } } - /// Makes a new inode of the type `kind` for `mkdirat`, `mknodat` and `symlinkat`. `f` gets - /// the directory, the name and the permission bits after the umask or the default ACL of the - /// directory masks `perm`. + /// Makes a new inode of the type `kind` for `mkdirat`, `mknodat` and `symlinkat`. `check` is + /// the LSM check, which gets the directory and the name. `f` gets the directory, the name and + /// the permission bits after the umask or the default ACL of the directory masks `perm`. fn make( &self, start: &Location, path: &[u8], kind: FileType, perm: u32, + check: impl FnOnce(&Location, &[u8]) -> Result<()>, f: impl FnOnce(&InodeRef, &[u8], u32) -> Result, ) -> Result { let parent = self.parent(start, path)?; @@ -591,6 +620,8 @@ impl FsContext { } // As `filename_create` of Linux, a name that exists gives `EEXIST` before `EROFS`. parent.dir.mount.want_write()?; + // The LSM check comes before the checks of `may_create`, as `do_mkdirat` of Linux. + check(&parent.dir, &parent.name)?; if parent.dir.dentry.is_dead() { return Err(Errno::ENOENT); } @@ -623,12 +654,15 @@ impl FsContext { perm: u32, f: impl FnOnce(&InodeRef, &[u8], u32) -> Result, ) -> Result { - self.make(start, path, FileType::Regular, perm & mode::PERM_MASK, f) + self.make(start, path, FileType::Regular, perm & mode::PERM_MASK, |_, _| Ok(()), f) } /// `mkdirat`. pub fn mkdir(&self, start: &Location, path: &[u8], perm: u32) -> Result { - self.make(start, path, FileType::Directory, perm & 0o1777, |dir, name, perm| { + let check = |dir: &Location, name: &[u8]| { + crate::security::path_mkdir(dir, name, mode::S_IFDIR | (perm & 0o1777)) + }; + self.make(start, path, FileType::Directory, perm & 0o1777, check, |dir, name, perm| { dir.create(name, FileType::Directory, perm, 0) }) } @@ -643,7 +677,11 @@ impl FsContext { if matches!(kind, FileType::Directory | FileType::Symlink) { return Err(Errno::EINVAL); } - self.make(start, path, kind, mode & mode::PERM_MASK, |dir, name, perm| { + let check = |dir: &Location, name: &[u8]| { + let full = kind.mode_bits() | (mode & mode::PERM_MASK); + crate::security::path_mknod(dir, name, full, rdev) + }; + self.make(start, path, kind, mode & mode::PERM_MASK, check, |dir, name, perm| { if matches!(kind, FileType::CharDevice | FileType::BlockDevice) && !self.cred.capable(cap::MKNOD) { @@ -661,7 +699,10 @@ impl FsContext { if target.is_empty() { return Err(Errno::ENOENT); } - self.make(start, path, FileType::Symlink, 0o777, |dir, name, _| dir.symlink(name, target)) + let check = |dir: &Location, name: &[u8]| crate::security::path_symlink(dir, name, target); + self.make(start, path, FileType::Symlink, 0o777, check, |dir, name, _| { + dir.symlink(name, target) + }) } /// `linkat`. `old` is the location of the file, after the walk of the old path. @@ -681,6 +722,7 @@ impl FsContext { return Err(Errno::ENOENT); } self.cred.may_link(protected::get(&protected::HARDLINKS), &**old.inode())?; + crate::security::path_link(old, &parent.dir, &parent.name)?; self.may_change(&parent.dir)?; check_immutable(&**parent.dir.inode())?; check_fixed(&**old.inode())?; @@ -723,6 +765,12 @@ impl FsContext { } let at = self.child(&parent)?; let is_dir = is_type(at.inode(), FileType::Directory); + // The LSM check comes before `may_delete`, as `do_rmdir` and `do_unlinkat` of Linux. + if dir { + crate::security::path_rmdir(&parent.dir, &at)?; + } else { + crate::security::path_unlink(&parent.dir, &at)?; + } // The checks of `may_delete` of Linux. self.may_change(&parent.dir)?; check_fixed(&**parent.dir.inode())?; @@ -790,7 +838,7 @@ impl FsContext { let from = self.child(&old)?; let from_dir = is_type(from.inode(), FileType::Directory); if exchange { - return self.exchange(&old, &new, &from); + return self.exchange(&old, &new, &from, flags); } if (old.trailing_slash || new.trailing_slash) && !from_dir { return Err(Errno::ENOTDIR); @@ -803,9 +851,6 @@ impl FsContext { if from_dir && new.dir.dentry.is_under(&from.dentry) { return Err(Errno::EINVAL); } - if to.as_ref().is_some_and(|to| Arc::ptr_eq(from.inode(), to.inode())) { - return Ok(()); - } // A directory above the old parent is not empty, as the `trap` of `lock_rename` in // `do_renameat2` of Linux gives. The lock of the target below needs this check first. if let Some(to) = &to @@ -814,6 +859,11 @@ impl FsContext { { return Err(Errno::ENOTEMPTY); } + // The LSM check comes before `vfs_rename`, as `do_renameat2` of Linux. + crate::security::path_rename(&from, &new.dir, &new.name, to.as_ref(), flags)?; + if to.as_ref().is_some_and(|to| Arc::ptr_eq(from.inode(), to.inode())) { + return Ok(()); + } // The checks of `may_delete` and `may_create` of Linux, in their order: the permission // checks come before the checks of the types. A directory that moves to a new parent // must be writable, because its `..` changes. @@ -880,13 +930,21 @@ impl FsContext { /// The part of `renameat2` for `RENAME_EXCHANGE`, which exchanges `from`, the child of `old`, /// and the child of `new`. The caller examined the flags and the mounts of the parents. - fn exchange(&self, old: &Parent, new: &Parent, from: &Location) -> Result<()> { + fn exchange(&self, old: &Parent, new: &Parent, from: &Location, flags: u32) -> Result<()> { let to = self.child(new)?; let from_dir = is_type(from.inode(), FileType::Directory); let to_dir = is_type(to.inode(), FileType::Directory); if (old.trailing_slash && !from_dir) || (new.trailing_slash && !to_dir) { return Err(Errno::ENOTDIR); } + if from_dir && new.dir.dentry.is_under(&from.dentry) { + return Err(Errno::EINVAL); + } + if to_dir && old.dir.dentry.is_under(&to.dentry) { + return Err(Errno::EINVAL); + } + // The LSM check comes before `vfs_rename`, as `do_renameat2` of Linux. + crate::security::path_rename(from, &new.dir, &new.name, Some(&to), flags)?; if Arc::ptr_eq(from.inode(), to.inode()) { return Ok(()); } @@ -895,12 +953,6 @@ impl FsContext { return Err(Errno::EBUSY); } } - if from_dir && new.dir.dentry.is_under(&from.dentry) { - return Err(Errno::EINVAL); - } - if to_dir && old.dir.dentry.is_under(&to.dentry) { - return Err(Errno::EINVAL); - } // Each name is deleted from its directory and made in the other, as `may_delete` and // `may_create` of Linux examine for an exchange. for (dir, child) in [(&old.dir, from), (&new.dir, &to)] { diff --git a/fs/rk-vfs/src/security/hooks.rs b/fs/rk-vfs/src/security/hooks.rs index 20b2d4f..8d559a6 100644 --- a/fs/rk-vfs/src/security/hooks.rs +++ b/fs/rk-vfs/src/security/hooks.rs @@ -424,6 +424,76 @@ pub trait SecurityHooks: Sync { Ok(()) } + /// `path_unlink`: `unlink` of the file `at` in the directory `dir`. + fn path_unlink(&self, dir: &Location, at: &Location) -> Result<()> { + Ok(()) + } + + /// `path_mkdir`: `mkdir` of the new directory `name` in `dir` with the mode `mode`. + fn path_mkdir(&self, dir: &Location, name: &[u8], mode: u32) -> Result<()> { + Ok(()) + } + + /// `path_rmdir`: `rmdir` of the directory `at` in the directory `dir`. + fn path_rmdir(&self, dir: &Location, at: &Location) -> Result<()> { + Ok(()) + } + + /// `path_mknod`: `mknod` of the new file `name` in `dir` with the mode `mode`, with the + /// file type, and the device `dev`. + fn path_mknod(&self, dir: &Location, name: &[u8], mode: u32, dev: u64) -> Result<()> { + Ok(()) + } + + /// `path_truncate`: `truncate` of the file `at`. + fn path_truncate(&self, at: &Location) -> Result<()> { + Ok(()) + } + + /// `path_symlink`: `symlink` of the new link `name` in `dir` to `target`. + fn path_symlink(&self, dir: &Location, name: &[u8], target: &[u8]) -> Result<()> { + Ok(()) + } + + /// `path_link`: `link` of the file `old` to the new name `name` in `dir`. + fn path_link(&self, old: &Location, dir: &Location, name: &[u8]) -> Result<()> { + Ok(()) + } + + /// `path_rename`: `rename` of the file `old` to the name `name` in `dir`. `new` is the file + /// that has the name now, if there is one. `flags` are the flags of `renameat2`. + fn path_rename( + &self, + old: &Location, + dir: &Location, + name: &[u8], + new: Option<&Location>, + flags: u32, + ) -> Result<()> { + Ok(()) + } + + /// `path_chmod`: `chmod` of the file `at` to the mode `mode`. + fn path_chmod(&self, at: &Location, mode: u32) -> Result<()> { + Ok(()) + } + + /// `path_chown`: `chown` of the file `at` to the user `uid` and the group `gid`. `None` + /// keeps the ID. + fn path_chown(&self, at: &Location, uid: Option, gid: Option) -> Result<()> { + Ok(()) + } + + /// `inode_getattr`: `stat` of the file `at`. + fn inode_getattr(&self, at: &Location) -> Result<()> { + Ok(()) + } + + /// `file_truncate`: `ftruncate` of `file`. + fn file_truncate(&self, file: &File) -> Result<()> { + Ok(()) + } + /// `task_alloc`: `child` is made by `clone` with `clone_flags`. `child.security` is the new /// task blob, and it has a copy of the blob of the parent. fn task_alloc(&self, child: &Task<'_>, clone_flags: u64) -> Result<()> { diff --git a/fs/rk-vfs/src/security/mod.rs b/fs/rk-vfs/src/security/mod.rs index 810b67b..f016382 100644 --- a/fs/rk-vfs/src/security/mod.rs +++ b/fs/rk-vfs/src/security/mod.rs @@ -441,6 +441,72 @@ pub fn file_open(file: &File, cred: &Cred, flags: u32) -> Result<()> { call_int_hook!(file_open, file, cred, flags) } +/// `security_path_unlink`. +pub fn path_unlink(dir: &Location, at: &Location) -> Result<()> { + call_int_hook!(path_unlink, dir, at) +} + +/// `security_path_mkdir`. +pub fn path_mkdir(dir: &Location, name: &[u8], mode: u32) -> Result<()> { + call_int_hook!(path_mkdir, dir, name, mode) +} + +/// `security_path_rmdir`. +pub fn path_rmdir(dir: &Location, at: &Location) -> Result<()> { + call_int_hook!(path_rmdir, dir, at) +} + +/// `security_path_mknod`. +pub fn path_mknod(dir: &Location, name: &[u8], mode: u32, dev: u64) -> Result<()> { + call_int_hook!(path_mknod, dir, name, mode, dev) +} + +/// `security_path_truncate`. +pub fn path_truncate(at: &Location) -> Result<()> { + call_int_hook!(path_truncate, at) +} + +/// `security_path_symlink`. +pub fn path_symlink(dir: &Location, name: &[u8], target: &[u8]) -> Result<()> { + call_int_hook!(path_symlink, dir, name, target) +} + +/// `security_path_link`. +pub fn path_link(old: &Location, dir: &Location, name: &[u8]) -> Result<()> { + call_int_hook!(path_link, old, dir, name) +} + +/// `security_path_rename`. +pub fn path_rename( + old: &Location, + dir: &Location, + name: &[u8], + new: Option<&Location>, + flags: u32, +) -> Result<()> { + call_int_hook!(path_rename, old, dir, name, new, flags) +} + +/// `security_path_chmod`. +pub fn path_chmod(at: &Location, mode: u32) -> Result<()> { + call_int_hook!(path_chmod, at, mode) +} + +/// `security_path_chown`. +pub fn path_chown(at: &Location, uid: Option, gid: Option) -> Result<()> { + call_int_hook!(path_chown, at, uid, gid) +} + +/// `security_inode_getattr`. +pub fn inode_getattr(at: &Location) -> Result<()> { + call_int_hook!(inode_getattr, at) +} + +/// `security_file_truncate`. +pub fn file_truncate(file: &File) -> Result<()> { + call_int_hook!(file_truncate, file) +} + /// `security_task_alloc`. pub fn task_alloc(child: &Task<'_>, clone_flags: u64) -> Result<()> { call_int_hook!(task_alloc, child, clone_flags) diff --git a/sec/rk-lsm-apparmor/src/domain.rs b/sec/rk-lsm-apparmor/src/domain.rs index cdefe61..c6d2799 100644 --- a/sec/rk-lsm-apparmor/src/domain.rs +++ b/sec/rk-lsm-apparmor/src/domain.rs @@ -258,15 +258,22 @@ fn accum(a: &mut Perms, b: &Perms) { /// The permissions of the file rules for the path `name`, `aa_str_perms` of Linux. `owner` /// tells if the file system user ID of the task is the owner of the file. pub fn str_perms(pdb: &Pdb, name: &[u8], owner: bool) -> Perms { - let Some(d) = pdb.dfa.as_ref() else { return Perms::default() }; - lookup_fperms(pdb, d.match_bytes(pdb.start[class::FILE], name), owner) + str_perms_at(pdb, pdb.start[class::FILE], name, owner).1 +} + +/// As [`str_perms`], but from the state `start`. Also gives the state after the name, for the +/// second name of a pair of names. +pub fn str_perms_at(pdb: &Pdb, start: State, name: &[u8], owner: bool) -> (State, Perms) { + let Some(d) = pdb.dfa.as_ref() else { return (NOMATCH, Perms::default()) }; + let state = d.match_bytes(start, name); + (state, lookup_fperms(pdb, state, owner)) } /// The characters of the file permissions in the log, `aa_file_perm_chrs` of Linux. const FILE_PERM_CHRS: &[u8] = b"xwracd km l "; /// The permissions that the log of a file operation shows, `AA_AUDIT_FILE_MASK` of Linux. -const AUDIT_FILE_MASK: u32 = perm::READ +pub const AUDIT_FILE_MASK: u32 = perm::READ | perm::WRITE | perm::EXEC | perm::APPEND diff --git a/sec/rk-lsm-apparmor/src/file.rs b/sec/rk-lsm-apparmor/src/file.rs index c2554da..4638f6c 100644 --- a/sec/rk-lsm-apparmor/src/file.rs +++ b/sec/rk-lsm-apparmor/src/file.rs @@ -9,14 +9,17 @@ //! in [`crate::domain`]. Each profile of a stack makes its record, and the access gives the error //! of the last profile that denies it, as `fn_for_each` of Linux. +use crate::dfa::NOMATCH; use crate::domain::{self, FileAudit, audit_file, str_perms}; use crate::policy::{Mode, Profile}; -use crate::unpack::{Perms, perm}; +use crate::unpack::{Perms, class, perm, x}; +use alloc::string::String; use alloc::sync::Arc; use alloc::vec::Vec; use rk_abi::Errno; use rk_abi::native::mman::{MAP_PRIVATE, PROT_EXEC, PROT_READ, PROT_WRITE}; use rk_abi::native::open as o; +use rk_abi::native::stat::{S_IFDIR, S_IFMT}; use rk_vfs::cred::Cred; use rk_vfs::dentry::Location; use rk_vfs::file::File; @@ -41,6 +44,30 @@ pub mod op { pub const FLOCK: &str = "file_lock"; /// `OP_FMMAP`. pub const FMMAP: &str = "file_mmap"; + /// `OP_UNLINK`. + pub const UNLINK: &str = "unlink"; + /// `OP_MKDIR`. + pub const MKDIR: &str = "mkdir"; + /// `OP_RMDIR`. + pub const RMDIR: &str = "rmdir"; + /// `OP_MKNOD`. An open that makes a file also has this name. + pub const MKNOD: &str = "mknod"; + /// `OP_TRUNC`. + pub const TRUNC: &str = "truncate"; + /// `OP_LINK`. + pub const LINK: &str = "link"; + /// `OP_SYMLINK`. + pub const SYMLINK: &str = "symlink"; + /// `OP_RENAME_SRC`. + pub const RENAME_SRC: &str = "rename_src"; + /// `OP_RENAME_DEST`. + pub const RENAME_DEST: &str = "rename_dest"; + /// `OP_CHMOD`. + pub const CHMOD: &str = "chmod"; + /// `OP_CHOWN`. + pub const CHOWN: &str = "chown"; + /// `OP_GETATTR`. + pub const GETATTR: &str = "getattr"; } /// `LOCK_EX` of `flock`. @@ -48,6 +75,18 @@ const LOCK_EX: u32 = 2; /// `LOCK_NB` of `flock`. const LOCK_NB: u32 = 4; +/// `RENAME_EXCHANGE` of `renameat2`. +const RENAME_EXCHANGE: u32 = 2; + +/// The permission that tells that a link must be a subset of its target, `AA_LINK_SUBSET` of +/// Linux. It has the bit of the lock. +const LINK_SUBSET: u32 = perm::LOCK; + +/// The permissions that the old name of a rename asks for. +const RENAME_SRC: u32 = perm::READ | perm::GETATTR | perm::WRITE | perm::SETATTR | perm::DELETE; +/// The permissions that the new name of a rename asks for. +const RENAME_DEST: u32 = perm::WRITE | perm::SETATTR | perm::CREATE; + /// The byte offset of the part of AppArmor in the blob of a file, or `None` if the module is /// not active. fn file_offset() -> Option { @@ -108,24 +147,54 @@ pub fn path_name(path: Vec, dir: bool) -> Vec { name } -/// Checks the permissions `request` on the file at `at` for the profile `p`, and logs the -/// result, as `profile_path_perm` and `__aa_path_perm` of Linux. `fsuid` is the file system -/// user ID of the task. +/// The condition of a check of a path, `struct path_cond` of Linux: the owner of the file and +/// the type. The name of a directory ends with a `/`. +#[derive(Clone, Copy)] +struct Cond { + uid: u32, + dir: bool, +} + +impl Cond { + /// The condition of the file at `at`. + fn of(at: &Location) -> Self { + let meta = at.inode().metadata(); + Self { uid: meta.uid, dir: meta.file_type() == Some(FileType::Directory) } + } +} + +/// The name of the new file `name` in the directory `dir`, for a file that is not there yet. +fn child_name(dir: &Location, name: &[u8], is_dir: bool) -> Vec { + let mut path = dir.absolute_path(); + if path != b"/" { + path.push(b'/'); + } + path.extend_from_slice(name); + path_name(path, is_dir) +} + +/// Checks the permissions `request` on the file with the name `name` for the profile `p`, and +/// logs the result, as `__aa_path_perm` of Linux. `fsuid` is the file system user ID of the +/// task. /// /// # Errors /// /// Gives `EACCES` if the policy does not allow the request, except in complain mode. -fn path_perm(op: &'static str, p: &Profile, at: &Location, request: u32, fsuid: u32) -> Result<()> { +fn name_perm( + op: &'static str, + p: &Profile, + name: &[u8], + request: u32, + cond: Cond, + fsuid: u32, +) -> Result<()> { if p.mode == Mode::Unconfined { return Ok(()); } - let meta = at.inode().metadata(); - let dir = meta.file_type() == Some(FileType::Directory); - let name = path_name(at.absolute_path(), dir); - let owner = fsuid == meta.uid; - let perms = p.rules.as_ref().map_or(Perms::default(), |r| str_perms(&r.file, &name, owner)); + let owner = fsuid == cond.uid; + let perms = p.rules.as_ref().map_or(Perms::default(), |r| str_perms(&r.file, name, owner)); let error = (request & !perms.allow != 0).then_some(Errno::EACCES); - let a = FileAudit { op, request, name: &name, target: None, ouid: meta.uid, info: None, error }; + let a = FileAudit { op, request, name, target: None, ouid: cond.uid, info: None, error }; audit_file(Some(p), &perms, &a) } @@ -135,6 +204,28 @@ fn path_perm(op: &'static str, p: &Profile, at: &Location, request: u32, fsuid: /// # Errors /// /// Gives the error of the last profile that denies the request. +fn label_name_perm( + op: &'static str, + l: &[Arc], + name: &[u8], + request: u32, + cond: Cond, + fsuid: u32, +) -> Result<()> { + let mut result = Ok(()); + for p in l.iter().filter(|p| p.mode != Mode::Unconfined) { + if let Err(e) = name_perm(op, p, name, request, cond, fsuid) { + result = Err(e); + } + } + result +} + +/// [`label_name_perm`] for the file at `at`, `aa_path_perm` of Linux. +/// +/// # Errors +/// +/// Gives the error of the last profile that denies the request. fn label_path_perm( op: &'static str, l: &[Arc], @@ -142,15 +233,180 @@ fn label_path_perm( request: u32, fsuid: u32, ) -> Result<()> { + let cond = Cond::of(at); + let name = path_name(at.absolute_path(), cond.dir); + label_name_perm(op, l, &name, request, cond, fsuid) +} + +/// The label of the current task and its file system user ID. `None` is `unconfined`, which +/// has no check. +fn current_label() -> Option<(domain::Label, u32)> { + let cred = rk_vfs::cred::current(); + if domain::label_id(&cred) == 0 { + return None; + } + Some((domain::label_of(&cred), cred.fsuid)) +} + +/// The check of the permissions `request` on the file at `at` by the current task, +/// `common_perm_cond` of Linux. It is for a remove, a truncate, a `chmod`, a `chown` and a +/// `stat`. +/// +/// # Errors +/// +/// Gives `EACCES` if the policy does not allow the request. +pub fn path_cond(op: &'static str, at: &Location, request: u32) -> Result<()> { + let Some((l, fsuid)) = current_label() else { return Ok(()) }; + label_path_perm(op, &l, at, request, fsuid) +} + +/// The check of a new file `name` with the mode `mode` in the directory `dir`, +/// `common_perm_create` of Linux. The task is the owner of the new file. +/// +/// # Errors +/// +/// Gives `EACCES` if the policy does not allow the create. +pub fn path_create(op: &'static str, dir: &Location, name: &[u8], mode: u32) -> Result<()> { + let Some((l, fsuid)) = current_label() else { return Ok(()) }; + let cond = Cond { uid: fsuid, dir: mode & S_IFMT == S_IFDIR }; + let name = child_name(dir, name, cond.dir); + label_name_perm(op, &l, &name, perm::CREATE, cond, fsuid) +} + +/// `apparmor_path_rename` of Linux: a rename of the file `old` to the name `name` in `dir`. `new` +/// is the file that has the new name now. The old name asks for the permissions of a read, a +/// write and a delete, and the new name for a write and a create. An exchange also checks the +/// two names the other way. +/// +/// # Errors +/// +/// Gives `EACCES` if the policy does not allow the rename. +pub fn path_rename( + old: &Location, + dir: &Location, + name: &[u8], + new: Option<&Location>, + flags: u32, +) -> Result<()> { + let Some((l, fsuid)) = current_label() else { return Ok(()) }; + let cond = Cond::of(old); + if flags & RENAME_EXCHANGE != 0 + && let Some(new) = new + { + // As Linux, the condition of the other way has the type of the new file and the owner + // of the old file. + let other = Cond { uid: cond.uid, dir: Cond::of(new).dir }; + let new_name = child_name(dir, name, other.dir); + label_name_perm(op::RENAME_SRC, &l, &new_name, RENAME_SRC, other, fsuid)?; + let old_name = path_name(old.absolute_path(), other.dir); + label_name_perm(op::RENAME_DEST, &l, &old_name, RENAME_DEST, other, fsuid)?; + } + let old_name = path_name(old.absolute_path(), cond.dir); + label_name_perm(op::RENAME_SRC, &l, &old_name, RENAME_SRC, cond, fsuid)?; + let new_name = child_name(dir, name, cond.dir); + label_name_perm(op::RENAME_DEST, &l, &new_name, RENAME_DEST, cond, fsuid) +} + +/// `apparmor_path_link` and `aa_path_link` of Linux: a hard link with the name `name` in `dir` +/// to the file `old`. +/// +/// # Errors +/// +/// Gives `EACCES` if the policy does not allow the link. +pub fn path_link(old: &Location, dir: &Location, name: &[u8]) -> Result<()> { + let Some((l, fsuid)) = current_label() else { return Ok(()) }; + let cond = Cond::of(old); + // The names of a link have no `/` at the end, as `aa_path_link` of Linux. + let lname = child_name(dir, name, false); + let tname = old.absolute_path(); let mut result = Ok(()); for p in l.iter().filter(|p| p.mode != Mode::Unconfined) { - if let Err(e) = path_perm(op, p, at, request, fsuid) { + if let Err(e) = profile_path_link(p, &lname, &tname, cond, fsuid) { result = Err(e); } } result } +/// Tells if the exec transition of a link is a subset of the transition of its target, +/// `xindex_is_subset` of Linux. +fn xindex_is_subset(link: u32, target: u32) -> bool { + (link & !x::UNSAFE) == (target & !x::UNSAFE) + && (link & x::UNSAFE == 0 || target & x::UNSAFE != 0) +} + +/// The check of a link with the name `lname` to the file `tname` for the profile `p`, +/// `profile_path_link` of Linux. The pair of the names must have the link permission. If the +/// pair has the subset permission, the permissions of the link must be a subset of the +/// permissions of the target. +/// +/// # Errors +/// +/// Gives `EACCES` if the policy does not allow the link, except in complain mode. +fn profile_path_link( + p: &Profile, + lname: &[u8], + tname: &[u8], + cond: Cond, + fsuid: u32, +) -> Result<()> { + let owner = fsuid == cond.uid; + let pdb = p.rules.as_ref().map(|r| &r.file); + let mut request = perm::LINK; + let mut info = None; + let (state, mut lperms) = match pdb { + Some(f) => domain::str_perms_at(f, f.start[class::FILE], lname, owner), + None => (NOMATCH, Perms::default()), + }; + let error = 'check: { + let Some(f) = pdb.filter(|_| lperms.allow & perm::LINK != 0) else { + break 'check Some(Errno::EACCES); + }; + // The target follows the link name after a null byte. + let next = f.dfa.as_ref().map_or(NOMATCH, |d| d.next(state, 0)); + let (_, perms) = domain::str_perms_at(f, next, tname, owner); + // The audit, quiet and kill masks of a link are in the entry of the pair. + lperms.audit = perms.audit; + lperms.quiet = perms.quiet; + lperms.kill = perms.kill; + if perms.allow & perm::LINK == 0 { + info = Some("target restricted"); + lperms = perms; + break 'check Some(Errno::EACCES); + } + if perms.allow & LINK_SUBSET == 0 { + break 'check None; + } + // The permissions of the link, other than the link, must be a subset of the + // permissions of the target. + let perms = str_perms(f, tname, owner); + request = lperms.allow & !perm::LINK; + lperms.allow &= perms.allow | perm::LINK; + request |= domain::AUDIT_FILE_MASK & (lperms.allow & !perms.allow); + if request & !lperms.allow != 0 { + break 'check Some(Errno::EACCES); + } + if lperms.allow & perm::EXEC != 0 && !xindex_is_subset(lperms.xindex, perms.xindex) { + lperms.allow &= !perm::EXEC; + request |= perm::EXEC; + info = Some("link not subset of target"); + break 'check Some(Errno::EACCES); + } + None + }; + let target = String::from_utf8_lossy(tname); + let a = FileAudit { + op: op::LINK, + request, + name: lname, + target: Some(&target), + ouid: cond.uid, + info, + error, + }; + audit_file(Some(p), &lperms, &a) +} + /// `apparmor_file_alloc_security` of Linux: the file gets the label of the task. pub fn file_alloc(file: &File) { if let Some(off) = file_offset() { @@ -293,6 +549,15 @@ mod tests { assert_eq!(perm::READ & !str_perms(file, b"/usr/bin/", true).allow, perm::READ); } + #[test] + fn the_exec_of_a_link_must_be_the_exec_of_its_target() { + let name = x::NAME | 1; + assert!(xindex_is_subset(name, name)); + assert!(xindex_is_subset(name, name | x::UNSAFE)); + assert!(!xindex_is_subset(name | x::UNSAFE, name)); + assert!(!xindex_is_subset(name, x::NAME | 2)); + } + #[test] fn a_directory_has_a_slash_at_the_end_of_its_name() { assert_eq!(path_name(b"/tmp".to_vec(), true), b"/tmp/"); diff --git a/sec/rk-lsm-apparmor/src/lib.rs b/sec/rk-lsm-apparmor/src/lib.rs index 9939c79..d638813 100644 --- a/sec/rk-lsm-apparmor/src/lib.rs +++ b/sec/rk-lsm-apparmor/src/lib.rs @@ -28,10 +28,13 @@ pub mod policy; pub mod task; pub mod unpack; +use crate::unpack::perm; use alloc::string::String; use alloc::vec::Vec; use core::sync::atomic::{AtomicBool, Ordering}; +use rk_abi::native::stat::{S_IFDIR, S_IFLNK}; use rk_vfs::cred::Cred; +use rk_vfs::dentry::Location; use rk_vfs::file::File; use rk_vfs::inode::Result; use rk_vfs::security::{ @@ -67,6 +70,62 @@ impl SecurityHooks for AppArmor { file::mmap_file(f, prot, flags) } + fn path_unlink(&self, _dir: &Location, at: &Location) -> Result<()> { + file::path_cond(file::op::UNLINK, at, perm::DELETE) + } + + fn path_mkdir(&self, dir: &Location, name: &[u8], _mode: u32) -> Result<()> { + file::path_create(file::op::MKDIR, dir, name, S_IFDIR) + } + + fn path_rmdir(&self, _dir: &Location, at: &Location) -> Result<()> { + file::path_cond(file::op::RMDIR, at, perm::DELETE) + } + + fn path_mknod(&self, dir: &Location, name: &[u8], mode: u32, _dev: u64) -> Result<()> { + file::path_create(file::op::MKNOD, dir, name, mode) + } + + fn path_truncate(&self, at: &Location) -> Result<()> { + file::path_cond(file::op::TRUNC, at, perm::WRITE | perm::SETATTR) + } + + fn path_symlink(&self, dir: &Location, name: &[u8], _target: &[u8]) -> Result<()> { + file::path_create(file::op::SYMLINK, dir, name, S_IFLNK) + } + + fn path_link(&self, old: &Location, dir: &Location, name: &[u8]) -> Result<()> { + file::path_link(old, dir, name) + } + + fn path_rename( + &self, + old: &Location, + dir: &Location, + name: &[u8], + new: Option<&Location>, + flags: u32, + ) -> Result<()> { + file::path_rename(old, dir, name, new, flags) + } + + fn path_chmod(&self, at: &Location, _mode: u32) -> Result<()> { + file::path_cond(file::op::CHMOD, at, perm::CHMOD) + } + + fn path_chown(&self, at: &Location, _uid: Option, _gid: Option) -> Result<()> { + file::path_cond(file::op::CHOWN, at, perm::CHOWN) + } + + fn inode_getattr(&self, at: &Location) -> Result<()> { + file::path_cond(file::op::GETATTR, at, perm::GETATTR) + } + + fn file_truncate(&self, f: &File) -> Result<()> { + // As Linux, the check is the check of the path of the file. + f.location().map_or(Ok(()), |at| self.path_truncate(at)) + } + fn getselfattr(&self, attr: u64, task: &Task<'_>) -> Result> { domain::getselfattr(attr, task) } diff --git a/tests/user/init.c b/tests/user/init.c index 7ebc071..d839be3 100644 --- a/tests/user/init.c +++ b/tests/user/init.c @@ -15838,12 +15838,14 @@ static int aa_write(const char *path, const char *data, size_t len, int flags) * that the file checks log: an open with a create in /tmp, an exclusive lock of that file, an * open of /etc/rk_test.aa, an executable map of it and an open of the directory /usr/bin. * 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 and an exec with EACCES, - * and gives 0 or the number of the first check that failed. It does not read its label, because - * the rules do not allow it. + * "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 + * 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) { + struct stat st; char buf[256]; int n, w; char *args[] = { "rk-sub", NULL }; @@ -15856,6 +15858,27 @@ static int aa_label_helper(int argc, char **argv) return 3; if (flock(w, LOCK_EX) == 0 || errno != EACCES) return 4; + /* As on Linux, the map of the old format of the policy allows getattr in each state. */ + if (stat("/etc/rk_test.aa", &st) != 0) + return 6; + if (chmod("/etc/rk_test.aa", 0644) == 0 || errno != EACCES) + return 7; + if (argc < 3 || truncate("/etc/rk_test.aa", atol(argv[2])) == 0 || errno != EACCES) + return 8; + if (link("/etc/rk_test.aa", "/etc/rk-aa-l") == 0 || errno != EACCES) + return 9; + if (rename("/etc/rk_test.aa", "/etc/rk-aa-r") == 0 || errno != EACCES) + return 10; + if (mkdir("/etc/rk-aa-d", 0755) == 0 || errno != EACCES) + return 11; + if (rmdir("/usr/bin") == 0 || errno != EACCES) + return 12; + if (symlink("x", "/etc/rk-aa-s") == 0 || errno != EACCES) + return 13; + if (mknod("/etc/rk-aa-n", S_IFIFO | 0644, 0) == 0 || errno != EACCES) + return 14; + if (unlink("/etc/rk_test.aa") == 0 || errno != EACCES) + return 15; execv("/tmp/rk-sub", args); return errno == EACCES ? 0 : 5; } @@ -16162,7 +16185,8 @@ static void test_apparmor(void) { /* The flags of rk_test: not a hat, and the mode 1 (complain). The mode 0 is enforce. */ static const char flags[] = "flags\0\x07\x02\0\0\0\0\x02\x01"; - char *five[] = { "rk-test", "deny", NULL }; + char size[32]; + char *five[] = { "rk-test", "deny", size, NULL }; int i, at = -1; fd = open("/etc/rk_test.aa", O_RDONLY); @@ -16174,13 +16198,15 @@ static void test_apparmor(void) at = i; if (at >= 0) big[at + sizeof(flags) - 2] = 0; + /* A truncate to the same size changes nothing if the check fails. */ + snprintf(size, sizeof(size), "%d", n); check("apparmor: a replace of rk_test in enforce mode works", at >= 0 && aa_write("/sys/kernel/security/apparmor/.replace", big, n, 0) == 0 && aa_read("/sys/kernel/security/apparmor/profiles", buf, 256) > 0 && strstr(buf, "rk_test (enforce)\n") != NULL); pid = fork(); if (pid == 0) { execv("/usr/bin/rk-test", five); _exit(127); } - check("apparmor: in enforce mode, an open, a lock 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", 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(); @@ -16196,6 +16222,8 @@ static void test_apparmor(void) n = klogctl(3, buf, sizeof(buf) - 1); buf[n > 0 ? n : 0] = 0; 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 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);