diff options
author | github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> | 2023-08-24 12:01:05 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-24 12:01:05 +0000 |
commit | 432839113250d0119f626730a9bd6a1a9f7a48fa (patch) | |
tree | 99b96890f16e663fe218580d7d13f6c70c8793a8 /nixos | |
parent | 18a3b4a2ac2221970dbd2c725e38fc332b3f31ee (diff) | |
parent | 1c7948a78096b97f00c482a09b63e0658639524b (diff) | |
download | nixlib-432839113250d0119f626730a9bd6a1a9f7a48fa.tar nixlib-432839113250d0119f626730a9bd6a1a9f7a48fa.tar.gz nixlib-432839113250d0119f626730a9bd6a1a9f7a48fa.tar.bz2 nixlib-432839113250d0119f626730a9bd6a1a9f7a48fa.tar.lz nixlib-432839113250d0119f626730a9bd6a1a9f7a48fa.tar.xz nixlib-432839113250d0119f626730a9bd6a1a9f7a48fa.tar.zst nixlib-432839113250d0119f626730a9bd6a1a9f7a48fa.zip |
Merge master into staging-next
Diffstat (limited to 'nixos')
-rw-r--r-- | nixos/modules/security/wrappers/default.nix | 10 | ||||
-rw-r--r-- | nixos/modules/security/wrappers/wrapper.c | 109 | ||||
-rw-r--r-- | nixos/modules/security/wrappers/wrapper.nix | 4 | ||||
-rw-r--r-- | nixos/modules/system/boot/binfmt.nix | 10 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 2 | ||||
-rw-r--r-- | nixos/tests/listmonk.nix | 23 | ||||
-rw-r--r-- | nixos/tests/wrappers.nix | 11 |
7 files changed, 128 insertions, 41 deletions
diff --git a/nixos/modules/security/wrappers/default.nix b/nixos/modules/security/wrappers/default.nix index 24f368b3e967..12255d8392fe 100644 --- a/nixos/modules/security/wrappers/default.nix +++ b/nixos/modules/security/wrappers/default.nix @@ -5,8 +5,8 @@ let parentWrapperDir = dirOf wrapperDir; - securityWrapper = sourceProg : pkgs.callPackage ./wrapper.nix { - inherit sourceProg; + securityWrapper = pkgs.callPackage ./wrapper.nix { + inherit parentWrapperDir; }; fileModeType = @@ -91,7 +91,8 @@ let , ... }: '' - cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}" + cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}" + echo -n "${source}" > "$wrapperDir/${program}.real" # Prevent races chmod 0000 "$wrapperDir/${program}" @@ -118,7 +119,8 @@ let , ... }: '' - cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}" + cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}" + echo -n "${source}" > "$wrapperDir/${program}.real" # Prevent races chmod 0000 "$wrapperDir/${program}" diff --git a/nixos/modules/security/wrappers/wrapper.c b/nixos/modules/security/wrappers/wrapper.c index 2cf1727a31c8..17776a97af81 100644 --- a/nixos/modules/security/wrappers/wrapper.c +++ b/nixos/modules/security/wrappers/wrapper.c @@ -17,10 +17,6 @@ #include <syscall.h> #include <byteswap.h> -#ifndef SOURCE_PROG -#error SOURCE_PROG should be defined via preprocessor commandline -#endif - // 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 @@ -28,6 +24,10 @@ extern char **environ; +// The WRAPPER_DIR macro is supplied at compile time so that it cannot +// be changed at runtime +static char *wrapper_dir = WRAPPER_DIR; + // Wrapper debug variable name static char *wrapper_debug = "WRAPPER_DEBUG"; @@ -151,20 +151,115 @@ static int make_caps_ambient(const char *self_path) { return 0; } +int readlink_malloc(const char *p, char **ret) { + size_t l = FILENAME_MAX+1; + int r; + + for (;;) { + char *c = calloc(l, sizeof(char)); + if (!c) { + return -ENOMEM; + } + + ssize_t n = readlink(p, c, l-1); + if (n < 0) { + r = -errno; + free(c); + return r; + } + + if ((size_t) n < l-1) { + c[n] = 0; + *ret = c; + return 0; + } + + free(c); + l *= 2; + } +} + int main(int argc, char **argv) { ASSERT(argc >= 1); + char *self_path = NULL; + int self_path_size = readlink_malloc("/proc/self/exe", &self_path); + if (self_path_size < 0) { + 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 + // `X.real' file, to allow arbitrary programs from being executed + // with elevated capabilities. + int len = strlen(wrapper_dir); + if (len > 0 && '/' == wrapper_dir[len - 1]) + --len; + ASSERT(!strncmp(self_path, wrapper_dir, len)); + ASSERT('/' == wrapper_dir[0]); + ASSERT('/' == self_path[len]); + + // 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); + + // 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))); + + // Read the path of the real (wrapped) program from <self>.real. + char real_fn[PATH_MAX + 10]; + int real_fn_size = snprintf(real_fn, sizeof(real_fn), "%s.real", self_path); + ASSERT(real_fn_size < sizeof(real_fn)); + + int fd_self = open(real_fn, O_RDONLY); + ASSERT(fd_self != -1); + + char source_prog[PATH_MAX]; + len = read(fd_self, source_prog, PATH_MAX); + ASSERT(len != -1); + ASSERT(len < sizeof(source_prog)); + ASSERT(len > 0); + source_prog[len] = 0; + + close(fd_self); // Read the capabilities set on the wrapper and raise them in to // the ambient set so the program we're wrapping receives the // capabilities too! - if (make_caps_ambient("/proc/self/exe") != 0) { + if (make_caps_ambient(self_path) != 0) { + free(self_path); return 1; } + free(self_path); - execve(SOURCE_PROG, argv, environ); + execve(source_prog, argv, environ); fprintf(stderr, "%s: cannot run `%s': %s\n", - argv[0], SOURCE_PROG, strerror(errno)); + argv[0], source_prog, strerror(errno)); return 1; } diff --git a/nixos/modules/security/wrappers/wrapper.nix b/nixos/modules/security/wrappers/wrapper.nix index aec43412404e..e3620fb222d2 100644 --- a/nixos/modules/security/wrappers/wrapper.nix +++ b/nixos/modules/security/wrappers/wrapper.nix @@ -1,4 +1,4 @@ -{ stdenv, linuxHeaders, sourceProg, debug ? false }: +{ stdenv, linuxHeaders, parentWrapperDir, debug ? false }: # For testing: # $ nix-build -E 'with import <nixpkgs> {}; pkgs.callPackage ./wrapper.nix { parentWrapperDir = "/run/wrappers"; debug = true; }' stdenv.mkDerivation { @@ -7,7 +7,7 @@ stdenv.mkDerivation { dontUnpack = true; hardeningEnable = [ "pie" ]; CFLAGS = [ - ''-DSOURCE_PROG="${sourceProg}"'' + ''-DWRAPPER_DIR="${parentWrapperDir}"'' ] ++ (if debug then [ "-Werror" "-Og" "-g" ] else [ diff --git a/nixos/modules/system/boot/binfmt.nix b/nixos/modules/system/boot/binfmt.nix index bf1688feb19e..5172371d0afb 100644 --- a/nixos/modules/system/boot/binfmt.nix +++ b/nixos/modules/system/boot/binfmt.nix @@ -137,14 +137,8 @@ let magicOrExtension = ''\x00asm''; mask = ''\xff\xff\xff\xff''; }; - x86_64-windows = { - magicOrExtension = "exe"; - recognitionType = "extension"; - }; - i686-windows = { - magicOrExtension = "exe"; - recognitionType = "extension"; - }; + x86_64-windows.magicOrExtension = "MZ"; + i686-windows.magicOrExtension = "MZ"; }; in { diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 4354fb3ad628..19aaac694594 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -434,7 +434,7 @@ in { lightdm = handleTest ./lightdm.nix {}; lighttpd = handleTest ./lighttpd.nix {}; limesurvey = handleTest ./limesurvey.nix {}; - listmonk = handleTest ./listmonk.nix {}; + listmonk = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./listmonk.nix {}; litestream = handleTest ./litestream.nix {}; lldap = handleTest ./lldap.nix {}; locate = handleTest ./locate.nix {}; diff --git a/nixos/tests/listmonk.nix b/nixos/tests/listmonk.nix index 91003653c09e..938c36026a7f 100644 --- a/nixos/tests/listmonk.nix +++ b/nixos/tests/listmonk.nix @@ -42,20 +42,27 @@ import ./make-test-python.nix ({ lib, ... }: { machine.wait_for_open_port(9000) machine.succeed("[[ -f /var/lib/listmonk/.db_settings_initialized ]]") + assert json.loads(machine.succeed(generate_listmonk_request("GET", 'health')))['data'], 'Health endpoint returned unexpected value' + + # A sample subscriber is guaranteed to exist at install-time + # A sample transactional template is guaranteed to exist at install-time + subscribers = json.loads(machine.succeed(generate_listmonk_request('GET', "subscribers")))['data']['results'] + templates = json.loads(machine.succeed(generate_listmonk_request('GET', "templates")))['data'] + tx_template = templates[2] + # Test transactional endpoint - # subscriber_id=1 is guaranteed to exist at install-time - # template_id=2 is guaranteed to exist at install-time and is a transactional template (1 is a campaign template). - machine.succeed( - generate_listmonk_request('POST', 'tx', data={'subscriber_id': 1, 'template_id': 2}) - ) - assert 'Welcome John Doe' in machine.succeed( + print(machine.succeed( + generate_listmonk_request('POST', 'tx', data={'subscriber_id': subscribers[0]['id'], 'template_id': tx_template['id']}) + )) + + assert 'Welcome Anon Doe' in machine.succeed( "curl --fail http://localhost:8025/api/v2/messages" - ) + ), "Failed to find Welcome John Doe inside the messages API endpoint" # Test campaign endpoint # Based on https://github.com/knadh/listmonk/blob/174a48f252a146d7e69dab42724e3329dbe25ebe/cmd/campaigns.go#L549 as docs do not exist. campaign_data = json.loads(machine.succeed( - generate_listmonk_request('POST', 'campaigns/1/test', data={'template_id': 1, 'subscribers': ['john@example.com'], 'name': 'Test', 'subject': 'NixOS is great', 'lists': [1], 'messenger': 'email'}) + generate_listmonk_request('POST', 'campaigns/1/test', data={'template_id': templates[0]['id'], 'subscribers': ['john@example.com'], 'name': 'Test', 'subject': 'NixOS is great', 'lists': [1], 'messenger': 'email'}) )) assert campaign_data['data'] # This is a boolean asserting if the test was successful or not: https://github.com/knadh/listmonk/blob/174a48f252a146d7e69dab42724e3329dbe25ebe/cmd/campaigns.go#L626 diff --git a/nixos/tests/wrappers.nix b/nixos/tests/wrappers.nix index 1f5f43286384..391e9b42b45b 100644 --- a/nixos/tests/wrappers.nix +++ b/nixos/tests/wrappers.nix @@ -84,17 +84,6 @@ in 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') - # Test that in nonewprivs environment the wrappers simply exec their target. - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/suid_root_busybox id -u', '${toString userUid}') - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/suid_root_busybox id -ru', '${toString userUid}') - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/suid_root_busybox id -g', '${toString usersGid}') - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/suid_root_busybox id -rg', '${toString usersGid}') - - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/sgid_root_busybox id -u', '${toString userUid}') - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/sgid_root_busybox id -ru', '${toString userUid}') - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/sgid_root_busybox id -g', '${toString usersGid}') - test_as_regular('${pkgs.util-linux}/bin/setpriv --no-new-privs /run/wrappers/bin/sgid_root_busybox id -rg', '${toString usersGid}') - # 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')) |