Implement O_TMPFILE support and related tests

This commit is contained in:
YanLien 2026-05-24 18:22:03 +08:00 committed by Tate, Hongliang Tian
parent 153e530fe2
commit 67b41fba8b
11 changed files with 487 additions and 13 deletions

View File

@ -3,7 +3,6 @@
use bitflags::bitflags;
bitflags! {
// TODO: Add O_TMPFILE
pub struct CreationFlags: u32 {
/// create file if it does not exist
const O_CREAT = 1 << 6;
@ -19,5 +18,7 @@ bitflags! {
const O_NOFOLLOW = 1 << 17;
/// close on exec
const O_CLOEXEC = 1 << 19;
/// create an unnamed temporary file
const O_TMPFILE = 1 << 22;
}
}

View File

@ -19,7 +19,27 @@ impl OpenArgs {
pub fn from_flags_and_mode(flags: u32, inode_mode: InodeMode) -> Result<Self> {
let creation_flags = CreationFlags::from_bits_truncate(flags);
let status_flags = StatusFlags::from_bits_truncate(flags);
if creation_flags.contains(CreationFlags::O_CREAT)
let access_mode = AccessMode::from_u32(flags)?;
// When `O_PATH` is set, all other flags (including `O_TMPFILE`) are
// ignored, so the `O_TMPFILE` validations are skipped.
// Reference: <https://man7.org/linux/man-pages/man2/open.2.html>.
if creation_flags.contains(CreationFlags::O_TMPFILE)
&& !status_flags.contains(StatusFlags::O_PATH)
{
if !creation_flags.contains(CreationFlags::O_DIRECTORY) {
return_errno_with_message!(Errno::EINVAL, "O_TMPFILE requires O_DIRECTORY");
}
if !access_mode.is_writable() {
return_errno_with_message!(Errno::EINVAL, "O_TMPFILE requires O_RDWR or O_WRONLY");
}
if creation_flags.contains(CreationFlags::O_CREAT) {
return_errno_with_message!(
Errno::EINVAL,
"O_TMPFILE and O_CREAT are mutually exclusive"
);
}
} else if creation_flags.contains(CreationFlags::O_CREAT)
&& creation_flags.contains(CreationFlags::O_DIRECTORY)
{
return_errno_with_message!(
@ -27,7 +47,7 @@ impl OpenArgs {
"O_CREAT and O_DIRECTORY cannot be specified together"
);
}
let access_mode = AccessMode::from_u32(flags)?;
Ok(Self {
creation_flags,
status_flags,
@ -52,4 +72,10 @@ impl OpenArgs {
|| self.creation_flags.contains(CreationFlags::O_CREAT)
&& self.creation_flags.contains(CreationFlags::O_EXCL))
}
/// Returns whether this is an `O_TMPFILE` open request.
pub fn is_tmpfile(&self) -> bool {
self.creation_flags.contains(CreationFlags::O_TMPFILE)
&& !self.status_flags.contains(StatusFlags::O_PATH)
}
}

View File

@ -26,7 +26,10 @@ use crate::{
utils::{CStr256, DirentVisitor},
vfs::{
file_system::{FileSystem, FsEventSubscriberStats, SuperBlock},
inode::{Extension, FallocMode, FileOps, Inode, Metadata, MknodType, SymbolicLink},
inode::{
Extension, FallocMode, FileOps, HardLinkability, Inode, Metadata, MknodType,
SymbolicLink,
},
path::{is_dot, is_dot_or_dotdot, is_dotdot},
registry::{FsCreationCtx, FsProperties, FsType},
xattr::{XattrName, XattrNamespace, XattrSetFlags},
@ -108,6 +111,7 @@ impl RamFs {
this: weak_root.clone(),
fs: weak_fs.clone(),
container_dev_id: root_dev_id,
hard_linkability: HardLinkability::Linkable,
extension: Extension::new(),
xattr: RamXattr::new(),
}),
@ -162,6 +166,10 @@ pub(super) struct RamInode {
/// Detached inodes such as `memfd` store it directly
/// because they do not have a valid fs reference.
container_dev_id: DeviceId,
/// Hard linkability.
/// All inodes except temporary files are set to linkable
/// Linkability of temporary files is specified via [`RamInode::create_tmpfile`]
hard_linkability: HardLinkability,
/// Extensions
extension: Extension,
/// Extended attributes
@ -323,6 +331,12 @@ impl InodeMeta {
}
}
pub fn new_tmpfile(mode: InodeMode, uid: Uid, gid: Gid) -> Self {
let mut meta = Self::new(mode, uid, gid);
meta.nlinks = 0;
meta
}
pub fn resize(&mut self, new_size: usize) {
self.size = new_size;
self.blocks = new_size.align_up(BLOCK_SIZE) / BLOCK_SIZE;
@ -504,6 +518,7 @@ impl RamInode {
this: weak_self.clone(),
fs: Arc::downgrade(fs),
container_dev_id: fs.sb.container_dev_id,
hard_linkability: HardLinkability::Linkable,
extension: Extension::new(),
xattr: RamXattr::new(),
})
@ -518,6 +533,28 @@ impl RamInode {
this: weak_self.clone(),
fs: Arc::downgrade(fs),
container_dev_id: fs.sb.container_dev_id,
hard_linkability: HardLinkability::Linkable,
extension: Extension::new(),
xattr: RamXattr::new(),
})
}
fn new_tmpfile(
fs: &Arc<RamFs>,
mode: InodeMode,
uid: Uid,
gid: Gid,
hard_linkability: HardLinkability,
) -> Arc<Self> {
Arc::new_cyclic(|weak_self| RamInode {
inner: Inner::new_file(),
metadata: SpinLock::new(InodeMeta::new_tmpfile(mode, uid, gid)),
ino: fs.alloc_id(),
typ: InodeType::File,
this: weak_self.clone(),
fs: Arc::downgrade(fs),
container_dev_id: fs.sb.container_dev_id,
hard_linkability,
extension: Extension::new(),
xattr: RamXattr::new(),
})
@ -539,6 +576,7 @@ impl RamInode {
this: Weak::new(),
fs: Weak::new(),
container_dev_id: dev_id,
hard_linkability: HardLinkability::Linkable,
extension: Extension::new(),
xattr: RamXattr::new(),
}
@ -553,6 +591,7 @@ impl RamInode {
this: weak_self.clone(),
fs: Arc::downgrade(fs),
container_dev_id: fs.sb.container_dev_id,
hard_linkability: HardLinkability::Linkable,
extension: Extension::new(),
xattr: RamXattr::new(),
})
@ -579,6 +618,7 @@ impl RamInode {
this: weak_self.clone(),
fs: Arc::downgrade(fs),
container_dev_id: fs.sb.container_dev_id,
hard_linkability: HardLinkability::Linkable,
extension: Extension::new(),
xattr: RamXattr::new(),
})
@ -593,6 +633,7 @@ impl RamInode {
this: weak_self.clone(),
fs: Arc::downgrade(fs),
container_dev_id: fs.sb.container_dev_id,
hard_linkability: HardLinkability::Linkable,
extension: Extension::new(),
xattr: RamXattr::new(),
})
@ -607,6 +648,7 @@ impl RamInode {
this: weak_self.clone(),
fs: Arc::downgrade(fs),
container_dev_id: fs.sb.container_dev_id,
hard_linkability: HardLinkability::Linkable,
extension: Extension::new(),
xattr: RamXattr::new(),
})
@ -911,6 +953,25 @@ impl Inode for RamInode {
Ok(new_inode)
}
fn create_tmpfile(
&self,
mode: InodeMode,
hard_linkability: HardLinkability,
) -> Result<Arc<dyn Inode>> {
if self.typ != InodeType::Dir {
return_errno_with_message!(Errno::ENOTDIR, "self is not dir");
}
let fs = self.fs.upgrade().unwrap();
Ok(RamInode::new_tmpfile(
&fs,
mode,
Uid::new_root(),
Gid::new_root(),
hard_linkability,
))
}
fn link(&self, old: &Arc<dyn Inode>, name: &str) -> Result<()> {
if !Arc::ptr_eq(&self.fs(), &old.fs()) {
return_errno_with_message!(Errno::EXDEV, "not same fs");
@ -925,24 +986,29 @@ impl Inode for RamInode {
if old.typ == InodeType::Dir {
return_errno_with_message!(Errno::EPERM, "old is a dir");
}
if old.hard_linkability == HardLinkability::Unlinkable {
return_errno_with_message!(Errno::ENOENT, "tmpfile is not linkable");
}
let mut self_dir = self.inner.as_direntry().unwrap().write();
if self_dir.contains_entry(name) {
return_errno_with_message!(Errno::EEXIST, "entry exist");
}
self_dir.append_entry(name, old.this.upgrade().unwrap());
let now = now();
// An `O_TMPFILE` inode starts with zero links. Bump the link count
// before exposing the new entry to concurrent lookup or unlink.
let mut old_meta = old.metadata.lock();
old_meta.inc_nlinks();
old_meta.set_ctime(now);
drop(old_meta);
drop(self_dir);
let now = now();
let mut self_meta = self.metadata.lock();
self_meta.set_mtime(now);
self_meta.set_ctime(now);
self_meta.inc_size();
drop(self_meta);
let mut old_meta = old.metadata.lock();
old_meta.inc_nlinks();
old_meta.set_ctime(now);
Ok(())
}

View File

@ -119,6 +119,19 @@ pub struct Metadata {
pub self_dev_id: Option<DeviceId>,
}
/// Describes whether an inode may get new hard links.
///
/// This does not control symbolic links. A symbolic link is a separate inode
/// whose target is a pathname string, so creating one does not link the target
/// inode directly.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HardLinkability {
/// Allows creating hard links to the inode.
Linkable,
/// Prevents creating hard links to the inode.
Unlinkable,
}
impl Metadata {
pub fn new_dir(ino: u64, mode: InodeMode, blk_size: usize, container_dev_id: DeviceId) -> Self {
let now = RealTimeCoarseClock::get().read_time();
@ -365,6 +378,14 @@ pub trait Inode: Any + FileOps + Send + Sync {
Err(Error::new(Errno::ENOTDIR))
}
fn create_tmpfile(
&self,
mode: InodeMode,
hard_linkability: HardLinkability,
) -> Result<Arc<dyn Inode>> {
Err(Error::new(Errno::EOPNOTSUPP))
}
fn mknod(&self, name: &str, mode: InodeMode, type_: MknodType) -> Result<Arc<dyn Inode>> {
Err(Error::new(Errno::ENOTDIR))
}

View File

@ -215,7 +215,6 @@ impl Dentry {
/// Creates a new anonymous `Dentry` with the given inode and parent.
///
/// See the [`Dentry`] type-level documentation for what "anonymous" means.
#[expect(dead_code, reason = "constructed by the upcoming O_TMPFILE support")]
pub(super) fn new_anonymous(inode: Arc<dyn Inode>, parent: Arc<Dentry>) -> Arc<Self> {
Self::new(inode, DentryOptions::Anonymous { parent })
}

View File

@ -22,7 +22,7 @@ use crate::{
pseudofs::NsInode,
vfs::{
file_system::{FileSystem, FsFlags},
inode::{Inode, Metadata, MknodType},
inode::{HardLinkability, Inode, Metadata, MknodType},
xattr::{XattrName, XattrNamespace, XattrSetFlags},
},
},
@ -76,6 +76,28 @@ impl Path {
Ok(Self::new(self.mount.clone(), new_child_dentry))
}
/// Creates a new `Path` to represent an unnamed temporary file.
///
/// The returned inode has no directory entry and is invisible to `readdir`.
/// If created hard-linkable, it may later be given a name via a link
/// operation; otherwise it can never be linked.
pub fn create_tmpfile(
&self,
mode: InodeMode,
hard_linkability: HardLinkability,
) -> Result<Self> {
if self
.inode()
.check_permission(Permission::MAY_WRITE)
.is_err()
{
return_errno!(Errno::EACCES);
}
let tmp_inode = self.inode().create_tmpfile(mode, hard_linkability)?;
let tmp_dentry = Dentry::new_anonymous(tmp_inode, self.dentry.clone());
Ok(Self::new(self.mount.clone(), tmp_dentry))
}
/// Creates a new pseudo `Path`.
pub(in crate::fs) fn new_pseudo(
mount: Arc<Mount>,

View File

@ -9,7 +9,10 @@ use crate::{
StatusFlags,
file_table::{FdFlags, RawFileDesc},
},
vfs::path::{AT_FDCWD, EmptyPathStr, FsPath, LookupResult, PathResolver},
vfs::{
inode::HardLinkability,
path::{AT_FDCWD, EmptyPathStr, FsPath, LookupResult, PathResolver},
},
},
prelude::*,
syscall::constants::MAX_FILENAME_LEN,
@ -82,6 +85,10 @@ fn do_open(
) -> Result<Arc<dyn FileLike>> {
let open_args = OpenArgs::from_flags_and_mode(flags, mode)?;
if open_args.is_tmpfile() {
return do_open_tmpfile(path_resolver, fs_path, &open_args);
}
let lookup_res = if open_args.follow_tail_link() {
path_resolver.lookup_unresolved(fs_path)?
} else {
@ -119,3 +126,37 @@ fn do_open(
Ok(file_handle)
}
fn do_open_tmpfile(
path_resolver: &PathResolver,
fs_path: &FsPath,
open_args: &OpenArgs,
) -> Result<Arc<dyn FileLike>> {
let dir_path = if open_args.follow_tail_link() {
path_resolver.lookup(fs_path)?
} else {
path_resolver.lookup_no_follow(fs_path)?
};
if dir_path.type_() != InodeType::Dir {
return_errno_with_message!(
Errno::ENOTDIR,
"O_TMPFILE requires the path to be a directory"
);
}
// `O_EXCL` with `O_TMPFILE` is allowed by Linux, but it prevents the tmpfile
// from being linked later by `linkat(..., AT_EMPTY_PATH)`.
// Reference: <https://man7.org/linux/man-pages/man2/open.2.html>.
let hard_linkability = if open_args.creation_flags.contains(CreationFlags::O_EXCL) {
HardLinkability::Unlinkable
} else {
HardLinkability::Linkable
};
let tmpfile_path = dir_path.create_tmpfile(open_args.inode_mode, hard_linkability)?;
Ok(Arc::new(InodeHandle::new_unchecked_access(
tmpfile_path,
open_args.access_mode,
open_args.status_flags,
)?))
}

View File

@ -10,6 +10,7 @@ SUBDIRS := \
procfs \
pseudofs \
symlink \
tmpfile \
utimensat \
include ../common/Makefile

View File

@ -131,4 +131,6 @@ echo "All mount bind file test passed."
./symlink/symlink
./tmpfile/tmpfile
./utimensat/utimensat

View File

@ -0,0 +1,3 @@
# SPDX-License-Identifier: MPL-2.0
include ../../common/Makefile

View File

@ -0,0 +1,292 @@
/* SPDX-License-Identifier: MPL-2.0 */
#define _GNU_SOURCE
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "../../common/test.h"
#define TEST_DIR "/tmp/aster_tmpfile_test"
#define CROSS_LINK_DIR "/ext2"
#define CROSS_LINK_NAME "cross_mount_tmpfile"
#define LINKED_NAME "linked_file"
#define LINKED_SECOND_NAME "linked_second_file"
#define LINKED_O_EXCL_NAME "linked_o_excl_file"
#define SYMLINK_NAME "tmpfile_symlink"
#define DATA "hello from tmpfile"
#define DATA_LEN (sizeof(DATA) - 1)
#define RAW_O_TMPFILE 020000000
/* O_TMPFILE may not be defined on older glibc. */
#ifndef __O_TMPFILE
#define __O_TMPFILE RAW_O_TMPFILE
#endif
#ifndef O_TMPFILE
#define O_TMPFILE (__O_TMPFILE | O_DIRECTORY)
#endif
#ifndef AT_EMPTY_PATH
#define AT_EMPTY_PATH 0x1000
#endif
static void cleanup_test_files(void)
{
unlink(TEST_DIR "/" LINKED_NAME);
unlink(TEST_DIR "/" LINKED_SECOND_NAME);
unlink(TEST_DIR "/" LINKED_O_EXCL_NAME);
unlink(TEST_DIR "/" SYMLINK_NAME);
unlink(CROSS_LINK_DIR "/" CROSS_LINK_NAME);
rmdir(TEST_DIR);
}
static int timespec_equal(struct timespec left, struct timespec right)
{
return left.tv_sec == right.tv_sec && left.tv_nsec == right.tv_nsec;
}
static int dir_is_unavailable_or_same_mount(const char *source_path,
const char *target_path)
{
struct stat source_stat;
struct stat target_stat;
if (stat(source_path, &source_stat) < 0 ||
stat(target_path, &target_stat) < 0) {
return 1;
}
return source_stat.st_dev == target_stat.st_dev;
}
FN_SETUP(prepare)
{
cleanup_test_files();
CHECK(mkdir(TEST_DIR, 0755));
}
END_SETUP()
FN_TEST(tmpfile_open_succeeds)
{
int fd;
fd = TEST_SUCC(open(TEST_DIR, O_TMPFILE | O_RDWR, 0666));
TEST_SUCC(close(fd));
}
END_TEST()
FN_TEST(tmpfile_open_write_only_succeeds)
{
int fd;
fd = TEST_SUCC(open(TEST_DIR, O_TMPFILE | O_WRONLY, 0666));
TEST_SUCC(close(fd));
}
END_TEST()
FN_TEST(tmpfile_open_read_only_returns_einval)
{
TEST_ERRNO(open(TEST_DIR, O_TMPFILE | O_RDONLY, 0666), EINVAL);
}
END_TEST()
FN_TEST(tmpfile_open_without_o_directory_returns_einval)
{
TEST_ERRNO(open(TEST_DIR, RAW_O_TMPFILE | O_RDWR, 0666), EINVAL);
}
END_TEST()
FN_TEST(tmpfile_open_with_o_creat_returns_einval)
{
TEST_ERRNO(open(TEST_DIR, O_TMPFILE | O_RDWR | O_CREAT, 0666), EINVAL);
}
END_TEST()
FN_TEST(tmpfile_open_with_o_path_yields_path_fd)
{
int fd;
char buf[1];
fd = TEST_SUCC(open(TEST_DIR, O_TMPFILE | O_PATH | O_RDWR, 0666));
TEST_ERRNO(read(fd, buf, sizeof(buf)), EBADF);
TEST_SUCC(close(fd));
}
END_TEST()
// FIXME: Add `O_TMPFILE` support for ext2.
#ifdef __asterinas__
FN_TEST(tmpfile_open_on_ext2_returns_eopnotsupp)
{
TEST_ERRNO(open(CROSS_LINK_DIR, O_TMPFILE | O_RDWR, 0666), EOPNOTSUPP);
}
END_TEST()
#endif
FN_TEST(tmpfile_open_with_o_excl_succeeds)
{
int fd;
fd = TEST_SUCC(open(TEST_DIR, O_TMPFILE | O_RDWR | O_EXCL, 0666));
TEST_SUCC(close(fd));
}
END_TEST()
FN_TEST(tmpfile_open_symlink_with_o_nofollow_returns_enotdir)
{
TEST_SUCC(symlink(TEST_DIR, TEST_DIR "/" SYMLINK_NAME));
TEST_ERRNO(open(TEST_DIR "/" SYMLINK_NAME,
O_TMPFILE | O_RDWR | O_NOFOLLOW, 0666),
ENOTDIR);
TEST_SUCC(unlink(TEST_DIR "/" SYMLINK_NAME));
}
END_TEST()
FN_TEST(tmpfile_open_non_dir_returns_enotdir)
{
TEST_ERRNO(open("/dev/null", O_TMPFILE | O_RDWR, 0666), ENOTDIR);
}
END_TEST()
FN_TEST(tmpfile_open_does_not_update_parent_timestamps)
{
struct stat before;
struct stat after;
int fd;
TEST_SUCC(stat(TEST_DIR, &before));
fd = TEST_SUCC(open(TEST_DIR, O_TMPFILE | O_RDWR, 0666));
TEST_SUCC(close(fd));
TEST_SUCC(stat(TEST_DIR, &after));
TEST_RES(timespec_equal(after.st_mtim, before.st_mtim), _ret);
TEST_RES(timespec_equal(after.st_ctim, before.st_ctim), _ret);
}
END_TEST()
FN_TEST(tmpfile_invisible_in_readdir)
{
DIR *dir;
int fd;
int found;
struct dirent *entry;
fd = TEST_SUCC(open(TEST_DIR, O_TMPFILE | O_RDWR, 0666));
dir = TEST_RES(opendir(TEST_DIR), _ret != NULL);
found = 0;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") != 0 &&
strcmp(entry->d_name, "..") != 0) {
found++;
}
}
TEST_RES(found, found == 0);
TEST_SUCC(closedir(dir));
TEST_SUCC(close(fd));
}
END_TEST()
FN_TEST(tmpfile_write_and_linkat)
{
DIR *dir;
char buf[DATA_LEN];
int dirfd;
int fd;
int found;
int linked_fd;
struct dirent *entry;
struct stat stat_after_link;
struct stat stat_after_second_link;
fd = TEST_SUCC(open(TEST_DIR, O_TMPFILE | O_RDWR, 0666));
TEST_RES(write(fd, DATA, DATA_LEN), _ret == DATA_LEN);
TEST_RES(pread(fd, buf, sizeof(buf), 0), _ret == DATA_LEN);
TEST_RES(memcmp(buf, DATA, DATA_LEN), _ret == 0);
dirfd = TEST_SUCC(open(TEST_DIR, O_RDONLY | O_DIRECTORY));
TEST_SUCC(linkat(fd, "", dirfd, LINKED_NAME, AT_EMPTY_PATH));
TEST_RES(fstat(fd, &stat_after_link), stat_after_link.st_nlink == 1);
dir = TEST_RES(opendir(TEST_DIR), _ret != NULL);
found = 0;
while ((entry = readdir(dir)) != NULL) {
found += strcmp(entry->d_name, LINKED_NAME) == 0;
}
TEST_RES(found, found == 1);
TEST_SUCC(closedir(dir));
TEST_SUCC(link(TEST_DIR "/" LINKED_NAME,
TEST_DIR "/" LINKED_SECOND_NAME));
TEST_RES(fstat(fd, &stat_after_second_link),
stat_after_second_link.st_nlink == 2);
linked_fd = TEST_SUCC(open(TEST_DIR "/" LINKED_NAME, O_RDONLY));
TEST_RES(pread(linked_fd, buf, sizeof(buf), 0), _ret == DATA_LEN);
TEST_RES(memcmp(buf, DATA, DATA_LEN), _ret == 0);
TEST_SUCC(close(linked_fd));
TEST_SUCC(close(dirfd));
TEST_SUCC(close(fd));
}
END_TEST()
FN_TEST(tmpfile_open_with_o_excl_cannot_be_linked)
{
int fd;
int dirfd;
fd = TEST_SUCC(open(TEST_DIR, O_TMPFILE | O_RDWR | O_EXCL, 0666));
dirfd = TEST_SUCC(open(TEST_DIR, O_RDONLY | O_DIRECTORY));
TEST_ERRNO(linkat(fd, "", dirfd, LINKED_O_EXCL_NAME, AT_EMPTY_PATH),
ENOENT);
TEST_SUCC(close(dirfd));
TEST_SUCC(close(fd));
}
END_TEST()
FN_TEST(tmpfile_linkat_cross_mount_returns_exdev)
{
int fd;
int tmpfd;
#ifdef __asterinas__
TEST_RES(dir_is_unavailable_or_same_mount(TEST_DIR, CROSS_LINK_DIR),
_ret == 0);
#else
SKIP_TEST_IF(
dir_is_unavailable_or_same_mount(TEST_DIR, CROSS_LINK_DIR));
#endif
fd = TEST_SUCC(open(TEST_DIR, O_TMPFILE | O_RDWR, 0666));
tmpfd = TEST_SUCC(open(CROSS_LINK_DIR, O_RDONLY | O_DIRECTORY));
TEST_ERRNO(linkat(fd, "", tmpfd, CROSS_LINK_NAME, AT_EMPTY_PATH),
EXDEV);
TEST_SUCC(close(tmpfd));
TEST_SUCC(close(fd));
}
END_TEST()
FN_SETUP(cleanup)
{
cleanup_test_files();
}
END_SETUP()