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

linux: Fix saving file with root ownership #22045

Merged
merged 20 commits into from
Dec 19, 2024

Conversation

0xtimsb
Copy link
Member

@0xtimsb 0xtimsb commented Dec 15, 2024

Closes #13585

Currently, saving files with root ownership or root as the group throws a Permission denied (os error 13). Please try again. error. This PR fixes the issue on Linux by prompting the user for a password and saving the file with elevated privileges.

It uses pkexec (Polkit), which is by default available on GNOME, KDE, and most Linux systems. I haven't implemented this for macOS as I don't have a device to test it on.

This implementation is similar to how Vscode handles it. Except, they don't show custom message.

Working:

When file saving fails due to a PermissionDenied error, we create a temporary file in the same directory as the target file and writes the data to this temporary file. After, the contents of this file are copied to the original file using the tee command instead of cp or mv. This ensures that the ownership and permissions of the original file are preserved. This command is executed using pkexec which will prompt user for their password.

Custom Message:

The message displayed to the user in the prompt is automatically retrieved from the org.zed.app.policy file, which is located at /usr/share/polkit-1/actions/. This file should be installed during the setup process. While the policy file is optional, omitting it will cause the user to see the underlying command being executed rather than a user-friendly message. Currently, VSCode does not display the user-friendly message.

The policy file must specify a unique binary, ensuring that only that binary can use the policy file. It cannot be as generic as a /bin/bash, as any software using bash to prompt will end up showing Zed’s custom message. To address this, we will create a custom bash script, as simple as the following, placed in /usr/bin/zed/elevate.sh. The script should have root ownership and should not reside in the home directory, since the policy file cannot resolve $HOME.

#!/bin/bash
eval "$@"

IMPORTANT NOTE

Since copying the policy file and our script requires sudo privileges, the installation script will now prompt for the password at very end. Only on Linux, if pexec is installed.

Screenshots:

KDE with policy file:
Screenshot from 2024-12-15 22-13-06

Gnome with policy file:
Screenshot from 2024-12-15 22-21-48

Gnome without policy file:
image

VSCode:
image

User declines the permission request:
image

Release Notes:

  • Fixed file saving with root ownership on Linux.

@cla-bot cla-bot bot added the cla-signed The user has signed the Contributor License Agreement label Dec 15, 2024
script/install.sh Outdated Show resolved Hide resolved
@0xtimsb
Copy link
Member Author

0xtimsb commented Dec 16, 2024

@mikayla-maki moved it to a separate optional script, as you asked. Also added docs for it in linux.md.

@0xtimsb 0xtimsb requested a review from mikayla-maki December 16, 2024 19:58
@mikayla-maki
Copy link
Contributor

mikayla-maki commented Dec 16, 2024

@0xtimsb thanks for making that change

That said, I think my suggestion has terrible UX… could we do something like detect if the user has Polkit installed, and then prompt them if they want to install our policy, and only then escalate the script’s permissions to su so we can do the installation?

@0xtimsb
Copy link
Member Author

0xtimsb commented Dec 16, 2024

Yes, it's weird to ask them to run script to get pretty message :P. Updated it to do it in install.sh itself.

Now, in case Polkit is detected, we prompt the user to ask if we should configure Polkit. Only when the user accepts (y/Y/ enter) will it ask for the user's password.

yes

If the user rejects the installation will proceed normally.

no

If Polkit is not detected, we simply add a warning and a docs link to install it. This should be very rare.

no

script/install.sh Outdated Show resolved Hide resolved
script/install.sh Outdated Show resolved Hide resolved
Comment on lines 145 to 146
echo "Zed needs sudo access to improve root file editing experience."
printf "Configure polkit? [Y/n] "
Copy link
Contributor

Choose a reason for hiding this comment

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

This could potentially be more beginner-friendly if worded as something like "Configure polkit to allow Zed to request elevated permissions when editing system files?"

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup, not super accurate (since it’d still work even after saying no), but let’s just write it this way for simplicity's sake.

script/install.sh Outdated Show resolved Hide resolved
crates/fs/src/fs.rs Outdated Show resolved Hide resolved
let mut cmd = Command::new(pkexec_path);
cmd.arg("--disable-internal-agent");

let script_path = PathBuf::from("/usr/libexec/zed/elevate.sh");
Copy link
Contributor

Choose a reason for hiding this comment

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

This is also not going to work on NixOS. Would be nice to have a way to specify this as a command line flag or env variable while building so we don't have to resort to patching the code in the NixOS package.

Copy link
Member Author

Choose a reason for hiding this comment

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

I’ve added the env variable which can be used in Nix installation. But, I'm not picking that in this PR as I'm not familiar with Nix yet. It should still work on NixOS as long as pkexec is in the PATH without custom message. I will create another PR after setting up NixOS VM and testing there.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks! In nixpkgs we wrap the executable in a shell script to add node.js into its PATH already so adding pkexec in that mix is trivial. Similarly setting the environment variable for the elevate script to point to /nix/store/<hash> is easily doable now. Updating the nix files in this repo can indeed be done separately (especially since this is not a change that breaks something that was working before).

