test: Gracefully handle running within user namespace with single user
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Sun, 18 Aug 2024 11:20:14 +0000 (13:20 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Sun, 18 Aug 2024 22:06:15 +0000 (00:06 +0200)
Unprivileged users often make themselves root by unsharing a user namespace
and then mapping their current user to root which does not require privileges.
Let's make sure our tests don't fail in such an environment by adding checks
where required to see if we're not running in a user namespace with only a
single user.

(cherry picked from commit ef31767ed7e21672a50b77e7b3935948aaba114c)

src/shared/tests.c
src/shared/tests.h
src/test/test-acl-util.c
src/test/test-capability.c
src/test/test-chase.c
src/test/test-chown-rec.c
src/test/test-condition.c
src/test/test-fs-util.c
src/test/test-rm-rf.c
src/test/test-socket-util.c

index 9169513e09cea2c9a3767d87e61d5f4950700b76..a9192126355070871fbf227068f32282ed53e922 100644 (file)
@@ -29,6 +29,7 @@
 #include "strv.h"
 #include "tests.h"
 #include "tmpfile-util.h"
+#include "uid-range.h"
 
 char* setup_fake_runtime_dir(void) {
         char t[] = "/tmp/fake-xdg-runtime-XXXXXX", *p;
@@ -166,6 +167,24 @@ bool have_namespaces(void) {
         assert_not_reached();
 }
 
+bool userns_has_single_user(void) {
+        _cleanup_(uid_range_freep) UIDRange *uidrange = NULL, *gidrange = NULL;
+
+        /* Check if we're in a user namespace with only a single user mapped in. We special case this
+         * scenario in a few tests because it's the only kind of namespace that can be created unprivileged
+         * and as such happens more often than not, so we make sure to deal with it so that all tests pass
+         * in such environments. */
+
+        if (uid_range_load_userns(NULL, UID_RANGE_USERNS_INSIDE, &uidrange) < 0)
+                return false;
+
+        if (uid_range_load_userns(NULL, GID_RANGE_USERNS_INSIDE, &gidrange) < 0)
+                return false;
+
+        return uidrange->n_entries == 1 && uidrange->entries[0].nr == 1 &&
+                gidrange->n_entries == 1 && gidrange->entries[0].nr == 1;
+}
+
 bool can_memlock(void) {
         /* Let's see if we can mlock() a larger blob of memory. BPF programs are charged against
          * RLIMIT_MEMLOCK, hence let's first make sure we can lock memory at all, and skip the test if we
index 21f00dbad4e2651c88827071f70bc1f07882c33a..6e4a187cb93d584455ac8d34a778dfefc102491b 100644 (file)
@@ -76,6 +76,7 @@ void test_setup_logging(int level);
 int write_tmpfile(char *pattern, const char *contents);
 
 bool have_namespaces(void);
+bool userns_has_single_user(void);
 
 /* We use the small but non-trivial limit here */
 #define CAN_MEMLOCK_SIZE (512 * 1024U)
index 0cc9afcf340ea810a97305fec88b22f85176c2e2..daab75e9c97809fdc3a73856545f5f997a72fa35 100644 (file)
@@ -41,7 +41,7 @@ TEST_RET(add_acls_for_user) {
         cmd = strjoina("getfacl -p ", fn);
         assert_se(system(cmd) == 0);
 
-        if (getuid() == 0) {
+        if (getuid() == 0 && !userns_has_single_user()) {
                 const char *nobody = NOBODY_USER_NAME;
                 r = get_user_creds(&nobody, &uid, NULL, NULL, NULL, 0);
                 if (r < 0)
index 34f3a91805720a9082d7c2316eee678821bdcd24..51bd80634809d84e21cf3352b0ef6fcf3d4cda77 100644 (file)
@@ -318,10 +318,13 @@ int main(int argc, char *argv[]) {
 
         show_capabilities();
 
-        test_drop_privileges();
+        if (!userns_has_single_user())
+                test_drop_privileges();
+
         test_update_inherited_set();
 
-        fork_test(test_have_effective_cap);
+        if (!userns_has_single_user())
+                fork_test(test_have_effective_cap);
 
         if (run_ambient)
                 fork_test(test_apply_ambient_caps);
index 13ee7028c87dc142d22a5b6d661d956a7ed9d781..c7ca3fd05170f412d5d320697475bf24dd88f81f 100644 (file)
@@ -183,7 +183,7 @@ TEST(chase) {
 
         /* Paths underneath the "root" with different UIDs while using CHASE_SAFE */
 
-        if (geteuid() == 0) {
+        if (geteuid() == 0 && !userns_has_single_user()) {
                 p = strjoina(temp, "/user");
                 ASSERT_OK(mkdir(p, 0755));
                 ASSERT_OK(chown(p, UID_NOBODY, GID_NOBODY));
@@ -313,7 +313,7 @@ TEST(chase) {
         r = chase(p, NULL, 0, &result, NULL);
         assert_se(r == -ENOENT);
 
-        if (geteuid() == 0) {
+        if (geteuid() == 0 && !userns_has_single_user()) {
                 p = strjoina(temp, "/priv1");
                 ASSERT_OK(mkdir(p, 0755));
 
index 5d83f5915a439853459817681dc2905c6486f1fc..7558de71385aa03dbca9c2add4c34b2ce188f2b9 100644 (file)
@@ -153,8 +153,8 @@ TEST(chown_recursive) {
 }
 
 static int intro(void) {
-        if (geteuid() != 0)
-                return log_tests_skipped("not running as root");
+        if (geteuid() != 0 || userns_has_single_user())
+                return log_tests_skipped("not running as root or in userns with single user");
 
         return EXIT_SUCCESS;
 }
index be83690ee506a4282a2396848dfa7dcb58d03f28..76b2af91a97b8f0357b218078f6aadc98b1f74d0 100644 (file)
@@ -1003,6 +1003,13 @@ TEST(condition_test_group) {
         condition_free(condition);
         free(gid);
 
+        /* In an unprivileged user namespace with the current user mapped to root, all the auxiliary groups
+         * of the user will be mapped to the nobody group, which means the user in the user namespace is in
+         * both the root and the nobody group, meaning the next test can't work, so let's skip it in that
+         * case. */
+        if (in_group(NOBODY_GROUP_NAME) && in_group("root"))
+                return (void) log_tests_skipped("user is in both root and nobody group");
+
         groupname = (char*)(getegid() == 0 ? NOBODY_GROUP_NAME : "root");
         condition = condition_new(CONDITION_GROUP, groupname, false, false);
         assert_se(condition);
index f2fa51f55faab3cdc4536a9fbfc903ec41caa828..09fd99573dcb918f44246aa0a599491525014403 100644 (file)
@@ -368,8 +368,8 @@ TEST(chmod_and_chown) {
         struct stat st;
         const char *p;
 
-        if (geteuid() != 0)
-                return;
+        if (geteuid() != 0 || userns_has_single_user())
+                return (void) log_tests_skipped("not running as root or in userns with single user");
 
         BLOCK_WITH_UMASK(0000);
 
index 4c69bd28c9d183c2ccd8aea4bbbb45dc3a774e96..e4a426324f83aa83c49cf138165580878500cf6d 100644 (file)
@@ -89,6 +89,9 @@ static void test_rm_rf_chmod_inner(void) {
 TEST(rm_rf_chmod) {
         int r;
 
+        if (getuid() == 0 && userns_has_single_user())
+                return (void) log_tests_skipped("running as root or in userns with single user");
+
         if (getuid() == 0) {
                 /* This test only works unpriv (as only then the access mask for the owning user matters),
                  * hence drop privs here */
index e34aa10e10d78e282ca5759a03e7dfd5a87b70c9..967ba9d9777be12e03fc86fbb8bfef00eb54770f 100644 (file)
@@ -170,7 +170,7 @@ TEST(getpeercred_getpeergroups) {
                 struct ucred ucred;
                 int pair[2] = EBADF_PAIR;
 
-                if (geteuid() == 0) {
+                if (geteuid() == 0 && !userns_has_single_user()) {
                         test_uid = 1;
                         test_gid = 2;
                         test_gids = (gid_t*) gids;