summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/.gitignore8
-rw-r--r--tests/Makefile121
-rw-r--r--tests/exploit.c159
-rw-r--r--tests/libcap_launch_test.c239
-rw-r--r--tests/libcap_psx_test.c65
-rw-r--r--tests/noop.c5
-rw-r--r--tests/psx_test.c150
-rw-r--r--tests/uns_test.c171
8 files changed, 918 insertions, 0 deletions
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644
index 0000000..d0b3f15
--- /dev/null
+++ b/tests/.gitignore
@@ -0,0 +1,8 @@
+noop
+psx_test
+libcap_psx_test
+libcap_launch_test
+libcap_psx_launch_test
+exploit
+noexploit
+uns_test
diff --git a/tests/Makefile b/tests/Makefile
new file mode 100644
index 0000000..ecb7d1b
--- /dev/null
+++ b/tests/Makefile
@@ -0,0 +1,121 @@
+#
+# NOTE the built tests are all designed to be run from this
+# working directory when built DYNAMIC=yes. That is, they
+# link to the shared libraries in ../libcap/ .
+#
+topdir=$(shell pwd)/..
+include ../Make.Rules
+#
+
+all:
+ @echo leave test building to test target
+
+install:
+ @echo nothing to install from tests
+
+ifeq ($(DYNAMIC),yes)
+LINKEXTRA=-Wl,-rpath,../libcap
+DEPS=../libcap/libcap.so
+ifeq ($(PTHREADS),yes)
+DEPS += ../libcap/libpsx.so
+endif
+else
+# For this build variant override the LDFLAGS to link statically from
+# libraries within the build tree. If you never want this, use
+# make DYNAMIC=yes ...
+LDFLAGS = --static
+DEPS=../libcap/libcap.a
+ifeq ($(PTHREADS),yes)
+DEPS += ../libcap/libpsx.a
+endif
+endif
+
+../libcap/libcap.so:
+ $(MAKE) -C ../libcap libcap.so
+
+../libcap/libcap.a:
+ $(MAKE) -C ../libcap libcap.a
+
+ifeq ($(PTHREADS),yes)
+../libcap/libpsx.so:
+ $(MAKE) -C ../libcap libpsx.so
+
+../libcap/libpsx.a:
+ $(MAKE) -C ../libcap libpsx.a
+endif
+
+../progs/tcapsh-static:
+ $(MAKE) -C ../progs tcapsh-static
+
+test:
+ifeq ($(PTHREADS),yes)
+ $(MAKE) run_psx_test run_libcap_psx_test
+endif
+
+sudotest: test
+ $(MAKE) run_uns_test
+ $(MAKE) run_libcap_launch_test
+ifeq ($(PTHREADS),yes)
+ $(MAKE) run_libcap_psx_launch_test run_exploit_test
+endif
+
+# unprivileged
+run_psx_test: psx_test
+ ./psx_test
+
+psx_test: psx_test.c $(DEPS)
+ $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $< -o $@ $(LINKEXTRA) $(LIBPSXLIB)
+
+run_libcap_psx_test: libcap_psx_test
+ ./libcap_psx_test
+
+libcap_psx_test: libcap_psx_test.c $(DEPS)
+ $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $< -o $@ $(LINKEXTRA) $(LIBCAPLIB) $(LIBPSXLIB)
+
+# privileged
+uns_test: uns_test.c $(DEPS)
+ $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $< -o $@ $(LINKEXTRA) $(LIBCAPLIB)
+
+run_uns_test: uns_test
+ echo exit | $(SUDO) ./uns_test
+
+run_libcap_launch_test: libcap_launch_test noop ../progs/tcapsh-static
+ $(SUDO) ./libcap_launch_test
+
+run_libcap_psx_launch_test: libcap_psx_launch_test ../progs/tcapsh-static
+ $(SUDO) ./libcap_psx_launch_test
+
+libcap_launch_test: libcap_launch_test.c $(DEPS)
+ $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $< -o $@ $(LINKEXTRA) $(LIBCAPLIB)
+
+# This varies only slightly from the above insofar as it currently
+# only links in the pthreads fork support. TODO() we need to change
+# the source to do something interesting with pthreads.
+libcap_psx_launch_test: libcap_launch_test.c $(DEPS)
+ $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) -DWITH_PTHREADS $< -o $@ $(LINKEXTRA) $(LIBCAPLIB) $(LIBPSXLIB)
+
+
+# This test demonstrates that libpsx is needed to secure multithreaded
+# programs that link against libcap.
+run_exploit_test: exploit noexploit
+ @echo exploit should succeed
+ $(SUDO) ./exploit ; if [ $$? -ne 0 ]; then exit 0; else exit 1 ; fi
+ @echo exploit should fail
+ $(SUDO) ./noexploit ; if [ $$? -eq 0 ]; then exit 0; else exit 1 ; fi
+
+exploit: exploit.o $(DEPS)
+ $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@ $(LINKEXTRA) $(LIBCAPLIB) -lpthread
+
+# Note, for some reason, the order of libraries is important to avoid
+# the exploit working for dynamic linking.
+noexploit: exploit.o $(DEPS)
+ $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@ $(LINKEXTRA) $(LIBPSXLIB) $(LIBCAPLIB)
+
+# This one runs in a chroot with no shared library files.
+noop: noop.c
+ $(CC) $(CFLAGS) $(CPPFLAGS) $< -o $@ --static
+
+clean:
+ rm -f psx_test libcap_psx_test libcap_launch_test uns_test *~
+ rm -f libcap_launch_test libcap_psx_launch_test core noop
+ rm -f exploit noexploit exploit.o
diff --git a/tests/exploit.c b/tests/exploit.c
new file mode 100644
index 0000000..814337c
--- /dev/null
+++ b/tests/exploit.c
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2020 Andrew G Morgan <morgan@kernel.org>
+ *
+ * This program exploit demonstrates why libcap alone in a
+ * multithreaded C/C++ program is inherently vulnerable to privilege
+ * escalation.
+ *
+ * The code also serves as a demonstration of how linking with libpsx
+ * can eliminate this vulnerability by maintaining a process wide
+ * common security state.
+ *
+ * The basic idea (which is well known and why POSIX stipulates "posix
+ * semantics" for security relevant state at the abstraction of a
+ * process) is that, because of shared memory, if a single thread alone
+ * is vulnerable to code injection, then it can cause any other thread
+ * to execute arbitrary code. As such, if all but one thread drops
+ * privilege, privilege escalation is somewhat trivial.
+ */
+
+/* as per "man sigaction" */
+#define _POSIX_C_SOURCE 200809L
+
+#include <pthread.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/capability.h>
+#include <sys/types.h>
+
+/* thread coordination */
+pthread_mutex_t mu;
+pthread_cond_t cond;
+int hits;
+
+/* evidence of highest privilege attained */
+ssize_t greatest_len;
+char *text;
+
+/*
+ * interrupt handler - potentially watching for an opportunity to
+ * perform an exploit when invoked as a privileged thread.
+ */
+static void handler(int signum, siginfo_t *info, void *ignore) {
+ ssize_t length;
+ char *working;
+ pthread_mutex_lock(&mu);
+
+ cap_t caps = cap_get_proc();
+ working = cap_to_text(caps, &length);
+ if (length > greatest_len) {
+ /*
+ * This is where the exploit code might go.
+ */
+ cap_free(text);
+ text = working;
+ greatest_len = length;
+ }
+ cap_free(caps);
+ hits++;
+
+ pthread_cond_signal(&cond);
+ pthread_mutex_unlock(&mu);
+
+}
+
+/*
+ * privileged thread code (imagine it doing whatever needs privilege).
+ */
+static void *victim(void *args) {
+ pthread_mutex_lock(&mu);
+ hits = 1;
+ printf("started privileged thread\n");
+ pthread_cond_signal(&cond);
+ pthread_mutex_unlock(&mu);
+
+ pthread_mutex_lock(&mu);
+ while (hits < 2) {
+ pthread_cond_wait(&cond, &mu);
+ }
+ pthread_mutex_unlock(&mu);
+
+ return NULL;
+}
+
+int main(int argc, char **argv) {
+ pthread_t peer;
+ cap_t caps = cap_init();
+ struct sigaction sig_action;
+
+ printf("program starting\n");
+ if (pthread_create(&peer, NULL, victim, NULL)) {
+ perror("unable to start the victim thread");
+ exit(1);
+ }
+
+ /*
+ * Wait until the peer thread is fully up.
+ */
+ pthread_mutex_lock(&mu);
+ while (hits < 1) {
+ pthread_cond_wait(&cond, &mu);
+ }
+ pthread_mutex_unlock(&mu);
+
+ printf("dropping privilege from main process thread\n");
+
+ if (cap_set_proc(caps)) {
+ perror("unable to drop capabilities from main process thread");
+ exit(1);
+ }
+ cap_free(caps);
+
+ /* confirm the low privilege of the process' main thread */
+
+ caps = cap_get_proc();
+ text = cap_to_text(caps, &greatest_len);
+ cap_free(caps);
+
+ printf("no privilege in main process thread: len:%ld, caps:\"%s\"\n",
+ greatest_len, text);
+ if (greatest_len != 1) {
+ printf("failed to lower privilege as expected\n");
+ exit(1);
+ }
+
+ /*
+ * So, we have confirmed that this running thread has no
+ * privilege. From this thread we setup an interrupt handler and
+ * then trigger it on the privileged peer thread.
+ */
+
+ sig_action.sa_sigaction = &handler;
+ sigemptyset(&sig_action.sa_mask);
+ sig_action.sa_flags = SA_SIGINFO | SA_RESTART;;
+ sigaction(SIGRTMIN, &sig_action, NULL);
+
+ pthread_kill(peer, SIGRTMIN);
+
+ /*
+ * Wait for the thread to exit.
+ */
+ pthread_join(peer, NULL);
+
+ /*
+ * Let's see how we did with the exploit.
+ */
+
+ printf("greatest privilege in main process thread: len:%ld, caps:\"%s\"\n",
+ greatest_len, text);
+
+ cap_free(text);
+ if (greatest_len != 1) {
+ printf("exploit succeeded\n");
+ exit(1);
+ }
+
+ printf("exploit failed\n");
+ exit(0);
+}
diff --git a/tests/libcap_launch_test.c b/tests/libcap_launch_test.c
new file mode 100644
index 0000000..b982573
--- /dev/null
+++ b/tests/libcap_launch_test.c
@@ -0,0 +1,239 @@
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/capability.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+/*
+ * tests for cap_launch.
+ */
+
+#define MORE_THAN_ENOUGH 20
+#define NO_MORE 1
+
+struct test_case_s {
+ int pass_on;
+ const char *chroot;
+ uid_t uid;
+ gid_t gid;
+ int ngroups;
+ const gid_t groups[MORE_THAN_ENOUGH];
+ const char *args[MORE_THAN_ENOUGH];
+ const char **envp;
+ const char *iab;
+ cap_mode_t mode;
+ int launch_abort;
+ int result;
+ int (*callback_fn)(void *detail);
+};
+
+#ifdef WITH_PTHREADS
+#include <pthread.h>
+#else /* WITH_PTHREADS */
+#endif /* WITH_PTHREADS */
+
+/*
+ * clean_out drops all process capabilities.
+ */
+static int clean_out(void *data) {
+ cap_t empty;
+ empty = cap_init();
+ if (cap_set_proc(empty) != 0) {
+ _exit(1);
+ }
+ cap_free(empty);
+ return 0;
+}
+
+int main(int argc, char **argv) {
+ static struct test_case_s vs[] = {
+ {
+ .args = { "../progs/tcapsh-static", "--", "-c", "echo hello" },
+ .result = 0
+ },
+ {
+ .args = { "../progs/tcapsh-static", "--", "-c", "echo hello" },
+ .callback_fn = &clean_out,
+ .result = 0
+ },
+ {
+ .callback_fn = &clean_out,
+ .result = 0
+ },
+ {
+ .args = { "../progs/tcapsh-static", "--is-uid=123" },
+ .result = 256
+ },
+ {
+ .args = { "/", "won't", "work" },
+ .launch_abort = 1,
+ },
+ {
+ .args = { "../progs/tcapsh-static", "--is-uid=123" },
+ .uid = 123,
+ .result = 0,
+ },
+ {
+ .args = { "../progs/tcapsh-static", "--is-uid=123" },
+ .callback_fn = &clean_out,
+ .uid = 123,
+ .launch_abort = 1,
+ },
+ {
+ .args = { "../progs/tcapsh-static", "--is-gid=123" },
+ .result = 0,
+ .gid = 123,
+ .ngroups = 1,
+ .groups = { 456 },
+ .iab = "",
+ },
+ {
+ .args = { "../progs/tcapsh-static", "--dropped=cap_chown",
+ "--has-i=cap_chown" },
+ .result = 0,
+ .iab = "!%cap_chown"
+ },
+ {
+ .args = { "../progs/tcapsh-static", "--dropped=cap_chown",
+ "--has-i=cap_chown", "--is-uid=234",
+ "--has-a=cap_chown", "--has-p=cap_chown" },
+ .uid = 234,
+ .result = 0,
+ .iab = "!^cap_chown"
+ },
+ {
+ .args = { "../progs/tcapsh-static", "--inmode=NOPRIV",
+ "--has-no-new-privs" },
+ .result = 0,
+ .mode = CAP_MODE_NOPRIV
+ },
+ {
+ .args = { "/noop" },
+ .result = 0,
+ .chroot = ".",
+ },
+ {
+ .pass_on = NO_MORE
+ },
+ };
+
+ if (errno != 0) {
+ perror("unexpected initial value for errno");
+ exit(1);
+ }
+
+ cap_t orig = cap_get_proc();
+ if (orig == NULL) {
+ perror("failed to get process capabilities");
+ exit(1);
+ }
+
+ int success = 1, i;
+ for (i=0; vs[i].pass_on != NO_MORE; i++) {
+ cap_launch_t attr = NULL;
+ const struct test_case_s *v = &vs[i];
+ if (cap_launch(attr, NULL) != -1) {
+ perror("NULL launch didn't fail");
+ exit(1);
+ }
+ printf("[%d] test should %s\n", i,
+ v->result || v->launch_abort ? "generate error" : "work");
+ if (v->args[0] != NULL) {
+ attr = cap_new_launcher(v->args[0], v->args, v->envp);
+ if (attr == NULL) {
+ perror("failed to obtain launcher");
+ exit(1);
+ }
+ if (v->callback_fn != NULL) {
+ cap_launcher_callback(attr, v->callback_fn);
+ }
+ } else {
+ attr = cap_func_launcher(v->callback_fn);
+ }
+ if (v->chroot) {
+ cap_launcher_set_chroot(attr, v->chroot);
+ }
+ if (v->uid) {
+ cap_launcher_setuid(attr, v->uid);
+ }
+ if (v->gid) {
+ cap_launcher_setgroups(attr, v->gid, v->ngroups, v->groups);
+ }
+ if (v->iab) {
+ cap_iab_t iab = cap_iab_from_text(v->iab);
+ if (iab == NULL) {
+ fprintf(stderr, "[%d] failed to decode iab [%s]", i, v->iab);
+ perror(":");
+ success = 0;
+ continue;
+ }
+ cap_iab_t old = cap_launcher_set_iab(attr, iab);
+ if (cap_free(old)) {
+ fprintf(stderr, "[%d] failed to decode iab [%s]", i, v->iab);
+ perror(":");
+ success = 0;
+ continue;
+ }
+ }
+ if (v->mode) {
+ cap_launcher_set_mode(attr, v->mode);
+ }
+
+ pid_t child = cap_launch(attr, NULL);
+
+ if (child <= 0) {
+ fprintf(stderr, "[%d] failed to launch: ", i);
+ perror("");
+ if (!v->launch_abort) {
+ success = 0;
+ }
+ continue;
+ }
+ if (cap_free(attr)) {
+ fprintf(stderr, "[%d] failed to free launcher: ", i);
+ perror("");
+ success = 0;
+ }
+ int result;
+ int ret = waitpid(child, &result, 0);
+ if (ret != child) {
+ fprintf(stderr, "[%d] failed to wait: ", i);
+ perror("");
+ success = 0;
+ continue;
+ }
+ if (result != v->result) {
+ fprintf(stderr, "[%d] bad result: got=%d want=%d: ", i, result,
+ v->result);
+ perror("");
+ success = 0;
+ continue;
+ }
+ }
+
+ cap_t final = cap_get_proc();
+ if (final == NULL) {
+ perror("unable to get final capabilities");
+ exit(1);
+ }
+ if (cap_compare(orig, final)) {
+ char *was = cap_to_text(orig, NULL);
+ char *is = cap_to_text(final, NULL);
+ printf("cap_launch_test: orig:'%s' != final:'%s'\n", was, is);
+ cap_free(is);
+ cap_free(was);
+ success = 0;
+ }
+ cap_free(final);
+ cap_free(orig);
+
+ if (!success) {
+ printf("cap_launch_test: FAILED\n");
+ exit(1);
+ }
+ printf("cap_launch_test: PASSED\n");
+ exit(0);
+}
diff --git a/tests/libcap_psx_test.c b/tests/libcap_psx_test.c
new file mode 100644
index 0000000..9ef8cac
--- /dev/null
+++ b/tests/libcap_psx_test.c
@@ -0,0 +1,65 @@
+#ifndef _DEFAULT_SOURCE
+#define _DEFAULT_SOURCE
+#endif
+
+#include <errno.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/capability.h>
+#include <sys/psx_syscall.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+static void *thread_fork_exit(void *data) {
+ usleep(1234);
+ pid_t pid = fork();
+ cap_t start = cap_get_proc();
+ if (start == NULL) {
+ perror("FAILED: unable to start");
+ exit(1);
+ }
+ if (pid == 0) {
+ if (cap_set_proc(start)) {
+ perror("setting empty caps failed");
+ exit(1);
+ }
+ exit(0);
+ }
+ int res;
+ if (waitpid(pid, &res, 0) != pid || res != 0) {
+ printf("FAILED: pid=%d wait returned %d and/or error: %d\n",
+ pid, res, errno);
+ exit(1);
+ }
+ cap_set_proc(start);
+ cap_free(start);
+ return NULL;
+}
+
+int main(int argc, char **argv) {
+ int i;
+ printf("hello libcap and libpsx ");
+ fflush(stdout);
+ cap_t start = cap_get_proc();
+ if (start == NULL) {
+ perror("FAILED: to actually start");
+ exit(1);
+ }
+ pthread_t ignored[10];
+ for (i = 0; i < 10; i++) {
+ pthread_create(&ignored[i], NULL, thread_fork_exit, NULL);
+ }
+ for (i = 0; i < 10; i++) {
+ printf("."); /* because of fork, this may print double */
+ fflush(stdout); /* try to limit the above effect */
+ if (cap_set_proc(start)) {
+ perror("failed to set proc");
+ exit(1);
+ }
+ usleep(1000);
+ }
+ printf(" PASSED\n");
+ exit(0);
+}
diff --git a/tests/noop.c b/tests/noop.c
new file mode 100644
index 0000000..ad51c89
--- /dev/null
+++ b/tests/noop.c
@@ -0,0 +1,5 @@
+#include <stdlib.h>
+
+int main(int argc, char *argv[]) {
+ exit(0);
+}
diff --git a/tests/psx_test.c b/tests/psx_test.c
new file mode 100644
index 0000000..7f16175
--- /dev/null
+++ b/tests/psx_test.c
@@ -0,0 +1,150 @@
+#ifndef _DEFAULT_SOURCE
+#define _DEFAULT_SOURCE
+#endif
+
+#include <pthread.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <sys/psx_syscall.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+typedef union tp {
+ long long unsigned raw;
+ pthread_t pt;
+} thread_ptr;
+
+static void say_hello_expecting(const char *title, int n, int kept) {
+ int keeper = prctl(PR_GET_KEEPCAPS);
+ thread_ptr tp;
+ tp.pt = pthread_self();
+
+ printf("hello [%d], %s<%d> %llx (keepcaps=%d vs. want=%d)\n",
+ getpid(), title, n, tp.raw, keeper, kept);
+ if (keeper != kept) {
+ printf("--> FAILURE %s thread=%llx has wrong keepcaps: got=%d want=%d\n",
+ title, tp.raw, keeper, kept);
+ exit(1);
+ }
+}
+
+pthread_mutex_t mu;
+pthread_cond_t cond;
+
+int global_kept = 0;
+int step = 0;
+int replies = 0;
+int launched = 0;
+int started = 0;
+
+static void *say_hello(void *args) {
+ int count = 0;
+
+ pthread_mutex_lock(&mu);
+ started++;
+ int this_step = step+1;
+ pthread_cond_broadcast(&cond);
+ pthread_mutex_unlock(&mu);
+
+ pthread_mutex_lock(&mu);
+ do {
+ while (this_step > step) {
+ pthread_cond_wait(&cond, &mu);
+ }
+ say_hello_expecting("thread", count, global_kept);
+
+ replies++;
+ pthread_cond_broadcast(&cond);
+ pthread_mutex_unlock(&mu);
+
+ this_step++;
+ pthread_mutex_lock(&mu);
+ } while (++count != 3);
+ pthread_mutex_unlock(&mu);
+
+ return NULL;
+}
+
+int main(int argc, char **argv) {
+ pthread_t tid[3];
+ int i;
+ pid_t child = 0;
+ char * const stop_argv[3] = { argv[0], strdup("stop"), NULL };
+
+ if (argc != 1) {
+ printf("child %d starting\n", getpid());
+ usleep(2000);
+ printf("child %d exiting\n", getpid());
+ exit(0);
+ }
+
+ for (i = 0; i<10; i++) {
+ printf("iteration [%d]: %d\n", getpid(), i);
+
+ pthread_mutex_lock(&mu);
+ global_kept = !global_kept;
+ replies = 0;
+ step = i;
+ pthread_mutex_unlock(&mu);
+
+ psx_syscall(SYS_prctl, PR_SET_KEEPCAPS, global_kept);
+
+ pthread_mutex_lock(&mu);
+ step++;
+ pthread_cond_broadcast(&cond);
+ pthread_mutex_unlock(&mu);
+
+ say_hello_expecting("main", i, global_kept);
+
+ pthread_mutex_lock(&mu);
+ while (replies < launched) {
+ pthread_cond_wait(&cond, &mu);
+ }
+ pthread_mutex_unlock(&mu);
+
+ if (i < 3) {
+ if (!child) {
+ child = fork();
+ if (!child) {
+ usleep(2000);
+ execve(argv[0], stop_argv, NULL);
+ perror("failed to exec");
+ exit(1);
+ } else {
+ printf("pid=%d forked -> %d\n", getpid(), child);
+ }
+ }
+ launched++;
+ pthread_create(&tid[i], NULL, say_hello, NULL);
+ /* Confirm that the thread is started. */
+ pthread_mutex_lock(&mu);
+ while (started < launched) {
+ printf("[%d] started=%d vs %d\n", getpid(), started, launched);
+ pthread_cond_wait(&cond, &mu);
+ }
+ printf("[%d] started=%d vs %d\n", getpid(), started, launched);
+ pthread_cond_broadcast(&cond);
+ pthread_mutex_unlock(&mu);
+ } else if (i < 6) {
+ /* Confirm one thread has finished. */
+ pthread_join(tid[i-3], NULL);
+ launched--;
+ }
+ }
+
+ if (child) {
+ int status;
+ waitpid(child, &status, 0);
+ if (status) {
+ printf("child %d FAILED: %d\n", child, status);
+ exit(1);
+ }
+ }
+ printf("%s PASSED\n", argv[0]);
+ exit(0);
+}
diff --git a/tests/uns_test.c b/tests/uns_test.c
new file mode 100644
index 0000000..3fe73af
--- /dev/null
+++ b/tests/uns_test.c
@@ -0,0 +1,171 @@
+/*
+ * Try unsharing where we remap the root user by rotating uids (0,1,2)
+ * and the corresponding gids too.
+ */
+
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/capability.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define STACK_RESERVED 10*1024
+
+struct my_pipe {
+ int to[2];
+ int from[2];
+};
+
+static int child(void *data) {
+ struct my_pipe *fdsp = data;
+ static const char * const args[] = {"bash", NULL};
+
+ close(fdsp->to[1]);
+ close(fdsp->from[0]);
+ if (write(fdsp->from[1], "1", 1) != 1) {
+ fprintf(stderr, "failed to confirm setuid(1)\n");
+ exit(1);
+ }
+ close(fdsp->from[1]);
+
+ char datum[1];
+ if (read(fdsp->to[0], datum, 1) != 1) {
+ fprintf(stderr, "failed to wait for parent\n");
+ exit(1);
+ }
+ close(fdsp->to[0]);
+ if (datum[0] == '!') {
+ /* parent failed */
+ exit(0);
+ }
+
+ setsid();
+
+ execv("/bin/bash", (const void *) args);
+ perror("execv failed");
+ exit(1);
+}
+
+int main(int argc, char **argv)
+{
+ static const char *file_formats[] = {
+ "/proc/%d/uid_map",
+ "/proc/%d/gid_map"
+ };
+ static const char id_map[] = "0 1 1\n1 2 1\n2 0 1\n3 3 49999997\n";
+ cap_value_t fscap = CAP_SETFCAP;
+ cap_t orig = cap_get_proc();
+ cap_flag_value_t present;
+
+ if (cap_get_flag(orig, CAP_SYS_ADMIN, CAP_EFFECTIVE, &present) != 0) {
+ perror("failed to read a capability flag");
+ exit(1);
+ }
+ if (present != CAP_SET) {
+ fprintf(stderr,
+ "environment missing cap_sys_admin - exploit not testable\n");
+ exit(0);
+ }
+
+ /* Run with this one lowered */
+ cap_set_flag(orig, CAP_EFFECTIVE, 1, &fscap, CAP_CLEAR);
+
+ struct my_pipe fds;
+ if (pipe(&fds.from[0]) || pipe(&fds.to[0])) {
+ perror("no pipes");
+ exit(1);
+ }
+
+ char *stack = mmap(NULL, STACK_RESERVED, PROT_READ|PROT_WRITE,
+ MAP_ANONYMOUS|MAP_PRIVATE|MAP_STACK, -1, 0);
+ if (stack == MAP_FAILED) {
+ perror("no map for stack");
+ exit(1);
+ }
+
+ if (cap_setuid(1)) {
+ perror("failed to cap_setuid(1)");
+ exit(1);
+ }
+
+ if (cap_set_proc(orig)) {
+ perror("failed to raise caps again");
+ exit(1);
+ }
+
+ pid_t pid = clone(&child, stack+STACK_RESERVED, CLONE_NEWUSER|SIGCHLD, &fds);
+ if (pid == -1) {
+ perror("clone failed");
+ exit(1);
+ }
+
+ close(fds.from[1]);
+ close(fds.to[0]);
+
+ if (cap_setuid(0)) {
+ perror("failed to cap_setuid(0)");
+ exit(1);
+ }
+
+ if (cap_set_proc(orig)) {
+ perror("failed to raise caps again");
+ exit(1);
+ }
+
+ char datum[1];
+ if (read(fds.from[0], datum, 1) != 1 || datum[0] != '1') {
+ fprintf(stderr, "failed to read child status\n");
+ exit(1);
+ }
+ close(fds.from[0]);
+
+ int i;
+ for (i=0; i<2; i++) {
+ char *map_file;
+ if (asprintf(&map_file, file_formats[i], pid) < 0) {
+ perror("allocate string");
+ exit(1);
+ }
+
+ FILE *f = fopen(map_file, "w");
+ free(map_file);
+ if (f == NULL) {
+ perror("fopen failed");
+ exit(1);
+ }
+ int len = fwrite(id_map, 1, strlen(id_map), f);
+ if (len != strlen(id_map)) {
+ goto bailok;
+ }
+ if (fclose(f)) {
+ goto bailok;
+ }
+ }
+
+ if (write(fds.to[1], ".", 1) != 1) {
+ perror("failed to write '.'");
+ exit(1);
+ }
+ close(fds.to[1]);
+
+ fprintf(stderr, "user namespace launched exploit worked - upgrade kernel\n");
+ if (wait(NULL) == pid) {
+ exit(1);
+ }
+ perror("launch failed");
+ exit(1);
+
+bailok:
+ fprintf(stderr, "exploit attempt failed\n");
+ if (write(fds.to[1], "!", 1) != 1) {
+ perror("failed to inform child [ignored]");
+ }
+ exit(0);
+}