crates/fs/src/fs.rs Outdated Show resolved Hide resolved
@jansol jansol mentioned this pull request Dec 18, 2024
1 task
@ConradIrwin
Copy link
Member

@0xtimsb thanks for this!

We should not add this functionality to the install.sh script. It should not prompt (so that it can be run in people's setup scripts without needing arguments to skip prompts) and it should not write outside of your home directory or ask for root (so that anyone can install Zed on any machine).

If we need polkit support for this feature, we should defer installing it to later as we do with the zed::InstallCli action on macOS. That said, if we need sudo access to install a thing just to get sudo access, we have a catch-22..

Is there a way of building this feature that does not require configuration changes on the machine? It sounds like we could get the same behavior as VSCode without it. If so that seems like the better trade-off (and if we call the script something like write-to-file-as-root hopefully it's clear enough what's going on)

@0xtimsb
Copy link
Member Author

0xtimsb commented Dec 18, 2024

@ConradIrwin thanks for the suggestions!

I've removed the policy and custom script, so there are no changes to the installation process now. I went with a similar approach to VSCode, creating a temporary script during runtime instead of setting up a script during the Zed installation.

It’s much simpler now - just the script name shows up in prompt instead of a bunch of arguments, so it’s clearer. This should also work on NixOS.

Gnome:
gnome

KDE:
kde

crates/fs/src/fs.rs Outdated Show resolved Hide resolved
crates/fs/src/fs.rs Outdated Show resolved Hide resolved
@mikayla-maki
Copy link
Contributor

Beyond the blocking IO, this implementation looks great!

@0xtimsb 0xtimsb force-pushed the prompt-for-sudo-access branch from e6a8cb4 to ff8b062 Compare December 19, 2024 07:34
crates/fs/src/fs.rs Outdated Show resolved Hide resolved
@0xtimsb 0xtimsb requested a review from mikayla-maki December 19, 2024 14:57
@mikayla-maki
Copy link
Contributor

This PR implementation looks good, and no longer changes our install script at all, so I'm going to merge it.

@mikayla-maki mikayla-maki added this pull request to the merge queue Dec 19, 2024
Merged via the queue into zed-industries:main with commit f64bfe8 Dec 19, 2024
13 checks passed
@ReillyBrogan
Copy link

Just got the notification that this PR existed otherwise I would have been here sooner.

The polkit file isn't in the changes anymore, is that intentional? I looked back at the history for it, and the following:

    <defaults>
      <allow_any>auth_admin</allow_any>
      <allow_inactive>auth_admin</allow_inactive>
      <allow_active>auth_admin</allow_active>
    </defaults>

Should be

    <defaults>
     <allow_inactive>no</allow_inactive>
     <allow_active>auth_admin</allow_active>
   </defaults>
        writeln!(
            script_file.as_file(),
            "#!/usr/bin/env sh\nset -eu\ncat \"{}\" > \"{}\"",
            try_quote(&temp_file_path.to_string_lossy())?,
            try_quote(&target_file_path.to_string_lossy())?
        )?;

Yeah this is completely insecure. This opens up the attack vector that the script can be modified by another process between it being written and it being executed allowing that process to execute arbitrary code as root with the user being none the wiser. The initial design of having a separate script that was installed by the install script was the correct one as, assuming Zed is installed to a system location that the user executing Zed does not have direct write access to, it completely mitigates that vector of attack.

Instead of using a script you could use a dedicated binary (faster, some systems have profile scripts that may introduce noticeable delay while a binary will be more consistent) or have a separate "command" that zed understands so it can pkexec itself. You should also hash the file contents and pass that hash to the helper script/binary so that it can verify that the temporary file hasn't been modified by another process.

@jansol
Copy link
Contributor

jansol commented Dec 20, 2024

assuming Zed is installed to a system location that the user executing Zed does not have direct write access to

If you use the official installer script this won't be the case.

@0xtimsb
Copy link
Member Author

0xtimsb commented Dec 20, 2024

This opens up the attack vector that the script can be modified by another process between it being written and it being executed allowing that process to execute arbitrary code as root with the user being none the wiser.

Isn't risk is more if script is pre-defined at some location (usually in .local dir)? This is also how VSCode currently does it, here.

@ReillyBrogan
Copy link

This opens up the attack vector that the script can be modified by another process between it being written and it being executed allowing that process to execute arbitrary code as root with the user being none the wiser.

Isn't risk is more if script is pre-defined at some location (usually in .local dir)? This is also how VSCode currently does it, here.

I don't believe that that is true. I instrumented process execution on my machine (using execsnoop from bcc) and see the following process started by vscode:

/usr/bin/pkexec --disable-internal-agent /bin/bash -c echo SUDOPROMPT; "/usr/share/vscode/bin/code" --file-write "/home/reilly/.config/Code/code-elevated-ekxS9Ub6"

Crucially we see that vscode is executing itself with the --file-write flag. Logic for which is available here, but the gist of which is that the value of --file-write is a metadata json file containing the source (the temporary file) and the destination. There's also logic around restoring file permissions (chmod/chown) that you'll want to implement as well since the newly replaced file will not inherit the permissions that the "old" file had (this could be problematic for a user if a service is supposed to have read/write based on group to a file that it loses since the new permissions will be root:root).

Though vscode doesn't implement it this could be further improved by checksumming the temporary file and passing that checksum to the "root" process, not sure why they're not doing that.

Additionally there's a bit of logic to prevent vscode from being ran as root unless that flag is passed here, which is probably a good idea to implement as well if similar does not already exist.

@ReillyBrogan
Copy link

Alright it's been two weeks since I reported this security issue and I don't see any follow-up PRs addressing the insecure behavior. If my understanding of the release schedule is correct this is going to go out to users next week correct? I suppose I'll have to file a CVE if that happens, I don't see that Zed currently has a CPE, I suppose the correct one to create would be zed_project:zed for the vendor:product combination. Is there a different combination that would be preferred?

CC @ConradIrwin

@0xtimsb
Copy link
Member Author

0xtimsb commented Jan 3, 2025

Hi, @ReillyBrogan

I have few questions regarding this, I would like if we can figure out some actionables to follow up security issues you mentioned. FYI, I'm not associated with Zed, just an external contributor.

Crucially we see that vscode is executing itself with the --file-write flag. Logic for which is available here, but the gist of which is that the value of --file-write is a metadata json file containing the source (the temporary file) and the destination.

I don’t understand how this differs from the current implementation. I see, instead of a script containing the file content, we have a script that points to the file path. But both approaches are similar in terms of temporary script creation, execution, and deletion. So, doesn’t VS Code’s script also have the same mentioned vulnerability when it comes to modifying the script?

There's also logic around restoring file permissions (chmod/chown) that you'll want to implement as well since the newly replaced file will not inherit the permissions that the "old" file had (this could be problematic for a user if a service is supposed to have read/write based on group to a file that it loses since the new permissions will be root:root).

We are not replacing the target file, only modifying its content, so the existing permissions will remain unchanged. The ownership may change to root, but since the file is already owned by root, this should not be an issue?

@ConradIrwin
Copy link
Member

@ReillyBrogan Sorry for the slow reply here.

To summarize the problem:

  • If you have a rogue user/process that has the ability to edit files owned by the current user, but not the ability to run code as root, they can use this Zed feature to privilege escalate.

Do I understand the threat model correctly?

It seems like we can simplify the implementation to avoid the issue by doing:

pkexec sh -c 'cat > {filename}'

and passing input on STDIN.

This avoids the need for an extra shell-script, and avoids the ability for the rogue entity to edit the copy of the file on disk. Does that fix the problem for you?

I'm not sure it's a huge issue (an actor with the ability to edit your files can achieve more reliable success by changing your shell configuration to make pkexec an alias that does nefarious things avoiding the need to win a race condition), but it seems nice to simplify the implementation regardless.

ConradIrwin added a commit that referenced this pull request Jan 8, 2025
@ConradIrwin
Copy link
Member

@0xtimsb I noticed when working on #22843 that while saving seems to work fine, we never get a notification that the file has changed on disk, and so Zed continues to think the file is unsaved/has a conflict.

Did you also experience that in the first implementation, or is there something different about our setups?

@0xtimsb
Copy link
Member Author

0xtimsb commented Jan 8, 2025

Oh wait, I misunderstood your question. I tried this (#22843), and something similar to what you're experiencing happened to me while working on it. I don't quite remember how I fixed it. I'm debugging.

The current latest preview works on my machine.

ConradIrwin added a commit that referenced this pull request Jan 15, 2025
ConradIrwin added a commit that referenced this pull request Jan 15, 2025
github-merge-queue bot pushed a commit that referenced this pull request Jan 15, 2025
Release Notes:

- (temporarily) Removes the linux "save file as root" feature while we
figure out bugs.

Updates #22045
gcp-cherry-pick-bot bot pushed a commit that referenced this pull request Jan 15, 2025
Release Notes:

- (temporarily) Removes the linux "save file as root" feature while we
figure out bugs.

Updates #22045
ConradIrwin added a commit that referenced this pull request Jan 15, 2025
…pick #23162) (#23168)

Cherry-picked Revert "linux: Fix saving file with root ownership
(#22045)" (#23162)

Release Notes:

- (temporarily) Removes the linux "save file as root" feature while we
figure out bugs.

Updates #22045

Co-authored-by: Conrad Irwin <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla-signed The user has signed the Contributor License Agreement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

RFE/Linux: polkit integration
5 participants