Skip to content

isSameDevice stats non-existent recovery path, falls back to read-only root #164

Closed

Description

@David-Noble-at-work

Bug

recovery_unix.go:isSameDevice calls os.Stat(path2) where path2 is the recovery directory path (~/Library/Caches/devlore/recovery). This directory doesn't exist yet at the time of the check — it gets created later by moveToRecovery via os.MkdirAll.

Since os.Stat fails on a non-existent path, isSameDevice returns false, and getRecoveryBase falls through to the fallback: filepath.Join("/", ".devlore_recovery") → /.devlore_recovery.

On macOS with SIP enabled, creating directories at / is blocked. This causes all recovery-based operations to fail: Remove, Unlink, RemoveAll, and any write that overwrites an existing file.

Reproduction

Any test that calls Remove, Unlink, or WriteText on a file in a temp directory:

--- FAIL: TestUnlink_RemovesSymlink (0.00s)
    provider_test.go:534: Unlink() error = failed to create recovery site: mkdir /.devlore_recovery: read-only file system
--- FAIL: TestRemove_RemovesFile (0.00s)
    provider_test.go:596: Remove() error = failed to create recovery site: mkdir /.devlore_recovery: read-only file system

Suggested Fix

In isSameDevice, when path2 doesn't exist, walk up to the nearest existing ancestor and compare devices against that. Alternatively, in getRecoveryBase, stat cacheDir (which always exists, e.g. ~/Library/Caches) instead of cachePath (which hasn't been created yet).

Files

  • pkg/op/provider/file/recovery_unix.go:52-59 — isSameDevice
  • pkg/op/provider/file/recovery_unix.go:13-29 — getRecoveryBase

Activity

  1. David-Noble-at-work commented on Feb 27, 2026

    @David-Noble-at-work
    ContributorAuthor

    Fixed in feature/binding-unification branch. Changes across recovery_unix.go, recovery_windows.go, and recovery.go:

    • getRecoveryBase returns (string, error) on both platforms
    • getFirstExistingAncestor walks up to a stattable ancestor before comparing devices
    • findMountPoint takes pre-resolved ancestor path and FileInfo, no longer falls back to /
    • isSameDevice uses comma-ok type assertions instead of panicking, returns (bool, error)
    • findMountPoint uses comma-ok type assertions, treats assertion failures inside the loop as boundaries
    • recovery_windows.go: removed dead isSameDevice, signature matches Unix
    • recovery.go: filepath.Abs error checked, os.IsNotExist replaced with errors.Is(err, fs.ErrNotExist)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions