Upstream kernels allow unprivileged users to create user namespaces
and change their uid/gid.

These patches update kdbus policy logic to handle this case and
improve our current checks across user namespaces.

So this patch adds:

* kdbus_test_waitpid() to get exit code of childs.
* kdbus_clone_userns_test() that performs the test inside a new
  user namespace.
* Converts all the other tests to return CHECK_OK, CHECK_SKIP or
  CHECK_ERR so we are consistent.

Currently we fail at kdbus_clone_userns_test() test #8. The next patch
will fix this issue.

Signed-off-by: Djalal Harouni <tix...@opendz.org>
---
 test/test-kdbus-policy.c | 503 +++++++++++++++++++++++++++++++++++------------
 1 file changed, 375 insertions(+), 128 deletions(-)

diff --git a/test/test-kdbus-policy.c b/test/test-kdbus-policy.c
index 8d7bcb5..a354290 100644
--- a/test/test-kdbus-policy.c
+++ b/test/test-kdbus-policy.c
@@ -12,6 +12,7 @@
 #include <fcntl.h>
 #include <pthread.h>
 #include <poll.h>
+#include <sched.h>
 #include <stdlib.h>
 #include <stddef.h>
 #include <stdint.h>
@@ -22,6 +23,8 @@
 #include <sys/wait.h>
 #include <sys/prctl.h>
 #include <sys/ioctl.h>
+#include <sys/eventfd.h>
+#include <sys/syscall.h>
 
 #include "kdbus-util.h"
 #include "kdbus-enum.h"
@@ -29,6 +32,30 @@
 #define MAX_CONN       64
 #define POLICY_NAME    "foo.test.policy-test"
 
+static int ok_cnt;
+static int skip_cnt;
+static int fail_cnt;
+
+static void print_test_status(int test_status)
+{
+       switch (test_status) {
+       case CHECK_OK:
+               printf("OK");
+               ok_cnt++;
+               break;
+       case CHECK_SKIP:
+               printf("SKIPPED");
+               skip_cnt++;
+               break;
+       case CHECK_ERR:
+       default:
+               printf("ERROR");
+               fail_cnt++;
+               break;
+       }
+
+       printf("\n");
+}
 
 /**
  * The purpose of these tests:
@@ -53,35 +80,59 @@ void kdbus_free_conn(struct conn *conn)
        }
 }
 
+static void *kdbus_recv_echo(void *ptr)
+{
+       int ret;
+       struct conn *conn = ptr;
+
+       ret = conn_recv(conn);
+
+       return (void *)(long)ret;
+}
+
 /* Trigger kdbus_policy_set() */
 static int kdbus_set_policy_talk(struct conn *conn,
                                 const char *name,
                                 uid_t id, unsigned int type)
 {
+       int ret;
        struct kdbus_policy_access access = {
                .type = type,
                .id = id,
                .access = KDBUS_POLICY_TALK,
        };
 
-       return conn_update_policy(conn, name, &access, 1);
+       ret = conn_update_policy(conn, name, &access, 1);
+       if (ret < 0)
+               return CHECK_ERR;
+
+       return CHECK_OK;
 }
 
-/* The policy access will be stored in a policy holder connection */
-static int kdbus_register_activator(char *bus, const char *name,
-                                   struct conn **c)
+/* return CHECK_OK or CHECK_ERR on failure */
+static int kdbus_register_same_activator(char *bus, const char *name,
+                                        struct conn **c)
 {
+       int ret;
        struct conn *activator;
 
        activator = kdbus_hello_activator(bus, name, NULL, 0);
-       if (!activator)
-               return -errno;
+       if (activator) {
+               *c = activator;
+               fprintf(stderr, "--- error was able to register name twice 
'%s'.\n",
+                       name);
+               return CHECK_ERR;
+       }
 
-       *c = activator;
+       ret = -errno;
+       /* -EEXIST means test succeeded */
+       if (ret == -EEXIST)
+               return CHECK_OK;
 
-       return 0;
+       return CHECK_ERR;
 }
 
