jdx/mise · error
refusing to follow symlink {} from an untrusted parent direc
Error message
refusing to follow symlink {} from an untrusted parent directory What it means
A TOCTOU/symlink-planting defense: when the component walk encounters a symlink mid-path, it only follows it if the containing directory is owned by root and not group/other-writable. Otherwise it refuses, because an untrusted parent directory would let any local user swap the symlink target between check and use. This protects privilege-sensitive managed directory creation.
Source
Thrown at src/system/managed_files.rs:1612
_ => bail!("invalid managed directory path: {}", path.display()),
})
.collect::<Result<Vec<_>>>()?;
let flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW;
let mut directory = open(Path::new("/"), flags, Mode::empty())?;
let mut current = PathBuf::from("/");
for (index, name) in components.iter().enumerate() {
let component_path = current.join(name);
directory = match openat(&directory, name.as_os_str(), flags, Mode::empty()) {
Ok(directory) => directory,
Err(open_error) => {
let metadata = fstatat(&directory, name.as_os_str(), AtFlags::AT_SYMLINK_NOFOLLOW);
if metadata.is_ok_and(|metadata| {
SFlag::from_bits_truncate(metadata.st_mode).contains(SFlag::S_IFLNK)
}) {
let parent = fstat(&directory)?;
if parent.st_uid != 0 || parent.st_mode & 0o022 != 0 {
bail!(
"refusing to follow symlink {} from an untrusted parent directory",
component_path.display()
);
}
let target = nix::fcntl::readlinkat(&directory, name.as_os_str())?;
let mut resolved = if Path::new(&target).is_absolute() {
PathBuf::from(target)
} else {
current.join(target)
};
resolved.extend(components.iter().skip(index + 1));
let resolved = resolved.absolutize()?.to_path_buf();
if resolved == Path::new("/") {
bail!(
"refusing to resolve managed directory {} to the filesystem root",
path.display()
);
}View on GitHub (pinned to afd2eddd3a)
Solutions
- Relocate the managed path under a root-owned, non-group-writable directory (e.g. /usr/local or /opt)
- Tighten the parent directory permissions (`chmod go-w <parent>`) and ensure root ownership (`chown root <parent>`)
- Remove the intermediate symlink and use a real directory instead
- Audit for unexpected symlinks: `find <prefix> -type l`
Example fix
// before lrwxrwxrwx /tmp/tools/current -> /home/evil/tools // parent /tmp is world-writable // after $ sudo mkdir -p /opt/tools && sudo ln -s /opt/tools-v1 /opt/tools/current $ sudo chmod 755 /opt/tools
Defensive patterns
Strategy: validation
Validate before calling
fn parent_is_trusted(path: &Path) -> std::io::Result<bool> {
use std::os::unix::fs::MetadataExt;
if let Some(parent) = path.parent() {
let m = std::fs::metadata(parent)?;
return Ok(m.uid() == 0 && m.mode() & 0o022 == 0);
}
Ok(false)
} Type guard
fn safe_to_follow(path: &Path) -> bool {
use std::os::unix::fs::MetadataExt;
path.parent()
.and_then(|p| std::fs::metadata(p).ok())
.map(|m| m.uid() == 0 && m.mode() & 0o022 == 0)
.unwrap_or(false)
} Try / catch
match result {
Err(e) if e.to_string().contains("untrusted parent directory") => {
fix_parent_ownership_and_permissions(path)?; // chown root, chmod go-w
retry();
}
Err(e) => return Err(e),
Ok(v) => v,
} Prevention
- Keep managed prefixes under root-owned, non-group-writable directories
- Avoid placing symlinks in /tmp or other world-writable dirs
- Audit with find <prefix> -type l for unexpected links
- Watch for umask changes that make parent dirs group-writable
When it happens
Trigger: Opening/creating a managed directory through a path where an intermediate component is a symlink living in a directory that is world- or group-writable, or not owned by uid 0 (checked via fstat of the parent directory).
Common situations: Managed paths under shared directories like /tmp or /var/tmp; symlink planted by another local user in a world-writable location; dotfile manager symlinks placed in user-owned group-writable directories; hardened umask changes making a previously-acceptable directory group-writable.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- expected world-writable ancestor to be refused
- created path component {} was replaced before it could be op
- brew-cask: refusing operation through untrusted directory {}
- brew-cask: staged symlink path escaped extraction root: {}
- brew-cask: refusing generic artifact source outside the extr
AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09).
Data as JSON: /api/errors/8fd00797c8845fc8.
Report an issue: GitHub.