Skip to content

fix(rattler_cache): detect read-only filesystems in PackageCacheLayer - #2594

Merged
baszalmstra merged 3 commits into
conda:mainfrom
olantwin:fix/is-readonly-mounted-filesystems
Jul 24, 2026
Merged

fix(rattler_cache): detect read-only filesystems in PackageCacheLayer#2594
baszalmstra merged 3 commits into
conda:mainfrom
olantwin:fix/is-readonly-mounted-filesystems

Conversation

@olantwin

Copy link
Copy Markdown
Contributor

Description

While testing @chrisburr's PR for rattler-fs (#2566) I noticed that is_readonly() only inspected the permission bits of the layer directory, so a cache layer on a filesystem that is mounted read-only (CVMFS, squashfs, a read-only bind mount) with mode-0755 directories was classified as writable. The cache then attempted to create lock files in the layer and failed with EROFS instead of treating it as a read-only layer.

As a work-around/solution, this PR also checks the ST_RDONLY flag of the containing filesystem via statvfs(3) on Unix. This unfortunately introduces a dependency on libc. Maybe there's a better way?

Repro: PackageCache::new_layered with a layer on a read-only bind mount (mount --bind dir mnt && mount -o remount,ro,bind mnt); split_layers() reports it as writable and acquire_global_lock() / get_or_fetch() fail with:
failed to interact with the package cache layer.

How Has This Been Tested?

Tested with #2566 on in an ALMA9 virtual machine with "mock" CMVFS mount, and on an AMLA9 cluster with a CVMFS mount (CERN's lxplus). No access to any suitable macOS or Windows machines to test.

AI Disclosure

  • This PR contains AI-generated content.
    • I have tested any AI-generated content in my PR.
    • I take responsibility for any AI-generated content in my PR.

Tools: {e.g., Claude, Codex, GitHub Copilot, ChatGPT, etc.}

Claude (Fable 5) helped me understand why the cache was mounted writable, where the relevant code is and proposed possible solutions. I selected, understood and tested the solution.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added sufficient tests to cover my changes.

COMMENT: Not sure how to best test this, as adding the right filesystems to the CI will add a lot of complexity to the tests...

@baszalmstra

Copy link
Copy Markdown
Collaborator

The inline #[cfg(unix)] block with nested if let / unsafe makes is_readonly hard to follow. We already have this exact pattern as a free function in flock.rs (is_on_nfs_mount). Can we do the same here?

pub fn is_readonly(&self) -> bool {
    self.path.metadata().is_ok_and(|m| m.permissions().readonly())
        || is_mounted_readonly(&self.path)
}

#[cfg(unix)]
fn is_mounted_readonly(path: &Path) -> bool { /* statvfs check */ }

#[cfg(not(unix))]
fn is_mounted_readonly(_path: &Path) -> bool { false }

rustix is already in our dependency tree via tempfile, and rustix::fs::statvfs(path)?.f_flag.contains(StatVfsMountFlags::RDONLY) would drop the unsafe block and the CString dance entirely.

@olantwin

Copy link
Copy Markdown
Contributor Author

Thanks for the prompt review!

The inline #[cfg(unix)] block with nested if let / unsafe makes is_readonly hard to follow. We already have this exact pattern as a free function in flock.rs (is_on_nfs_mount). Can we do the same here?

pub fn is_readonly(&self) -> bool {
    self.path.metadata().is_ok_and(|m| m.permissions().readonly())
        || is_mounted_readonly(&self.path)
}

#[cfg(unix)]
fn is_mounted_readonly(path: &Path) -> bool { /* statvfs check */ }

#[cfg(not(unix))]
fn is_mounted_readonly(_path: &Path) -> bool { false }

I agree that your proposed arrangement is much easier to follow! Thanks for the pointer to flock.rs, the pattern is pretty clear there.

rustix is already in our dependency tree via tempfile, and rustix::fs::statvfs(path)?.f_flag.contains(StatVfsMountFlags::RDONLY) would drop the unsafe block and the CString dance entirely.

As someone relatively new to rust, I had no idea about rustix! That's much nicer than using the C interfaces.

I'll update the PR ASAP. For review, do you prefer that I just amend the previous commit or a squash at the end?

@baszalmstra

Copy link
Copy Markdown
Collaborator

I'll update the PR ASAP. For review, do you prefer that I just amend the previous commit or a squash at the end?

The PR is squashed when merged. For reviewing its easier to just add new commits. But feel free to do whatever.

@olantwin
olantwin marked this pull request as ready for review July 17, 2026 14:17
@olantwin
olantwin force-pushed the fix/is-readonly-mounted-filesystems branch from 983366f to 4646ea8 Compare July 17, 2026 14:38
@olantwin

Copy link
Copy Markdown
Contributor Author

@baszalmstra May I ask you for a review, please?

@baszalmstra

Copy link
Copy Markdown
Collaborator

You still have CI issues.

@olantwin

Copy link
Copy Markdown
Contributor Author

You still have CI issues.

Apologies, since those where for the python bindings, I wasn't sure they were related. Investigating now.

@baszalmstra

Copy link
Copy Markdown
Collaborator

Maybe just a rebase/merge is enough

@olantwin
olantwin force-pushed the fix/is-readonly-mounted-filesystems branch from b1f4594 to 885ed82 Compare July 23, 2026 14:55
@olantwin

Copy link
Copy Markdown
Contributor Author

It seems to be a lock-file issue due to adding rustix to the crate. Hopefully regenerating them now fixed things.

@baszalmstra
baszalmstra enabled auto-merge (squash) July 23, 2026 15:08
olantwin and others added 3 commits July 23, 2026 17:39
…::is_readonly

is_readonly() only inspected the permission bits of the layer directory, so a
cache layer on a filesystem that is *mounted* read-only (CVMFS, squashfs, a
read-only bind mount) with mode-0755 directories was classified as writable.
The cache then attempted to create lock files in the layer and failed with
EROFS instead of treating it as a read-only layer.

Also check the ST_RDONLY flag of the containing filesystem via statvfs(3) on
Unix.

Repro: PackageCache::new_layered with a layer on a read-only bind mount
(mount --bind dir mnt && mount -o remount,ro,bind mnt); split_layers() reports
it as writable and acquire_global_lock() / get_or_fetch() fail with:
  failed to interact with the package cache layer.
…detection

Extract the mounted-read-only check out of is_readonly into an
is_mounted_readonly free-function pair (a #[cfg(unix)] implementation plus a

Replace the raw libc::statvfs call, the unsafe block and the CString dance with
rustix::fs::statvfs, which is already in the dependency tree. rattler_cache now
depends on rustix (fs feature) instead of libc on Unix.
auto-merge was automatically disabled July 23, 2026 15:39

Head branch was pushed to by a user without write access

@olantwin
olantwin force-pushed the fix/is-readonly-mounted-filesystems branch from 885ed82 to adc517e Compare July 23, 2026 15:39
@olantwin

Copy link
Copy Markdown
Contributor Author

The semver check seems to be exhausting the disk space on the runner.

@baszalmstra
baszalmstra merged commit 842b6f5 into conda:main Jul 24, 2026
17 of 19 checks passed
@octo-sts octo-sts Bot mentioned this pull request Jul 24, 2026
@olantwin
olantwin deleted the fix/is-readonly-mounted-filesystems branch July 24, 2026 14:20
@olantwin

Copy link
Copy Markdown
Contributor Author

@baszalmstra Thanks a lot for the feedback and guidance!

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.

2 participants