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

  1. Relocate the managed path under a root-owned, non-group-writable directory (e.g. /usr/local or /opt)
  2. Tighten the parent directory permissions (`chmod go-w <parent>`) and ensure root ownership (`chown root <parent>`)
  3. Remove the intermediate symlink and use a real directory instead
  4. 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

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


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/8fd00797c8845fc8. Report an issue: GitHub.