diff --git a/.gitignore b/.gitignore index c1dff8bfec..dad8be1a48 100644 --- a/.gitignore +++ b/.gitignore @@ -172,6 +172,7 @@ cscope.* /src/test/test-child /src/test/test-memwipe /src/test/test-ntor-cl +/src/test/test-switch-id /src/test/test_workqueue /src/test/test.exe /src/test/test-slow.exe @@ -179,6 +180,7 @@ cscope.* /src/test/test-child.exe /src/test/test-ntor-cl.exe /src/test/test-memwipe.exe +/src/test/test-switch-id.exe /src/test/test_workqueue.exe /src/test/test_zero_length_keys.sh /src/test/test_ntor.sh diff --git a/src/test/include.am b/src/test/include.am index d0a819fb7f..3fed3b7344 100644 --- a/src/test/include.am +++ b/src/test/include.am @@ -4,14 +4,16 @@ export SHELL=@SHELL@ export abs_top_srcdir=@abs_top_srcdir@ export builddir=@builddir@ -TESTSCRIPTS = src/test/test_zero_length_keys.sh +TESTSCRIPTS = src/test/test_zero_length_keys.sh \ + src/test/test_switch_id.sh if USEPYTHON TESTSCRIPTS += src/test/test_ntor.sh src/test/test_bt.sh endif TESTS += src/test/test src/test/test-slow src/test/test-memwipe \ - src/test/test_workqueue src/test/test_keygen.sh $(TESTSCRIPTS) + src/test/test_workqueue src/test/test_keygen.sh \ + $(TESTSCRIPTS) # These flavors are run using automake's test-driver and test-network.sh TEST_CHUTNEY_FLAVORS = basic-min bridges-min hs-min bridges+hs @@ -33,7 +35,8 @@ noinst_PROGRAMS+= \ src/test/test-slow \ src/test/test-memwipe \ src/test/test-child \ - src/test/test_workqueue + src/test/test_workqueue \ + src/test/test-switch-id endif src_test_AM_CPPFLAGS = -DSHARE_DATADIR="\"$(datadir)\"" \ @@ -131,6 +134,14 @@ src_test_test_workqueue_SOURCES = \ src_test_test_workqueue_CPPFLAGS= $(src_test_AM_CPPFLAGS) src_test_test_workqueue_CFLAGS = $(AM_CFLAGS) $(TEST_CFLAGS) +src_test_test_switch_id_SOURCES = \ + src/test/test_switch_id.c +src_test_test_switch_id_CPPFLAGS= $(src_test_AM_CPPFLAGS) +src_test_test_switch_id_CFLAGS = $(AM_CFLAGS) $(TEST_CFLAGS) +src_test_test_switch_id_LDADD = \ + src/common/libor-testing.a \ + @TOR_ZLIB_LIBS@ @TOR_LIB_MATH@ + src_test_test_LDFLAGS = @TOR_LDFLAGS_zlib@ @TOR_LDFLAGS_openssl@ \ @TOR_LDFLAGS_libevent@ src_test_test_LDADD = src/or/libtor-testing.a src/common/libor-testing.a \ diff --git a/src/test/test_switch_id.c b/src/test/test_switch_id.c new file mode 100644 index 0000000000..e85025c3b5 --- /dev/null +++ b/src/test/test_switch_id.c @@ -0,0 +1,181 @@ +/* Copyright (c) 2015, The Tor Project, Inc. */ +/* See LICENSE for licensing information */ + +#include "or.h" + +#ifdef HAVE_SYS_CAPABILITY_H +#include +#endif + +#define TEST_BUILT_WITH_CAPS 0 +#define TEST_HAVE_CAPS 1 +#define TEST_ROOT_CAN_BIND_LOW 2 +#define TEST_SETUID 3 +#define TEST_SETUID_KEEPCAPS 4 +#define TEST_SETUID_STRICT 5 + +static const char *username; + +static const struct { + const char *name; + int test_id; +} which_test[] = { + { "built-with-caps", TEST_BUILT_WITH_CAPS }, + { "have-caps", TEST_HAVE_CAPS }, + { "root-bind-low", TEST_ROOT_CAN_BIND_LOW }, + { "setuid", TEST_SETUID }, + { "setuid-keepcaps", TEST_SETUID_KEEPCAPS }, + { "setuid-strict", TEST_SETUID_STRICT }, + { NULL, 0 } +}; + +/* 0 on no, 1 on yes, -1 on failure. */ +static int +check_can_bind_low_ports(void) +{ + int port; + struct sockaddr_in sin; + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + + for (port = 600; port < 1024; ++port) { + sin.sin_port = htons(port); + tor_socket_t fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (! SOCKET_OK(fd)) { + perror("socket"); + return -1; + } + + int one = 1; + if (setsockopt(fd, SOL_SOCKET,SO_REUSEADDR, &one, sizeof(one))) { + perror("setsockopt"); + tor_close_socket_simple(fd); + return -1; + } + + int res = bind(fd, (struct sockaddr *)&sin, sizeof(sin)); + tor_close_socket_simple(fd); + + if (res == 0) { + /* bind was successful */ + return 1; + } else if (errno == EACCES || errno == EPERM) { + /* Got a permission-denied error. */ + return 0; + } else if (errno == EADDRINUSE) { + /* Huh; somebody is using that port. */ + } else { + perror("bind"); + } + } + + return -1; +} + +int +main(int argc, char **argv) +{ + const char *testname; + if (argc != 3) { + fprintf(stderr, "I want 2 arguments: a username and a command.\n"); + return 1; + } + if (getuid() != 0) { + fprintf(stderr, "This test only works when it's run as root.\n"); + return 1; + } + username = argv[1]; + testname = argv[2]; + int test_id = -1; + int i; + for (i = 0; which_test[i].name; ++i) { + if (!strcmp(which_test[i].name, testname)) { + test_id = which_test[i].test_id; + break; + } + } + if (test_id == -1) { + fprintf(stderr, "Unrecognized test '%s'\n", testname); + return 1; + } + +#ifdef HAVE_LINUX_CAPABILITIES + const int have_cap_support = 1; +#else + const int have_cap_support = 0; +#endif + + int okay; + + init_logging(1); + log_severity_list_t sev; + memset(&sev, 0, sizeof(sev)); + set_log_severity_config(LOG_WARN, LOG_ERR, &sev); + add_stream_log(&sev, "", fileno(stderr)); + + switch (test_id) + { + case TEST_BUILT_WITH_CAPS: + /* Succeed if we were built with capability support. */ + okay = have_cap_support; + break; + case TEST_HAVE_CAPS: + /* Succeed if "capabilities work" == "we were built with capability + * support." */ + okay = have_cap_support == have_capability_support(); + break; + case TEST_ROOT_CAN_BIND_LOW: + /* Succeed if root can bind low ports. */ + okay = check_can_bind_low_ports() == 1; + break; + case TEST_SETUID: + /* Succeed if we can do a setuid with no capability retention, and doing + * so makes us lose the ability to bind low ports */ + case TEST_SETUID_KEEPCAPS: + /* Succeed if we can do a setuid with capability retention, and doing so + * does not make us lose the ability to bind low ports */ + { + int keepcaps = (test_id == TEST_SETUID_KEEPCAPS); + okay = switch_id(username, keepcaps ? SWITCH_ID_KEEP_BINDLOW : 0) == 0; + if (okay) { + okay = check_can_bind_low_ports() == keepcaps; + } + break; + } + case TEST_SETUID_STRICT: + /* Succeed if, after a setuid, we cannot setuid back, and we cannot + * re-grab any capabilities. */ + okay = switch_id(username, SWITCH_ID_KEEP_BINDLOW) == 0; + if (okay) { + /* We'd better not be able to setuid back! */ + if (setuid(0) == 0 || errno != EPERM) { + okay = 0; + } + } +#ifdef HAVE_LINUX_CAPABILITIES + if (okay) { + cap_t caps = cap_get_proc(); + const cap_value_t caplist[] = { + CAP_SETUID, + }; + cap_set_flag(caps, CAP_PERMITTED, 1, caplist, CAP_SET); + if (cap_set_proc(caps) == 0 || errno != EPERM) { + okay = 0; + } + cap_free(caps); + } +#endif + break; + default: + fprintf(stderr, "Unsupported test '%s'\n", testname); + okay = 0; + break; + } + + if (!okay) { + fprintf(stderr, "Test %s failed!\n", testname); + } + + return (okay ? 0 : 1); +} + diff --git a/src/test/test_switch_id.sh b/src/test/test_switch_id.sh new file mode 100755 index 0000000000..1b4e0998b5 --- /dev/null +++ b/src/test/test_switch_id.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +if test "`id -u`" != '0'; then + echo "This test only works when run as root. Skipping." >&2 + exit 77 +fi + +if test "`id -u nobody`" = ""; then + echo "This test requires that your system have a 'nobody' user. Sorry." >&2 + exit 1 +fi + +"${builddir:-.}/src/test/test-switch-id" nobody setuid || exit 1 +"${builddir:-.}/src/test/test-switch-id" nobody root-bind-low || exit 1 +"${builddir:-.}/src/test/test-switch-id" nobody setuid-strict || exit 1 +"${builddir:-.}/src/test/test-switch-id" nobody built-with-caps || exit 0 +# ... Go beyond this point only if we were built with capability support. + +"${builddir:-.}/src/test/test-switch-id" nobody have-caps || exit 1 +"${builddir:-.}/src/test/test-switch-id" nobody setuid-keepcaps || exit 1 + + +echo "All okay" + +exit 0