Skip to content

feat: add rattler_vfs crate - #2566

Draft
chrisburr wants to merge 32 commits into
conda:mainfrom
chrisburr:main
Draft

feat: add rattler_vfs crate#2566
chrisburr wants to merge 32 commits into
conda:mainfrom
chrisburr:main

Conversation

@chrisburr

@chrisburr chrisburr commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Description

This builds on top of #2565 and the work on @Dagmar-Dinjens to add a rattler_fs crate. See #1182 for the original concept. Basically the idea is to serve data directly from the rattler cache directory using a virtual filesystem, therefore avoiding the overhead of actually making environments. The usual install-time tricks (prefix-subsitution, python entrypoints, shebang-fixing, apple's code signing, ...) are applied on-the-fly as the data is read.

I see several motivations for this but the main ones are:

  • Using environments in large scale distributed systems without the overhead of installing environments while being able to make use of a pre-existing rattler cache directory.
  • Running on systems with slow filesystems (e.g. NFS) where environment creation is slow due to limited IOPS.
  • Avoiding the disk space overhead of having many pixi environments locally, many of which are used infreqently.

This PR includes 3 VFS backends for RattlerFS which are supported on the following platforms:

Platform FUSE NFS ProjFS
Linux N/A
macOS N/A
Windows N/A

We default to FUSE on Linux, NFS on macOS and ProjFS on Windows which allows all versions to run in user space assuming the VFS drivers are available. Windows is notably less extensively tested than the others. I also looked into FSKit on macOS but it seems that it's not yet suitable for something like RattlerFS.

This PR also contains the concept of a writable overlay for FUSE and NFS which then allows for edits to be made on top of the read-only underlying environment. This also makes it possible to pip install packages on top, though in principle RattlerFS could be expanded to support wheels natively rather than needing the overlay for that case. I've laid the ground work for this but didn't finish it.

