···11+#define _GNU_SOURCE
12#include <stdlib.h>
23#include <stdio.h>
34#include <string.h>
···1617#include <syscall.h>
1718#include <byteswap.h>
18192020+// aborts when false, printing the failed expression
1921#define ASSERT(expr) ((expr) ? (void) 0 : assert_failure(#expr))
2222+// aborts when returns non-zero, printing the failed expression and errno
2323+#define MUSTSUCCEED(expr) ((expr) ? print_errno_and_die(#expr) : (void) 0)
20242125extern char **environ;
2226···37413842static noreturn void assert_failure(const char *assertion) {
3943 fprintf(stderr, "Assertion `%s` in NixOS's wrapper.c failed.\n", assertion);
4444+ fflush(stderr);
4545+ abort();
4646+}
4747+4848+static noreturn void print_errno_and_die(const char *assertion) {
4949+ fprintf(stderr, "Call `%s` in NixOS's wrapper.c failed: %s\n", assertion, strerror(errno));
4050 fflush(stderr);
4151 abort();
4252}
···177187 fprintf(stderr, "cannot readlink /proc/self/exe: %s", strerror(-self_path_size));
178188 }
179189190190+ unsigned int ruid, euid, suid, rgid, egid, sgid;
191191+ MUSTSUCCEED(getresuid(&ruid, &euid, &suid));
192192+ MUSTSUCCEED(getresgid(&rgid, &egid, &sgid));
193193+194194+ // If true, then we did not benefit from setuid privilege escalation,
195195+ // where the original uid is still in ruid and different from euid == suid.
196196+ int didnt_suid = (ruid == euid) && (euid == suid);
197197+ // If true, then we did not benefit from setgid privilege escalation
198198+ int didnt_sgid = (rgid == egid) && (egid == sgid);
199199+200200+180201 // Make sure that we are being executed from the right location,
181202 // i.e., `safe_wrapper_dir'. This is to prevent someone from creating
182203 // hard link `X' from some other location, along with a false
···189210 ASSERT('/' == wrapper_dir[0]);
190211 ASSERT('/' == self_path[len]);
191212192192- // Make *really* *really* sure that we were executed as
193193- // `self_path', and not, say, as some other setuid program. That
194194- // is, our effective uid/gid should match the uid/gid of
195195- // `self_path'.
213213+ // If we got privileges with the fs set[ug]id bit, check that the privilege we
214214+ // got matches the one one we expected, ie that our effective uid/gid
215215+ // matches the uid/gid of `self_path`. This ensures that we were executed as
216216+ // `self_path', and not, say, as some other setuid program.
217217+ // We don't check that if we did not benefit from the set[ug]id bit, as
218218+ // can be the case in nosuid mounts or user namespaces.
196219 struct stat st;
197220 ASSERT(lstat(self_path, &st) != -1);
198221199199- ASSERT(!(st.st_mode & S_ISUID) || (st.st_uid == geteuid()));
200200- ASSERT(!(st.st_mode & S_ISGID) || (st.st_gid == getegid()));
222222+ // if the wrapper gained privilege with suid, check that we got the uid of the file owner
223223+ ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == euid));
224224+ // if the wrapper gained privilege with sgid, check that we got the gid of the file group
225225+ ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == egid));
226226+ // same, but with suid instead of euid
227227+ ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == suid));
228228+ ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == sgid));
201229202230 // And, of course, we shouldn't be writable.
203231 ASSERT(!(st.st_mode & (S_IWGRP | S_IWOTH)));
+21
nixos/tests/wrappers.nix
···5555 out = machine.succeed(cmd_as_regular(cmd)).strip()
5656 assert out == expected, "Expected {0} to output {1}, but got {2}".format(cmd, expected, out)
57575858+ def test_as_regular_in_userns_mapped_as_root(cmd, expected):
5959+ out = machine.succeed(f"su -l regular -c '${pkgs.util-linux}/bin/unshare -rm {cmd}'").strip()
6060+ assert out == expected, "Expected {0} to output {1}, but got {2}".format(cmd, expected, out)
6161+5862 test_as_regular('${busybox pkgs}/bin/busybox id -u', '${toString userUid}')
5963 test_as_regular('${busybox pkgs}/bin/busybox id -ru', '${toString userUid}')
6064 test_as_regular('${busybox pkgs}/bin/busybox id -g', '${toString usersGid}')
···7074 test_as_regular('/run/wrappers/bin/sgid_root_busybox id -g', '0')
7175 test_as_regular('/run/wrappers/bin/sgid_root_busybox id -rg', '${toString usersGid}')
72767777+ test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -u', '0')
7878+ test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -ru', '0')
7979+ test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -g', '0')
8080+ test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/suid_root_busybox id -rg', '0')
8181+8282+ test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -u', '0')
8383+ test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -ru', '0')
8484+ test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -g', '0')
8585+ test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -rg', '0')
8686+7387 # We are only testing the permitted set, because it's easiest to look at with capsh.
7488 machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_CHOWN'))
7589 machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_SYS_ADMIN'))
7690 machine.succeed(cmd_as_regular('/run/wrappers/bin/capsh_with_chown --has-p=CAP_CHOWN'))
7791 machine.fail(cmd_as_regular('/run/wrappers/bin/capsh_with_chown --has-p=CAP_SYS_ADMIN'))
9292+9393+ # test a few "attacks" against which the wrapper protects itself
9494+ machine.succeed("cp /run/wrappers/bin/suid_root_busybox{,.real} /tmp/")
9595+ machine.fail(cmd_as_regular("/tmp/suid_root_busybox id -u"))
9696+9797+ machine.succeed("chmod u+s,a+w /run/wrappers/bin/suid_root_busybox")
9898+ machine.fail(cmd_as_regular("/run/wrappers/bin/suid_root_busybox id -u"))
7899 '';
79100})