use alloc::{
ffi::CString,
string::{String, ToString},
vec,
vec::Vec,
};
use core::{
ffi::{c_char, c_int},
mem::{offset_of, size_of},
time::Duration,
};
use ax_fs_ng::vfs::{FsContext, current_fs_context, sync_all_cached_files};
use ax_runtime::hal::time::wall_time;
use axfs_ng_vfs::{
DeviceId, DirectoryCursor, FileExtentTarget, MetadataUpdate, NodePermission, NodeType,
RenameOptions, VfsError, path::Path,
};
use linux_raw_sys::{
general::*,
ioctl::{FIOASYNC, FIONBIO, FS_IOC_FIEMAP},
};
use crate::{
Errno, StarryError, StarryResult,
file::{Directory, FileLike, current_fd_table, fd_is_path, get_file_like, resolve_at, with_fs},
mm::{VmMutPtr, VmPtr, vm_load_path_string, vm_load_string, vm_write_slice},
task::UserTaskRef,
time::TimeValueLike,
};
pub const FIOCLEX: u32 = 0x5451;
pub const FIONCLEX: u32 = 0x5450;
const FIEMAP_FLAG_SYNC: u32 = 0x0000_0001;
const FIEMAP_FLAG_XATTR: u32 = 0x0000_0002;
const FIEMAP_FLAG_CACHE: u32 = 0x0000_0004;
const FIEMAP_FLAGS_COMPAT: u32 = FIEMAP_FLAG_SYNC | FIEMAP_FLAG_XATTR;
const FIEMAP_EXTENT_LAST: u32 = 0x0000_0001;
const FIEMAP_EXTENT_NOT_ALIGNED: u32 = 0x0000_0100;
const FIEMAP_EXTENT_DATA_INLINE: u32 = 0x0000_0200;
const FIEMAP_EXTENT_UNWRITTEN: u32 = 0x0000_0800;
const FIEMAP_EXTENT_MERGED: u32 = 0x0000_1000;
#[repr(C)]
#[derive(Clone, Copy, Default, bytemuck::AnyBitPattern, bytemuck::NoUninit)]
struct FiemapHeader {
start: u64,
length: u64,
flags: u32,
mapped_extents: u32,
extent_count: u32,
reserved: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Default, bytemuck::NoUninit)]
struct FiemapExtent {
logical: u64,
physical: u64,
length: u64,
reserved64: [u64; 2],
flags: u32,
reserved: [u32; 3],
}
const _: () = {
assert!(size_of::<FiemapHeader>() == 32);
assert!(size_of::<FiemapExtent>() == 56);
assert!(offset_of!(FiemapExtent, flags) == 40);
};
fn path_info_at(dirfd: i32, path: &str) -> StarryResult<(String, bool)> {
with_fs(dirfd, |fs| {
let loc = fs.resolve_no_follow(path)?;
let is_dir = loc.metadata()?.node_type == NodeType::Directory;
Ok((loc.absolute_path()?.to_string(), is_dir))
})
}
pub fn sys_ioctl(current: &UserTaskRef, fd: i32, cmd: u32, arg: usize) -> StarryResult<isize> {
debug!("sys_ioctl <= fd: {fd}, cmd: {cmd}, arg: {arg}");
let f = get_file_like(fd)?;
if cmd == FIONBIO {
let val: i32 = (arg as *const i32).vm_read(current)?;
f.set_nonblocking(val != 0)?;
return Ok(0);
}
if cmd == FIOASYNC {
let val: i32 = (arg as *const i32).vm_read(current)?;
f.set_async_mode(val != 0)?;
return Ok(0);
}
if cmd == FS_IOC_FIEMAP {
return ioctl_fiemap(current, fd, arg).map(|()| 0);
}
if cmd == FIOCLEX || cmd == FIONCLEX {
current_fd_table()
.write()
.set_cloexec(fd as _, cmd == FIOCLEX)?;
return Ok(0);
}
f.ioctl(current, cmd, arg)
.map(|result| result as isize)
.inspect_err(|err| {
if matches!(err, StarryError::NotATty) {
debug!("ioctl {cmd} on non-tty fd {fd} -> ENOTTY (probe)");
}
})
}
fn ioctl_fiemap(current: &UserTaskRef, fd: i32, arg: usize) -> StarryResult<()> {
let (file, directory) = match crate::file::File::from_fd(fd) {
Ok(file) => (Some(file), None),
Err(StarryError::IsADirectory) => (None, Some(Directory::from_fd(fd)?)),
Err(StarryError::InvalidInput) => return Err(StarryError::OperationNotSupported),
Err(error) => return Err(error),
};
let header_ptr = arg as *mut FiemapHeader;
let mut header = header_ptr.vm_read(current)?;
if header.extent_count > u32::MAX / size_of::<FiemapExtent>() as u32 {
return Err(StarryError::InvalidInput);
}
header.flags &= !FIEMAP_FLAG_CACHE;
let incompatible = header.flags & !(FIEMAP_FLAGS_COMPAT | FIEMAP_FLAG_SYNC);
let mut result: StarryResult<_> = if incompatible != 0 {
header.flags = incompatible;
Err(StarryError::from(Errno::EBADR))
} else {
(|| -> StarryResult<_> {
if header.flags & FIEMAP_FLAG_SYNC != 0 {
if let Some(file) = &file {
file.inner().sync(false)?;
} else if let Some(directory) = &directory {
directory.inner().sync(false)?;
}
}
let target = if header.flags & FIEMAP_FLAG_XATTR != 0 {
header.flags &= !FIEMAP_FLAG_XATTR;
FileExtentTarget::ExtendedAttributes
} else {
FileExtentTarget::Data
};
let mappings = if let Some(file) = &file {
file.inner().map_extents(
header.start,
header.length,
target,
header.extent_count as usize,
)?
} else {
directory
.as_ref()
.ok_or(StarryError::OperationNotSupported)?
.inner()
.entry()
.as_dir()?
.inner()
.map_extents(
header.start,
header.length,
target,
header.extent_count as usize,
)?
};
Ok(mappings)
})()
};
let mut copied = 0u32;
if let Ok(mappings) = &result {
if header.extent_count == 0 {
copied = u32::try_from(mappings.mapped_extents)
.map_err(|_| StarryError::from(Errno::EOVERFLOW))?;
} else {
let extent_address = arg
.checked_add(size_of::<FiemapHeader>())
.ok_or(StarryError::BadAddress)?;
for (index, mapping) in mappings.extents.iter().enumerate() {
let mut flags = 0;
if mapping.state == axfs_ng_vfs::FileExtentState::Unwritten {
flags |= FIEMAP_EXTENT_UNWRITTEN;
}
if mapping.state == axfs_ng_vfs::FileExtentState::Inline {
flags |= FIEMAP_EXTENT_DATA_INLINE | FIEMAP_EXTENT_NOT_ALIGNED;
}
if mapping.merged {
flags |= FIEMAP_EXTENT_MERGED;
}
if mappings.complete && index + 1 == mappings.extents.len() {
flags |= FIEMAP_EXTENT_LAST;
}
let extent = FiemapExtent {
logical: mapping.logical_start,
physical: mapping.physical_start,
length: mapping.length,
flags,
..Default::default()
};
let byte_offset = index
.checked_mul(size_of::<FiemapExtent>())
.and_then(|offset| extent_address.checked_add(offset))
.ok_or(StarryError::BadAddress)?;
if let Err(error) = (byte_offset as *mut FiemapExtent).vm_write(current, extent) {
result = Err(error.into());
break;
}
copied += 1;
}
}
}
header.mapped_extents = copied;
header_ptr.vm_write(current, header)?;
result.map(|_| ())
}
#[ddebug::named]
pub fn sys_chdir(current: &UserTaskRef, path: *const c_char) -> StarryResult<isize> {
let path = vm_load_path_string(current, path)?;
debug_fn!("sys_chdir <= path: {path}");
let fs_context = current_fs_context();
let mut fs = fs_context.lock();
let entry = fs.resolve(path)?;
fs.set_current_dir(entry)?;
let cwd = fs.current_dir().absolute_path()?.to_string();
current.as_thread().proc_data.set_cwd_path(cwd);
Ok(0)
}
pub fn sys_fchdir(dirfd: i32) -> StarryResult<isize> {
debug!("sys_fchdir <= dirfd: {dirfd}");
let entry = with_fs(dirfd, |fs| Ok(fs.current_dir().clone()))?;
current_fs_context().lock().set_current_dir(entry)?;
Ok(0)
}
#[cfg(target_arch = "x86_64")]
pub fn sys_mkdir(current: &UserTaskRef, path: *const c_char, mode: u32) -> StarryResult<isize> {
sys_mkdirat(current, AT_FDCWD, path, mode)
}
#[cfg(target_arch = "x86_64")]
pub fn sys_mknod(
current: &UserTaskRef,
path: *const c_char,
mode: u32,
dev: u64,
) -> StarryResult<isize> {
sys_mknodat(current, AT_FDCWD, path, mode, dev)
}
pub fn sys_chroot(current: &UserTaskRef, path: *const c_char) -> StarryResult<isize> {
let path = vm_load_path_string(current, path)?;
debug!("sys_chroot <= path: {path}");
let fs_context = current_fs_context();
let mut fs = fs_context.lock();
let loc = fs.resolve(path)?;
if loc.node_type() != NodeType::Directory {
return Err(StarryError::NotADirectory);
}
*fs = FsContext::new(loc);
let root = fs.root_dir().absolute_path()?.to_string();
let cwd = fs.current_dir().absolute_path()?.to_string();
let proc_data = current.as_thread().proc_data.clone();
proc_data.set_root_path(root);
proc_data.set_cwd_path(cwd);
Ok(0)
}
ax_tracepoint::define_event_trace!(
sys_mkdirat,
TP_kops(crate::tracepoint::KernelTraceAux),
TP_system(syscalls),
TP_PROTO(path: &str, mode: u16),
TP_STRUCT__entry {
mode: u16,
path: [u8; 64],
},
TP_fast_assign {
mode: mode,
path: {
let mut buf = [0u8; 64];
let bytes = path.as_bytes();
let mut len = bytes.len().min(63);
while !path.is_char_boundary(len) {
len -= 1;
}
buf[..len].copy_from_slice(&bytes[..len]);
buf[len] = 0; buf
},
},
TP_ident(__entry),
TP_printk({
let nul = __entry
.path
.iter()
.position(|&b| b == 0)
.unwrap_or(__entry.path.len());
let path = core::str::from_utf8(&__entry.path[..nul]).unwrap_or("invalid utf8");
let mode = __entry.mode;
let mode = NodePermission::from_bits_truncate(mode);
alloc::format!("mkdir at {path} with mode {mode:?}")
})
);
pub fn sys_mkdirat(
current: &UserTaskRef,
dirfd: i32,
path: *const c_char,
mode: u32,
) -> StarryResult<isize> {
let thread = current.as_thread();
let path = vm_load_path_string(current, path)?;
debug!("sys_mkdirat <= dirfd: {dirfd}, path: {path}, mode: {mode}");
let mode = mode & !thread.proc_data.umask();
let mode = NodePermission::from_bits_truncate(mode as u16);
let cred = thread.cred();
let uid = cred.fsuid;
let gid = cred.fsgid;
trace_sys_mkdirat(&path, mode.bits());
let result = with_fs(dirfd, |fs| match fs.create_dir(&path, mode, uid, gid) {
Ok(_) => Ok(0),
Err(VfsError::InvalidInput) if !path.is_empty() && fs.resolve_no_follow(&path).is_ok() => {
Err(StarryError::AlreadyExists)
}
Err(err) => Err(err.into()),
});
if result.is_ok()
&& let Ok((path, _)) = path_info_at(dirfd, &path)
{
crate::file::inotify::notify_create_path(&path, true);
}
result
}
pub fn sys_mknodat(
current: &UserTaskRef,
dirfd: i32,
path: *const c_char,
mode: u32,
dev: u64,
) -> Result<isize, StarryError> {
let thread = current.as_thread();
let path = vm_load_path_string(current, path)?;
debug!(
"sys_mknodat <= dirfd: {}, path: {:?}, mode: {}, dev: {}",
dirfd, path, mode, dev
);
let ftype = mode & S_IFMT;
let mut perm = mode & !S_IFMT;
perm &= !thread.proc_data.umask();
let node_type = match ftype {
0 | S_IFREG => NodeType::RegularFile,
S_IFCHR => NodeType::CharacterDevice,
S_IFBLK => NodeType::BlockDevice,
S_IFIFO => NodeType::Fifo,
S_IFSOCK => NodeType::Socket,
S_IFDIR => return Err(StarryError::OperationNotPermitted),
_ => return Err(StarryError::InvalidInput),
};
let cred = thread.cred();
let uid = cred.fsuid;
let gid = cred.fsgid;
let res = with_fs(dirfd, |fs| {
let (dir, name) = fs.resolve_nonexistent(Path::new(&path))?;
let loc = dir.create(
name,
node_type,
NodePermission::from_bits_truncate(perm as u16),
uid,
gid,
)?;
if matches!(node_type, NodeType::CharacterDevice | NodeType::BlockDevice) {
loc.update_metadata(MetadataUpdate {
rdev: Some(DeviceId(dev)),
..Default::default()
})?;
}
Ok(0)
})?;
Ok(res)
}
const GETDENTS_BUFFER_SIZE: usize = 4096;
struct DirBuffer {
buf: Vec<u8>,
offset: usize,
}
impl DirBuffer {
fn new(len: usize) -> Self {
Self {
buf: vec![0; len],
offset: 0,
}
}
fn remaining_space(&self) -> usize {
self.buf.len().saturating_sub(self.offset)
}
fn write_entry(&mut self, d_ino: u64, d_off: i64, d_type: NodeType, name: &[u8]) -> bool {
const NAME_OFFSET: usize = offset_of!(linux_dirent64, d_name);
let len = NAME_OFFSET + name.len() + 1;
let len = len.next_multiple_of(align_of::<linux_dirent64>());
if self.remaining_space() < len {
return false;
}
unsafe {
let entry_ptr = self.buf.as_mut_ptr().add(self.offset);
entry_ptr.cast::<linux_dirent64>().write(linux_dirent64 {
d_ino,
d_off,
d_reclen: len as _,
d_type: d_type as _,
d_name: Default::default(),
});
let name_ptr = entry_ptr.add(NAME_OFFSET);
name_ptr.copy_from_nonoverlapping(name.as_ptr(), name.len());
name_ptr.add(name.len()).write(0);
}
self.offset += len;
true
}
}
pub fn sys_getdents64(
current: &UserTaskRef,
fd: i32,
buf: *mut u8,
len: u32,
) -> StarryResult<isize> {
debug!("sys_getdents64 <= fd: {fd}, buf: {buf:?}, len: {len}");
let dir = Directory::from_fd(fd)?;
let mut buffer = DirBuffer::new((len as usize).min(GETDENTS_BUFFER_SIZE));
let mut position = dir.position.lock();
let mut next_cursor = position.cursor;
if position.read_state.is_none() {
position.read_state = Some(dir.inner().open_directory_read_state()?);
}
let mut has_remaining = false;
dir.inner().read_dir_with_state(
position
.read_state
.as_deref_mut()
.ok_or(StarryError::BadState)?,
next_cursor,
&mut |name: &[u8], ino, node_type, cursor: DirectoryCursor| {
has_remaining = true;
if !buffer.write_entry(ino, cursor.offset() as _, node_type, name) {
return false;
}
next_cursor = cursor;
true
},
)?;
if has_remaining && buffer.offset == 0 {
return Err(StarryError::InvalidInput);
}
vm_write_slice(current, buf, &buffer.buf[..buffer.offset])?;
position.cursor = next_cursor;
Ok(buffer.offset as _)
}
pub fn sys_linkat(
current: &UserTaskRef,
old_dirfd: c_int,
old_path: *const c_char,
new_dirfd: c_int,
new_path: *const c_char,
flags: u32,
) -> StarryResult<isize> {
const LINKAT_VALID_FLAGS: u32 = AT_SYMLINK_FOLLOW | AT_EMPTY_PATH;
if flags & !LINKAT_VALID_FLAGS != 0 {
return Err(StarryError::InvalidInput);
}
let old_path = old_path
.nullable()
.map(|path| vm_load_path_string(current, path))
.transpose()?;
let new_path = vm_load_path_string(current, new_path)?;
debug!(
"sys_linkat <= old_dirfd: {old_dirfd}, old_path: {old_path:?}, new_dirfd: {new_dirfd}, \
new_path: {new_path}, flags: {flags}"
);
let resolve_flags = if flags & AT_SYMLINK_FOLLOW != 0 {
flags & AT_EMPTY_PATH
} else {
(flags & AT_EMPTY_PATH) | AT_SYMLINK_NOFOLLOW
};
let old = resolve_at(old_dirfd, old_path.as_deref(), resolve_flags)?
.into_file()
.ok_or(StarryError::BadFileDescriptor)?;
if old.is_dir() {
return Err(StarryError::OperationNotPermitted);
}
let (new_dir, new_name) = with_fs(new_dirfd, |fs| {
Ok(fs.resolve_nonexistent(Path::new(&new_path))?)
})?;
new_dir.link(new_name, &old)?;
Ok(0)
}
#[cfg(target_arch = "x86_64")]
pub fn sys_link(
current: &UserTaskRef,
old_path: *const c_char,
new_path: *const c_char,
) -> StarryResult<isize> {
sys_linkat(current, AT_FDCWD, old_path, AT_FDCWD, new_path, 0)
}
pub fn sys_unlinkat(
current: &UserTaskRef,
dirfd: i32,
path: *const c_char,
flags: i32,
) -> StarryResult<isize> {
let path = vm_load_path_string(current, path)?;
debug!("sys_unlinkat <= dirfd: {dirfd}, path: {path:?}, flags: {flags}");
if flags & !(AT_REMOVEDIR as i32) != 0 {
return Err(StarryError::InvalidInput);
}
let deleted = path_info_at(dirfd, &path).ok();
let result = with_fs(dirfd, |fs| {
if flags & AT_REMOVEDIR as i32 != 0 {
fs.remove_dir(&path)?;
} else {
fs.remove_file(&path)?;
}
Ok(0)
});
if result.is_ok()
&& let Some((path, is_dir)) = deleted
{
crate::file::inotify::notify_delete_path(&path, is_dir);
}
result
}
#[cfg(target_arch = "x86_64")]
pub fn sys_rmdir(current: &UserTaskRef, path: *const c_char) -> StarryResult<isize> {
sys_unlinkat(current, AT_FDCWD, path, AT_REMOVEDIR as _)
}
#[cfg(target_arch = "x86_64")]
pub fn sys_unlink(current: &UserTaskRef, path: *const c_char) -> StarryResult<isize> {
sys_unlinkat(current, AT_FDCWD, path, 0)
}
pub fn sys_getcwd(current: &UserTaskRef, buf: *mut u8, size: usize) -> StarryResult<isize> {
let cwd = current_fs_context().lock().current_dir().absolute_path()?;
debug!("sys_getcwd => cwd: {cwd}");
let cwd = CString::new(cwd.as_str()).map_err(|_| StarryError::InvalidInput)?;
let cwd = cwd.as_bytes_with_nul();
if cwd.len() <= size {
vm_write_slice(current, buf, cwd)?;
Ok(cwd.len() as _)
} else {
Err(StarryError::OutOfRange)
}
}
#[cfg(target_arch = "x86_64")]
pub fn sys_symlink(
current: &UserTaskRef,
target: *const c_char,
linkpath: *const c_char,
) -> StarryResult<isize> {
sys_symlinkat(current, target, AT_FDCWD, linkpath)
}
pub fn sys_symlinkat(
current: &UserTaskRef,
target: *const c_char,
new_dirfd: i32,
linkpath: *const c_char,
) -> StarryResult<isize> {
let target = vm_load_string(current, target)?;
let linkpath = vm_load_path_string(current, linkpath)?;
debug!("sys_symlinkat <= target: {target:?}, new_dirfd: {new_dirfd}, linkpath: {linkpath:?}");
let cred = current.as_thread().cred();
let uid = cred.fsuid;
let gid = cred.fsgid;
with_fs(new_dirfd, |fs| {
let (parent, name) = fs.resolve_parent(Path::new(&linkpath))?;
match parent.lookup_no_follow(&name) {
Ok(_) => return Err(StarryError::AlreadyExists),
Err(VfsError::NotFound) => {}
Err(err) => return Err(err.into()),
}
let meta = parent.metadata()?;
if !cred.has_cap_dac_override() {
let can_create = if cred.fsuid == meta.uid {
meta.mode
.contains(NodePermission::OWNER_WRITE | NodePermission::OWNER_EXEC)
} else if cred.in_group(meta.gid) {
meta.mode
.contains(NodePermission::GROUP_WRITE | NodePermission::GROUP_EXEC)
} else {
meta.mode
.contains(NodePermission::OTHER_WRITE | NodePermission::OTHER_EXEC)
};
if !can_create {
return Err(StarryError::PermissionDenied);
}
}
fs.symlink(target, linkpath, uid, gid)?;
Ok(0)
})
}
#[cfg(target_arch = "x86_64")]
pub fn sys_readlink(
current: &UserTaskRef,
path: *const c_char,
buf: *mut u8,
size: usize,
) -> StarryResult<isize> {
sys_readlinkat(current, AT_FDCWD, path, buf, size)
}
pub fn sys_readlinkat(
current: &UserTaskRef,
dirfd: i32,
path: *const c_char,
buf: *mut u8,
size: usize,
) -> StarryResult<isize> {
if size == 0 {
return Err(StarryError::InvalidInput);
}
let path = vm_load_path_string(current, path)?;
debug!("sys_readlinkat <= dirfd: {dirfd}, path: {path:?}");
let link = with_fs(dirfd, |fs| {
let entry = fs.resolve_no_follow(path)?;
Ok(entry.read_link()?)
})?;
let read = size.min(link.len());
vm_write_slice(current, buf, &link.as_bytes()[..read])?;
Ok(read as isize)
}
#[cfg(target_arch = "x86_64")]
pub fn sys_chown(
current: &UserTaskRef,
path: *const c_char,
uid: i32,
gid: i32,
) -> StarryResult<isize> {
sys_fchownat(current, AT_FDCWD, path, uid, gid, 0)
}
#[cfg(target_arch = "x86_64")]
pub fn sys_lchown(
current: &UserTaskRef,
path: *const c_char,
uid: i32,
gid: i32,
) -> StarryResult<isize> {
use linux_raw_sys::general::AT_SYMLINK_NOFOLLOW;
sys_fchownat(current, AT_FDCWD, path, uid, gid, AT_SYMLINK_NOFOLLOW)
}
pub fn sys_fchown(current: &UserTaskRef, fd: i32, uid: i32, gid: i32) -> StarryResult<isize> {
if fd < 0 {
return Err(StarryError::BadFileDescriptor);
}
sys_fchownat(current, fd, core::ptr::null(), uid, gid, AT_EMPTY_PATH)
}
pub fn sys_fchownat(
current: &UserTaskRef,
dirfd: i32,
path: *const c_char,
uid: i32,
gid: i32,
flags: u32,
) -> StarryResult<isize> {
const FCHOWNAT_VALID_FLAGS: u32 = AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW;
if flags & !FCHOWNAT_VALID_FLAGS != 0 {
return Err(StarryError::InvalidInput);
}
let path = path
.nullable()
.map(|path| vm_load_path_string(current, path))
.transpose()?;
let loc = resolve_at(dirfd, path.as_deref(), flags)?
.into_file()
.ok_or(StarryError::BadFileDescriptor)?;
let meta = loc.metadata()?;
let cred = current.as_thread().cred();
let changing_owner = uid != -1 && uid as u32 != meta.uid;
let changing_group = gid != -1 && gid as u32 != meta.gid;
if changing_owner && !cred.has_cap_chown() {
return Err(StarryError::OperationNotPermitted);
}
if changing_group && !cred.has_cap_chown() {
if cred.fsuid != meta.uid {
return Err(StarryError::OperationNotPermitted);
}
if !cred.in_group(gid as u32) {
return Err(StarryError::OperationNotPermitted);
}
}
let mut mode = meta.mode;
let is_dir = meta.node_type == NodeType::Directory;
if !is_dir {
mode.remove(NodePermission::SET_UID);
if mode.contains(NodePermission::GROUP_EXEC) {
mode.remove(NodePermission::SET_GID);
}
}
let uid = if uid == -1 { meta.uid } else { uid as _ };
let gid = if gid == -1 { meta.gid } else { gid as _ };
loc.update_metadata(MetadataUpdate {
owner: Some((uid, gid)),
mode: Some(mode),
..Default::default()
})?;
Ok(0)
}
#[cfg(target_arch = "x86_64")]
pub fn sys_chmod(current: &UserTaskRef, path: *const c_char, mode: u32) -> StarryResult<isize> {
sys_fchmodat(current, AT_FDCWD, path, mode, 0)
}
pub fn sys_fchmod(current: &UserTaskRef, fd: i32, mode: u32) -> StarryResult<isize> {
if fd < 0 {
return Err(StarryError::BadFileDescriptor);
}
sys_fchmodat(current, fd, core::ptr::null(), mode, AT_EMPTY_PATH)
}
pub fn sys_fchmodat(
current: &UserTaskRef,
dirfd: i32,
path: *const c_char,
mode: u32,
flags: u32,
) -> StarryResult<isize> {
const FCHMODAT_VALID_FLAGS: u32 = AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW;
if flags & !FCHMODAT_VALID_FLAGS != 0 {
return Err(StarryError::InvalidInput);
}
let path = path
.nullable()
.map(|path| vm_load_path_string(current, path))
.transpose()?;
let path_is_empty = path.as_deref().is_none_or(|s| s.is_empty());
if path_is_empty && flags & AT_EMPTY_PATH != 0 && fd_is_path(dirfd) {
return Err(StarryError::BadFileDescriptor); }
if let Some(p) = path.as_deref()
&& let Some(rest) = p.strip_prefix("/proc/self/fd/")
&& let Ok(n) = rest.parse::<i32>()
&& fd_is_path(n)
{
return Err(StarryError::BadFileDescriptor); }
let loc = resolve_at(dirfd, path.as_deref(), flags)?
.into_file()
.ok_or(StarryError::BadFileDescriptor)?;
let cred = current.as_thread().cred();
if !cred.has_cap_fowner() {
let meta = loc.metadata()?;
if cred.fsuid != meta.uid {
return Err(StarryError::OperationNotPermitted);
}
}
loc.update_metadata(MetadataUpdate {
mode: Some(NodePermission::from_bits_truncate(mode as u16)),
..Default::default()
})?;
Ok(0)
}
#[cfg(target_arch = "x86_64")]
fn update_times(
current: &UserTaskRef,
dirfd: i32,
path: *const c_char,
atime: Option<Duration>,
mtime: Option<Duration>,
flags: u32,
) -> StarryResult<()> {
let path = path
.nullable()
.map(|path| vm_load_string(current, path))
.transpose()?;
resolve_at(dirfd, path.as_deref(), flags)?
.into_file()
.ok_or(StarryError::BadFileDescriptor)?
.update_metadata(MetadataUpdate {
atime,
mtime,
..Default::default()
})?;
Ok(())
}
#[cfg(target_arch = "x86_64")]
#[allow(non_camel_case_types)]
#[repr(C)]
#[derive(Clone, Copy, bytemuck::AnyBitPattern)]
pub struct utimbuf {
actime: linux_raw_sys::general::__kernel_old_time_t,
modtime: linux_raw_sys::general::__kernel_old_time_t,
}
#[cfg(target_arch = "x86_64")]
pub fn sys_utime(
current: &UserTaskRef,
path: *const c_char,
times: *const utimbuf,
) -> StarryResult<isize> {
let (atime, mtime) = if let Some(times) = times.nullable() {
let times = unsafe { times.vm_read_uninit(current)?.assume_init() };
(
Duration::from_secs(times.actime as _),
Duration::from_secs(times.modtime as _),
)
} else {
let time = wall_time();
(time, time)
};
update_times(current, AT_FDCWD, path, Some(atime), Some(mtime), 0)?;
Ok(0)
}
#[cfg(target_arch = "x86_64")]
pub fn sys_utimes(
current: &UserTaskRef,
path: *const c_char,
times: *const [linux_raw_sys::general::timeval; 2],
) -> StarryResult<isize> {
let (atime, mtime) = if let Some(times) = times.nullable() {
let [atime, mtime] = unsafe { times.vm_read_uninit(current)?.assume_init() };
(atime.try_into_time_value()?, mtime.try_into_time_value()?)
} else {
let time = wall_time();
(time, time)
};
update_times(current, AT_FDCWD, path, Some(atime), Some(mtime), 0)?;
Ok(0)
}
pub fn sys_utimensat(
current: &UserTaskRef,
dirfd: i32,
path: *const c_char,
times: *const [timespec; 2],
mut flags: u32,
) -> StarryResult<isize> {
const UTIMENSAT_VALID_FLAGS: u32 = AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW;
if flags & !UTIMENSAT_VALID_FLAGS != 0 {
return Err(StarryError::InvalidInput);
}
if path.is_null() && dirfd != AT_FDCWD {
flags |= AT_EMPTY_PATH;
}
fn utime_to_duration(time: ×pec) -> Option<StarryResult<Duration>> {
match time.tv_nsec {
val if val == UTIME_OMIT as _ => None,
val if val == UTIME_NOW as _ => Some(Ok(wall_time())),
_ => Some(time.try_into_time_value()),
}
}
let (atime, mtime, write_permission_suffices) = if let Some(times) = times.nullable() {
let [atime, mtime] = unsafe { times.vm_read_uninit(current)?.assume_init() };
let write_permission_suffices =
atime.tv_nsec == UTIME_NOW as _ && mtime.tv_nsec == UTIME_NOW as _;
(
utime_to_duration(&atime).transpose()?,
utime_to_duration(&mtime).transpose()?,
write_permission_suffices,
)
} else {
let time = wall_time();
(Some(time), Some(time), true)
};
if atime.is_none() && mtime.is_none() {
return Ok(0);
}
if path.is_null() && dirfd == AT_FDCWD && flags & AT_EMPTY_PATH == 0 {
return Err(StarryError::BadAddress);
}
let path = path
.nullable()
.map(|path| vm_load_path_string(current, path))
.transpose()?;
let loc = resolve_at(dirfd, path.as_deref(), flags)?
.into_file()
.ok_or(StarryError::BadFileDescriptor)?;
let cred = current.as_thread().cred();
if !cred.has_cap_fowner() {
let meta = loc.metadata()?;
if cred.fsuid != meta.uid {
if !write_permission_suffices {
return Err(StarryError::OperationNotPermitted);
}
let has_write = if cred.has_cap_dac_override() {
true
} else if cred.in_group(meta.gid) {
meta.mode.contains(NodePermission::GROUP_WRITE)
} else {
meta.mode.contains(NodePermission::OTHER_WRITE)
};
if !has_write {
return Err(StarryError::PermissionDenied);
}
}
}
loc.update_metadata(MetadataUpdate {
atime,
mtime,
..Default::default()
})?;
Ok(0)
}
#[cfg(target_arch = "x86_64")]
pub fn sys_rename(
current: &UserTaskRef,
old_path: *const c_char,
new_path: *const c_char,
) -> StarryResult<isize> {
sys_renameat(current, AT_FDCWD, old_path, AT_FDCWD, new_path)
}
#[cfg(not(target_arch = "riscv64"))]
pub fn sys_renameat(
current: &UserTaskRef,
old_dirfd: i32,
old_path: *const c_char,
new_dirfd: i32,
new_path: *const c_char,
) -> StarryResult<isize> {
sys_renameat2(current, old_dirfd, old_path, new_dirfd, new_path, 0)
}
pub fn sys_renameat2(
current: &UserTaskRef,
old_dirfd: i32,
old_path: *const c_char,
new_dirfd: i32,
new_path: *const c_char,
flags: u32,
) -> StarryResult<isize> {
const RENAMEAT2_SUPPORTED_FLAGS: u32 = RENAME_NOREPLACE | RENAME_EXCHANGE | RENAME_WHITEOUT;
if flags & !RENAMEAT2_SUPPORTED_FLAGS != 0 {
return Err(StarryError::InvalidInput);
}
let options = match flags {
0 => RenameOptions::REPLACE,
RENAME_NOREPLACE => RenameOptions::NO_REPLACE,
RENAME_EXCHANGE => RenameOptions::EXCHANGE,
RENAME_WHITEOUT => RenameOptions::WHITEOUT,
value if value == RENAME_NOREPLACE | RENAME_WHITEOUT => RenameOptions::WHITEOUT_NO_REPLACE,
_ => return Err(StarryError::InvalidInput),
};
let old_path = vm_load_path_string(current, old_path)?;
let new_path = vm_load_path_string(current, new_path)?;
debug!(
"sys_renameat2 <= old_dirfd: {old_dirfd}, old_path: {old_path:?}, new_dirfd: {new_dirfd}, \
new_path: {new_path}, flags: {flags}"
);
let (old_dir, old_name) =
with_fs(old_dirfd, |fs| Ok(fs.resolve_parent(Path::new(&old_path))?))?;
let (new_dir, new_name) =
with_fs(new_dirfd, |fs| Ok(fs.resolve_parent(Path::new(&new_path))?))?;
if flags & RENAME_NOREPLACE != 0 {
old_dir.lookup_no_follow(&old_name)?;
match new_dir.lookup_no_follow(&new_name) {
Ok(_) => return Err(StarryError::AlreadyExists),
Err(VfsError::NotFound) => {}
Err(err) => return Err(err.into()),
}
}
old_dir.rename_with_options(&old_name, &new_dir, &new_name, options)?;
Ok(0)
}
fn run_sync_stages<PageSync, RootSync, BlockSync>(
page_sync: PageSync,
root_sync: RootSync,
block_sync: BlockSync,
) -> StarryResult<isize>
where
PageSync: FnOnce() -> StarryResult<()>,
RootSync: FnOnce() -> StarryResult<()>,
BlockSync: FnOnce() -> StarryResult<()>,
{
if let Err(error) = page_sync() {
warn!("sync(2) page-cache writeback failed: {error:?}");
}
if let Err(error) = root_sync() {
warn!("sync(2) root-filesystem writeback failed: {error:?}");
}
if let Err(error) = block_sync() {
warn!("sync(2) block-cache writeback failed: {error:?}");
}
Ok(0)
}
pub fn sys_sync() -> StarryResult<isize> {
run_sync_stages(
|| {
sync_all_cached_files(false)?;
Ok(())
},
|| {
current_fs_context().lock().root_dir().sync(false)?;
Ok(())
},
|| {
#[cfg(any(feature = "ext4", feature = "fat"))]
ax_fs_ng::sync_all_block_caches()?;
Ok(())
},
)
}
pub fn sys_syncfs(fd: c_int) -> StarryResult<isize> {
debug!("sys_syncfs <= fd: {fd}");
let any = get_file_like(fd)?;
sync_all_cached_files(false)?;
if let Some(f) = any.downcast_ref::<crate::file::File>() {
f.inner().location().filesystem().flush()?;
} else if let Some(d) = any.downcast_ref::<Directory>() {
d.inner().filesystem().flush()?;
}
Ok(0)
}
#[cfg(all(test, not(axtest)))]
mod tests {
use core::cell::Cell;
use super::*;
#[test]
fn sync_attempts_every_stage_and_returns_success() {
let page_called = Cell::new(false);
let root_called = Cell::new(false);
let block_called = Cell::new(false);
let result = run_sync_stages(
|| {
page_called.set(true);
Err(StarryError::Io)
},
|| {
root_called.set(true);
Err(StarryError::ReadOnlyFilesystem)
},
|| {
block_called.set(true);
Err(StarryError::NoMemory)
},
);
assert!(page_called.get());
assert!(
root_called.get(),
"root sync was skipped after a page-cache error"
);
assert!(
block_called.get(),
"global block-cache sync was skipped after an earlier error"
);
assert!(matches!(result, Ok(0)));
}
}