Merge pull request #4829 from RalfJung/readdir-special

readdir: also emit the special directory entries . and ..
This commit is contained in:
Ralf Jung 2026-01-24 11:19:15 +00:00 committed by GitHub
commit 1046e2f06d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 160 additions and 131 deletions

View file

@ -1,15 +1,17 @@
//! File and file system access
use std::borrow::Cow;
use std::ffi::OsString;
use std::fs::{
DirBuilder, File, FileType, OpenOptions, ReadDir, TryLockError, read_dir, remove_dir,
remove_file, rename,
self, DirBuilder, File, FileType, OpenOptions, TryLockError, read_dir, remove_dir, remove_file,
rename,
};
use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use rustc_abi::Size;
use rustc_data_structures::either::Either;
use rustc_data_structures::fx::FxHashMap;
use rustc_target::spec::Os;
@ -20,6 +22,40 @@ use crate::shims::sig::check_min_vararg_count;
use crate::shims::unix::fd::{FlockOp, UnixFileDescription};
use crate::*;
/// An open directory, tracked by DirHandler.
#[derive(Debug)]
struct OpenDir {
/// The "special" entries that must still be yielded by the iterator.
/// Used for `.` and `..`.
special_entries: Vec<&'static str>,
/// The directory reader on the host.
read_dir: fs::ReadDir,
/// The most recent entry returned by readdir().
/// Will be freed by the next call.
entry: Option<Pointer>,
}
impl OpenDir {
fn new(read_dir: fs::ReadDir) -> Self {
Self { special_entries: vec!["..", "."], read_dir, entry: None }
}
fn next_host_entry(&mut self) -> Option<io::Result<Either<fs::DirEntry, &'static str>>> {
if let Some(special) = self.special_entries.pop() {
return Some(Ok(Either::Right(special)));
}
let entry = self.read_dir.next()?;
Some(entry.map(Either::Left))
}
}
#[derive(Debug)]
struct DirEntry {
name: OsString,
ino: u64,
d_type: i32,
}
impl UnixFileDescription for FileHandle {
fn pread<'tcx>(
&self,
@ -116,6 +152,71 @@ impl UnixFileDescription for FileHandle {
}
}
/// The table of open directories.
/// Curiously, Unix/POSIX does not unify this into the "file descriptor" concept... everything
/// is a file, except a directory is not?
#[derive(Debug)]
pub struct DirTable {
/// Directory iterators used to emulate libc "directory streams", as used in opendir, readdir,
/// and closedir.
///
/// When opendir is called, a directory iterator is created on the host for the target
/// directory, and an entry is stored in this hash map, indexed by an ID which represents
/// the directory stream. When readdir is called, the directory stream ID is used to look up
/// the corresponding ReadDir iterator from this map, and information from the next
/// directory entry is returned. When closedir is called, the ReadDir iterator is removed from
/// the map.
streams: FxHashMap<u64, OpenDir>,
/// ID number to be used by the next call to opendir
next_id: u64,
}
impl DirTable {
#[expect(clippy::arithmetic_side_effects)]
fn insert_new(&mut self, read_dir: fs::ReadDir) -> u64 {
let id = self.next_id;
self.next_id += 1;
self.streams.try_insert(id, OpenDir::new(read_dir)).unwrap();
id
}
}
impl Default for DirTable {
fn default() -> DirTable {
DirTable {
streams: FxHashMap::default(),
// Skip 0 as an ID, because it looks like a null pointer to libc
next_id: 1,
}
}
}
impl VisitProvenance for DirTable {
fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
let DirTable { streams, next_id: _ } = self;
for dir in streams.values() {
dir.entry.visit_provenance(visit);
}
}
}
fn maybe_sync_file(
file: &File,
writable: bool,
operation: fn(&File) -> std::io::Result<()>,
) -> std::io::Result<i32> {
if !writable && cfg!(windows) {
// sync_all() and sync_data() will return an error on Windows hosts if the file is not opened
// for writing. (FlushFileBuffers requires that the file handle have the
// GENERIC_WRITE right)
Ok(0i32)
} else {
let result = operation(file);
result.map(|_| 0i32)
}
}
impl<'tcx> EvalContextExtPrivate<'tcx> for crate::MiriInterpCx<'tcx> {}
trait EvalContextExtPrivate<'tcx>: crate::MiriInterpCxExt<'tcx> {
fn write_stat_buf(
@ -178,14 +279,11 @@ trait EvalContextExtPrivate<'tcx>: crate::MiriInterpCxExt<'tcx> {
interp_ok(0)
}
fn file_type_to_d_type(
&mut self,
file_type: std::io::Result<FileType>,
) -> InterpResult<'tcx, i32> {
fn file_type_to_d_type(&self, file_type: std::io::Result<FileType>) -> InterpResult<'tcx, i32> {
#[cfg(unix)]
use std::os::unix::fs::FileTypeExt;
let this = self.eval_context_mut();
let this = self.eval_context_ref();
match file_type {
Ok(file_type) => {
match () {
@ -216,86 +314,32 @@ trait EvalContextExtPrivate<'tcx>: crate::MiriInterpCxExt<'tcx> {
}
}
}
}
/// An open directory, tracked by DirHandler.
#[derive(Debug)]
struct OpenDir {
/// The directory reader on the host.
read_dir: ReadDir,
/// The most recent entry returned by readdir().
/// Will be freed by the next call.
entry: Option<Pointer>,
}
impl OpenDir {
fn new(read_dir: ReadDir) -> Self {
Self { read_dir, entry: None }
}
}
/// The table of open directories.
/// Curiously, Unix/POSIX does not unify this into the "file descriptor" concept... everything
/// is a file, except a directory is not?
#[derive(Debug)]
pub struct DirTable {
/// Directory iterators used to emulate libc "directory streams", as used in opendir, readdir,
/// and closedir.
///
/// When opendir is called, a directory iterator is created on the host for the target
/// directory, and an entry is stored in this hash map, indexed by an ID which represents
/// the directory stream. When readdir is called, the directory stream ID is used to look up
/// the corresponding ReadDir iterator from this map, and information from the next
/// directory entry is returned. When closedir is called, the ReadDir iterator is removed from
/// the map.
streams: FxHashMap<u64, OpenDir>,
/// ID number to be used by the next call to opendir
next_id: u64,
}
impl DirTable {
#[expect(clippy::arithmetic_side_effects)]
fn insert_new(&mut self, read_dir: ReadDir) -> u64 {
let id = self.next_id;
self.next_id += 1;
self.streams.try_insert(id, OpenDir::new(read_dir)).unwrap();
id
}
}
impl Default for DirTable {
fn default() -> DirTable {
DirTable {
streams: FxHashMap::default(),
// Skip 0 as an ID, because it looks like a null pointer to libc
next_id: 1,
}
}
}
impl VisitProvenance for DirTable {
fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
let DirTable { streams, next_id: _ } = self;
for dir in streams.values() {
dir.entry.visit_provenance(visit);
}
}
}
fn maybe_sync_file(
file: &File,
writable: bool,
operation: fn(&File) -> std::io::Result<()>,
) -> std::io::Result<i32> {
if !writable && cfg!(windows) {
// sync_all() and sync_data() will return an error on Windows hosts if the file is not opened
// for writing. (FlushFileBuffers requires that the file handle have the
// GENERIC_WRITE right)
Ok(0i32)
} else {
let result = operation(file);
result.map(|_| 0i32)
fn dir_entry_fields(
&self,
entry: Either<fs::DirEntry, &'static str>,
) -> InterpResult<'tcx, DirEntry> {
let this = self.eval_context_ref();
interp_ok(match entry {
Either::Left(dir_entry) => {
DirEntry {
name: dir_entry.file_name(),
d_type: this.file_type_to_d_type(dir_entry.file_type())?,
// If the host is a Unix system, fill in the inode number with its real value.
// If not, use 0 as a fallback value.
#[cfg(unix)]
ino: std::os::unix::fs::DirEntryExt::ino(&dir_entry),
#[cfg(not(unix))]
ino: 0u64,
}
}
Either::Right(special) =>
DirEntry {
name: special.into(),
d_type: this.eval_libc("DT_DIR").to_u8()?.into(),
ino: 0,
},
})
}
}
@ -923,14 +967,9 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
err_ub_format!("the DIR pointer passed to `readdir` did not come from opendir")
})?;
let entry = match open_dir.read_dir.next() {
let entry = match open_dir.next_host_entry() {
Some(Ok(dir_entry)) => {
// If the host is a Unix system, fill in the inode number with its real value.
// If not, use 0 as a fallback value.
#[cfg(unix)]
let ino = std::os::unix::fs::DirEntryExt::ino(&dir_entry);
#[cfg(not(unix))]
let ino = 0u64;
let dir_entry = this.dir_entry_fields(dir_entry)?;
// Write the directory entry into a newly allocated buffer.
// The name is written with write_bytes, while the rest of the
@ -955,19 +994,14 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
// }
//
// On FreeBSD:
// pub struct dirent{
// pub struct dirent {
// pub d_fileno: uint32_t,
// pub d_reclen: uint16_t,
// pub d_type: uint8_t,
// pub d_namlen: uint8_t,
// pub d_name: [c_char; 256]
// pub d_name: [c_char; 256],
// }
let mut name = dir_entry.file_name(); // not a Path as there are no separators!
name.push("\0"); // Add a NUL terminator
let name_bytes = name.as_encoded_bytes();
let name_len = u64::try_from(name_bytes.len()).unwrap();
// We just use the pointee type here since determining the right pointee type
// independently is highly non-trivial: it depends on which exact alias of the
// function was invoked (e.g. `fstat` vs `fstat64`), and then on FreeBSD it also
@ -976,8 +1010,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
let dirent_ty = dest.layout.ty.builtin_deref(true).unwrap();
let dirent_layout = this.layout_of(dirent_ty)?;
let fields = &dirent_layout.fields;
let last_field = fields.count().strict_sub(1);
let d_name_offset = fields.offset(last_field).bytes();
let d_name_offset = fields.offset(fields.count().strict_sub(1)).bytes();
// Determine the size of the buffer we have to allocate.
let mut name = dir_entry.name; // not a Path as there are no separators!
name.push("\0"); // Add a NUL terminator
let name_bytes = name.as_encoded_bytes();
let name_len = u64::try_from(name_bytes.len()).unwrap();
let size = d_name_offset.strict_add(name_len);
let entry = this.allocate_ptr(
@ -988,11 +1027,16 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
)?;
let entry = this.ptr_to_mplace(entry.into(), dirent_layout);
// Write common fields
// Write the name.
// The name is not a normal field, we already computed the offset above.
let name_ptr = entry.ptr().wrapping_offset(Size::from_bytes(d_name_offset), this);
this.write_bytes_ptr(name_ptr, name_bytes.iter().copied())?;
// Write common fields.
let ino_name =
if this.tcx.sess.target.os == Os::FreeBsd { "d_fileno" } else { "d_ino" };
this.write_int_fields_named(
&[(ino_name, ino.into()), ("d_reclen", size.into())],
&[(ino_name, dir_entry.ino.into()), ("d_reclen", size.into())],
&entry,
)?;
@ -1000,20 +1044,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
if let Some(d_off) = this.try_project_field_named(&entry, "d_off")? {
this.write_null(&d_off)?;
}
if let Some(d_namlen) = this.try_project_field_named(&entry, "d_namlen")? {
this.write_int(name_len.strict_sub(1), &d_namlen)?;
}
let file_type = this.file_type_to_d_type(dir_entry.file_type())?;
if let Some(d_type) = this.try_project_field_named(&entry, "d_type")? {
this.write_int(file_type, &d_type)?;
this.write_int(dir_entry.d_type, &d_type)?;
}
// The name is not a normal field, we already computed the offset above.
let name_ptr = entry.ptr().wrapping_offset(Size::from_bytes(d_name_offset), this);
this.write_bytes_ptr(name_ptr, name_bytes.iter().copied())?;
Some(entry.ptr())
}
None => {
@ -1059,8 +1096,9 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
let open_dir = this.machine.dirs.streams.get_mut(&dirp).ok_or_else(|| {
err_unsup_format!("the DIR pointer passed to readdir_r did not come from opendir")
})?;
interp_ok(match open_dir.read_dir.next() {
interp_ok(match open_dir.next_host_entry() {
Some(Ok(dir_entry)) => {
let dir_entry = this.dir_entry_fields(dir_entry)?;
// Write into entry, write pointer to result, return 0 on success.
// The name is written with write_os_str_to_c_str, while the rest of the
// dirent struct is written using write_int_fields.
@ -1076,36 +1114,27 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
// }
let entry_place = this.deref_pointer_as(entry_op, this.libc_ty_layout("dirent"))?;
let name_place = this.project_field_named(&entry_place, "d_name")?;
let file_name = dir_entry.file_name(); // not a Path as there are no separators!
// Write the name.
let name_place = this.project_field_named(&entry_place, "d_name")?;
let (name_fits, file_name_buf_len) = this.write_os_str_to_c_str(
&file_name,
&dir_entry.name,
name_place.ptr(),
name_place.layout.size.bytes(),
)?;
let file_name_len = file_name_buf_len.strict_sub(1);
if !name_fits {
throw_unsup_format!(
"a directory entry had a name too large to fit in libc::dirent"
);
}
// If the host is a Unix system, fill in the inode number with its real value.
// If not, use 0 as a fallback value.
#[cfg(unix)]
let ino = std::os::unix::fs::DirEntryExt::ino(&dir_entry);
#[cfg(not(unix))]
let ino = 0u64;
let file_type = this.file_type_to_d_type(dir_entry.file_type())?;
// Write the other fields.
this.write_int_fields_named(
&[
("d_reclen", 0),
("d_namlen", file_name_len.into()),
("d_type", file_type.into()),
("d_ino", ino.into()),
("d_reclen", entry_place.layout.size.bytes().into()),
("d_namlen", file_name_buf_len.strict_sub(1).into()),
("d_type", dir_entry.d_type.into()),
("d_ino", dir_entry.ino.into()),
("d_seekoff", 0),
],
&entry_place,

View file

@ -654,7 +654,7 @@ fn test_readdir() {
}
assert_eq!(libc::closedir(dirp), 0);
entries.sort();
assert_eq!(&entries, &["file1.txt", "file2.txt"]);
assert_eq!(&entries, &[".", "..", "file1.txt", "file2.txt"]);
}
remove_file(&file1).unwrap();