The main component here is the rattler_fs crate which is used by the draft Pixi PR (prefix-dev/pixi#6548). There is a rattler mount command added to rattler_bin with an optional feature. I could see this as being useful in it's own right by systems which want to mount an environment from a lock file but don't want all of the the workspace functionality of pixi.

How Has This Been Tested?

There are End-To-End tests included which have been useful during development to make sure all three platforms continue to work. Also see prefix-dev/pixi#6548.

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.

There have been far too many sessions involved in the creation of this to track exactly what tools have been used and which parts are hand written vs AI generated. It's more AI-generated than I would like but equally it contains far more functionality and edge case handling than I would have managed to find time for alone. This project represents most of my rust experience so I'm not in a position to evaluate the quality of the code but I think it's in a reasonable state all things considered.

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.

@baszalmstra

baszalmstra commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

This is pretty amazing work! Thanks @chrisburr ! Coincidentally @Dagmar-Dinjens is also working on this in #2543.

Ill be going over the code!

@chrisburr
chrisburr marked this pull request as ready for review July 8, 2026 09:05
@chrisburr

Copy link
Copy Markdown
Contributor Author

I guess you meant @Dagmar-Dinjens? (Hi 👋! I'm glad to see the internship worked out!) I'd missed #2543 and I fear that PR is proabably orthogonal to this one...

If you'd like to chat about this PR on a call I'm happy to do so as it might be easier for getting up to speed as there is a lot going on here.

@baszalmstra baszalmstra left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I did a thorough review and tested the implementation on my windows machine. I think the code is already in a good state and apart from some tweaks here and there I would be fine if we merge this soon and then iterate on it in followup PRs.

Unfortunately I think this far surpasses the work that @Dagmar-Dinjens has pushed so far. @chrisburr I propose that we get this merged and let @Dagmar-Dinjens continue with followups? Would be happy to jump on a call to discuss this in more detail if that helps

I left way more comments than what I think is actually required to merge this. I think a lot of them we can address in follow-up PRs.

My main blockers are:

  • I think we should name the crate rattler_vfs instead of rattler_fs. But I can be persuaded.
  • We should not yet publish this crate automatically. This requires annotating the Cargo.toml with package.publish = false.

Btw why is windows nfs not supported?

Comment thread crates/rattler_vfs/src/overlay_fs/inode.rs
pub(crate) struct UpperInodeMap {
path_to_ino: HashMap<PathBuf, u64>,
ino_to_path: HashMap<u64, PathBuf>,
next_ino: AtomicU64,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The rest of this struct requires locking for concurrency, so what does an atomic u64 buy us here.

Comment thread crates/rattler_vfs/src/overlay_fs/inode.rs
Comment thread crates/rattler_vfs/src/codesign.rs
Comment thread crates/rattler_vfs/Cargo.toml Outdated
Comment on lines +32 to +37
# NFS transport (optional)
nfs3_server = { version = "0.10", optional = true }
nfs3_types = { version = "0.5", optional = true }

[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.62", features = ["Win32_Storage_ProjectedFileSystem", "Win32_Foundation", "Win32_System_LibraryLoader"] }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Lets use workspace dependencies.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@Dagmar-Dinjens If you can look into this perhaps.

Comment thread Cargo.toml Outdated
dbus-secret-service-keyring-store = { version = "1.0.0", default-features = false }
dunce = "1.0.5"
enum_dispatch = "0.3.13"
env_logger = "0.11.9"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How is this used?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this can just go @chrisburr ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think this is left over from an earlier version of this before I figured out how to interface pixi more cleanly.

Comment thread crates/rattler-bin/src/commands/mount.rs
Comment thread crates/rattler_vfs/src/fuse_adapter.rs
@@ -0,0 +1,691 @@
//! NFS transport adapter.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Out of curiosity, why do we use nfs3 instead of nfs4?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think the best supported crate I found only had v3 and I wasn't aware of anything about v4 really mattering.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Another thing to add here is https://github.com/facebook/sapling/blob/61b3cde3bffdd7553094a9b4738af369abdf935b/eden/fs/docs/macOS.md

In general I found EdenFS's documentation to be a useful reference. For example the Windows docs were what made me finally give up on trying to support the read-only flag for ProjFS.

Comment thread crates/rattler_vfs/src/vfs_ops.rs
@baszalmstra

baszalmstra commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Also some notes from fable worth looking at:

Review: #2566feat: add rattler_fs crate

What it does

Adds a new rattler_fs crate that serves conda environments as virtual filesystems (FUSE on Linux/macOS, a localhost NFS server, ProjFS on Windows) directly from the package cache, applying install-time transforms — prefix replacement, Python entry points, shebang fixes, macOS ad-hoc code signing — on the fly during reads instead of materializing files on disk. A writable overlay layer (whiteouts + copy-up) lets users edit files and pip install on top of the read-only view. Supporting changes: paths.json gains optional offsets/shebang_length fields, link.rs gains offset-based replacement variants, plus a mount CLI command, an e2e nushell harness, and a CI workflow.

Blocking issues

1. The link.rs change corrupts real installs (not just mounts)

copy_and_replace_textual_placeholder_offsets (crates/rattler/src/install/link.rs:895) strips the shebang line and rebases source_bytes = rest, but then applies offsets that are absolute file positions. Every offset is now wrong by the shebang length: the loop injects the prefix mid-body and, for offsets near EOF, &source_bytes[last_match..offset] panics with an out-of-range slice. Any package that ships the new offsets field and has a shebang'd text file installs corrupted or crashes the installer. This is in the production install path, gated only by whether offsets is populated. The one test combining shebang + offsets passes an empty offsets vec, so it never triggers. This is the most important finding — it's a regression in already-shipping code.
https://github.com/conda/rattler/pull/2565/changes#r3544983267

2. Untrusted paths.json offsets panic inside filesystem callbacks

offsets comes straight from a package's paths.json (attacker-controllable) and is never validated. Empty binary groups panic at group.split_at(group.len() - 1) (usize underflow); out-of-range/out-of-order offsets panic at &source[src_start..src_end]. A package with "offsets": [[1000, 2000]] on a 10-byte file kills the FUSE/NFS read thread → whole mount dies. The identical arithmetic in the install-time variants panics the installer. Needs a validation pass (sorted, in-bounds, spaced ≥ placeholder length) at build/mount time.
https://github.com/conda/rattler/pull/2565/changes#r3544991157

3. Overlay copy-up has three independent data-corruption bugs

All in overlay_fs.rs::copy_to_upper, all untested, all hit by ordinary pip install/mv on a conda env:

  • Symlinks are copied up as empty regular files — only Directory is special-cased; a symlink's content_source returns ENOENT, falls to read (returns vec![] for symlinks), and fs::write creates a 0-byte file that permanently shadows the link. Conda envs are full of libfoo.so → libfoo.so.1.
  • Directory rename loses lower children — the upper_path.exists() early-return fires before the directory-recursion check, so a partially-materialized dir isn't recursed; renaming it drops any lower-only children, then a whiteout hides the originals.
  • Exec bit dropped on copy-upfs::write creates 0644 with no permission copy, so chmod/mv/truncate on bin/python yields a non-executable copy.

Plus a lookup/opaque ordering bug (overlay_fs.rs:308): lookup returns ENOENT for opaque dirs before checking the upper layer, so a freshly created file in a reinstalled package dir is visible via readdir but not stat/open — breaking the exact pip uninstall/reinstall pattern the feature targets.
#2582

4. NFS server has no per-user access control (security)

Two independent findings converge here. The server binds 127.0.0.1 (remote hosts excluded — good), but that bind is the only gate: NFS AUTH_UNIX is unauthenticated, the adapter never consults caller credentials, and access via raw RPC bypasses the mount point's Unix permission bits. Any other local user can speak NFS3 to the port and read (and, on writable mounts, write/delete) another user's environment, forging any uid. Compounding it, NFS wire filenames aren't sanitized: CREATE(dir, "../../../home/user/.bashrc") escapes the overlay directory via overlay_dir.join(...). A loopback-TCP NFS server is inherently shared among local users; at minimum this needs prominent documentation, ideally peer-credential rejection.
#2579

5. Packaging will break the release pipeline

rattler_fs has no description and no publish = false, and release-plz release runs on every push to main → publish fails (crates.io requires a description). Separately, rattler-bin (a published crate) now has rattler_fs = { path = "../rattler_fs", optional = true } with no version → cargo publish of rattler-bin fails. Convention is to add the crate to the pinned internal-crate list in the workspace Cargo.toml and use workspace = true.

Major issues (fix before real-world use)

  • Mount-path-longer-than-placeholder panics the read path. binary_ranged_read has assert!(new_prefix.len() <= old_prefix.len()); on Linux it's called directly, so mounting a short-placeholder package (e.g. the 32-char /opt/anaconda1anaconda2anaconda3) at a longer path aborts the FUSE thread. The installer returns a graceful InvalidData error for the same case.
  • VFS diverges from the installer in ways that break environments: vfs: VFS diverges from the installer in ways that break environments #2580
  • macOS codesign can't be byte-identical. Install shells out to /usr/bin/codesign (rebuilds the whole superblob, changes size/bytes); adhoc_resign only patches page hashes and only the first CodeDirectory (dual-signed / SHA-1-first binaries fail or go stale). This breaks the parity premise for signed binaries and is undocumented/untested.
  • codesign slice panics on malformed Mach-O — inconsistent n_code_slots vs code_limit produces &data[4096..100] → panic in the read handler. ~~ vfs: Replace codesigning with standardized implementation #2571
  • ProjFS enumeration sort uses str::cmp instead of PrjFileNameCompare (Windows case-insensitive collation); mis-ordering can make ProjFS drop or duplicate directory entries. vfs: enumeration sort uses str::cmp instead of PrjFileNameCompare #2581
  • ProjFS /DELAYLOAD never reaches the binarybuild.rs's rustc-link-arg applies only to rattler_fs's own targets, not to rattler.exe. rattler-bin has no build.rs, so the Windows binary still hard-imports projectedfslib.dll and dies at startup on machines without ProjFS — defeating the ProjFsDllMissing graceful-degradation path. vfs: dynamic load projfs api #2572

Performance (will dominate real-world experience)

  • ~~O(N²) ranged reads. Both text_ranged_read and binary_ranged_read walk the output stream byte-by-byte from position 0 on every call. Reading a 500 MB .so through FUSE in 128 KB chunks is ~terabyte-scale byte work. Offsets are sorted → binary-search to the start position and extend_from_slice the spans.~~ vfs: O(N²) ranged reads #2583
  • Eager full-file scan at mount time. offsets is a new rattler-only extension absent from essentially all real packages, so VirtualFS::with_platform falls back to fs::read on every prefix-bearing file (all the .sos and binaries), serially, on the caller's thread — defeating the "instant no-copy mount" goal. vfs: Eager full-file scan at mount time #2584
  • Overlay metadata: full-JSON rewrite + fsync per op under the state mutex → O(N²) for a bulk pip uninstall, blocking all concurrent lookups. vfs: Overlay metadata: full-JSON rewrite + fsync per op #2585

Minor / cleanup

Accidental unused workspace deps (env_logger, and the obscure mem = "0.5.0" — looks like a mangled memchr edit, remove it); memchr added to rattler_conda_types/Cargo.toml but unused there; CI path filter omits crates/rattler-bin/** and root pixi.toml (so a mount.rs-only PR skips the e2e); action pins lag repo convention (checkout v4 vs v6); mutex .lock().unwrap() poisoning throughout turns one panic into a mount-wide outage; single non-looping read() treats a short read as EOF; stat-failure fabricates a 0-byte file instead of EIO; overlay copy-up isn't crash-safe (no tmp+rename+fsync on the data path, unlike the metadata path); .rattler_fs_state.json is reachable and writable from inside the mount; PathType::Directory entries modeled as file leaves give inconsistent stat/readdir; VirtualFS::new is pub but takes a pub(crate) type; MountHandle::Fuse(fuser::...) leaks fuser into the public API (SemVer hazard).

Test coverage — the core weakness

The unit and parity suites are good but cover only internally-consistent happy paths. Nothing exercises: shebang files (finding 1 & shebang divergence), malformed offsets (finding 2), overlay symlinks / dir-rename-with-children / exec-bit / lookup-in-opaque-dir (finding 3), adversarial NFS filenames (finding 4), mount-path-longer-than-placeholder, Windows entry-point layout, clobbering, dual-CD codesign, or any of the three adapters at unit level. The e2e nushell harness is rigorous (I had an agent verify its pass/fail detection empirically) but validates behavior, not these boundaries. Adding cases in these gaps is where the real hardening is.
#2586

Recommendation

Request changes. The concept and structure are excellent and worth landing, but I'd block on: (1) the link.rs shebang+offsets install corruption, (2) untrusted-offset panic validation, (3) the three overlay copy-up corruptions + opaque-lookup bug, (4) the NFS access-control/path-traversal exposure (at least documented, ideally fixed), and (5) the release-pipeline breakage. The mount-path panic, VFS/installer divergences (shebang, clobber, Windows entry points, pypi), and the two structural performance issues should follow before this is used on real environments at scale. Given the surface area, splitting the paths.json/link.rs offset work into its own reviewed PR (it's independently useful and independently risky) would de-risk the rest.

@chrisburr

Copy link
Copy Markdown
Contributor Author

@chrisburr I propose that we get this merged and let @Dagmar-Dinjens continue with followups? Would be happy to jump on a call to discuss this in more detail if that helps

Yes, I am very happy to have @Dagmar-Dinjens pick this up again, especially as I'd like to spend some time on the package cache itself to help with distribution via CVMFS which is orhtogonal but useful for this.

If you want to arrange a chat, the conda-forge Zulip is probably the most reliable way for you to get my attention.

I left way more comments than what I think is actually required to merge this. I think a lot of them we can address in follow-up PRs.

How do you want to proceed on that side? I'll work my way though replying and implement some of them? Or do you want to use the comments as a chance to onboard @Dagmar-Dinjens?

I think we should name the crate rattler_vfs instead of rattler_fs. But I can be persuaded.

I'm not sure how many people know VFS as a concept. That said, the name has been in my brain for a long time so the problem might just be that it's different from what I've become used to. I don't have a strong opinion either way.

We should not yet publish this crate automatically. This requires annotating the Cargo.toml with package.publish = false.

👍

Btw why is windows nfs not supported?

Initially I tried to use only NFS and just ProjFS for later but I ran into a bunch of limitations. My memory is a bit fuzzy as this was a couple of months ago I think the main ones were:

  • it couldn't satisfy the pixi use case in of mounting the environment in %CWD%/.pixi/... without hacky tricks
  • limited to ~20 mounts before you run out of drive letters

@baszalmstra

Copy link
Copy Markdown
Collaborator

How do you want to proceed on that side? I'll work my way though replying and implement some of them? Or do you want to use the comments as a chance to onboard @Dagmar-Dinjens?

I have created issues for all the non-blocking items in this PR (prefixed with vfs:). I also updated the fable review. So all the items that are still open should be tackled in this PR. I tagged @Dagmar-Dinjens in some of them, I hope she can take care of those? @chrisburr Would you be able to take care of the remaining ones?

Once merged we can work on the individual issues. We can simply assign people to make sure we dont work on the same things.

Does that help/make sense?

I'm not sure how many people know VFS as a concept. That said, the name has been in my brain for a long time so the problem might just be that it's different from what I've become used to. I don't have a strong opinion either way.

So I asked ChatGPT for the deciding vote:

I’d vote rattler_vfs.

Why:

rattler_fs sounds like a broad filesystem utility crate for Rattler: path helpers, atomic writes, cache layout helpers, symlink handling, file locking, etc.

This implementation is much more specific: it presents conda environments as a virtual filesystem, backed by FUSE/NFS/ProjFS, with on-the-fly prefix replacement, generated files, code signing handling, and overlays. The PR itself describes it as serving data “using a virtual filesystem” and having “3 VFS backends.” The changed files also include vfs_ops.rs and virtual_fs.rs, which makes vfs the more precise abstraction.

@chrisburr Does that resonate with you?

@chrisburr

Copy link
Copy Markdown
Contributor Author

That's a compelling arugment for rattler_vfs and it's growing on me over time. I've made the change 👍

We should not yet publish this crate automatically. This requires annotating the Cargo.toml with package.publish = false.

This is in contradiction with having rattler_vfs be a non-optional feature in rattler_bin: #2566 (comment)

For now I've gone with removing the feature and publishing the crate.

The biggest open question from my perspective is conda/ceps#179 and especially the handling of non-UTF-8 strings.

@chrisburr
chrisburr marked this pull request as draft July 9, 2026 21:41
@chrisburr chrisburr changed the title feat: add rattler_fs crate feat: add rattler_vfs crate Jul 9, 2026
@chrisburr
chrisburr force-pushed the main branch 2 times, most recently from 9a9e341 to 61cc1af Compare July 9, 2026 22:31
DagmarFiep and others added 7 commits July 17, 2026 14:48
Finalise the offset-based prefix replacement: a structured Offsets enum (text
vs binary), memchr-based NUL scanning, and the
`copy_and_replace_placeholders_with_offsets` family, so install (and other
consumers) can replace the install prefix at known byte offsets instead of
rescanning each file. The `offsets` field on PathsEntry/PrefixPlaceholder is
optional and skipped when absent, so paths.json stays backward-compatible
(paths_version unchanged).
Correct the doc comments on `Offsets`, `PrefixPlaceholder::offsets` and
`PrefixPlaceholder::shebang_length` to match the draft CEP "Prefix placeholder
offsets in paths.json" (conda#2565):

- `offsets` exclude occurrences inside the shebang region, and for binary
  files the group's last value is the NUL terminator offset or the file size
  when the final C string is unterminated at end-of-file.
- `shebang_length` is present if and only if `offsets` is present, `file_mode`
  is text, and the file starts with `#!` — independent of whether the first
  line contains the placeholder.
- The `Offsets` shape is determined normatively by `file_mode`, not inferred
  from the JSON structure.
Bring install-time prefix replacement into line with the draft CEP "Prefix
placeholder offsets in paths.json" (conda#2565). `offsets` are
absolute byte positions that exclude the shebang region and are spliced
uniformly on every platform; the shebang region (first `shebang_length`
bytes) is transformed separately.

- Plumb `shebang_length` from `PrefixPlaceholder` into the offset functions
  and use it as the region boundary instead of re-deriving it from content,
  validating it equals first-0x0A-index + 1.
- Add the missing non-rewriting-target rule: on Windows (e.g. a noarch
  package) the shebang region gets plain placeholder replacement rather than
  being left untouched. On Unix it is rewritten by the shebang rules (region
  minus its trailing newline, newline copied through verbatim).
- Report producer non-conformance (in-region offset, shape/mode mismatch,
  out-of-range or unordered offsets, missing placeholder bytes, empty binary
  outer list, `#!` file without `shebang_length`) as a distinguishable
  `OffsetReplaceError::InconsistentMetadata`. The offset functions validate
  before writing anything, so `link_file` falls back to the search-based path
  (with a warning) using the still-empty destination instead of failing the
  install.
- Fix a pre-existing panic in the search-based `copy_and_replace_textual_
  placeholder`: a `#!` file with no newline fed an empty line into
  `replace_shebang`, tripping its `starts_with("#!")` assertion.
- Fix the binary test that recorded the NUL terminator one byte past the
  actual `\x00`, and align the suite with the CEP's informative test-vector
  list (short/long Unix prefixes, non-rewriting target, no trailing newline,
  empty offsets, multiple in-shebang occurrences, over-long shebang with no
  occurrence, unterminated final C string, and the fallback for non-conformant
  input).
Add read-only `offsets` and `shebang_length` getters to the py-rattler
`PrefixPlaceholder` so consumers can inspect the CEP fields
(conda#2565). `offsets` returns `None`, a `list[int]` for text-mode
files, or a `list[list[int]]` for binary-mode files (grouped by C string).
- "Check intra-doc links" (`-D rustdoc::private-intra-doc-links`): the public
  `copy_and_replace_textual_placeholder_offsets` doc linked to the private
  `replace_shebang` fn; demote it to a plain code span.
- Windows-x86_64: `test_replace_long_prefix_in_text_file_offsets` hardcoded
  `shebang_length: Some(44)`, which is wrong when the `shebang_test.txt`
  fixture is checked out with CRLF (the carriage return shifts the first
  newline to 45). Derive `shebang_length` from the file contents instead.
The offsets CEP (conda/ceps#179) was restructured on Jul 15: `offsets` is
no longer a bare list of positions but a list of offset groups, each
recording the occurrences under one encoding:

    "offsets": [{"encoding": "utf-8", "ranges": [10, 45]}]

Installers do not agree on which encodings of the placeholder they
replace (conda searches UTF-8 plus UTF-16/UTF-32 variants; rattler and
libmamba search UTF-8 only), so each installer applies exactly the groups
its own search-based replacement covers and one package serves every
installer.

- `Offsets` becomes `OffsetRanges` inside a new `OffsetGroup` carrying an
  `OffsetEncoding`; unknown encoding names and unrecognized group members
  parse (they must not fail the whole paths.json) but mark the metadata
  as unusable.
- `select_utf8_offset_ranges` validates the CEP's structural rules and
  picks the UTF-8 group; rattler skips the other defined encodings, whose
  occurrences its own search would not have replaced either.
- The installer treats invalid metadata as inconsistent (falling back to
  search-based replacement) and copies files with no UTF-8 group through
  unchanged apart from text shebang handling; pre-CEP flat offsets
  deserialize as absent rather than failing the parse.
- Adds the CEP's new test vector 9 (a binary file with occurrences under
  more than one encoding) and the Examples-section entries as tests.
chrisburr added a commit to chrisburr/pixi that referenced this pull request Jul 17, 2026
Move the rattler patch pins from the frozen feat/rattler-fs snapshot to the
fork's main branch (the conda/rattler#2566 PR branch), which renames
rattler_fs to rattler_vfs and carries the encoding-tagged paths.json offset
groups plus a rebase onto upstream with the CEP-42 gateway:

- rename the crate and all uses: rattler_fs -> rattler_vfs;
- migrate the three gateway query call sites to RepoDataQueryOutput,
  keeping only the repodata buckets (pre-CEP-42 behavior);
- patch the rattler_build_* crates to a chrisburr/rattler-build shim
  branch (rattler-fs, cut from the rattler_build_core-v0.2.8 publish tag)
  because the crates.io releases predate both the fork's gateway API and
  the new PrefixPlaceholder offsets fields.

Cargo.lock is intentionally untouched: it cannot resolve until the fork
branches are pushed; regenerate with `cargo update` afterwards.
chrisburr and others added 17 commits July 17, 2026 16:51
…ental_

Per review discussion on conda#2565: until conda/ceps#179 is finalized the
schema may still change, so mark the Rust API as unstable by renaming the
PrefixPlaceholder fields to `experimental_offsets` and
`experimental_shebang_length` (py-rattler properties follow suit). The
serialized form is unchanged — serde renames keep `offsets` and
`shebang_length` on the wire, as the CEP specifies.

One self-contained commit so it can be reverted wholesale once the CEP
merges and the fields become stable.
Dagmar's foundational implementation of the on-demand virtual filesystem that
serves conda environments from the package cache. Later renamed to rattler_vfs
and hardened (mount transports, overlay, codesign, prefix-replacement parity).

(Reworded from the original "initial commit with multiple major problems still
needing fixing"; authorship preserved.)
Rename Dagmar's rattler_fs foundation to rattler_vfs and harden it into the
installable crate: FUSE/NFS/ProjFS transports, overlay, codesign, and the
ranged prefix-replacement paths (byte-identical to rattler's install path,
consuming its CEP-conformant offset APIs).
Add `rattler mount` to mount a locked environment through rattler_vfs. Gated
behind the default-on `mount` feature (opt out with --no-default-features);
the NFS transport is enabled for the bundled crate.
Add the rattler-vfs end-to-end harness: a nushell mount/verify script, a GitHub
Actions workflow, the pixi `rattler-vfs` environment, and a test-data fixture
environment for the mount tests.
Pull the shebang-region transformation out of
copy_and_replace_textual_placeholder_offsets into a reusable, public
`replace_shebang_region` helper: on Unix targets the region (minus its trailing
newline) is rewritten by the shebang rules and may collapse to
`#!/usr/bin/env <program>`; on non-rewriting targets it receives plain
placeholder replacement.

This makes the helper the single source of truth for how the shebang region is
patched, shared by install-time replacement here and rattler_vfs mount-time
ranged reads so the two stay byte-identical. Behavior is unchanged (verified by
the existing link tests).
`text_ranged_read` spliced offsets uniformly, so a mounted shebang script kept
its original (unreplaced) first line and reported the wrong size — diverging
from an install once a CEP-conformant producer excludes the shebang region from
`offsets`.

Introduce `plan_text_replacement`, which mirrors the installer: it transforms
the shebang region via the shared `replace_shebang_region` helper (collapsing an
over-long line to `#!/usr/bin/env <program>` on Unix, or plain-replacing it on
non-rewriting targets) and records the remaining occurrences as body offsets.
`text_ranged_read` now emits the transformed region followed by the body-spliced
tail, and `getattr` sizes are computed the same way.

The install-vs-mount parity tests gain shebang coverage: line kept, collapsed
for an over-long prefix, no trailing newline, multiple occurrences in the line,
only-in-shebang, and a non-rewriting (Windows) target — plus ranged reads that
cross the region/body boundary.
Two failures surfaced on the offsets work in CI:

- "Check intra-doc links" (`-D rustdoc::private-intra-doc-links`): the public
  docs for `replace_shebang_region` and
  `copy_and_replace_textual_placeholder_offsets` linked to the private
  `replace_shebang` fn. Demote those to plain code spans.
- Windows-x86_64: `test_replace_long_prefix_in_text_file_offsets` hardcoded
  `shebang_length: Some(44)`, which is wrong when the `shebang_test.txt`
  fixture is checked out with CRLF (the extra carriage return shifts the first
  newline to 45). Derive `shebang_length` from the file contents instead, so
  the test is robust to the checkout's line endings.
The Windows-only code paths never compile on the Linux/macOS CI, so three
edition-2024 / `-D warnings` errors only surfaced on the Windows job:

- projfs_adapter.rs: the `unsafe extern "system"` ProjFS callbacks perform raw
  pointer / FFI operations directly in the fn body, which Rust 2024's
  `unsafe_op_in_unsafe_fn` rejects. The whole module is inherently unsafe FFI,
  so allow the lint module-wide rather than wrapping ~30 sites.
- lib.rs `mount_nfs`: on targets without NFS support the platform block
  `bail!`s, making the success tail (`tracing::info!` + `Ok(..)`) unreachable.
  Gate that tail behind `cfg(any(macos, linux))` so it is only compiled where
  it is reachable.
- nfs_adapter.rs: `NfsMountHandle::mount_point` is only read by the macOS/Linux
  `umount` paths, so it is dead code elsewhere — allow it on non-NFS targets.
Add TextPlan::from_recorded, which builds a text replacement plan
straight from the offsets recorded in paths.json instead of scanning
the file, and use it during VirtualFS construction: one stat for the
size arithmetic plus at most shebang_length bytes read for the shebang
region. The recorded offsets are the producer's contract per the CEP
and are trusted as-is; scanning remains as the fallback for pre-CEP
packages and for metadata that fails the cheap shebang sanity check
(logged so a non-conformant producer is self-diagnosing).

Also log a warning before the macOS codesign materialization path falls
back to serving raw bytes, instead of degrading silently.
Follow rattler_conda_types' restructure of `offsets` (conda/ceps#179):
plan construction now resolves the recorded metadata once through
`select_utf8_offset_ranges` — rattler applies exactly the groups its own
search-based replacement covers (UTF-8 only):

- usable metadata yields the UTF-8 group's ranges for the text/binary
  plan; valid metadata with no UTF-8 group plans zero splices (the wide
  string occurrences would not have been replaced by rattler's search
  either), serving the bytes verbatim;
- structurally invalid or unrecognized metadata falls back to scanning
  the file, with a warning — the mount-side analogue of the installer's
  search-based fallback;
- the selected ranges themselves remain trusted as-is, keeping the
  never-panic policy of the ranged reads.

The macOS codesign path hands the dispatcher a synthesized UTF-8 group
around the plan's c-string groups, and is skipped when there is nothing
to replace (the bytes are served verbatim, so the original signature
remains valid).
Reinstate `OverlayMismatch` (Error/Adopt), `MountConfig::with_overlay_mismatch`,
and `overlay::recorded_env_hash`, which were dropped when the crate was
restructured after review. pixi's mount backend depends on them: its
`mount-overlay-mismatch` config defaults to adopting a mismatched overlay
(keeping e.g. `pip install` results across `pixi add`) with a client-side
warning, and uses the side-effect-free probe to detect the mismatch before
mounting. Without the policy every mismatch hard-fails the mount.

The behavior is unchanged from what was reviewed at 742a2d8, modulo the
crate rename; the default remains `OverlayMismatch::Error`.
…as root

MountOption::CUSTOM("noatime") is placed in the kernel mount(2) data
string, which the FUSE driver rejects with EINVAL. This only triggers
when mounting as root: fuser then calls mount(2) directly, while the
unprivileged path delegates to fusermount3, which parses noatime itself
and converts it into MS_NOATIME.

Use the typed MountOption::NoAtime, which fuser translates into the
MS_NOATIME mount flag on both paths.

Repro: run any FUSE mount (e.g. rattler mount / pixi mount) as root on
Linux; it fails with:
  failed to mount: Error calling mount() at ... with
  "fd=10,rootmode=40755,user_id=0,group_id=0,noatime": EINVAL
…king

The rebase onto upstream main picked up fs4 1.x, which renamed
try_lock_exclusive to try_lock in line with std's file-locking API
(stabilized in Rust 1.89). std's inherent File::try_lock covers the
overlay lock's needs directly, so drop the fs4 dependency from
rattler_vfs entirely.
Companion to the rattler_conda_types commit marking the paths.json
offsets fields experimental until conda/ceps#179 is finalized. One
self-contained commit so both can be reverted wholesale once the fields
become stable.
Dagmar-Dinjens added a commit to Dagmar-Dinjens/rattler that referenced this pull request Jul 22, 2026
Replace the extract-to-disk install in `rattler exec` with a lazy
`rattler_vfs` mount served from the shared package cache: solve, build an
in-memory lock file from the solved records, and mount it (read-only where
supported). The solved lock file is cached at `<prefix>/.exec-lock.yml` so
warm runs skip repodata + solve. The mount handle is unmounted explicitly
before `process::exit`, and a stale mount from a crashed run is force-cleared
on the next invocation.

`rattler_vfs`/`rattler_lock` are made non-optional in rattler-bin (exec uses
them on every run); the `mount` feature now only gates the `mount` subcommand.

Stacked on conda#2566 (rattler_vfs crate).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"noacl,nolock,soft,timeo=100,retrans=3,vers=3,tcp,port={port},mountport={port},rsize=1048576"
);
if read_only {
opts.push_str(",ro");

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.

As the read-only mount serves an immutable package set and nothing will change for as long as the mount is active, we could let the client cache attributes aggressively instead of periodically re-validating them with fresh GETTR/LOOKUP round trips. It helps longer running mounts with serving repeat metadata from its cache.

Suggested change
opts.push_str(",ro");
opts.push_str(",actimeo=3600,ro");

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.

5 participants