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

Cargo and Rustup safe file discovery. #3279

Open
wants to merge 16 commits into
base: master
Choose a base branch
from

Conversation

ehuss
Copy link
Contributor

@ehuss ehuss commented Jun 8, 2022

This RFC is a proposal to fix a security issue with how Cargo and Rustup discover their files.

In consultation with the Security Response Working Group, we decided to disembargo this and solicit feedback and testing before this change is made. We feel like the relative security risk of this is low compared to the other risks involved with using these tools. Additionally, we think that the risk is high that this will be a very disruptive change. We would like to get feedback from the community and let you have a chance to test and prepare your environments ahead of time instead of pushing a new stable release immediately.

However, we are planning to move this change through quickly so that the protection is available to users as soon as possible. Our current thinking is to aim for stabilizing it in July for the 1.64 release which will be released in September. The implementation will be available on nightly soon, and a pre-release version of Rustup will also be made available as soon as possible.

Rendered

@ehuss ehuss added the T-cargo Relevant to the Cargo team, which will review and decide on the RFC. label Jun 8, 2022
Since the settings file is private to Rustup,
the following CLI options can be used to manage this setting:

* `rustup set safe-directories add PATH`
Copy link
Contributor

Choose a reason for hiding this comment

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

does rustup set safe-directories add . expand to the absolute path of the current directory? do other relative paths expand?

Copy link

Choose a reason for hiding this comment

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

I would expect to process such paths through std::fs::canonicalize() if that's what you mean.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for raising this point!

The current PR does not attempt to guess what the user intended, and adds it literally. However, I see that is not a good approach as I imagine something like rustup set safe-directories add . would not be unreasonable.

I started down the path of figuring out the best way to deal with this, but I've run out of time/energy. Using fs::canonicalize is difficult and fraught with problems, particularly on Windows. I want to avoid putting verbatim paths in the config (as they get exposed to the user). Unfortunately fs::absolute isn't stable (I would very much prefer to use GetFullPathNameW over anything else). I considered using Cargo's hacky normalizer, but that has its own problems. And I'm finding it difficult to consider the security considerations of these approaches.

I'll continue to work on it when I can.

This could significantly ease the burden of dealing with this issue,
as it will avoid needing to set safe directories in almost all use cases where they would be needed.

However, I feel like this approach is too risky.
Copy link
Member

@nagisa nagisa Jun 8, 2022

Choose a reason for hiding this comment

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

OTOH this makes use-cases that use group-wide permissions (i.e. rwxrwx---) and a umask setup in a certain way work mostly out of the box.

I'm somewhat invested in this, since I tried to utilize that sort of approach, though not with Rust...

(unless they have elevated permissions like root).
I believe this is the case for all major operating systems and filesystems.

One (major) known exception is if a network-mounted filesystem is configured to map all files to the local user, which is not uncommon.
Copy link
Member

Choose a reason for hiding this comment

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

Another major exception is UID reuse – where the attacker creates an user, poisons the filesystem and then later a genuine user gets created at that specific UID.

systemd comes to mind as an example of a tool that a) creates users left and right; and b) has mitigations for this problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I have added that as another concern. I personally don't feel like the risk here is very high, as there are likely other avenues than Rustup/Cargo for escalating once an attacker has control of a temporary new user. However, it definitely is a concern.

@ChrisDenton
Copy link
Member

Should mention be made of TOCTOU concerns? Or is that not considered a threat?

If you have a directory `Foo`, and you type `cd foo`,
then the operating system will report the current directory is `foo`.

I am not aware of an easy way to detect if a filesystem is case-sensitive.
Copy link
Contributor

Choose a reason for hiding this comment

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

A typical case-sensitivity check creates two files with names that only differ in case. If the second create fails, or listing the directory only returns one file, then the filesystem is case-insensitive.