+/* return CHECK_OK or CHECK_ERR on failure */
 static int kdbus_register_policy_holder(char *bus, const char *name,
                                        struct conn **conn)
 {
@@ -99,54 +150,88 @@ static int kdbus_register_policy_holder(char *bus, const 
char *name,
        c = kdbus_hello_registrar(bus, name, access, 2,
                                  KDBUS_HELLO_POLICY_HOLDER);
        if (!c)
-               return -errno;
+               return CHECK_ERR;
 
        *conn = c;
 
-       return 0;
+       return CHECK_OK;
 }
 
-static void *kdbus_recv_echo(void *ptr)
+/* return CHECK_OK or CHECK_ERR on failure */
+static int kdbus_receiver_acquire_name(char *bus, const char *name,
+                                       struct conn **conn)
 {
        int ret;
-       int cnt = 3;
-       struct pollfd fd;
-       struct conn *conn = ptr;
+       struct conn *c;
 
-       fd.fd = conn->fd;
-       fd.events = POLLIN | POLLPRI | POLLHUP;
-       fd.revents = 0;
+       c = kdbus_hello(bus, 0, NULL, 0);
+       if (!c)
+               return CHECK_ERR;
 
-       while (cnt) {
-               cnt--;
-               ret = poll(&fd, 1, 2000);
-               if (ret == 0) {
-                       ret = -ETIMEDOUT;
-                       break;
-               }
+       add_match_empty(c->fd);
 
-               if (ret > 0 && fd.revents & POLLIN) {
-                       printf("-- Connection id: %llu received new message:\n",
-                               (unsigned long long)conn->id);
-                       ret = msg_recv(conn);
-               }
+       ret = name_acquire(c, name, 0);
+       if (ret < 0)
+               return CHECK_ERR;
 
-               if (ret >= 0 || ret != -EAGAIN)
-                       break;
+       *conn = c;
+
+       return CHECK_OK;
+}
+
+/**
+ * Return the exit code of the child process and set the test_done
+ * variable if the test was performed:
+ * In case of WIFEXITED():
+ *   1) If exit code != EXIT_FAILURE assume test reached and set the
+ *      'test_done' variable if provided to 1.
+ *   2) If exit code != EXIT_FAILURE and exit code != EXIT_SUCCESS:
+ *      Convert the exit code to a proper -errno and return it.
+ * In all other cases:
+ *   set the exit code to EXIT_FAILURE and return it.
+ */
+static int kdbus_test_waitpid(pid_t pid, int *test_done)
+{
+       int ret;
+       int status;
+
+       int test_performed = 0;
+
+       ret = waitpid(pid, &status, 0);
+       if (ret < 0) {
+               ret = -errno;
+               fprintf(stderr, "error waitpid: %d (%m)\n", ret);
+               return ret;
        }
 
-       return (void *)(long)ret;
+       if (WIFEXITED(status)) {
+               ret = WEXITSTATUS(status);
+               if (ret != EXIT_FAILURE) {
+                       if (ret != EXIT_SUCCESS)
+                               ret |= -1 << 8; /* get -errno */
+
+                       /* test reached */
+                       test_performed = 1;
+               }
+       } else {
+               ret = EXIT_FAILURE;
+       }
+
+       if (test_done)
+               *test_done = test_performed;
+
+       return ret;
 }
 
 /**
- * Just run a normal test, the 'conn_db' will be populated by
- * newly created connections. Caller should free all allocated
- * connections.
+ * Create new threads for receiving from multiple senders,
+ * The 'conn_db' will be populated by newly created connections.
+ * Caller should free all allocated connections.
  *
- * return 0 on success, a non-zero on failure.
+ * return 0 on success, negative errno on failure.
  */
-static int kdbus_normal_test(const char *bus, const char *name,
-                            struct conn **conn_db)
+static int kdbus_recv_in_threads(const char *bus, const char *name,
+                                struct conn **conn_db)
 {
        int ret;
        unsigned int i, tid;
@@ -198,18 +283,38 @@ static int kdbus_normal_test(const char *bus, const char 
*name,
        return ret;
 }
 
-static int kdbus_fork_test(const char *bus, const char *name,
+/* Return: CHECK_OK or CHECK_ERR on failure */
+static int kdbus_normal_test(const char *bus, const char *name,
                           struct conn **conn_db)
 {
-       int ret = 0;
-       int status;
+       int ret;
+
+       ret = kdbus_recv_in_threads(bus, name, conn_db);
+       if (ret < 0)
+               return CHECK_ERR;
+
+       return CHECK_OK;
+}
+
+/*
+ * Return: CHECK_OK, CHECK_ERR or CHECK_SKIP
+ * we return CHECK_OK only if the childs return with the expected
+ * 'exit_code' that is specified as an argument.
+ */
+static int kdbus_fork_test(const char *bus, const char *name,
+                          struct conn **conn_db, int exit_code)
+{
        pid_t pid;
+       int ret = 0;
        int test_done = 0;
+       int test_status = CHECK_ERR;
 
+       setbuf(stdout, NULL);
+       printf("STARTING the (FORK+DROP) test...............\n");
        if (geteuid() > 0) {
                fprintf(stderr, "error geteuid() != 0, %s() needs root\n",
                        __func__);
-               goto out;
+               return CHECK_SKIP;
        }
 
        pid = fork();
@@ -228,7 +333,7 @@ static int kdbus_fork_test(const char *bus, const char 
*name,
                if (ret < 0)
                        goto child_fail;
 
-               ret = kdbus_normal_test(bus, POLICY_NAME, conn_db);
+               ret = kdbus_recv_in_threads(bus, name, conn_db);
 
                /*
                 * Here cached connections belong to child, they will
@@ -240,31 +345,203 @@ child_fail:
                _exit(EXIT_FAILURE);
        }
 
-       ret = waitpid(pid, &status, 0);
+       ret = kdbus_test_waitpid(pid, &test_done);
+
+out:
+       /* test reached */
+       if (test_done) {
+               if (ret == exit_code)
+                       test_status = CHECK_OK;
+               else
+                       fprintf(stderr,
+                               "error TEST exit code: %d  was expecting code: 
%d\n",
+                               ret, exit_code);
+       }
+
+       return test_status;
+}
+
+/* Return EXIT_SUCCESS, EXIT_FAILURE or negative errno */
+static int __kdbus_clone_userns_test(const char *bus,
+                                    const char *name,
+                                    struct conn **conn_db)
+{
+       int efd;
+       pid_t pid;
+       int ret = 0;
+       unsigned int uid = 65534;
+
+       ret = drop_privileges(uid, uid);
+       if (ret < 0)
+               return ret;
+
+       /**
+        * Since we just dropped privileges, the dumpable flag was just
+        * cleared which makes the /proc/$clone_child/uid_map to be
+        * owned by root, hence any userns uid mapping will fail with
+        * -EPERM since the mapping will be done by uid 65534.
+        *
+        * To avoid this set the dumpable flag again which makes procfs
+        * update the /proc/$clone_child/ inodes owner to 65534.
+        *
+        * Using this we will be able write to /proc/$clone_child/uid_map
+        * as uid 65534 and map the uid 65534 to 0 inside the user
+        * namespace.
+        */
+       ret = prctl(PR_SET_DUMPABLE, SUID_DUMP_USER);
        if (ret < 0) {
                ret = -errno;
-               fprintf(stderr, "error waitpid: %d (%m)\n", ret);
+               fprintf(stderr, "error prctl: %d (%m)\n", ret);
+               return ret;
+       }
+
+       /* sync parent/child */
+       efd = eventfd(0, EFD_CLOEXEC);
+       if (efd < 0) {
+               ret = -errno;
+               fprintf(stderr, "error eventfd: %d (%m)\n", ret);
+               return ret;
+       }
+
+       pid = syscall(__NR_clone, SIGCHLD|CLONE_NEWUSER, NULL);
+       if (pid < 0) {
+               ret = -errno;
+               fprintf(stderr, "error clone: %d (%m)\n", ret);
+               /*
+                * Normal user not allowed to create userns,
+                * so nothing to worry about ?
+                */
+               if (ret == -EPERM) {
+                       printf("-- CLONE_NEWUSER TEST Failed for uid: %u\n"
+                               "-- Make sure that your kernel do not allow 
CLONE_NEWUSER for unprivileged users\n"
+                               "-- Upstream Commit: 
https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=5eaf563e53294d6696e651466697eb9d491f3946\n";,
+                               uid);
+                       ret = 0;
+               }
+
                goto out;
        }
 
-       if (WIFEXITED(status)) {
-               ret = WEXITSTATUS(status);
-               if (ret != EXIT_FAILURE) {
-                       if (ret != EXIT_SUCCESS)
-                               ret |= -1 << 8; /* get -errno */
+       if (pid == 0) {
+               struct conn *conn_src;
+               eventfd_t event_status = 0;
 
-                       test_done = 1;  /* assume test reached */
+               setbuf(stdout, NULL);
+               ret = prctl(PR_SET_PDEATHSIG, SIGKILL);
+               if (ret < 0) {
+                       ret = -errno;
+                       fprintf(stderr, "error prctl: %d (%m)\n", ret);
+                       goto child_fail;
                }
+
+               ret = eventfd_read(efd, &event_status);
+               if (ret < 0 || event_status != 1)
+                       /* event_stats == 1 to continue */
+                       goto child_fail;
+
+               /* ping connection from the new user namespace */
+               conn_src = kdbus_hello(bus, 0, NULL, 0);
+               if (!conn_src)
+                       goto child_fail;
+
+               add_match_empty(conn_src->fd);
+               ret = msg_send(conn_src, name, 0xabcd1234,
+                              0, 0, 0, KDBUS_DST_ID_NAME);
+
+               close(conn_src->fd);
+               free(conn_src);
+               _exit(ret);
+
+child_fail:
+               _exit(EXIT_FAILURE);
+       }
+
+       ret = userns_map_uid_gid(pid, "0 65534 1", "0 65534 1");
+       if (ret < 0) {
+               /* send error to child */
+               eventfd_write(efd, 2);
+               fprintf(stderr, "error mapping uid/gid in new user 
namespace\n");
+               goto out;
        }
 
+       /* Tell child we are ready */
+       ret = eventfd_write(efd, 1);
+       if (ret < 0) {
+               ret = -errno;
+               fprintf(stderr, "error eventfd_write: %d (%m)\n", ret);
+               goto out;
+       }
+
+       ret = kdbus_test_waitpid(pid, NULL);
+
 out:
-       /* test not reached, return -EIO and avoid EXIT_FAILURE */
-       if (!test_done)
-               ret = -EIO;
+       close(efd);
 
        return ret;
 }
 
+static int kdbus_clone_userns_test(const char *bus,
+                                  const char *name,
+                                  struct conn **conn_db,
+                                  int exit_code)
+{
+       pid_t pid;
+       int ret = 0;
+       int test_done = 0;
+       int test_status = CHECK_ERR;
+
+       setbuf(stdout, NULL);
+       printf("STARTING TEST connections in a new user namespace..\n");
+       if (geteuid() > 0) {
+               fprintf(stderr, "error geteuid() != 0, %s() needs root\n",
+                       __func__);
+               return CHECK_SKIP;
+       }
+
+       pid = fork();
+       if (pid < 0) {
+               ret = -errno;
+               fprintf(stderr, "error fork(): %d (%m)\n", ret);
+               goto out;
+       }
+
+       if (pid == 0) {
+               ret = prctl(PR_SET_PDEATHSIG, SIGKILL);
+               if (ret < 0)
+                       goto child_fail;
+
+               ret = __kdbus_clone_userns_test(bus, name, conn_db);
+               _exit(ret);
+child_fail:
+               _exit(EXIT_FAILURE);
+       }
+
+       /* Receive in the original (root privileged) user namespace */
+       ret = conn_recv(conn_db[0]);
+       if (!ret) {
+               fprintf(stderr,
+                       "--- error received packet from unprivileged user 
namespace\n");
+               goto out;
+       }
+
+       ret = kdbus_test_waitpid(pid, &test_done);
+
+out:
+       /* test reached */
+       if (test_done) {
+               /* Set to CHECK_OK if we did get the right exit_code */
+               if (ret == exit_code)
+                       test_status = CHECK_OK;
+               else
+                       fprintf(stderr,
+                               "error TEST exit code: %d  was expecting code: 
%d\n",
+                               ret, exit_code);
+       }
+
+       return test_status;
+}
+
+/* Return CHECK_OK, CHECK_ERR or CHECK_SKIP */
 static int kdbus_check_policy(char *bus)
 {
        int i;
@@ -282,107 +559,74 @@ static int kdbus_check_policy(char *bus)
                                           &policy_holder);
        printf("-- TEST 1) register a policy holder for '%s' ",
                POLICY_NAME);
-       if (ret < 0) {
-               printf("FAILED\n");
+       print_test_status(ret);
+       if (ret == CHECK_ERR)
                goto out_free_connections;
-       }
-
-       printf("OK\n");
 
        /* Try to register the same name with an activator */
-       ret = kdbus_register_activator(bus, POLICY_NAME, &activator);
+       ret = kdbus_register_same_activator(bus, POLICY_NAME,
+                                           &activator);
        printf("-- TEST 2) register again '%s' as an activator ",
                POLICY_NAME);
-       if (ret == 0) {
-               printf("succeeded: TEST FAILED\n");
-               fprintf(stderr, "--- error was able to register twice '%s'.\n",
-                       POLICY_NAME);
-               ret = -1;
-               goto out_free_connections;
-       } else if (ret < 0) {
-               /* -EEXIST means test succeeded */
-               if (ret == -EEXIST) {
-                       ret = 0;
-                       printf("failed: TEST OK\n");
-               } else {
-                       printf("FAILED\n");
-                       goto out_free_connections;
-               }
-       }
-
-       /* The name receiver */
-       conn_db[0] = kdbus_hello(bus, 0, NULL, 0);
-       if (!conn_db[0]) {
-               ret = -1;
+       print_test_status(ret);
+       if (ret == CHECK_ERR)
                goto out_free_connections;
-       }
-
-       add_match_empty(conn_db[0]->fd);
 
-       ret = name_acquire(conn_db[0], POLICY_NAME, 0);
+       ret = kdbus_receiver_acquire_name(bus, POLICY_NAME, &conn_db[0]);
        printf("-- TEST 3) acquire '%s' name..... ", POLICY_NAME);
-       if (ret < 0) {
-               printf("FAILED\n");
+       print_test_status(ret);
+       if (ret == CHECK_ERR)
                goto out_free_connections;
-       }
-
-       printf("OK\n");
 
        ret = kdbus_normal_test(bus, POLICY_NAME, conn_db);
        printf("-- TEST 4) testing connections (NORMAL TEST).... ");
-       if (ret != 0) {
-               printf("FAILED\n");
+       print_test_status(ret);
+       if (ret == CHECK_ERR)
                goto out_free_connections;
-       }
-
-       printf("OK\n");
 
        name_list(conn_db[0], KDBUS_NAME_LIST_NAMES |
                              KDBUS_NAME_LIST_UNIQUE |
                              KDBUS_NAME_LIST_ACTIVATORS |
                              KDBUS_NAME_LIST_QUEUED);
 
-       ret = kdbus_fork_test(bus, POLICY_NAME, conn_db);
+       ret = kdbus_fork_test(bus, POLICY_NAME, conn_db, EXIT_SUCCESS);
        printf("-- TEST 5) testing connections (FORK+DROP)...... ");
-       if (ret != 0) {
-               printf("FAILED\n");
+       print_test_status(ret);
+       if (ret == CHECK_ERR)
                goto out_free_connections;
-       }
-
-       printf("OK\n");
 
        /*
         * Connections that can talk are perhaps being destroyed now.
         * Restrict the policy and purge cache entries where the
         * conn_db[0] is the destination.
+        *
+        * Now only connections with uid == 0 are allowed to talk.
         */
        ret = kdbus_set_policy_talk(policy_holder, POLICY_NAME,
                                    geteuid(), KDBUS_POLICY_ACCESS_USER);
        printf("-- TEST 6) restricting '%s' policy TALK access ",
                POLICY_NAME);
-       if (ret < 0) {
-               printf("FAILED\n");
+       print_test_status(ret);
+       if (ret == CHECK_ERR)
                goto out_free_connections;
-       }
 
-       printf("OK\n");
-
-       /* After setting the policy re-check connections */
-       ret = kdbus_fork_test(bus, POLICY_NAME, conn_db);
+       printf("-- TEST 7) testing connections (FORK+DROP) again\n");
+       /*
+        * After setting the policy re-check connections
+        * we expect the childs to fail with -EPERM
+        */
+       ret = kdbus_fork_test(bus, POLICY_NAME, conn_db, -EPERM);
        printf("-- TEST 7) testing connections (FORK+DROP) again ");
-       if (ret == 0) {
-               printf("FAILED\n");
-               fprintf(stderr, "--- error policy rules: send to all 
succeeded.\n");
-               ret = -1;
-       } else if (ret < 0) {
-               /* -EPERM means tests succeeded */
-               if (ret == -EPERM) {
-                       ret = 0;
-                       printf("OK\n");
-               } else {
-                       printf("FAILED\n");
-               }
-       }
+       print_test_status(ret);
+       if (ret == CHECK_ERR)
+               goto out_free_connections;
+
+       printf("-- TEST 8) testing connections in a new user namespace\n");
+       /* Check if the name can be reached in a new userns */
+       ret = kdbus_clone_userns_test(bus, POLICY_NAME,
+                                     conn_db, -EPERM);
+       printf("-- TEST 8) testing connections in a new user namespace ");
+       print_test_status(ret);
 
 out_free_connections:
        kdbus_free_conn(activator);
@@ -413,7 +657,7 @@ int main(int argc, char *argv[])
                uint64_t n_type;
                char name[64];
        } bus_make;
-       int fdc, ret, i;
+       int fdc, ret;
        char *bus;
 
        printf("-- opening /dev/" KBUILD_MODNAME "/control\n");
@@ -451,13 +695,16 @@ int main(int argc, char *argv[])
 
        ret = kdbus_check_policy(bus);
 
-       printf("RUNNING TEST 'policy db check' ");
-       for (i = 0; i < 17; i++)
-               printf(".");
-       printf(" ");
+       printf("\nSUMMARY: %d tests passed, %d skipped%s, %d failed\n",
+               ok_cnt, skip_cnt,
+               skip_cnt ? " (need privileges)" : "", fail_cnt);
 
-       if (ret < 0) {
-               printf("FAILED\n");
+       if (skip_cnt > 0)
+               printf("For security reasons make sure to re-run skipped 
tests.\n");
+
+       printf("RUNNING TEST 'policy db check'................ ");
+       if (fail_cnt > 0) {
+               printf("Failed\n");
                return EXIT_FAILURE;
        }
 
-- 
1.9.3

_______________________________________________
systemd-devel mailing list
systemd-devel@lists.freedesktop.org
http://lists.freedesktop.org/mailman/listinfo/systemd-devel

Reply via email to