Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider making the src cache read-only. #9455

Open
Tracked by #84
ehuss opened this issue May 3, 2021 · 35 comments
Open
Tracked by #84

Consider making the src cache read-only. #9455

ehuss opened this issue May 3, 2021 · 35 comments
Labels
A-caching Area: caching of dependencies, repositories, and build artifacts A-filesystem Area: issues with filesystems S-needs-design Status: Needs someone to work further on the design for the feature or fix. NOT YET accepted.

Comments

@ehuss
Copy link
Contributor

ehuss commented May 3, 2021

Registry dependencies get extracted into cargo home (in the src directory) with whatever metadata is in the tar file. One issue with this is that the files are usually writeable. This can cause a problem if the user accidentally modifies these files, which breaks cargo's assumption that they are immutable and reusable. One way this can happen is that in some editors, when there is an error or warning, they may open those files to display the error/warning (particularly with macros). The user may not realize that this is from a remote location, and may not understand the consequence of making changes.

We may want to consider making those files read-only when extracting them. This would help with confusing situations where the src cache is inadvertently corrupted.

This would not protect from general filesystem corruption, which is also an issue. This is also an issue with git dependencies, which may be more difficult to adjust permissions on.

There is some risk that this would introduce new problems. For example, if people are using tools to clean the src directory, and those tools have trouble with read-only files.

@ehuss ehuss added the A-caching Area: caching of dependencies, repositories, and build artifacts label May 3, 2021
@Eh2406
Copy link
Contributor

Eh2406 commented Oct 13, 2021

@matthiaskrgr would making files read-only give any problems to cargo-cache?

@matthiaskrgr
Copy link
Member

Oh that's an interesting idea!
While I fully agree that it makes sense to make the cache (or at least .cargo/registry/src and .cargo/git/checkout) write-only, it does indeed prevent cargo-cache from cleaning up anything;

$ cargo cache --autoclean
Clearing cache...

Warning: failed to recursively remove directory "/home/matthias/.cargo/registry/src".
error: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }
Warning: failed to recursively remove directory "/home/matthias/.cargo/git/checkouts".
error: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }

BUT I am already happy that it does not panic :D
If it turned out that cargo-cache does modify the cache in any way (anything that goes beyond just removing whole files) I would indeed consider this a bug.

Right now I am just running remove_dir_all::remove_dir_all() on directories without doing any checks whatsoever.
https://crates.io/crates/remove_dir_all
I never had a read-only cache in mind when writing cargo-cache but adding a little bit of extra logic which tries to change file permission to read-write before removing them does not sound too difficult, assuming that we have a crate around that handles file permissions on linux, macos and windows.

@eminence
Copy link
Contributor

I'm bringing up a new rust user on my team, and I found that they ended up editing some files in $CARGO_HOME (which is actually quite easy to do with VSCode+RA) to add some debugging messages. A read-only src cache might have given them a sufficient hint that what they were doing was not the right approach.

@weihanglo
Copy link
Member

Just did a PoC and benchmarks with rust-lang/cargo itself. It turns out that there is a slight performance hit (~5%) on macOS. It's in expectation, as there are some extra permission operations on every file for the first time checking out/unpacking a crate (Windows might do it better since file permissions can be inherited, though).

However, it does get new problems 🤯. When I removed sources residing in ~/.cargo/registry/src and re-run the build again, I got a permission denied error.

$ ./target/release/cargo b
$ rm -rf ~/.cargo/registry/src
$ ./target/release/cargo b
error: failed to run custom build command for `curl-sys v0.4.55+curl-7.83.1`

Caused by:
  process didn't exit successfully: `/Users/myuser/projects/cargo/target/debug/build/curl-sys-f4af01668b6e1680/build-script-build` (exit status: 101)
  --- stdout
  ...
  --- stderr
  fatal: not a git repository (or any of the parent directories): .git
  thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }', /Users/myuser/.cargo/registry/src/gh.neting.cc-1ecc6299db9ec823/curl-sys-0.4.55+curl-7.83.1/build.rs:93:10
  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

I guess its curl-sys's build script that uses fs::copy to copy contents from registry/src to build script's OUT_DIR, but unfortunately fs::copy also copies permission bits. Any rebuild that triggers a re-run of the same build-script binary might fail because it cannot fs::copy from cache into existing readonly files under target-dir. I am afraid that more cases in the wild will be broken if registry/src becomes readonly. Here is the search result that a build.rs contains fs::copy throughout GitHub.

