diff --git a/kernel/src/fs/file/file_attr/creation_flags.rs b/kernel/src/fs/file/file_attr/creation_flags.rs index a181a9cdd..142446463 100644 --- a/kernel/src/fs/file/file_attr/creation_flags.rs +++ b/kernel/src/fs/file/file_attr/creation_flags.rs @@ -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; } } diff --git a/kernel/src/fs/file/file_attr/open_args.rs b/kernel/src/fs/file/file_attr/open_args.rs index 8b8474a3b..279c8dfbb 100644 --- a/kernel/src/fs/file/file_attr/open_args.rs +++ b/kernel/src/fs/file/file_attr/open_args.rs @@ -19,7 +19,27 @@ impl OpenArgs { pub fn from_flags_and_mode(flags: u32, inode_mode: InodeMode) -> Result { 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: . + 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) + } } diff --git a/kernel/src/fs/fs_impls/ramfs/fs.rs b/kernel/src/fs/fs_impls/ramfs/fs.rs index 44c4a2f66..7530b7f24 100644 --- a/kernel/src/fs/fs_impls/ramfs/fs.rs +++ b/kernel/src/fs/fs_impls/ramfs/fs.rs @@ -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, + mode: InodeMode, + uid: Uid, + gid: Gid, + hard_linkability: HardLinkability, + ) -> Arc { + 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> { + 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, 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(()) } diff --git a/kernel/src/fs/vfs/fs_apis/inode.rs b/kernel/src/fs/vfs/fs_apis/inode.rs index 886169ffa..480415192 100644 --- a/kernel/src/fs/vfs/fs_apis/inode.rs +++ b/kernel/src/fs/vfs/fs_apis/inode.rs @@ -119,6 +119,19 @@ pub struct Metadata { pub self_dev_id: Option, } +/// 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> { + Err(Error::new(Errno::EOPNOTSUPP)) + } + fn mknod(&self, name: &str, mode: InodeMode, type_: MknodType) -> Result> { Err(Error::new(Errno::ENOTDIR)) } diff --git a/kernel/src/fs/vfs/path/dentry.rs b/kernel/src/fs/vfs/path/dentry.rs index 6b90f4456..092d98d9c 100644 --- a/kernel/src/fs/vfs/path/dentry.rs +++ b/kernel/src/fs/vfs/path/dentry.rs @@ -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, parent: Arc) -> Arc { Self::new(inode, DentryOptions::Anonymous { parent }) } diff --git a/kernel/src/fs/vfs/path/mod.rs b/kernel/src/fs/vfs/path/mod.rs index 4c2b29aa6..1011e338a 100644 --- a/kernel/src/fs/vfs/path/mod.rs +++ b/kernel/src/fs/vfs/path/mod.rs @@ -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 { + 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, diff --git a/kernel/src/syscall/open.rs b/kernel/src/syscall/open.rs index af14c61ee..a1b3be48e 100644 --- a/kernel/src/syscall/open.rs +++ b/kernel/src/syscall/open.rs @@ -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> { 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> { + 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: . + 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, + )?)) +} diff --git a/test/initramfs/src/regression/fs/Makefile b/test/initramfs/src/regression/fs/Makefile index 0c294aed6..1bd65bbd4 100644 --- a/test/initramfs/src/regression/fs/Makefile +++ b/test/initramfs/src/regression/fs/Makefile @@ -10,6 +10,7 @@ SUBDIRS := \ procfs \ pseudofs \ symlink \ + tmpfile \ utimensat \ include ../common/Makefile diff --git a/test/initramfs/src/regression/fs/run_test.sh b/test/initramfs/src/regression/fs/run_test.sh index 8408512f6..240d62c49 100755 --- a/test/initramfs/src/regression/fs/run_test.sh +++ b/test/initramfs/src/regression/fs/run_test.sh @@ -131,4 +131,6 @@ echo "All mount bind file test passed." ./symlink/symlink +./tmpfile/tmpfile + ./utimensat/utimensat diff --git a/test/initramfs/src/regression/fs/tmpfile/Makefile b/test/initramfs/src/regression/fs/tmpfile/Makefile new file mode 100644 index 000000000..a1e0b1931 --- /dev/null +++ b/test/initramfs/src/regression/fs/tmpfile/Makefile @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MPL-2.0 + +include ../../common/Makefile diff --git a/test/initramfs/src/regression/fs/tmpfile/tmpfile.c b/test/initramfs/src/regression/fs/tmpfile/tmpfile.c new file mode 100644 index 000000000..d3758a40d --- /dev/null +++ b/test/initramfs/src/regression/fs/tmpfile/tmpfile.c @@ -0,0 +1,292 @@ +/* SPDX-License-Identifier: MPL-2.0 */ + +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include + +#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()