From 4d4513c1fed6140d2d5588fed5f0eddc935439f3 Mon Sep 17 00:00:00 2001
From: Daan De Meyer <daan.j.demeyer@gmail.com>
Date: Sun, 18 Aug 2024 13:20:14 +0200
Subject: [PATCH 0856/1160] test: Gracefully handle running within user
 namespace with single user

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)
(cherry picked from commit ec5cdf9ba0e003de6f824a000c0bbe46fb4e0925)
---
 src/shared/tests.c          | 15 +++++++++++++++
 src/shared/tests.h          |  1 +
 src/test/test-acl-util.c    |  2 +-
 src/test/test-capability.c  |  7 +++++--
 src/test/test-chase.c       |  4 ++--
 src/test/test-chown-rec.c   |  4 ++--
 src/test/test-condition.c   |  7 +++++++
 src/test/test-fs-util.c     |  4 ++--
 src/test/test-rm-rf.c       |  3 +++
 src/test/test-socket-util.c |  2 +-
 10 files changed, 39 insertions(+), 10 deletions(-)

diff --git a/src/shared/tests.c b/src/shared/tests.c
index 3882a180c4..114d6933d2 100644
--- a/src/shared/tests.c
+++ b/src/shared/tests.c
@@ -28,6 +28,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;
@@ -165,6 +166,20 @@ bool have_namespaces(void) {
         assert_not_reached();
 }
 
+bool userns_has_single_user(void) {
+        _cleanup_(uid_range_freep) UidRange *uidrange = 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(&uidrange, NULL) < 0)
+                return false;
+
+        return uidrange->n_entries == 1 && uidrange->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
diff --git a/src/shared/tests.h b/src/shared/tests.h
index d76cf2edbe..7a43ed5a96 100644
--- a/src/shared/tests.h
+++ b/src/shared/tests.h
@@ -49,6 +49,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)
diff --git a/src/test/test-acl-util.c b/src/test/test-acl-util.c
index eb9678a7d9..acd45a100b 100644
--- a/src/test/test-acl-util.c
+++ b/src/test/test-acl-util.c
@@ -34,7 +34,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)
diff --git a/src/test/test-capability.c b/src/test/test-capability.c
index e8a0569737..5bb2234268 100644
--- a/src/test/test-capability.c
+++ b/src/test/test-capability.c
@@ -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);
diff --git a/src/test/test-chase.c b/src/test/test-chase.c
index 59b51a3088..5964fde170 100644
--- a/src/test/test-chase.c
+++ b/src/test/test-chase.c
@@ -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_se(mkdir(p, 0755) >= 0);
                 assert_se(chown(p, UID_NOBODY, GID_NOBODY) >= 0);
@@ -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_se(mkdir(p, 0755) >= 0);
 
diff --git a/src/test/test-chown-rec.c b/src/test/test-chown-rec.c
index 5d83f5915a..7558de7138 100644
--- a/src/test/test-chown-rec.c
+++ b/src/test/test-chown-rec.c
@@ -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;
 }
diff --git a/src/test/test-condition.c b/src/test/test-condition.c
index bb987613e1..f294be45c5 100644
--- a/src/test/test-condition.c
+++ b/src/test/test-condition.c
@@ -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);
diff --git a/src/test/test-fs-util.c b/src/test/test-fs-util.c
index b32feffd30..41c4a880af 100644
--- a/src/test/test-fs-util.c
+++ b/src/test/test-fs-util.c
@@ -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);
 
diff --git a/src/test/test-rm-rf.c b/src/test/test-rm-rf.c
index 4c69bd28c9..e4a426324f 100644
--- a/src/test/test-rm-rf.c
+++ b/src/test/test-rm-rf.c
@@ -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 */
diff --git a/src/test/test-socket-util.c b/src/test/test-socket-util.c
index e9c776a8c5..6497775c4d 100644
--- a/src/test/test-socket-util.c
+++ b/src/test/test-socket-util.c
@@ -170,7 +170,7 @@ TEST(getpeercred_getpeergroups) {
                 struct ucred ucred;
                 int pair[2];
 
-                if (geteuid() == 0) {
+                if (geteuid() == 0 && !userns_has_single_user()) {
                         test_uid = 1;
                         test_gid = 2;
                         test_gids = (gid_t*) gids;
-- 
2.33.0