Personally, I love to see this enhancement landed, but it seems a bit risky to me after the analysis.

@ehuss
Copy link
Contributor Author

ehuss commented Jun 1, 2022

That's really unfortunate, I hadn't thought about that.

I'm wondering if a compromise would be to only adjust permissions on packages that don't have a build script? I don't know if it would be easy to detect that (that might require parsing Cargo.toml, which might be too much overhead). I also don't know what % of packages that would cover.

It's also unfortunate that the tar API doesn't allow us to override the permissions directly. Another option is to make the files readonly when creating the .crate file. That would only help with newly published packages, but might be an option to consider.

@nagisa
Copy link
Member

nagisa commented Jun 5, 2022

Given that most of these cases would be caught by crater, maybe its worthwhile to just make the change and make sure crates are fixed up?

Alternatively: maybe only apply this for crates published after a certain timestamp?

@ehuss
Copy link
Contributor Author

ehuss commented Jun 10, 2022

@weihanglo Asked about how to do a crater run to test the impact of changing the readonly status. The rough steps are:

  1. In your clone of cargo, make the changes to incorporate the new behavior.
  2. Get a clone of https://github.com/rust-lang/rust/
  3. Check out a branch to add your changes.
  4. Modify .gitmodules to point to your clone and branch of cargo with the changes you want to test. For example:
    git submodule set-url src/tools/cargo https://github.com/ehuss/cargo.git
    git submodule set-branch --branch my-awesome-feature src/tools/cargo
    git submodule update --remote src/tools/cargo
    git add .gitmodules src/tools/cargo
    git commit
  5. Push the changes to GitHub and make a PR . Clearly label the PR as an experiment, and assign yourself or @ghost.
  6. Make a "try" build. I think all cargo members have this permission. Write a comment @bors try.
  7. After the try build finishes (an hour? I forget), ask someone to make a crater run. The Cargo team does not have that permission, so just ask someone on Zulip.

I think doing a "check" run should be sufficient? I'm concerned that by default it will not get dev-dependencies, so maybe add the --all-targets flag? I'm not sure if that's worth it. There's a bunch of documentation at https://github.com/rust-lang/crater/blob/master/docs/bot-usage.md on crater commands. You may also consider doing a top-100 run first just to make sure the experiment is configured properly.


I still feel that it might be good to ease into the change, such as only marking read-only if there are no build scripts. We discussed some other options to consider:

  • Adding some kind of option so that build-scripts can say "don't mark me read-only" or "don't mark these directories read-only".
  • Make all packages read-only on an edition boundary. This wouldn't be able to support automatic migrations, which conflicts with the spirit of Editions being easy to transition.

Doing a crater run can maybe give us a sense of the scale of the problem.

@kornelski
Copy link
Contributor

This could be limited to only *.rs files, leaving other data files with a writeable bit.

@bjorn3
Copy link
Member

bjorn3 commented Jun 13, 2022

Cargo.{toml,lock} should probably also be read-only.

bors added a commit that referenced this issue Jan 24, 2023
…ources-dir, r=epage

Improve CI caching by skipping mtime checks for paths in $CARGO_HOME

Skip mtime checks for paths pointing into `$CARGO_HOME` to avoid rebuilds when only caching $CARGO_HOME/registry/{index, cache} and $CARGO_HOME/git/db and some of the dependencies have `rerun-if-changed=directory` in their `build.rs`

I considered skipping mtime checking only on `$CARGO_HOME/registry/src` but it looks like other functionality (like downloading a newer version of dependency) is unaffected by this and this way we also cover the same issue with git based dependencies (except the part where `cargo` is forced to re-fetch submodules if `$CARGO_HOME/git/checkouts` is missing) and it is more in line with the discussion in #9455

Fix #11083

Credit `@weihanglo` for the test (I did add a case of checking that dependency update still triggers a rebuild but they are the original author of the rest of the test)
@torhovland
Copy link
Contributor

As suggested here, a possible alternative is to compute a hash when downloading a dependency, and to verify that hash whenever Cargo needs to access that dependency.

@matklad
Copy link
Member

matklad commented Aug 23, 2023

Somewhat related, we might consider sanitizing the executable bit as well, if we consider distribution of compiled executables via crate tarballs an unsupported use-case.

