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

allow setuid and setgid wrappers to run in user namespaces #231673

Merged
merged 1 commit into from Aug 10, 2023

Conversation

symphorien
Copy link
Member

In user namespaces where an unprivileged user is mapped as root and root is unmapped, setuid bits have no effect. However setuid root executables like mount are still usable in the namespace as the user already has the required privileges. This commit detects the situation where the wrapper gained no privileges that the parent process did not already have and in this case does less sanity checking. In short there is no need to be picky since the parent already can execute the foo.real executable themselves.

Details:
man 7 user_namespaces:
Set-user-ID and set-group-ID programs
When a process inside a user namespace executes a set-user-ID
(set-group-ID) program, the process's effective user (group) ID
inside the namespace is changed to whatever value is mapped for
the user (group) ID of the file. However, if either the user or
the group ID of the file has no mapping inside the namespace, the
set-user-ID (set-group-ID) bit is silently ignored: the new
program is executed, but the process's effective user (group) ID
is left unchanged. (This mirrors the semantics of executing a
set-user-ID or set-group-ID program that resides on a filesystem
that was mounted with the MS_NOSUID flag, as described in
mount(2).)

The effect of the setuid bit is that the real user id is preserved and the effective and set user ids are changed to the owner of the wrapper. We detect that no privilege was gained by checking that euid == suid == ruid. In this case we stop checking that euid == owner of the wrapper file.

As a reminder here are the values of euid, ruid, suid, stat.st_uid and stat.st_mode & S_ISUID in various cases when running a setuid 42 executable as user 1000:

Normal case:
ruid=1000 euid=42 suid=42
setuid=2048, st_uid=42

nosuid mount:
ruid=1000 euid=1000 suid=1000
setuid=2048, st_uid=42

inside unshare -rm:
ruid=0 euid=0 suid=0
setuid=2048, st_uid=65534

inside unshare -rm, on a suid mount:
ruid=0 euid=0 suid=0
setuid=2048, st_uid=65534

Fixes #42117

Description of changes
Things done
  • Built on platform(s)
    • x86_64-linux
    • aarch64-linux
    • x86_64-darwin
    • aarch64-darwin
  • For non-Linux: Is sandbox = true set in nix.conf? (See Nix manual)
  • Tested, as applicable:
  • Tested compilation of all packages that depend on this change using nix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD". Note: all changes have to be committed, also see nixpkgs-review usage
  • Tested basic functionality of all binary files (usually in ./result/bin/)
  • 23.05 Release Notes (or backporting 22.11 Release notes)
    • (Package updates) Added a release notes entry if the change is major or breaking
    • (Module updates) Added a release notes entry if the change is significant
    • (Module addition) Added a release notes entry if adding a new NixOS module
  • Fits CONTRIBUTING.md.

Copy link
Contributor

@majiru majiru left a comment

Choose a reason for hiding this comment

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

I ran in to this issue recently and can confirm this patch fixes it. Thank you!

SuperSandro2000 added a commit to SuperSandro2000/nixpkgs that referenced this pull request Jul 13, 2023
In combination with NixOS#231673 this
allows hardened services to use nullmailer's sendmail.
SuperSandro2000 added a commit to SuperSandro2000/nixpkgs that referenced this pull request Jul 13, 2023
In combination with NixOS#231673 this
allows hardened services to use nullmailer's sendmail.
SuperSandro2000 added a commit to SuperSandro2000/nixpkgs that referenced this pull request Jul 13, 2023
In combination with NixOS#231673 this
allows hardened services to use nullmailer's sendmail.
SuperSandro2000 added a commit to SuperSandro2000/nixpkgs that referenced this pull request Jul 13, 2023
In combination with NixOS#231673 this
allows hardened services to use nullmailer's sendmail.

// If true, then we did not benefit from setuid privilege escalation,
// where the original uid is still in ruid and different from euid == suid.
int no_suid = ruid == euid && euid == suid;
Copy link
Contributor

Choose a reason for hiding this comment

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

I do prefer () for readability

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 added parens.

@Mindavi
Copy link
Contributor

Mindavi commented Jul 13, 2023

I like the extra testing that's done.

@@ -177,6 +178,17 @@ int main(int argc, char **argv) {
fprintf(stderr, "cannot readlink /proc/self/exe: %s", strerror(-self_path_size));
}

unsigned int ruid, euid, suid, rgid, egid, sgid;
ASSERT(!getresuid(&ruid, &euid, &suid));
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 those asserts are really bad at communicating what happens, I'd rather have strerror properly.


// If true, then we did not benefit from setuid privilege escalation,
// where the original uid is still in ruid and different from euid == suid.
int no_suid = (ruid == euid) && (euid == suid);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
int no_suid = (ruid == euid) && (euid == suid);
int didnt_suid = (ruid == euid) && (euid == suid);

no_suid feels like this is a no SUID binary when in practice "it is", the difference is that we didn't actively set the UID because we are already that UID.

So the variable name should reflect this, possess_already_target_uid, or whatever. This makes the code more readable to the non-expert.

struct stat st;
ASSERT(lstat(self_path, &st) != -1);

ASSERT(!(st.st_mode & S_ISUID) || (st.st_uid == geteuid()));
ASSERT(!(st.st_mode & S_ISGID) || (st.st_gid == getegid()));
ASSERT(!((st.st_mode & S_ISUID) && !no_suid) || (st.st_uid == geteuid()));
Copy link
Member

Choose a reason for hiding this comment

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

Add a comment saying, this expresses:

Suggested change
ASSERT(!((st.st_mode & S_ISUID) && !no_suid) || (st.st_uid == geteuid()));
// if the binary is SUID and this is a privilege escalation, then the file's UID has to match the effective one (`egid`)
ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == euid));

@RaitoBezarius
Copy link
Member

@Mic92 can you take a look to this please?

@RaitoBezarius
Copy link
Member

@symphorien commit should follow contributing guide: nixos/wrappers: do not check for set IDs if they were already available I guess?

@symphorien
Copy link
Member Author

Thank you for your feedback. I pushed a version that should take it into account.

Copy link
Member

@RaitoBezarius RaitoBezarius left a comment

Choose a reason for hiding this comment

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

I am still wondering about the last comment, other than that, LGTM.

@samueldr
Copy link
Member

samueldr commented Aug 8, 2023

Since GitHub's UI leaves a lot to be desired, that last comment is in this review:

…paces

In user namespaces where an unprivileged user is mapped as root and root
is unmapped, setuid bits have no effect. However setuid root
executables like mount are still usable *in the namespace* as the user
already has the required privileges. This commit detects the situation
where the wrapper gained no privileges that the parent process did not
already have and in this case does less sanity checking. In short there
is no need to be picky since the parent already can execute the foo.real
executable themselves.

Details:
man 7 user_namespaces:
   Set-user-ID and set-group-ID programs
       When a process inside a user namespace executes a set-user-ID
       (set-group-ID) program, the process's effective user (group) ID
       inside the namespace is changed to whatever value is mapped for
       the user (group) ID of the file.  However, if either the user or
       the group ID of the file has no mapping inside the namespace, the
       set-user-ID (set-group-ID) bit is silently ignored: the new
       program is executed, but the process's effective user (group) ID
       is left unchanged.  (This mirrors the semantics of executing a
       set-user-ID or set-group-ID program that resides on a filesystem
       that was mounted with the MS_NOSUID flag, as described in
       mount(2).)

The effect of the setuid bit is that the real user id is preserved and
the effective and set user ids are changed to the owner of the wrapper.
We detect that no privilege was gained by checking that euid == suid
== ruid. In this case we stop checking that euid == owner of the
wrapper file.

As a reminder here are the values of euid, ruid, suid, stat.st_uid and
stat.st_mode & S_ISUID in various cases when running a setuid 42 executable as user 1000:

Normal case:
ruid=1000 euid=42 suid=42
setuid=2048, st_uid=42

nosuid mount:
ruid=1000 euid=1000 suid=1000
setuid=2048, st_uid=42

inside unshare -rm:
ruid=0 euid=0 suid=0
setuid=2048, st_uid=65534

inside unshare -rm, on a suid mount:
ruid=0 euid=0 suid=0
setuid=2048, st_uid=65534
@symphorien
Copy link
Member Author

My bad, fixed

@RaitoBezarius RaitoBezarius merged commit ec409e6 into NixOS:master Aug 10, 2023
18 checks passed
@joepie91
Copy link
Contributor

Will this be backported into stable as well?

@RaitoBezarius
Copy link
Member

Will this be backported into stable as well?

Once we get more experience with this running in production (or someone explicitly tries it in user ns contexts, to confirm it works "better" for their usecases and not synthetic ones), I would not be against backporting it.

@Atemu
Copy link
Member

Atemu commented Aug 21, 2023

I personally don't think this should be backported. If you can't wait < half a year for a potentially breaking feature such as this one, you should not be using the stable channel.

@joepie91
Copy link
Contributor

@Atemu That seems like an unnecessarily accusative/confrontational comment. It's not even clear to me what exactly is "potentially breaking" here - as far as I can tell, it resolves a bug.

@symphorien
Copy link
Member Author

I agree with Atemu. In my opinion, this is not the kind of low-risk, definitely-non-breaking bug-fixes that I expect to be backported to stable releases. This looks more like an improvement than a bug fix to me.

@symphorien symphorien deleted the suid_wrappers_userns branch August 21, 2023 15:04
@RaitoBezarius
Copy link
Member

While I understand from where you are coming, I would argue this qualifies definitely as a bug, even though we are adding features to the C wrapper, user namespaces are a classical feature of the Linux kernel and we kind of expect being able to run stuff in them without getting this hard to debug NixOS only error.

Nevertheless, given the code delta and actual "in-production" experience, I do not see this as a "high risk" backport contrary to other stuff I have seen being backported.

That being said, the next stable is in November, that is, in 3 months. If the actual "in-production" experience cannot be obtained fast enough, I would deem this to not be a priority because we would find ourselves dealing with stabilizing unstable.

I personally don't think this should be backported. If you can't wait < half a year for a potentially breaking feature such as this one, you should not be using the stable channel.

Let's not urge people to use unstable :P — I think it's a valid demand and it's not a breaking change because I am almost certain no one is relying on the semantics of user namespace to make all wrapped NixOS software fail in those contexts :).


Either case, I do not see a big problem with such a backport, I only think it may be counterproductive given that 23.11 will land "soon" for some value of 'soon' and I would urge anyone who wants to use this in 23.05 to just apply the patch as a PR (no negotiation) or work towards getting a backport in (negotiation with stakeholders).

@nixos-discourse
Copy link

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/cannot-start-rootless-podman-compose-from-dynamic-user-systemd-service/34081/1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

NixOS setuid wrapper prevent running sudo in user namespace
9 participants