How to Fix: [v0.3] dirfd.stat_at(“/”) unexpectedly succeeds
A dirfd.stat_at(“/”) call succeeding is a spec violation, not a harmless quirk. In the filesystem API, any path beginning with / must be rejected with not-permitted, because path resolution from a directory descriptor is only valid for relative paths inside the sandboxed scope. If absolute paths slip through, the implementation breaks the security model and can allow resolution outside the intended directory boundary.
Table of Contents
The Problem
The reported behavior is straightforward: calling stat_at on a directory file descriptor with the path “/” unexpectedly succeeds, even though the spec requires that any path starting with a leading slash be rejected as not-permitted.
That requirement exists for an important reason. Functions like stat_at, open_at, and similar descriptor-relative operations are designed to resolve paths relative to the supplied dirfd. An absolute path such as “/” does not refer to a child of that directory descriptor. It refers to the process or virtual filesystem root, which violates the contract of descriptor-scoped resolution.
In other words, if your implementation accepts “/”, it is treating an absolute path as if it were legal input for a relative lookup API.
Understanding the Root Cause
The root cause is usually one of these implementation mistakes:
- The path validation logic checks for empty paths or malformed segments, but does not explicitly reject leading slashes.
- The runtime forwards the path to an underlying host syscall or library routine that naturally accepts absolute paths, and no sandbox policy is enforced first.
- The implementation normalizes the path before permission checks, accidentally converting “/” into something considered resolvable.
- The code assumes that because the operation is called with a dirfd, the host will automatically keep traversal relative. That assumption is false if absolute paths are passed through.
Technically, a descriptor-relative API should enforce a simple rule before any filesystem interaction begins:
If the input path starts with ‘/’, fail immediately with not-permitted.
This needs to happen before normalization, canonicalization, symlink handling, or host syscall dispatch. If the implementation delays that check, an absolute path may be resolved by the host layer and incorrectly succeed.
A typical problematic flow looks like this:
fn stat_at(dirfd: DirFd, path: &str) -> Result<Metadata, Error> {
let normalized = normalize(path);
host_stat_at(dirfd, normalized)
}
If normalize(“/”) returns “/” and the host accepts it, the operation succeeds. That is precisely the bug.
The correct flow is:
fn stat_at(dirfd: DirFd, path: &str) -> Result<Metadata, Error> {
if path.starts_with('/') {
return Err(Error::NotPermitted);
}
let normalized = normalize_relative_path(path)?;
host_stat_at(dirfd, normalized)
}
This keeps the behavior aligned with the spec and preserves the intended sandboxing guarantees.
Step-by-Step Solution
The fix is small but should be applied carefully across all descriptor-relative path APIs.
1. Add a leading-slash guard
Wherever stat_at or equivalent functions accept a path, add an early rejection for absolute paths.
fn validate_descriptor_relative_path(path: &str) -> Result<(), Error> {
if path.starts_with('/') {
return Err(Error::NotPermitted);
}
Ok(())
}
2. Call validation before normalization or syscall dispatch
This is the critical part. Do not wait until after path cleaning.
fn stat_at(dirfd: DirFd, path: &str) -> Result<Metadata, Error> {
validate_descriptor_relative_path(path)?;
let normalized = normalize_relative_path(path)?;
host_stat_at(dirfd, &normalized)
}
3. Reuse the same validator across related APIs
If the issue exists in stat_at, it may also exist in other calls such as:
- open_at
- unlink_at
- rename_at
- readlink_at
- set_times_at
Create one shared validator so behavior stays consistent.
fn validate_relative_lookup_path(path: &str) -> Result<(), Error> {
if path.starts_with('/') {
return Err(Error::NotPermitted);
}
Ok(())
}
4. Add a regression test for the exact bug
The test should verify that absolute paths are rejected, specifically including “/”.
#[test]
fn stat_at_rejects_root_absolute_path() {
let dirfd = open_sandbox_dir();
let result = dirfd.stat_at("/");
assert_eq!(result.unwrap_err(), Error::NotPermitted);
}
5. Add broader absolute-path tests
Do not stop at “/”. Test other absolute path forms too.
#[test]
fn stat_at_rejects_any_absolute_path() {
let dirfd = open_sandbox_dir();
for path in ["/", "/tmp", "/etc/passwd", "/a/b"] {
let result = dirfd.stat_at(path);
assert_eq!(result.unwrap_err(), Error::NotPermitted, "path was {}", path);
}
}
6. Verify host integration does not override policy
If your implementation wraps OS-specific syscalls, ensure the absolute-path rejection happens in the high-level API layer rather than relying on platform behavior.
fn host_stat_at_guarded(dirfd: DirFd, path: &str) -> Result<Metadata, Error> {
validate_relative_lookup_path(path)?;
host_stat_at(dirfd, path)
}
This ensures the spec contract is enforced consistently across Linux, macOS, Windows compatibility layers, or virtualized runtimes.
7. Update issue references and changelog notes
Because this is a conformance and security-boundary fix, document it clearly in the release notes. Mention that descriptor-relative filesystem operations now correctly reject absolute paths with not-permitted.
Common Edge Cases
Fixing “/” is only part of the story. Here are the cases that commonly surface once the main bug is addressed.
Paths like “//” or “///tmp”
If your check only matches the exact string “/”, you will miss other absolute paths. Always reject any path where path.starts_with(‘/’) is true.
Normalization happening too early
If the runtime normalizes paths before validation, it may unintentionally preserve or transform absolute paths into values that continue through the pipeline. Validation must happen first.
Backslash handling on cross-platform runtimes
If your runtime abstracts multiple platforms, think carefully about whether alternative separators should be treated specially. For a spec that defines POSIX-like path syntax, / is the key separator, but adapter layers must avoid accidentally treating host-native syntax in a way that bypasses policy.
Dot-segments such as “./” and “../”
Rejecting absolute paths does not automatically make traversal safe. You still need proper handling for . and .. to ensure callers cannot escape the preopened directory scope.
Symlink traversal
Even when the incoming path is relative, symlink resolution can still produce surprising results if your implementation does not follow the spec’s sandboxing constraints. Absolute-path rejection is necessary, but not sufficient, for full path safety.
Empty string behavior
Be explicit about what should happen for an empty path. Some implementations accidentally conflate empty input with root-like lookup behavior. Treat it according to the spec, independently from the absolute-path rule.
Inconsistent error mapping
The issue specifically calls for not-permitted. If your host layer returns a different error such as invalid argument or no such file, map it correctly at the API boundary so callers observe spec-compliant behavior.
FAQ
Why must stat_at(“/”) return not-permitted instead of succeeding?
Because stat_at is a descriptor-relative operation. The path must be resolved relative to the provided directory handle, and absolute paths are outside that model. Returning not-permitted enforces the sandbox boundary defined by the spec.
Is checking for “/” enough to fix the bug?
No. You must reject any path beginning with ‘/’, including values like “/tmp”, “//”, and deeper absolute paths. A narrow fix for only the exact root string will leave the implementation non-compliant.
Should this validation live in stat_at only?
No. Any API that performs path lookup relative to a directory descriptor should use the same validation rule. Centralizing the check reduces drift and prevents one filesystem call from remaining vulnerable while others are fixed.
The key takeaway is simple: absolute paths and descriptor-relative APIs do not mix. To resolve the [v0.3] dirfd.stat_at(“/”) unexpectedly succeeds issue, enforce an early leading-slash rejection, return not-permitted, and back the fix with regression tests across every related filesystem entry point.