Merge pull request #251095 from NixOS/revert-199599-suidwrapnoreal

Revert "nixos/security/wrappers: simplifications and a fix for #98863"

authored by Pierre Bourdon and committed by GitHub 8d56c1fe 64d0bc67

+110 -24
+6 -4
nixos/modules/security/wrappers/default.nix
··· 5 5 6 6 parentWrapperDir = dirOf wrapperDir; 7 7 8 - securityWrapper = sourceProg : pkgs.callPackage ./wrapper.nix { 9 - inherit sourceProg; 8 + securityWrapper = pkgs.callPackage ./wrapper.nix { 9 + inherit parentWrapperDir; 10 10 }; 11 11 12 12 fileModeType = ··· 91 91 , ... 92 92 }: 93 93 '' 94 - cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}" 94 + cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}" 95 + echo -n "${source}" > "$wrapperDir/${program}.real" 95 96 96 97 # Prevent races 97 98 chmod 0000 "$wrapperDir/${program}" ··· 118 119 , ... 119 120 }: 120 121 '' 121 - cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}" 122 + cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}" 123 + echo -n "${source}" > "$wrapperDir/${program}.real" 122 124 123 125 # Prevent races 124 126 chmod 0000 "$wrapperDir/${program}"
+102 -7
nixos/modules/security/wrappers/wrapper.c
··· 17 17 #include <syscall.h> 18 18 #include <byteswap.h> 19 19 20 - #ifndef SOURCE_PROG 21 - #error SOURCE_PROG should be defined via preprocessor commandline 22 - #endif 23 - 24 20 // aborts when false, printing the failed expression 25 21 #define ASSERT(expr) ((expr) ? (void) 0 : assert_failure(#expr)) 26 22 // aborts when returns non-zero, printing the failed expression and errno 27 23 #define MUSTSUCCEED(expr) ((expr) ? print_errno_and_die(#expr) : (void) 0) 28 24 29 25 extern char **environ; 26 + 27 + // The WRAPPER_DIR macro is supplied at compile time so that it cannot 28 + // be changed at runtime 29 + static char *wrapper_dir = WRAPPER_DIR; 30 30 31 31 // Wrapper debug variable name 32 32 static char *wrapper_debug = "WRAPPER_DEBUG"; ··· 151 151 return 0; 152 152 } 153 153 154 + int readlink_malloc(const char *p, char **ret) { 155 + size_t l = FILENAME_MAX+1; 156 + int r; 157 + 158 + for (;;) { 159 + char *c = calloc(l, sizeof(char)); 160 + if (!c) { 161 + return -ENOMEM; 162 + } 163 + 164 + ssize_t n = readlink(p, c, l-1); 165 + if (n < 0) { 166 + r = -errno; 167 + free(c); 168 + return r; 169 + } 170 + 171 + if ((size_t) n < l-1) { 172 + c[n] = 0; 173 + *ret = c; 174 + return 0; 175 + } 176 + 177 + free(c); 178 + l *= 2; 179 + } 180 + } 181 + 154 182 int main(int argc, char **argv) { 155 183 ASSERT(argc >= 1); 184 + char *self_path = NULL; 185 + int self_path_size = readlink_malloc("/proc/self/exe", &self_path); 186 + if (self_path_size < 0) { 187 + fprintf(stderr, "cannot readlink /proc/self/exe: %s", strerror(-self_path_size)); 188 + } 189 + 190 + unsigned int ruid, euid, suid, rgid, egid, sgid; 191 + MUSTSUCCEED(getresuid(&ruid, &euid, &suid)); 192 + MUSTSUCCEED(getresgid(&rgid, &egid, &sgid)); 193 + 194 + // If true, then we did not benefit from setuid privilege escalation, 195 + // where the original uid is still in ruid and different from euid == suid. 196 + int didnt_suid = (ruid == euid) && (euid == suid); 197 + // If true, then we did not benefit from setgid privilege escalation 198 + int didnt_sgid = (rgid == egid) && (egid == sgid); 199 + 200 + 201 + // Make sure that we are being executed from the right location, 202 + // i.e., `safe_wrapper_dir'. This is to prevent someone from creating 203 + // hard link `X' from some other location, along with a false 204 + // `X.real' file, to allow arbitrary programs from being executed 205 + // with elevated capabilities. 206 + int len = strlen(wrapper_dir); 207 + if (len > 0 && '/' == wrapper_dir[len - 1]) 208 + --len; 209 + ASSERT(!strncmp(self_path, wrapper_dir, len)); 210 + ASSERT('/' == wrapper_dir[0]); 211 + ASSERT('/' == self_path[len]); 212 + 213 + // If we got privileges with the fs set[ug]id bit, check that the privilege we 214 + // got matches the one one we expected, ie that our effective uid/gid 215 + // matches the uid/gid of `self_path`. This ensures that we were executed as 216 + // `self_path', and not, say, as some other setuid program. 217 + // We don't check that if we did not benefit from the set[ug]id bit, as 218 + // can be the case in nosuid mounts or user namespaces. 219 + struct stat st; 220 + ASSERT(lstat(self_path, &st) != -1); 221 + 222 + // if the wrapper gained privilege with suid, check that we got the uid of the file owner 223 + ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == euid)); 224 + // if the wrapper gained privilege with sgid, check that we got the gid of the file group 225 + ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == egid)); 226 + // same, but with suid instead of euid 227 + ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == suid)); 228 + ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == sgid)); 229 + 230 + // And, of course, we shouldn't be writable. 231 + ASSERT(!(st.st_mode & (S_IWGRP | S_IWOTH))); 232 + 233 + // Read the path of the real (wrapped) program from <self>.real. 234 + char real_fn[PATH_MAX + 10]; 235 + int real_fn_size = snprintf(real_fn, sizeof(real_fn), "%s.real", self_path); 236 + ASSERT(real_fn_size < sizeof(real_fn)); 237 + 238 + int fd_self = open(real_fn, O_RDONLY); 239 + ASSERT(fd_self != -1); 240 + 241 + char source_prog[PATH_MAX]; 242 + len = read(fd_self, source_prog, PATH_MAX); 243 + ASSERT(len != -1); 244 + ASSERT(len < sizeof(source_prog)); 245 + ASSERT(len > 0); 246 + source_prog[len] = 0; 247 + 248 + close(fd_self); 156 249 157 250 // Read the capabilities set on the wrapper and raise them in to 158 251 // the ambient set so the program we're wrapping receives the 159 252 // capabilities too! 160 - if (make_caps_ambient("/proc/self/exe") != 0) { 253 + if (make_caps_ambient(self_path) != 0) { 254 + free(self_path); 161 255 return 1; 162 256 } 257 + free(self_path); 163 258 164 - execve(SOURCE_PROG, argv, environ); 259 + execve(source_prog, argv, environ); 165 260 166 261 fprintf(stderr, "%s: cannot run `%s': %s\n", 167 - argv[0], SOURCE_PROG, strerror(errno)); 262 + argv[0], source_prog, strerror(errno)); 168 263 169 264 return 1; 170 265 }
+2 -2
nixos/modules/security/wrappers/wrapper.nix
··· 1 - { stdenv, linuxHeaders, sourceProg, debug ? false }: 1 + { stdenv, linuxHeaders, parentWrapperDir, debug ? false }: 2 2 # For testing: 3 3 # $ nix-build -E 'with import <nixpkgs> {}; pkgs.callPackage ./wrapper.nix { parentWrapperDir = "/run/wrappers"; debug = true; }' 4 4 stdenv.mkDerivation { ··· 7 7 dontUnpack = true; 8 8 hardeningEnable = [ "pie" ]; 9 9 CFLAGS = [ 10 - ''-DSOURCE_PROG="${sourceProg}"'' 10 + ''-DWRAPPER_DIR="${parentWrapperDir}"'' 11 11 ] ++ (if debug then [ 12 12 "-Werror" "-Og" "-g" 13 13 ] else [
-11
nixos/tests/wrappers.nix
··· 84 84 test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -g', '0') 85 85 test_as_regular_in_userns_mapped_as_root('/run/wrappers/bin/sgid_root_busybox id -rg', '0') 86 86 87 - # Test that in nonewprivs environment the wrappers simply exec their target. 88 - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/suid_root_busybox id -u', '${toString userUid}') 89 - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/suid_root_busybox id -ru', '${toString userUid}') 90 - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/suid_root_busybox id -g', '${toString usersGid}') 91 - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/suid_root_busybox id -rg', '${toString usersGid}') 92 - 93 - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/sgid_root_busybox id -u', '${toString userUid}') 94 - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/sgid_root_busybox id -ru', '${toString userUid}') 95 - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/sgid_root_busybox id -g', '${toString usersGid}') 96 - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/sgid_root_busybox id -rg', '${toString usersGid}') 97 - 98 87 # We are only testing the permitted set, because it's easiest to look at with capsh. 99 88 machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_CHOWN')) 100 89 machine.fail(cmd_as_regular('${pkgs.libcap}/bin/capsh --has-p=CAP_SYS_ADMIN'))