about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>2023-08-24 12:01:05 +0000
committerGitHub <noreply@github.com>2023-08-24 12:01:05 +0000
commit432839113250d0119f626730a9bd6a1a9f7a48fa (patch)
tree99b96890f16e663fe218580d7d13f6c70c8793a8 /nixos
parent18a3b4a2ac2221970dbd2c725e38fc332b3f31ee (diff)
parent1c7948a78096b97f00c482a09b63e0658639524b (diff)
downloadnixlib-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.nix10
-rw-r--r--nixos/modules/security/wrappers/wrapper.c109
-rw-r--r--nixos/modules/security/wrappers/wrapper.nix4
-rw-r--r--nixos/modules/system/boot/binfmt.nix10
-rw-r--r--nixos/tests/all-tests.nix2
-rw-r--r--nixos/tests/listmonk.nix23
-rw-r--r--nixos/tests/wrappers.nix11
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'))