But it's faster to get the metadata of an existing file, via a path with a different case:

  1. List the directory containing the file a
  2. If the directory contains two entries for the file a that only differ by case, the filesystem is case-sensitive
  3. Get the metadata of the file A, which has a name that only differs by case
  4. If the query is successful, the filesystem is case-insensitive
  5. Otherwise, if it returns a file not found error, the filesystem is case-sensitive

Unfortunately there's no easy API for this, because Windows can change case-sensitivity by directory, macOS by process (rarely), and most OSes can change it by filesystem.

@RalfJung
Copy link
Member

I don't entirely get the attacker model here. Let's say we are in the specific situation where there is a Cargo.toml in my current directory. If I do cargo build, that will run build scripts, which can do arbitrary things. What is the additional risk posed by Cargo.toml specifically that makes it so that we need a special defense against evil Cargo.toml but nothing similar against evil build scripts?

I agree that using files arbitrarily far up the folder hierarchy can be a problem, as described in the RFC. But I clearly trust the code in my current directory if I build+run it, so I don't see any harm coming from also trusting the Cargo.toml there.

Comment on lines +355 to +431
This may be particularly difficult for a user running Windows `cargo.exe` from within WSL.
Paths in this environment look like `\\wsl$\Ubuntu-20.04\home\eric\foo`.
I don't know how common this use case is, as I would expect someone doing things in WSL would install the native rustup/cargo binaries instead of using the ones from Windows.
Copy link

@BlackHoleFox BlackHoleFox Jul 7, 2022

Choose a reason for hiding this comment

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

That does sound like a tad pain. I even wrote a build definition that relies on being able to call the Windows installation of Cargo from WSL to make building easier.

I think that quite a few pieces of software have become unreasonably frustrating when trying to enforce permissions while running in WSL, fwiw.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Interesting! I was not expecting anyone to be doing that, though I guess I'm biased by mostly using mingw instead.

I think the error message should provide something that should work to copy-and-paste, so I hope that should be sufficient. This is also something that we may be able to iterate on without breaking things.

@BlackHoleFox
Copy link

I agree that using files arbitrarily far up the folder hierarchy can be a problem, as described in the RFC. But I clearly trust the code in my current directory if I build+run it, so I don't see any harm coming from also trusting the Cargo.toml there.

Yeah, this also makes sense to me. Opening a directory in your shell (or editor, but they already have solutions for this problem) is an explicit action taken by the user. Opening it means they're saying "I, my person, wants to do something here."

Hypothetically, if cargo allowed implicitly trusted everything in pwdir downwards, wouldn't that also get rid of a huge swathe of usability footguns listed in this RFC? sudo cargo would work (even if this is a certified bad idea 99% of the time), running Win32 cargo from WSL would work, and probably others too. That is, at least in common use cases though. Even with that, saying that you can't build a crate in a workspace from that crate's source directory from a different user account (or OS like WSL), but instead need to go from the crate workspace sounds more reasonable then blocking it in all cases.

@ehuss
Copy link
Contributor Author

ehuss commented Jul 11, 2022

Should mention be made of TOCTOU concerns? Or is that not considered a threat?

The original contained a note about that, but I removed it since I didn't want to list all the things that won't go wrong. I added a small note back in. I do not consider it a threat since the attacker would need to have write access to the user's files, and that means they already have escalated privileges which I consider out of scope for this fix.

Did you have any particular threat scenario that you are concerned about?

@ehuss
Copy link
Contributor Author

ehuss commented Jul 11, 2022

I agree that using files arbitrarily far up the folder hierarchy can be a problem, as described in the RFC. But I clearly trust the code in my current directory if I build+run it, so I don't see any harm coming from also trusting the Cargo.toml there.

This was a point we discussed quite a bit, and it is difficult for me to confidently make this exception. I have added some more discussion in the "Honor the user of the current directory" section.

I concede that the risk is small, but I am concerned about the possible inconsistency. If there is broad agreement that this exception is worth it, then I would consider it.