@wizeguyy
Copy link

wizeguyy commented Nov 1, 2023

If these files are made readonly, what would the process be for updating dependencies in the cache? i.e. if cargo update detects a new version of a crate in the cache, it will need write permissions to update that crate.

Will cargo temporarily modify the file permissions before performing an update? On unix systems it might make more sense for cargo to run as its own user. Other users have read-only access, but the cargo user has write access. Thoughts?

@bjorn3
Copy link
Member

bjorn3 commented Nov 1, 2023

Every crate version is stored in a separate directory. This is necessary to be able to cache multiple crate versions at once. And because of this we never need to modify the directory containing the cached crate after we are done unpacking the downloaded tarball.

@bjorn3
Copy link
Member

bjorn3 commented Nov 1, 2023

On unix systems it might make more sense for cargo to run as its own user.

There is no way to do that as an unprivileged user. And requiring root permission to install rust would be a really bad idea.

@weihanglo
Copy link
Member

Cargo doesn't update dependencies at per-file-level. Cargo basically downloads new tarballs, unpacks them, and put files in separate directories.

@wizeguyy
Copy link

wizeguyy commented Nov 1, 2023

Ahh... I should have seen that. In that case, disregard my comment. I am fully in support of making the registry src files read-only.

@sanmai-NL
Copy link

We can in addition or instead of ownership or permission bits, use OS-specific flags for finer-grained semantics, like chattr to make files immutable under Linux. Packages that try to break these restrictions will often be detected and the fixes will benefit even users of other operating systems.

@weihanglo
Copy link
Member

@sanmai-NL could you elaborate more on how the approach resolves the concern in #9455 (comment)? It may still fail people's builds, no?

@weihanglo weihanglo added S-needs-design Status: Needs someone to work further on the design for the feature or fix. NOT YET accepted. A-filesystem Area: issues with filesystems labels Dec 4, 2023
@bjorn3
Copy link
Member

bjorn3 commented Dec 4, 2023

The immutable flag on linux can only be set by root as it prevents even root from modifying it.

@sanmai-NL
Copy link

sanmai-NL commented Dec 4, 2023

@weihanglo Better performance can be attained by setting the right inode metadata along with file creation. Can you test setting a umask to make newly created files by Cargo read-only by default? The immutable bit tip was more of an implementation enhancement suggestion on attributes other than performance.

As for the breakage, I think that it's a defect to perform these file manipulations in build scripts.

I don't know if it is possible within the Cargo architecture to restrict build scripts to not perform some actions. That would be good in general.

@bjorn3
Copy link
Member

bjorn3 commented Dec 4, 2023

The umask is process global, but someone may be creating files concurrently with cargo when using cargo as library. Also for directories setting the umask I believe will require using chmod anyway to make them writable again while creating files inside the directory. And on Unix I'm not too worried about a single extra chmod per file and directory. On Windows I would be more concerned, but the Windows kernel doesn't have anything like umask afaik. The CRT supports the function, but I wouldn't be surprised if it internally calls the equivalent of chmod after every file creation.

@kornelski
Copy link
Contributor

