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
- 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.
- Move the profiles root to a location not managed by cloud-sync placeholder features, or mark the folder 'always keep on this device'.
- If the symlink was intentional (dotfiles management), stop managing that file through the library and maintain it externally.
- 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
- Do not symlink managed profile files into dotfiles/cloud folders
- Keep profile directories outside OneDrive/Dropbox placeholder management
- Restrict write access to the profiles directory to the running user
- Replace intentional symlinks with hard copies before handing the path to the library
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
- runtime candidate directory is a symlink or reparse point: {
- profile directory is a symlink, reparse point, or non-direct
- materialization journal is not a regular file: {}
- private materialization artifact is not a regular file: {}
- materialization journal targets reserved private storage
AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08).
Data as JSON: /api/errors/98f42ba0008ccac2.
Report an issue: GitHub.