Opening it means they're saying "I, my person, wants to do something here."

This was one of the leading concerns that led git to implement their fix. The concern is that navigating to a directory may not necessarily be a signal that "I trust everything in this directory" (particularly for beginners). The example is a shared directory used by multiple students as a scratch space, where they may cd into that root space. I think cd to most users, including myself, is not a strong signal of that trust.

@RalfJung
Copy link
Member

RalfJung commented Jul 12, 2022

This was one of the leading concerns that led git to implement their fix. The concern is that navigating to a directory may not necessarily be a signal that "I trust everything in this directory" (particularly for beginners). The example is a shared directory used by multiple students as a scratch space, where they may cd into that root space. I think cd to most users, including myself, is not a strong signal of that trust.

No, cd is indeed not a signal of trust. Neither is git status, so I agree with the git decisions. But cargo build is to an extend (if people are familiar with build scripts and proc macros), and cargo {run,test} definitely are!

When I do ./run-thing, I also don't get a warning from my shell that it is running a thing. Such a warning would be rather disruptive and serve little purpose other than incur warning fatigue. This feels similar.

(To be clear, I would love to be able to do cargo build without having to trust the crate authors. But that is not the discussion we are having here.)

Or is the point that users are not aware that cargo build runs build scripts and proc macros and we need to teach them? The RFC is not phrased like that.


