]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
test: Gracefully handle running within user namespace with single user 34026/head
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 19:53:52 +0000 (21:53 +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.

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 dbafed92c58e6f4476bec9f43aeaa3d9480af13d..50b30ca17d55112575e5167afd3ac5333fca6f41 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 e98cc9edfbebe77b8702fba11b0bc3156315006e..c1a282c62d2a07f68fe0cea1589a6bf26abd8f5f 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 8139af83ce6bb8b845815b1bd0d3c893b653ab9f..3da3caf4ab9b4029d824647d1b565c32e0dea762 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 516bddefbe6f46f6595302215d7f59cf22ed566c..f7b31aeb46f628d169717e1a04fefcf7dee25df9 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;