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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
56 changes: 55 additions & 1 deletion core/rk-syscall/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -784,6 +789,10 @@ fn lookup(
flags: u32,
) -> Result<(Metadata, Option<rk_vfs::InodeRef>, 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())))
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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() };
Expand Down
2 changes: 2 additions & 0 deletions core/rk-syscall/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand Down
2 changes: 2 additions & 0 deletions fs/rk-vfs/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
120 changes: 86 additions & 34 deletions fs/rk-vfs/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<Arc<File>> {
let meta = at.inode().metadata();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<InodeRef>,
) -> Result<Location> {
let parent = self.parent(start, path)?;
Expand All @@ -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);
}
Expand Down Expand Up @@ -623,12 +654,15 @@ impl FsContext {
perm: u32,
f: impl FnOnce(&InodeRef, &[u8], u32) -> Result<InodeRef>,
) -> Result<Location> {
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<Location> {
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)
})
}
Expand All @@ -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)
{
Expand All @@ -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.
Expand All @@ -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())?;
Expand Down Expand Up @@ -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())?;
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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(());
}
Expand All @@ -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)] {
Expand Down
Loading
Loading