Skip to content

fix(aqua): skip in-place link when src and dst alias same inode#10012

Merged
jdx merged 1 commit into
jdx:mainfrom
tvararu:symlink-fix
May 25, 2026
Merged

fix(aqua): skip in-place link when src and dst alias same inode#10012
jdx merged 1 commit into
jdx:mainfrom
tvararu:symlink-fix

Conversation

@tvararu

@tvararu tvararu commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • On case-insensitive filesystems (macOS APFS, NTFS) an aqua package whose registry entry names a file link where src and dst differ only in case — like godot's Godot.app/Contents/MacOS/Godotgodot — resolves both paths to the same on-disk file. AquaBackend::create_file_link then removed the source binary and replaced it with a self-referential symlink, leaving the install broken.
  • Added a guard at the top of create_file_link: when dst exists and resolves to the same on-disk entry as src, return early. The check uses dev+ino on unix and canonicalize on other platforms.
  • Affects all three branches (hard:true hardlink, Windows copy, unix symlink), since each previously called remove_file(dst) before recreating the link.

Reproducer

mise install godot@4.6.3-stable
ls -la ~/.local/share/mise/installs/godot/4.6.3-stable/Godot.app/Contents/MacOS/

Before this PR: godot -> Godot (broken symlink), no Godot binary.
After this PR: Godot binary present, sibling godot symlink works.

Approach

The !dst.exists() precondition at the call site was removed earlier to support hard:true overwrite semantics, which exposed the case-insensitive aliasing case. Rather than restore that precondition (which would re-break overwrite), the guard lives inside create_file_link and only short-circuits when the two paths demonstrably refer to the same inode/canonical path — a no-op situation either way.

fs::canonicalize-based comparison alone isn't enough: it correctly identifies APFS/NTFS case-insensitive aliases but not hardlinked siblings. Using dev+ino on unix covers both, which keeps the regression test portable on case-sensitive Linux CI.

Other backends that create similar links (conda, github, etc.) retain their own !dst.exists() guards, so they aren't affected by this pattern.

Note for affected users

This fix prevents new installs from being corrupted. Existing installs that already have a self-referential symlink (godot, plus any other aqua packages with case-only link renames on macOS) still need a manual repair:

mise uninstall godot
mise install godot

This code was generated with help from Opus 4.7 in Claude Code

@greptile-apps

greptile-apps Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a same-inode/same-canonical-path guard inside AquaBackend::create_file_link to prevent self-referential symlinks on case-insensitive filesystems (macOS APFS, NTFS), where e.g. Godot and godot resolve to the same on-disk file.

  • On Unix, same_disk_entry compares dev+ino via fs::metadata (follows symlinks), covering both case-insensitive aliases and hardlinked siblings.
  • On non-Unix platforms, fs::canonicalize is used, which correctly handles case-folding on NTFS/APFS but does not cover hardlinked siblings at separate paths — an acknowledged limitation that doesn't cause regressions.
  • The guard fires before all three branches (hard, Windows copy, Unix symlink), all of which previously called remove_file(dst) unconditionally.

Confidence Score: 5/5

The change is a narrowly-scoped, additive guard that only fires when both paths already resolve to the same file — it cannot remove or corrupt data.

The guard correctly uses dev+ino on Unix (covering both case-insensitive aliases and hardlinked siblings) and canonicalize on Windows (covering the NTFS case-folding bug). It fires before all three link-creation branches, the fallback on error is conservatively false (proceed rather than silently skip), and the regression test exercises the exact inode-aliasing scenario on Unix CI. No existing code paths are altered outside the early-return.

No files require special attention.

Important Files Changed

Filename Overview
src/backend/aqua.rs Adds an early-return guard in create_file_link to skip overwriting when src and dst resolve to the same on-disk entry (handles case-insensitive APFS/NTFS aliasing), plus a same_disk_entry helper using dev+ino on Unix and canonicalize on other platforms, and a #[cfg(unix)] regression test.

Reviews (2): Last reviewed commit: "fix(aqua): skip in-place link when src a..." | Re-trigger Greptile

Comment thread src/backend/aqua.rs

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a guard in AquaBackend::create_file_link to prevent overwriting files with self-referential links on case-insensitive filesystems by checking if the source and destination refer to the same disk entry. It includes a new helper function same_disk_entry with platform-specific implementations for Unix and non-Unix systems, along with a corresponding unit test. I have no feedback to provide.

On case-insensitive filesystems like macOS APFS, `link.src` and
`link.dst` can differ as strings but resolve to the same on-disk file;
the symlink branch then removes the source binary before recreating it,
leaving a broken self-referential symlink.
@jdx jdx merged commit 9df0700 into jdx:main May 25, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants