diff --git a/nixos/modules/security/wrappers/wrapper.c b/nixos/modules/security/wrappers/wrapper.c index a21ec500208d..17776a97af81 100644 --- a/nixos/modules/security/wrappers/wrapper.c +++ b/nixos/modules/security/wrappers/wrapper.c @@ -1,3 +1,4 @@ +#define _GNU_SOURCE #include #include #include @@ -16,7 +17,10 @@ #include #include +// aborts when false, printing the failed expression #define ASSERT(expr) ((expr) ? (void) 0 : assert_failure(#expr)) +// aborts when returns non-zero, printing the failed expression and errno +#define MUSTSUCCEED(expr) ((expr) ? print_errno_and_die(#expr) : (void) 0) extern char **environ; @@ -41,6 +45,12 @@ static noreturn void assert_failure(const char *assertion) { abort(); } +static noreturn void print_errno_and_die(const char *assertion) { + fprintf(stderr, "Call `%s` in NixOS's wrapper.c failed: %s\n", assertion, strerror(errno)); + fflush(stderr); + abort(); +} + int get_last_cap(unsigned *last_cap) { FILE* file = fopen("/proc/sys/kernel/cap_last_cap", "r"); if (file == NULL) { @@ -177,6 +187,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; + MUSTSUCCEED(getresuid(&ruid, &euid, &suid)); + MUSTSUCCEED(getresgid(&rgid, &egid, &sgid)); + + // 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 didnt_suid = (ruid == euid) && (euid == suid); + // If true, then we did not benefit from setgid privilege escalation + int didnt_sgid = (rgid == egid) && (egid == sgid); + + // Make sure that we are being executed from the right location, // i.e., `safe_wrapper_dir'. This is to prevent someone from creating // hard link `X' from some other location, along with a false @@ -189,15 +210,22 @@ int main(int argc, char **argv) { ASSERT('/' == wrapper_dir[0]); ASSERT('/' == self_path[len]); - // Make *really* *really* sure that we were executed as - // `self_path', and not, say, as some other setuid program. That - // is, our effective uid/gid should match the uid/gid of - // `self_path'. + // If we got privileges with the fs set[ug]id bit, check that the privilege we + // got matches the one one we expected, ie that our effective uid/gid + // matches the uid/gid of `self_path`. This ensures that we were executed as + // `self_path', and not, say, as some other setuid program. + // We don't check that if we did not benefit from the set[ug]id bit, as + // can be the case in nosuid mounts or user namespaces. 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())); + // if the wrapper gained privilege with suid, check that we got the uid of the file owner + ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == euid)); + // if the wrapper gained privilege with sgid, check that we got the gid of the file group + ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == egid)); + // same, but with suid instead of euid + ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == suid)); + ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == sgid)); // And, of course, we shouldn't be writable. ASSERT(!(st.st_mode & (S_IWGRP | S_IWOTH))); diff --git a/nixos/tests/wrappers.nix b/nixos/tests/wrappers.nix index 08c1ad0b6b99..391e9b42b45b 100644 --- a/nixos/tests/wrappers.nix +++ b/nixos/tests/wrappers.nix @@ -55,6 +55,10 @@ in out = machine.succeed(cmd_as_regular(cmd)).strip() assert out == expected, "Expected {0} to output {1}, but got {2}".format(cmd, expected, out) + def test_as_regular_in_userns_mapped_as_root(cmd, expected): + out = machine.succeed(f"su -l regular -c '${pkgs.util-linux}/bin/unshare -rm {cmd}'").strip() + assert out == expected, "Expected {0} to output {1}, but got {2}".format(cmd, expected, out) + test_as_regular('${busybox pkgs}/bin/busybox id -u', '${toString userUid}') test_as_regular('${busybox pkgs}/bin/busybox id -ru', '${toString userUid}') test_as_regular('${busybox pkgs}/bin/busybox id -g', '${toString usersGid}') @@ -70,10 +74,27 @@ in test_as_regular('/run/wrappers/bin/sgid_root_busybox id -g', '0') test_as_regular('/run/wrappers/bin/sgid_root_busybox id -rg', '${toString usersGid}') + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -u', '0') + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -ru', '0') + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -g', '0') + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -rg', '0') + + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -u', '0') + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -ru', '0') + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -g', '0') + test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -rg', '0') + # We are only testing the permitted set, because it's easiest to look at with capsh. machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_CHOWN')) machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_SYS_ADMIN')) machine.succeed(cmd_as_regular('/run/wrappers/bin/capsh_with_chown --has-p=CAP_CHOWN')) machine.fail(cmd_as_regular('/run/wrappers/bin/capsh_with_chown --has-p=CAP_SYS_ADMIN')) + + # test a few "attacks" against which the wrapper protects itself + machine.succeed("cp /run/wrappers/bin/suid_root_busybox{,.real} /tmp/") + machine.fail(cmd_as_regular("/tmp/suid_root_busybox id -u")) + + machine.succeed("chmod u+s,a+w /run/wrappers/bin/suid_root_busybox") + machine.fail(cmd_as_regular("/run/wrappers/bin/suid_root_busybox id -u")) ''; })