```text
error: `C:\Projects\MyProject\Cargo.toml` is owned by a different user
For safety reasons, Cargo does not allow opening manifests owned by
Copy link
Member

@RalfJung RalfJung Jul 12, 2022

Choose a reason for hiding this comment

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

"For safety reasons" is pretty useless information. It's the kind of wording someone uses when they don't want to justify their actions towards the public or the press.

For users to be able to do an informed decision, this should explain what the associated safety risks are. Is this referring to build scripts and proc macros, or some other kind of safety risk?

I am thinking of something like this:

error: manifest file `C:\Projects\MyProject\Cargo.toml` is owned by a different user
    If you proceed, cargo will build and run code as instructed by that file.
    Even just 'cargo build' or 'cargo check' will run code controlled by the manifest.
[...]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I have tweaked the wording to briefly explain what the safety concern is.

@ChrisDenton
Copy link
Member

ChrisDenton commented Jul 13, 2022

Should mention be made of TOCTOU concerns? Or is that not considered a threat?

The original contained a note about that, but I removed it since I didn't want to list all the things that won't go wrong. I added a small note back in. I do not consider it a threat since the attacker would need to have write access to the user's files, and that means they already have escalated privileges which I consider out of scope for this fix.

Did you have any particular threat scenario that you are concerned about?

Ok, I'm still trying to get a handle on what the threat model is. So we take ownership to mean that if the file gives another user rename/delete or write permissions then that is something the owner presumably intended and is therefore "safe" under this model.

What about ancestor directories? The RFC says:

I don't see a particularly strong reason to check the ownership along the way.

But surely if there's a symlink in the path or an ancestor directory can be renamed by another user then that can be exploited? Or is that not considered a threat here?

@comex
Copy link

comex commented Jul 14, 2022

This fix assumes that a user can't in any way create a file owned by another user (unless they have elevated permissions like root). I believe this is the case for all major operating systems and filesystems.

On macOS, and on Linux with fs.protected_hardlinks=0 (most distros set it to 1 though), any user can create a hard link to a file owned by another user, putting it in an arbitrary directory they have write access to with an arbitrary name. The resulting link will have the same ownership and permissions as the original and is generally indistinguishable from any other file. (Well, you can detect whether a file has multiple links to it by checking st_nlink, but that doesn't help if the original file is deleted for any reason, in which case st_nlink goes back to 1.)

The hard link creator can't affect the contents of the file, so for this to be dangerous, there would have to be some existing file owned by the target user, at some other path in the same filesystem, whose content would be dangerous if interpreted as config.toml, rust-toolchain.toml, etc. But it's not hard to imagine ways this might occur.

This issue doesn't affect Git's directory ownership check… I think… because macOS and Linux don't support hard links to directories, at least by default. (macOS does or did support them for HFS+ mounts, but not the newer APFS.)

text/0000-cargo-rustup-discovery.md Outdated Show resolved Hide resolved
causing it to open repositories from every directory the user enters.

`git` has been changed so that it will not open a repository owned by a different user, and instead return an error.
For most cases, it will check that the ownership of the directory *containing* the `.git` directory matches the current user.
Copy link
Member

Choose a reason for hiding this comment

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

Might worth mentioning that a fix for CVE-2022-29187 (missing part of CVE-2022-24765) was released two weeks ago: git/git@3b0bf27

Basically, git now takes into account the ownership of gitdir and gitfile as well. Unlike git, this RFC proposes to check files themselves, so I don't think we need a revise accordingly.


## Rustup toolchain override discovery

When searching for a `rust-toolchain`/`rust-toolchain.toml` file, Rustup will check that the ownership of the file matches the current user.
Copy link
Member

Choose a reason for hiding this comment

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

I think this only needs to apply if the toolchain is specified using a local path rather than an official toolchain.

Copy link
Contributor Author

@ehuss ehuss May 20, 2023

Choose a reason for hiding this comment

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

As mentioned in Granular capabilities, we didn't want to go down the route of trying to do fine-grained checks of the contents of files or determining exactly which parts would be safe and which wouldn't. I don't have confidence that we would be able to do that securely now, and particularly as things are added in the future. It might be possible to have something similar to what gitoxide has by marking what is safe and not (possibly at the type level), but that would likely be a very intrusive change.

I'd also not be so confident that official toolchains are "safe". For example, an attacker could force you to use an older version of Rust which has some vulnerability that they could then exploit in some way.

This could possibly be done in the future, but I'm not sure there would be a large value if it was done in just rustup and not cargo, since the false-positive scenario would likely involve both.

@ehuss
Copy link
Contributor Author

ehuss commented May 20, 2023

I'm sorry for letting this languish for so long. Certain issues came up last year, and then I intended to return to this in February, but other things interrupted again.

I have pushed several updates to the RFC incorporating some feedback, and updating for more recent changes.


Or is the point that users are not aware that cargo build runs build scripts and proc macros and we need to teach them? The RFC is not phrased like that.

I've tried to clarify the RFC on the threats that this is intended to address. This is not quite related to build scripts or proc-macros. It is related to cargo or rustup or rustc (or any proxy) being able to run arbitrary executables if a user has write access to a directory above them. Cargo and rustup provide a myriad of ways to execute programs via various config options, where build scripts and proc-macros are only a part of that.


Ok, I'm still trying to get a handle on what the threat model is. So we take ownership to mean that if the file gives another user rename/delete or write permissions then that is something the owner presumably intended and is therefore "safe" under this model.

I don't see a particularly strong reason to check the ownership along the way.

But surely if there's a symlink in the path or an ancestor directory can be renamed by another user then that can be exploited? Or is that not considered a threat here?

I'm not sure if "intended" is quite the right word, since it may or may not be intentional. But I think it is somewhat outside of the scope of this RFC to have cargo or rustup police the ACLs of the files it accesses to require that they not be changeable by other users.

I have added an extended TOCTOU section to try to address some of the concerns. I'll admit that this is one area where I feel the least confident in, so if anyone can provide counter-arguments with concrete examples, that would be extremely helpful.

@ehuss
Copy link
Contributor Author

ehuss commented Jun 6, 2023

I pushed a change to add a temporary warning period to give users advance notice of this change. I'm hoping that should ease the disruption and make it easier for users to prepare for it.

@ehuss
Copy link
Contributor Author

ehuss commented Jun 6, 2023

I'm moving to go ahead and move to merge this.

This will also require the approval of the rustup team. However, since they are not configured for rfcbot, and rfcbot does not handle multi-team approvals very well, I'm going to handle them manually. This will not be merged without similar approval from that team. (rustup team: feel free to check a box, or leave a comment, or any blocking concerns.)

@rfcbot fcp merge

@rfcbot
Copy link
Collaborator

rfcbot commented Jun 6, 2023

Team member @ehuss has proposed to merge this. The next step is review by the rest of the tagged team members:

Concerns:

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. labels Jun 6, 2023
@ehuss
Copy link
Contributor Author

ehuss commented Jun 6, 2023

rustup review tracking:

@epage
Copy link
Contributor

epage commented Jun 6, 2023

I pushed a change to add a temporary warning period to give users advance notice of this change. I'm hoping that should ease the disruption and make it easier for users to prepare for it.

As collecting feedback is the hardest problem, having a warning period helps resolve that and we can pivot if we need to. I'm good with moving forward with this.

As a wild idea, we could possibly express this as a diagnostic that we change the level for (rust-lang/cargo#12235). The main issue being that we need to know the users desired level before we even parse to find the package/level. I mentioned in that Issue that this is a general problem and we might need some cargo-diagnostic control in the config file for these non-package, non-workspace use cases.

@ChrisDenton
Copy link
Member

Should you perhaps add a blocking concern mentioning the rustup team so rfcbot won't proceed until that's manually resolved?

@ehuss
Copy link
Contributor Author

ehuss commented Jun 6, 2023

@rfcbot concern rustup-approval

@rbtcollins
Copy link

@ChrisDenton that would be good - I'm not quite sure how to do that - I haven't interacted with the RFC automation so far. https://www.rust-lang.org/governance/teams/dev-tools#Rustup%20team is up to datish (I'm not sure if @kinnison wants to step down until he's more available or not), so any leadership bits that need to be set should be set already.

I will try the bot though, just in case.

@rfcbot concern rustup hasn't reviewed

Context for that: This started while @kinnison was active, but he's stepped away for a while, and I haven't had time to read or think about this thing end to end. I've given it a quick glance, and I think its not clear what the threat model we're actually tackling is (for instance, if its 'shared parent directories can permit injection of arbitrary code via toolchain files', then the next release of rustup will correct that. But a toolchain file could still pick a pre-safe-aware version of cargo, and then attack that. But neither of these address the things that vscode and I presume other IDEs aim at with their 'trusted directory' UI, which is downloading of arbitrary git repos from the internet.

Introducing this will be a breaking change (by definition - an opt in security feature rarely works), so I'd like us to at least consider all the cases that are clear and visible today, which I will prioritise amongst the time I have available to do rustup contributions.

I appreciate the RFC has been around for a while, but this is the nature of volunteer personal-time contributions.

@kinnison
Copy link

kinnison commented Jun 7, 2023

I'm not sure if @kinnison wants to step down until he's more available or not

I have submitted a PR to step down from the team for now, I'm not going to be able to offer help in the medium term at least.

@cbeuw
Copy link
Contributor

cbeuw commented Jul 13, 2023

and I think its not clear what the threat model we're actually tackling is

While security is the main intention of the RFC, I want to mention that the RFC is also functionally necessary on some trusted but weirdly configured shared systems: rust-lang/cargo#12080

In my case, this is ETH Zürich's HPC cluster Euler. The OS creates a file under the parent of the user's home directory whenever an open syscall is attempted on a non-existent file. This dummy file does not give anyone read permissions, so the open fails with permission denied instead of file doesn't exist.

To scale the cluster to the thousands of users that we have, and to enable seamless login for any ETH member, an automounter mounts home directories onto /cluster/home on demand. Each time someone accesses /cluster/home/X and /cluster/home/X does not exist, the automounter is run. Due to technical limitations of the automounter and the operating system, this takes a few seconds, during which the system is noticeably slower for some I/O. Since some software continuously tries to “go up” the filesystem tree to find file /cluster/home/X, the system can be put under a denial of service (DOS) if /cluster/home/X does not exist. We have therefore been forced to create dummy entries for failed file lookups: these are what you encounter.

IMO this is a very janky solution, but as a user I can't do anything about it. Nonetheless, most other software that does recursive lookups (git, Python with modules) handle this without issue, but Cargo just breaks when a Cargo.toml file exists but has no permission to read it.

@rbtcollins
Copy link

@cbeuw I can imagine Cargo breaking on existing but unreadable files even with this RFC implemented. I suggest you file a bug for that (and perhaps work on a PR).

@rbtcollins
Copy link

Ok, so full review of the RFC finally - appreciate the patience with volunteer timescales.

A nit: On Windows C:\ is not world writable by default. The default permissions are read-only + create folders for users, with only Administrators having full write access. Threat actors that can create C:\Cargo.toml can also directly take over ownership of the target folders and write hostile code to e.g. replace rustup proxies. Any user could create a .cargo at the root though, but I don't know Cargo well enough to know if that is exploitable. Of course, systems can be misconfigured too, including labs, network servers and so on.

The threat statement : The primary threat this RFC is aiming to address is the situation where cargo or rustc enables other users or malicious programs to elevate their access by running arbitrary programs under your account is good; when I read that, it by definition includes downloaded git repositories, because asking users to audit every single vector present in what they downloaded is unreasonable. (Concretely, every vector the RFC covers directly can also be present in a downloaded repository, plus more (such as build.rs scripts with malicious dependencies and probably other more creative ways to cause havoc)). IDEs like vscode have a similar trust feature.

I propose we consider expanding this RFC's scope a little:- explicitly consider these threats and align with IDE behaviour. For instance, refusing to build a downloaded directory that uses any settings that can be escalated (e.g. build.rs, rustc wrappers and so on). The impact should be minimal, and it avoids have two back to back contract breaks, rather than one.

I realise the RFC rules this out today:

This RFC is not aiming to address the general threats of arbitrary execution of code defined in dependencies (such as build scripts or proc-macros) intentionally added by the user. Those threats still require reviewing the code and trust relationship with those dependencies.

I think the existing initiatives like cargo vet are great for empowering developers writing build.rs scripts or using crates with proc-macros to make good informed choices about dependencies. The specific case I'm highlighting is 'user has downloaded source' vs 'existing developer is adding dependencies'. They are likely related, but are quite different, and 'freshly downloaded code' looks closer to 'building in a tree with untrusted content' than 'editing source and evolving things'.

Moving on, I agree that Rustup will need this. Even with relative path toolchains removed, a hostile local directory that has been prepared can reference a hostile absolute path based toolchain : refusing relative paths makes downloading repositories safe, not running from arbitrary local directories. To me it makes sense to honour CARGO_SAFE_DIRECTORIES and $CARGO_HOME/cargo.toml rather than having users have to set this based on whether they use rustup or not - while any one user would only have to set it in one place, it is still complexity, and one has to be rigorous to drive complexity down. It is better for us to decide on one place to set it rather than pushing that complexity off onto users.

I don't agree with this line:

Rustup sets the RUSTUP_SAFE_DIRECTORIES environment variable when launching a tool via the Rustup proxies. This allows the user to configure their safe directories in one place (with Rustup), and have tools like Cargo inherit those settings.

I don't think it makes sense for rustup to set that variable when a proxy is invoked: if it is set already, it will pass through to the child process. If it is not set already, there are no sensible values for rustup to add : the current working directory can't be assumed to be safe, and the toolchain directory should be irrelevant. Perhaps the intent was to say that rustup's config file can store a safe directories list too; in which case the guide explanation listing 3 places for the setting should actually list 4 :)

https://github.com/ehuss/rfcs/blob/cargo-rustup-discovery/text/0000-cargo-rustup-discovery.md#cargo-config-discovery doesn't specify about the behaviour when a mismatch occurs. I suspect the intent is to error.
I'd like to propose an additional behaviour, copied from the Git CVE blog post. Once a directory has different ownership, stop searching with no error. If a needed file (e.g. Cargo.toml hasn't been located that is still an error, but it should also mention that the search stopped because had different ownership - to support diagnosing the situation) This prevents the attack and should avoid needing to set the safe variable as often.

While I agree that

Protecting against an attacker that has the ability to create files owned by the victim, or to replace an ancestor directory is out of scope for this RFC.

Not being TOCTOU safe opens risks that do not require creating files owned by the victim. For instance, a shared lab with world writable common directory allows for a victim created .cargo/config.toml that is then replaced by the attacker. APIs for reading metadata from file handles exist for both unix and windows, the only wrinkle being that processing a symlink stepwise is poorly defined on Unix, and will necessarily introduce a race. Considering the ownership of the target rather than the link itself would eliminate the race. And I think we should do that, because an attacker that can rename a legitimate symlink and replace it with a bad one: a hard race to win but possible without being able to impersonate ownership, and likely possible in the motivating configurations. That or we should write a very clear doc about how to secure Cargo/Rustup in these situations.

On the drawbacks. Rustup does have system configuration, so the ability to clear the setting would be useful if we decide to use rustup's config store.
I think adding interpolation later would be a compatible change, particularly if we document that consideration now.

I agree that we shouldn't honor the user of the current directory: its not obvious at all that cd'ing to a directory means trusting its contents. For instance shell prompts that query Cargo metadata could be silent ways that arbitrary code would run just by cding.

On the Check-the-ownership-of-directories-while-traversing: the overhead should be extremely low but we could test easily enough. But a middle-ground would be to behave as though we had. Concretely when we find a mismatched config file, before erroring, see if the parent directory would have triggered stopping searching, and if so, just ignore the file and stop searching.

On WSL: I don't think we should do anything special here. Yes, Microsoft has muddied the waters with some transparent glue, but the expected behaviour is definitely installing host-native tools and rustup versions.

On checking for case sensitivity... this is probably impossible in all cases, but a decent approximation is to assume that Linux is always case sensitive (e.g. ignoring mounted FAT fs's), and on windows query the file system https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumeinformationa . There may be a per-directory setting as well - there are windows CLI tools that can control that, but I haven't found the underlying API. @ChrisDenton probably knows :).

@ChrisDenton
Copy link
Member

On checking for case sensitivity... this is probably impossible in all cases, but a decent approximation is to assume that Linux is always case sensitive (e.g. ignoring mounted FAT fs's), and on windows query the file system https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumeinformationa . There may be a per-directory setting as well - there are windows CLI tools that can control that, but I haven't found the underlying API. @ChrisDenton probably knows :).

You can check per-directory case sensitivity using GetFileInformationEx and the FileCaseSensitiveInfo class but I would suggest you don't. You can either get the canonical case of a path or if you want to know if two paths lead to the same file (or directory) then open them both (and keep them open) then test if they're the same.

I think insisting that users use the canonical case in config files, etc is a fair compromise in an initial implementation. Though admittedly it will annoy some users and those users may strongly disagree with me on this point.

@ehuss
Copy link
Contributor Author

ehuss commented Jul 27, 2023

Thanks @rbtcollins for the review!

A nit: On Windows C:\ is not world writable by default. The default permissions are read-only + create folders for users, with only Administrators having full write access.

Thanks for the clarification. I didn't want to get into the details of when the root is writeable since I believe it is a complicated question (depending on various factors). I tried to update the text to make it clearer that it only applies in some situations.

I propose we consider expanding this RFC's scope a little:- explicitly consider these threats and align with IDE behaviour. For instance, refusing to build a downloaded directory that uses any settings that can be escalated (e.g. build.rs, rustc wrappers and so on). The impact should be minimal, and it avoids have two back to back contract breaks, rather than one.

I may be misunderstanding this suggestion, but it sounds like a very large change with a large impact. The intent of this RFC is that the vast majority of users will not be affected and won't even know this is a thing. This sounds like it is suggesting that every directory needs to be approved before it can be used, which sounds like it would affect essentially everyone. Can you clarify what is meant here, or why it seems like it should be a minimal impact?

To me it makes sense to honour CARGO_SAFE_DIRECTORIES and $CARGO_HOME/cargo.toml rather than having users have to set this based on whether they use rustup or not - while any one user would only have to set it in one place, it is still complexity, and one has to be rigorous to drive complexity down. It is better for us to decide on one place to set it rather than pushing that complexity off onto users.

I'm not sure I understand this completely. Are you suggesting that rustup should check for CARGO_SAFE_DIRECTORIES and read $CARGO_HOME/config.toml (I assume that was a typo)? And the intent is so that there is one place to set this whether the user is using rustup or not?

I would have some pretty high concerns about rustup trying to parse and understand cargo's config files. Trying to coordinate changes across the tools I think will be challenging, and potentially confusing (users may use rustup without cargo, for example). I'd also be concerned about the performance impact, since toml parsing can be a little slow, and the proxy startup time is already a concern.

I don't think it makes sense for rustup to set that variable when a proxy is invoked: if it is set already, it will pass through to the child process. If it is not set already, there are no sensible values for rustup to add : the current working directory can't be assumed to be safe, and the toolchain directory should be irrelevant. Perhaps the intent was to say that rustup's config file can store a safe directories list too; in which case the guide explanation listing 3 places for the setting should actually list 4 :)

The intent is that rustup is setting the environment variable from rustup's config. The place that lists the three options is specifying what cargo will read. The next section is for rustup which specifies what rustup will read. I altered the text to try to make that clear.

Once a directory has different ownership, stop searching with no error.

Perhaps I'm not understanding this suggestion properly, but as I'm reading it, it seems to be suggesting that no errors are ever generated? That seems to be a pretty large change to this RFC, perhaps you can clarify it in more detail?

In general, I don't think this is an option because that could lead to fragmented or broken configurations that could be quite challenging to diagnose and fix. For example, Cargo's workspaces could be broken such that a workspace member would load, but the workspace configuration would not. Also, the intent of the error is to be an alarm to alert the user something suspicious is happening. Although we want to avoid false positives as much as possible (and almost all errors are going to be false positives, since exploiting this is probably going to be very unlikely), I don't think we want to avoid the main purpose of the check.

Not being TOCTOU safe opens risks that do not require creating files owned by the victim. For instance, a shared lab with world writable common directory allows for a victim created .cargo/config.toml that is then replaced by the attacker.

This is an interesting point, and I'd like to dive into understanding it in more depth in a followup.

Just FYI, I'm mostly basing this off of git's implementation, which also does not do handle-based checks, and thus has limited TOCTOU protections. Implementing something that combines traversal, checks, and opening of files will likely be intrusive, making it harder to incorporate this change.

@ehuss
Copy link
Contributor Author

ehuss commented May 9, 2024

@rust-lang/rustup Just checking in again to see if this is something ya'll can look at for approval? Thanks!

@rami3l
Copy link
Member

rami3l commented May 10, 2024

@ehuss I have no more questions regarding this RFC. Can you tick the box for me? Thanks!

Update: I've made rust-lang/rustup#3935 to track the implementation on Rustup's side.

@djc
Copy link
Contributor

djc commented May 13, 2024

Ticked my box, LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. T-cargo Relevant to the Cargo team, which will review and decide on the RFC.
Projects
Status: FCP blocked
Status: Final Comment
Development

Successfully merging this pull request may close these issues.