Skip to main content

Dir

Struct Dir 

Source
pub struct Dir { /* private fields */ }
Expand description

A directory file descriptor opened O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC.

All entry-level operations are relative to this fd, preventing TOCTOU races that path-based lookups are vulnerable to.

The fd is held behind an Arc so per-entry operations can move an owned reference into their spawn_blocking closure. spawn_blocking tasks are not cancellable: if the surrounding future is dropped (timeout, fail_early abort, Ctrl-C) the closure keeps running detached. Cloning the Arc (a refcount bump, no syscall) keeps the open file description alive for the closure’s full duration even if the originating Dir is dropped mid-flight, preserving the openat TOCTOU guarantee. Later fd-relative methods (open_file_read, create_file, make_dir, read_entries, …) must follow this same clone-Arc-into-closure shape.

Implementations§

Source§

impl Dir

Source

pub fn side(&self) -> Side

Which filesystem side this directory lives on (for congestion gating).

Source

pub async fn open_root_dir( path: &Path, dereference: bool, side: Side, ) -> Result<Dir>

Open path as a directory fd.

The final component is always opened with O_NOFOLLOW. If dereference is false and the final component is a symlink, the call fails with ELOOP. If dereference is true and the final component is a symlink, the call is retried without O_NOFOLLOW so the symlink is followed.

The parent prefix is resolved normally (it is trusted) — unless strict operand resolution is armed (--require-toctou-safe), in which case the whole path is resolved RESOLVE_NO_SYMLINKS and a symlink in ANY component fails closed with ELOOP.

Source

pub async fn open_parent_dir(path: &Path, side: Side) -> Result<TrustedDir>

Open a TRUSTED command-line parent-prefix directory, resolving symlinks normally (the final component IS followed if it is a symlink).

The trusted-boundary model (docs/tocttou.md, “Trusted boundary”) trusts the directory named on the command line up to and including itself; only entries strictly BELOW it are hardened with O_NOFOLLOW. The parent prefix that CONTAINS the operand is therefore resolved like a normal path open — a symlinked parent (e.g. rcp file symlink_to_dir/out, where symlink_to_dir is a symlink to a real directory) must be followed into the real directory, not rejected with ELOOP/ENOTDIR.

This differs from Self::open_root_dir, which O_NOFOLLOWs the final component (the named operand itself) and only follows it when dereference is set. Use open_parent_dir for the operand’s CONTAINER directory; use open_root_dir for the operand entry. Every descendant openat during the walk still uses O_NOFOLLOW, so the hardening below the named root is unaffected.

Returns a TrustedDir: this is the ONLY constructor of that type, so a symlink-following open can be obtained nowhere else. Crossing into the hardened tree below the named root is the explicit TrustedDir::into_tree step.

Under strict operand resolution (--require-toctou-safe) the prefix must already be symlink-free: it is resolved RESOLVE_NO_SYMLINKS, and a symlink in any component fails closed with ELOOP instead of being followed. Pass fully-resolved operands (realpath output) in that mode.

Source

pub async fn open_dir(&self, name: &OsStr) -> Result<Dir>

Open a child directory entry by name, refusing to follow symlinks.

Fails with ELOOP if name refers to a symlink, or ENOTDIR if it refers to a non-directory entry. The returned Dir carries the same congestion side as self.

Source

pub async fn open_file_read(&self, name: &OsStr) -> Result<(File, FileMeta)>

Open a child regular file for reading, refusing to follow symlinks and never blocking on a FIFO. Returns the open file plus its metadata snapshot.

O_NONBLOCK is included so that if an attacker races the directory entry to a FIFO between getdents and this open, the open returns immediately (O_RDONLY|O_NONBLOCK on a FIFO never blocks on Linux) rather than blocking forever waiting for a writer. O_NOFOLLOW prevents symlink following but does not catch FIFOs (they are not symlinks); the subsequent fstat + S_ISREG check rejects any non-regular file (FIFO, device, directory) with EINVAL. O_NONBLOCK persists on the returned File, which is harmless for regular-file I/O on a local fs.

Fails with EINVAL if name is not a single path component, ELOOP if name is a symlink, or EINVAL (after open, via the fstat+S_ISREG check) if the entry is any non-regular type such as a FIFO, device, or directory.

This is the canonical regular-file payload+metadata read: the returned FileMeta (not the classify Handle’s metadata) is what callers must apply/send, so bytes and metadata come from the same fd (read-side fidelity, see docs/tocttou.md).

Source

pub async fn meta(&self) -> Result<FileMeta>

fstat this directory’s own held fd, returning its metadata snapshot.

Lets a caller apply/send a directory’s metadata from the SAME fd whose read_entries produced its contents (read-side fidelity, see docs/tocttou.md), rather than from a separately-opened classify Handle that a concurrent swap could desync from the enumerated contents. Gated as Stat.

Source

pub async fn child(&self, name: &OsStr) -> Result<Handle>

Open a child entry by name, classifying it without following symlinks.

Uses O_PATH|O_NOFOLLOW, which yields a valid fd even for symlinks. The stat is then obtained via fstatat with AT_EMPTY_PATH on the resulting fd so the classification is always consistent with the opened entry.

Source

pub async fn recheck(&self, name: &OsStr, expected: &Handle) -> Result<Handle>

Re-open name and confirm it still refers to the same inode as expected (same dev + ino). Returns the fresh Handle on match.

On mismatch — the directory entry was swapped to a different inode between when expected was obtained and this call — returns ESTALE. Callers fail closed: they must not proceed with an operation that assumed a specific identity for the entry.

§Soundness

