libnyanpasu/clash-nyanpasu · warning

refusing to write through unexpected symlink or reparse poin

Error message

refusing to write through unexpected symlink or reparse point at {}

What it means

Before writing a managed profile file, ensure_not_symlink resolves the managed path and checks the on-disk entry with symlink_metadata. If it exists as a symlink or Windows reparse point, the write is refused: writing through a symlink would modify a file outside the managed profiles root, which may be an attack (symlink planting) or accidental misconfiguration. Only plain files (or non-existent paths, which are created fresh) are acceptable.

Source

Thrown at backend/tauri/src/service/profile_file.rs:1556

        AtomicFile::new(&full, OverwriteBehavior::AllowOverwrite)
            .write(|file| file.write_all(content.as_bytes()))
            .with_context(|| format!("atomic write {}", full.display()))
    }

    fn remove(&self, path: &ManagedProfilePath) -> anyhow::Result<()> {
        let full = self.resolve(path)?;
        Self::remove_nofollow(&full)
    }

    fn read_external(&self, target: &ExternalProfilePath) -> anyhow::Result<String> {
        std::fs::read_to_string(target.as_path())
            .with_context(|| format!("read external profile target {target}"))
    }

    fn ensure_not_symlink(&self, path: &ManagedProfilePath) -> anyhow::Result<()> {
        let full = self.resolve(path)?;
        match std::fs::symlink_metadata(&full) {
            Ok(meta) if is_symlink_or_reparse(&meta) => bail!(
                "refusing to write through unexpected symlink or reparse point at {}",
                full.display()
            ),
            Ok(_) => Ok(()),
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
            Err(e) => Err(e).with_context(|| format!("inspect profile file {}", full.display())),
        }
    }

    fn ensure_symlink(
        &self,
        path: &ManagedProfilePath,
        target: &ExternalProfilePath,
    ) -> anyhow::Result<()> {
        let full = self.resolve(path)?;
        self.ensure_managed_parent(&full)?;
        match std::fs::symlink_metadata(&full) {
            Ok(meta) if meta.file_type().is_symlink() => {

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Replace the symlink/reparse point at the reported path with a real regular file (copy the link target's content back if needed), then retry the write.
  2. Move the profiles root to a location not managed by cloud-sync placeholder features, or mark the folder 'always keep on this device'.
  3. If the symlink was intentional (dotfiles management), stop managing that file through the library and maintain it externally.
  4. In multi-user environments, restrict write access to the profiles directory to the running user to prevent symlink planting.

Example fix

// before: write refuses through the user's symlink
service.write_profile(&path, contents).await?;
// after: replace the symlink with a real file first
let full = resolve(path);
let meta = std::fs::symlink_metadata(&full)?;
if meta.is_symlink() {
    let target = std::fs::read_link(&full)?;
    std::fs::remove_file(&full)?;
    std::fs::copy(target, &full)?;
}
service.write_profile(&path, contents).await?;
Defensive patterns

Strategy: validation

Validate before calling

fn writable_regular_target(resolve: impl Fn(&ManagedProfilePath) -> std::io::Result<PathBuf>, path: &ManagedProfilePath) -> Result<bool, std::io::Error> {
    match std::fs::symlink_metadata(resolve(path)?) {
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(true),
        Err(e) => Err(e),
        Ok(m) => Ok(!m.is_symlink()),
    }
}

Type guard

fn is_symlink_or_reparse(m: &std::fs::Metadata) -> bool {
    m.is_symlink()
        || m.file_attributes()
            .map(|a| a & 0x400 != 0) // FILE_ATTRIBUTE_REPARSE_POINT
            .unwrap_or(false)
}

Try / catch

match service.write_profile(&path, contents).await {
    Err(e) if e.to_string().contains("refusing to write through unexpected symlink") => {
        // surface to the user: replace the symlink with a real file, then retry
    }
    other => other?,
}

Prevention

When it happens

Trigger: Saving/updating a managed profile whose target path on disk has been replaced by a symlink or reparse point — e.g. a user symlinked the profile file into another location, a sync tool created placeholders, or an attacker planted a symlink pointing at a sensitive file to trick the app into overwriting it.

Common situations: Users replacing profile files with symlinks to keep them in a dotfiles repo or cloud folder; OneDrive 'files on demand' reparse points on Windows; malicious symlink placement in shared/multi-user profile directories; leftover junctions after moving the profiles directory.

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 libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/98f42ba0008ccac2. Report an issue: GitHub.