This could be opt-in per crate. Cargo currently makes tarballs with -rw-r--r-- permissions (ignores original files' permissions). Cargo could be taught to make tarballs with -r--r--r-- permissions, and preserve that when unarchiving.

@sanmai-NL
Copy link

sanmai-NL commented Dec 5, 2023

Just an idea here off the top of my head, but why extract source archives at all? Can't we get their contents as needed, in-memory? That may sound costly but I wonder how often that really happens and if extracting them amortizes well (the extra cost being the filesystem operations that notably result in syscalls). That would reduce the number of files to be created and changed, so even particular file metadata like immutable flag can be set at the cost of $O(n)$ instead of $O(nm)$ time in the average case, where $n$ is the number of crates (tar archives) and $m$ is the number of files per crate.

@sanmai-NL
Copy link

This could be opt-in per crate. Cargo currently makes tarballs with -rw-r--r-- permissions (ignores original files' permissions). Cargo could be taught to make tarballs with -r--r--r-- permissions, and preserve that when unarchiving.

@kornelski

https://docs.rs/tar/latest/tar/struct.Entry.html#method.set_mask

@bjorn3
Copy link
Member

bjorn3 commented Dec 5, 2023

Can't we get their contents as needed, in-memory?

Rustc currently doesn't support that. We could add it, but build scripts and proc macros will still need it extracted somewhere on the filesystem.

@mathstuf
Copy link
Contributor

mathstuf commented Dec 5, 2023

Can't we get their contents as needed, in-memory?

Tools communicate by filesystem paths, so how do you propose to send an in-memory "path" to sccache for caching? Or coverage tools that render the coverage data with the source code? That's even if we do manage to ignore build.rs scripts that do…whatever they do. Without some cross-platform FUSE-like solution that exposes such paths to arbitrary other tools, this would restrict the current set of use cases. Maybe that's fine and can be opt-in, but opt-out is, I believe, only going to continue finding more and more corner cases.

@kornelski
Copy link
Contributor

The invention of not-a-tarball crate storage system is irrelevant here, because the problem is crates relying on files existing in the file system, and existing with specific permissions. If you succeed in removing the files from src dir, you'll break them even worse.

@sanmai-NL
Copy link

Can't we get their contents as needed, in-memory?

Tools communicate by filesystem paths, so how do you propose to send an in-memory "path" to sccache for caching? Or coverage tools that render the coverage data with the source code? That's even if we do manage to ignore build.rs scripts that do…whatever they do. Without some cross-platform FUSE-like solution that exposes such paths to arbitrary other tools, this would restrict the current set of use cases. Maybe that's fine and can be opt-in, but opt-out is, I believe, only going to continue finding more and more corner cases.

Does sccache cache source code and would that be important for Rust? The challenge would be more to let rustc compile tar archives, no?

@sanmai-NL
Copy link

The invention of not-a-tarball crate storage system is irrelevant here, because the problem is crates relying on files existing in the file system, and existing with specific permissions. If you succeed in removing the files from src dir, you'll break them even worse.

What is your opinion on this breakage? Would it preclude moving forward with a more performant design in any case?

@bjorn3
Copy link
Member

bjorn3 commented Dec 5, 2023

Sccache doesn't cache source code. It reads the .d file emitted by rustc to know when it needs to recompile. If rustc were to compile tarballs, this .d file would simply mention the tarball and thus sccache would work as expected. For something entirely in-memory passed in through eg an fd or unix socket, that wouldn't work though.

@sanmai-NL
Copy link

sanmai-NL commented Dec 5, 2023

Let's also discuss differentation between crates. Crates without build scripts were mentioned. But only-binary or only-library is also relevant. Binary-only crates's source as some specific version needs to be only read very infrequently compared to libraries, especially popular libraries. So for binary-only, non-buildscript crates you'd most want to avoid extracting the tar archive (even though they may be smaller, in comparison, perhaps) and perhaps doing other filesystem operations such as setting permissions.

@sanmai-NL
Copy link

Sccache doesn't cache source code. It reads the .d file emitted by rustc to know when it needs to recompile. If rustc were to compile tarballs, this .d file would simply mention the tarball and thus sccache would work as expected. For something entirely in-memory passed in through eg an fd or unix socket, that wouldn't work though.

Yes, and the latter would probably require much more significant architecture reengineering than supporting tar-like archives directly.

@sanmai-NL
Copy link

Let's also discuss differentation between crates. Crates without build scripts were mentioned. But only-binary or only-library is also relevant. Binary-only crates's source as some specific version needs to be only read very infrequently compared to libraries, especially popular libraries. So for binary-only, non-buildscript crates you'd most want to avoid extracting the tar archive (even though they may be smaller, in comparison, perhaps) and perhaps doing other filesystem operations such as setting permissions.

And this assuming that no incremental compilation cache is available. In my experience one sometimes cleans this cache. But not too often I suppose.

@kornelski
Copy link
Contributor

Can this be done for 2024 edition crates?

Cargo.toml is usually at the very beginning of tarballs, so it's relatively cheap to check edition of a .crate file before decompressing it for real to the file system.

@epage
Copy link
Contributor

epage commented May 4, 2024

Independent of the question of whether this can be tied to an Edition, they asked for Edition 2024 changes to have their PRs posted by May 1st. There is a slim chance something small can be rushed in late but members of the Cargo team have already been exhausting themselves to meet the May 1st date with what was already committed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-caching Area: caching of dependencies, repositories, and build artifacts A-filesystem Area: issues with filesystems S-needs-design Status: Needs someone to work further on the design for the feature or fix. NOT YET accepted.
Projects
None yet
Development

No branches or pull requests