expected’s O_PATH fd pins the old inode alive for the duration of the call: as long as any fd referencing an inode is open, the kernel cannot recycle that inode number. A matching (dev, ino) therefore genuinely proves the two fds refer to the same inode — there is no window in which the number could have been reused.

Source

pub async fn make_dir(&self, name: &OsStr, mode: u32) -> Result<Dir>

Create a child directory and return an open Dir handle to it (same side as self).

Fails with EINVAL if name is not a single path component, or EEXIST if a directory (or any other entry) at name already exists.

This is a two-step operation: mkdirat (gated as MkDir) to create the directory, followed by open_dir (gated as Stat) to open and return it.

Source

pub async fn read_entries(&self) -> Result<Vec<(OsString, Option<EntryKind>)>>

Enumerate the directory’s entries (excluding . and ..).

Returns each entry’s name and its getdents d_type as a best-effort EntryKind hint (None when the filesystem reports DT_UNKNOWN). The hint is advisory only — callers MUST confirm type via child/fstat before acting (TOCTOU safety).

This method acquires only the static ops rate gate (not the congestion probe). Directory enumeration is deliberately not probed because buffered getdents produces bimodal latency (cache hit vs. real kernel call) that would pollute the congestion controller’s baseline — see walk::next_entry_probed for the full rationale.

Remove a child non-directory entry by name, gated on this directory’s own congestion side.

For a symlink, this unlinks the link itself — never its target.

Fails with EINVAL if name is not a single path component, or EISDIR if name refers to a directory.

Source

pub async fn rmdir_at(&self, name: &OsStr) -> Result<()>

Remove a child empty directory by name, gated on this directory’s own congestion side.

Fails with EINVAL if name is not a single path component, ENOTEMPTY if the directory is not empty, or ENOTDIR if name is not a directory.

Create a symlink nametarget in this directory, returning a fd-pinned Handle to the just-created link.

The returned handle has kind() == EntryKind::Symlink and can be used to apply metadata to the link race-free. target is the link contents — it is an arbitrary path and is not restricted to a single component.

Fails with EINVAL if name is not a single path component, or EEXIST if an entry at name already exists.

Read the target of a child symlink.

Fails with EINVAL if name is not a single path component, or EINVAL (from readlinkat) if name is not a symlink.

Create a hard link at dst/dst_name pointing to this directory’s name.

Uses AtFlags::empty() (flags=0, no AT_SYMLINK_FOLLOW), so if name is a symlink, the link target is the symlink inode itself — the target file gains no new hard link.

Fails with EINVAL if either name or dst_name is not a single path component.

Create a hard link at self/dst_name pointing to the EXACT inode that src_handle pins — never re-resolving the source by name.

self is the DESTINATION directory. The source is identified solely by src_handle’s O_PATH file descriptor: the link is made via linkat(AT_FDCWD, "/proc/self/fd/N", dst_fd, dst_name, AT_SYMLINK_FOLLOW), where N is the handle’s fd. AT_SYMLINK_FOLLOW makes linkat follow the /proc magic symlink to the handle’s pinned inode, so the new hard link targets that exact inode regardless of any concurrent rename / symlink swap of the original directory entry.

§Why /proc and not the source-name linkat or AT_EMPTY_PATH

Dir::hard_link_at re-resolves the source by name, which is a TOCTOU window: an attacker who controls the source tree can replace name with a different inode (symlink, FIFO, another file) between classification and the linkat, so the link would target the replacement. Linking the pinned fd closes that window. linkat(fd, "", .., AT_EMPTY_PATH) would also be inode-exact but requires CAP_DAC_READ_SEARCH; the /proc/self/fd form does not, mirroring chmod_via_proc_fd.

§Behavior
  • Inode-exact happy path: a stable regular-file handle links exactly as the by-name path did (same inode, same content).
  • Fail-closed under attack: if the pinned inode’s last directory entry was removed (link count 0, e.g. the attacker renamed name away), the kernel refuses to resurrect it and linkat fails with ENOENT. It never links a swapped-in replacement.
  • Directories: linkat refuses to hard-link a directory (EPERM), exactly as the by-name path did. Callers must only pass a regular-file handle.
§Errors

EINVAL if dst_name is not a single path component; ENOENT if the pinned inode has no remaining links (fail-closed); EEXIST if an entry at dst_name already exists; EPERM if the handle refers to a directory. Requires /proc mounted (same precondition as chmod_via_proc_fd).

Source

pub async fn create_file(&self, name: &OsStr, mode: u32) -> Result<File>

Create a new child file, failing if it already exists and never following a symlink.

mode is the creation mode (subject to umask); exact permissions are set later via fchmod. Returns the open writable File on success.

O_EXCL is the primary guard: combined with O_CREAT, it fails with EEXIST on any pre-existing entry — including a symlink — without following it. O_NOFOLLOW is the fallback that would still refuse to follow a symlink (with ELOOP) should O_EXCL ever be bypassed.

Fails with EINVAL if name is not a single path component, or EEXIST if a file or symlink at name already exists.

Trait Implementations§

Source§

impl Debug for Dir

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl Freeze for Dir

§

impl RefUnwindSafe for Dir

§

impl Send for Dir

§

impl Sync for Dir

§

impl Unpin for Dir

§

impl UnsafeUnpin for Dir

§

impl UnwindSafe for Dir

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoRequest<T> for T

Source§

fn into_request(self) -> Request<T>

Wrap the input message T in a tonic::Request
Source§

impl<L> LayerExt<L> for L

Source§

fn named_layer<S>(&self, service: S) -> Layered<<L as Layer<S>>::Service, S>
where L: Layer<S>,

Applies the layer to a service and wraps it in Layered.
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more