From c6a9a165ec9b5133d3b21ee6d16f265d50ac1c50 Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Tue, 16 Sep 2025 01:18:50 -0400 Subject: [PATCH] Implement fdopendir and fdclosedir `fdopendir` takes ownership of a directory file descriptor instead of opening one with a path. `fdclosedir` is a FreeBSD extension that transfers ownership of the file descriptor in DIR to the user. --- src/header/dirent/mod.rs | 44 ++++- tests/Makefile | 1 + tests/dirent/fdopendir.c | 153 ++++++++++++++++++ .../bins_dynamic/dirent/fdopendir.stderr | 0 .../bins_dynamic/dirent/fdopendir.stdout | 0 .../bins_static/dirent/fdopendir.stderr | 0 .../bins_static/dirent/fdopendir.stdout | 0 7 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 tests/dirent/fdopendir.c create mode 100644 tests/expected/bins_dynamic/dirent/fdopendir.stderr create mode 100644 tests/expected/bins_dynamic/dirent/fdopendir.stdout create mode 100644 tests/expected/bins_static/dirent/fdopendir.stderr create mode 100644 tests/expected/bins_static/dirent/fdopendir.stdout diff --git a/src/header/dirent/mod.rs b/src/header/dirent/mod.rs index e1a196a035..cace3f8195 100644 --- a/src/header/dirent/mod.rs +++ b/src/header/dirent/mod.rs @@ -16,7 +16,10 @@ use crate::{ platform::{self, types::*, Pal, Sys}, }; -use super::errno::{EINVAL, EIO, ENOMEM}; +use super::{ + errno::{self, EINVAL, EIO, ENOMEM, ENOTDIR}, + sys_stat, +}; const INITIAL_BUFSIZE: usize = 512; @@ -43,6 +46,26 @@ impl DIR { opaque_offset: 0, })) } + pub fn from_fd(fd: c_int) -> Result, Errno> { + let mut stat = sys_stat::stat::default(); + unsafe { + Sys::fstat(fd, &mut stat)?; + } + if (stat.st_mode & sys_stat::S_IFMT) != sys_stat::S_IFDIR { + return Err(Errno(ENOTDIR)); + } + Sys::fcntl(fd, fcntl::F_SETFD, fcntl::FD_CLOEXEC as _)?; + + // Take ownership now but not earlier so we don't close the fd on error. + let file = File::new(fd); + Ok(Self { + file, + buf: Vec::with_capacity(INITIAL_BUFSIZE), + buf_offset: 0, + opaque_offset: 0, + } + .into()) + } fn next_dirent(&mut self) -> Result<*mut dirent, Errno> { let mut this_dent = self.buf.get(self.buf_offset..).ok_or(Errno(EIO))?; if this_dent.is_empty() { @@ -161,6 +184,19 @@ pub extern "C" fn closedir(dir: Box) -> c_int { dir.close().map(|()| 0).or_minus_one_errno() } +/// See +/// +/// FreeBSD extension that transfers ownership of the directory file descriptor to the user. +/// +/// It doesn't matter if DIR was opened with [`opendir`] or [`fdopendir`]. +#[no_mangle] +pub extern "C" fn fdclosedir(dir: Box) -> c_int { + let mut file = dir.file; + file.reference = true; + + *file +} + /// See . #[no_mangle] pub extern "C" fn dirfd(dir: &mut DIR) -> c_int { @@ -175,6 +211,12 @@ pub unsafe extern "C" fn opendir(path: *const c_char) -> *mut DIR { DIR::new(path).or_errno_null_mut() } +/// See . +#[no_mangle] +pub extern "C" fn fdopendir(fd: c_int) -> *mut DIR { + DIR::from_fd(fd).or_errno_null_mut() +} + /// See . // #[no_mangle] pub extern "C" fn posix_getdents( diff --git a/tests/Makefile b/tests/Makefile index 4fccc679b0..7164358616 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -27,6 +27,7 @@ EXPECT_NAMES=\ crypt/sha256 \ crypt/sha512 \ destructor \ + dirent/fdopendir \ dirent/scandir \ endian \ err \ diff --git a/tests/dirent/fdopendir.c b/tests/dirent/fdopendir.c new file mode 100644 index 0000000000..cb9857935d --- /dev/null +++ b/tests/dirent/fdopendir.c @@ -0,0 +1,153 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +__attribute__((nonnull)) +static bool check_dot(struct dirent* dirent) { + const char* dots[2] = { + ".", + ".." + }; + + for (size_t i = 0; i < 2; i++) { + if (strcmp(dots[i], dirent->d_name) == 0) { + return true; + } + } + + return false; +} + +int main(void) { + int status = EXIT_FAILURE; + + char template[] = "/tmp/fdotest.XXXXXX"; + if (!mkdtemp(template)) { + perror("mkdtemp"); + goto bye; + } + + const char* movies[] = { + "big_lebowski", + "blade_runner", + "grand_budapest_hotel", + "taxi_driver" + }; + const size_t movies_len = sizeof(movies)/sizeof(char*); + char paths[sizeof(movies)/sizeof(char*)][PATH_MAX] = {0}; + for (size_t i = 0; i < movies_len; ++i) { + // Concat the path + const size_t len = sizeof(template) - 1; + char buf[PATH_MAX] = {0}; + memcpy(buf, template, len); + buf[len] = '/'; + + memcpy(&buf[len + 1], movies[i], strlen(movies[i])); + memcpy(paths[i], buf, PATH_MAX); + + // And now create the file + int fd = open(buf, O_CREAT); + if (fd == -1) { + perror("open"); + goto rmfiles; + } + close(fd); + } + + // FIXME: Redox requires read perms for the dir while Linux/BSD don't. + int dir = open(template, O_DIRECTORY | O_RDONLY); + if (dir == -1) { + perror("open"); + goto rmfiles; + } + + DIR* iter = fdopendir(dir); + if (!iter) { + perror("fdopendir"); + goto closedirfd; + } + + for (size_t i = 0; i < movies_len; ++i) { + errno = 0; + + struct dirent* dirent = readdir(iter); + if (!dirent) { + if (errno) { + perror("readdir"); + } + fprintf( + stderr, + "Expected entry #%lu but directory stream is complete\n", + i + ); + + goto closediriter; + } + + // Skip . and .. + if (check_dot(dirent)) { + continue; + } + + // Check that the entry matches one of the names. + // readdir's order is indeterministic and looping over the names + // is simpler than qsort for a test. + for (size_t j = 0; j < movies_len; ++j) { + if (strcmp(movies[j], dirent->d_name) == 0) { + goto continue_outer; + } + } + + fprintf( + stderr, + "Unexpected entry: %s\n", + dirent->d_name + ); + goto closediriter; + + continue_outer: + continue; + } + + // fdclosedir returns ownership of the original fd. + int returned_fd = fdclosedir(iter); + // Internally, both closedir and fdclosedir consume the boxed DIR. + iter = NULL; + if (returned_fd != dir) { + fputs("fdclosedir returned the wrong descriptor\n", stderr); + goto closedirfd; + } + + // Check that the file descriptor is still valid. + struct stat stat = {0}; + if (fstatat(returned_fd, "", &stat, AT_EMPTY_PATH) == -1) { + perror("fstatat"); + fputs("fdclosedir shouldn't have closed the fd\n", stderr); + goto closedirfd; + } + + status = EXIT_SUCCESS; +closediriter: + if (iter) { + closedir(iter); + } +closedirfd: + close(dir); +rmfiles: + for (size_t i = 0; i < movies_len; ++i) { + if (strnlen(paths[i], PATH_MAX) > 4) { + unlink(paths[i]); + } + } + rmdir(template); +bye: + return status; +} diff --git a/tests/expected/bins_dynamic/dirent/fdopendir.stderr b/tests/expected/bins_dynamic/dirent/fdopendir.stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_dynamic/dirent/fdopendir.stdout b/tests/expected/bins_dynamic/dirent/fdopendir.stdout new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_static/dirent/fdopendir.stderr b/tests/expected/bins_static/dirent/fdopendir.stderr new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/expected/bins_static/dirent/fdopendir.stdout b/tests/expected/bins_static/dirent/fdopendir.stdout new file mode 100644 index 0000000000..e69de29bb2