From patchwork Wed Apr 6 16:02:52 2016 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Stephen Smalley X-Patchwork-Id: 8763611 Return-Path: X-Original-To: patchwork-selinux@patchwork.kernel.org Delivered-To: patchwork-parsemail@patchwork2.web.kernel.org Received: from mail.kernel.org (mail.kernel.org [198.145.29.136]) by patchwork2.web.kernel.org (Postfix) with ESMTP id 5E2D7C0553 for ; Wed, 6 Apr 2016 16:15:53 +0000 (UTC) Received: from mail.kernel.org (localhost [127.0.0.1]) by mail.kernel.org (Postfix) with ESMTP id E594A201F2 for ; Wed, 6 Apr 2016 16:15:51 +0000 (UTC) Received: from emvm-gh1-uea08.nsa.gov (smtp.nsa.gov [8.44.101.8]) (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) (No client certificate requested) by mail.kernel.org (Postfix) with ESMTPS id 1156620121 for ; Wed, 6 Apr 2016 16:15:49 +0000 (UTC) X-TM-IMSS-Message-ID: <0f6706680008ea02@nsa.gov> Received: from tarius.tycho.ncsc.mil ([144.51.242.1]) by nsa.gov ([10.208.42.193]) with ESMTP (TREND IMSS SMTP Service 7.1) id 0f6706680008ea02 ; Wed, 6 Apr 2016 12:02:27 -0400 Received: from prometheus.infosec.tycho.ncsc.mil (prometheus [192.168.25.40]) by tarius.tycho.ncsc.mil (8.14.4/8.14.4) with ESMTP id u36G335n012758; Wed, 6 Apr 2016 12:03:09 -0400 Received: from tarius.tycho.ncsc.mil (tarius.infosec.tycho.ncsc.mil [144.51.242.1]) by prometheus.infosec.tycho.ncsc.mil (8.15.2/8.15.2) with ESMTP id u36G32Ye045719 for ; Wed, 6 Apr 2016 12:03:02 -0400 Received: from goalie.tycho.ncsc.mil (goalie [144.51.242.250]) by tarius.tycho.ncsc.mil (8.14.4/8.14.4) with ESMTP id u36G31Ib012701; Wed, 6 Apr 2016 12:03:01 -0400 X-IronPort-Anti-Spam-Filtered: true X-IronPort-Anti-Spam-Result: A1C5AwBVMgVXcincVdFcHAGEaq9OiG6ED4YNAoIVAQEBAQEBEwEKCxQfhHMBAQEDEhEECwENARsdAQMMBgMNDwISFAICIxEBBQEcBhMih28BAxKSBI9BgTE+MYs2gWqCV4dKChknDVGEQwEBAQEBAQEDAQEBAQEBARMBBQoFbYMcggmJUgyCLIJWBY5BiUCBU4cVhSOBZ4dEMYUwAoN/gh2HSC+BDoJZDRmBakyHP4E0AQEB X-IPAS-Result: A1C5AwBVMgVXcincVdFcHAGEaq9OiG6ED4YNAoIVAQEBAQEBEwEKCxQfhHMBAQEDEhEECwENARsdAQMMBgMNDwISFAICIxEBBQEcBhMih28BAxKSBI9BgTE+MYs2gWqCV4dKChknDVGEQwEBAQEBAQEDAQEBAQEBARMBBQoFbYMcggmJUgyCLIJWBY5BiUCBU4cVhSOBZ4dEMYUwAoN/gh2HSC+BDoJZDRmBakyHP4E0AQEB X-IronPort-AV: E=Sophos;i="5.24,447,1454994000"; d="scan'208";a="5355591" Received: from emvm-gh1-uea08.nsa.gov ([10.208.42.193]) by goalie.tycho.ncsc.mil with ESMTP; 06 Apr 2016 12:02:59 -0400 X-TM-IMSS-Message-ID: <0f6681ed0008e98f@nsa.gov> Received: from mail-pa0-f41.google.com (mail-pa0-f41.google.com [209.85.220.41]) by nsa.gov ([10.208.42.193]) with ESMTP (TREND IMSS SMTP Service 7.1; TLSv1/SSLv3 AES128-SHA (128/128)) id 0f6681ed0008e98f ; Wed, 6 Apr 2016 12:01:53 -0400 Received: by mail-pa0-f41.google.com with SMTP id zm5so35902853pac.0; Wed, 06 Apr 2016 09:02:56 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20120113; h=message-id:subject:from:to:cc:date:in-reply-to:references :mime-version:content-transfer-encoding; bh=sPYE3oKR7HnvJDAEDTT99XOu3ZtmgX3R2nBi8hceStU=; b=kFratQ7mnvWXrdGjMgj0KDwC85CiA7iBy+DUNKwUVkjhFmQrB9xgE92AyL518XsYe+ bu35oNe9RUws8DDLSHI902wjN1xsZJ5yl5A3K9zwvBvDHspKlYyOuY4nwRuZFG1/sG9Z nLd0tos0zabOWPbbRsVHEtIHUV6EAHpaBLtFt3GH4etJGNhlgnPsvKLlzZ2jbfsAOaU/ 9n/el37g48KKvJiiTBDhVSD2B0oNteqCYvw0ua4FAbYKTv/erYRxO0lpvGTPm2UWgZhW E3cMYRbW+s5kXWDYUszISCmvvCuzkb6bC60cjmLtukVZfi97GGMEXXvk2sJTIXUu+KZc lXfw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20130820; h=x-gm-message-state:message-id:subject:from:to:cc:date:in-reply-to :references:mime-version:content-transfer-encoding; bh=sPYE3oKR7HnvJDAEDTT99XOu3ZtmgX3R2nBi8hceStU=; b=hGSZJJV9sRcSy+3kC6UeblpEgPNSQiCN4tePknnLGAHH9Q/z3RQycDIioPh/LVx+qs qZSeOOp+5dyGMdiQPmWCcDVr16tpxTHzWC4u75WX9e8csdy8gAYQAR1OwFWqxBnTW3la 1mt/B04jMU9AUWjykPnxH3bBVyZYYkXyXyLo/tB0a1X88gGn0KS+L5lRdR9I/pCOQLgS XQV+zF2IqKlX0J1K6NiUNmvq7oK2zPl2XS+UAH5vrS9KvzRjbAkR1KmoytkvJ/fBu81N ZQg5lzOJrcXPOEF2PPDx1B+ktbVtiHpGr01+xAKUdIHeZTA5J2RPrQMF0cbyVH5ZM312 cdDQ== X-Gm-Message-State: AD7BkJIOjI8GJoP87b6zq4G9dY/gQKiViL1dfon443471oXZNofFYhOHkUE6MBe5zPoJTA== X-Received: by 10.66.157.129 with SMTP id wm1mr53858929pab.159.1459958575726; Wed, 06 Apr 2016 09:02:55 -0700 (PDT) Received: from moss-charon.infosec.tycho.ncsc.mil ([209.65.105.100]) by smtp.googlemail.com with ESMTPSA id b82sm5971289pfd.89.2016.04.06.09.02.54 (version=TLSv1/SSLv3 cipher=OTHER); Wed, 06 Apr 2016 09:02:55 -0700 (PDT) Message-ID: <1459958572.7680.6.camel@gmail.com> Subject: [RFC][PATCH] selinux-testsuite: Add tests for non-init userns capability checks From: Stephen Smalley To: selinux Date: Wed, 06 Apr 2016 09:02:52 -0700 In-Reply-To: <1459958221.7680.2.camel@gmail.com> References: <1459958221.7680.2.camel@gmail.com> X-Mailer: Evolution 3.20.0 (3.20.0-1.fc25) Mime-Version: 1.0 X-TM-AS-MML: disable X-BeenThere: selinux@tycho.nsa.gov X-Mailman-Version: 2.1.20 Precedence: list List-Id: "Security-Enhanced Linux \(SELinux\) mailing list" List-Post: List-Help: Cc: Stephen Smalley Errors-To: selinux-bounces@tycho.nsa.gov Sender: "Selinux" X-Spam-Status: No, score=-2.8 required=5.0 tests=BAYES_00, DKIM_ADSP_CUSTOM_MED, DKIM_SIGNED, FREEMAIL_FROM, RP_MATCHES_RCVD, T_DKIM_INVALID, UNPARSEABLE_RELAY autolearn=ham version=3.3.1 X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on mail.kernel.org X-Virus-Scanned: ClamAV using ClamSMTP Add tests for the non-init user namespace capability checks. The tests depend on the previously posted kernel patch and on a patch for refpolicy to define the new security class. Signed-off-by: Stephen Smalley ---  policy/Makefile                      |   2 +-  policy/test_cap_userns.te            |  27 ++++  tests/Makefile                       |   2 +-  tests/cap_userns/Makefile            |   5 +  tests/cap_userns/test                |  17 ++  tests/cap_userns/userns_child_exec.c | 298 +++++++++++++++++++++++++++++++++++  6 files changed, 349 insertions(+), 2 deletions(-)  create mode 100644 policy/test_cap_userns.te  create mode 100644 tests/cap_userns/Makefile  create mode 100755 tests/cap_userns/test  create mode 100644 tests/cap_userns/userns_child_exec.c --  2.8.0 diff --git a/policy/Makefile b/policy/Makefile index 98fccbc..33f3458 100644 --- a/policy/Makefile +++ b/policy/Makefile @@ -20,7 +20,7 @@ TARGETS = \   test_task_create.te test_task_getpgid.te test_task_getsched.te \   test_task_getsid.te test_task_setpgid.te test_task_setsched.te \   test_transition.te test_inet_socket.te test_unix_socket.te \ - test_wait.te test_mmap.te + test_wait.te test_mmap.te test_cap_userns.te    ifeq ($(shell [ $(POL_VERS) -ge 24 ] && echo true),true)  TARGETS += test_bounds.te diff --git a/policy/test_cap_userns.te b/policy/test_cap_userns.te new file mode 100644 index 0000000..ab74325 --- /dev/null +++ b/policy/test_cap_userns.te @@ -0,0 +1,27 @@ +################################# +# +# Policy for testing non-init userns capability checking. +# + +attribute capusernsdomain; + +# Domain for process that is allowed non-init userns capabilities +type test_cap_userns_t; +domain_type(test_cap_userns_t) +unconfined_runs_test(test_cap_userns_t) +typeattribute test_cap_userns_t testdomain; +typeattribute test_cap_userns_t capusernsdomain; + +# This domain is allowed sys_admin on non-init userns for mount. +allow test_cap_userns_t self:cap_userns sys_admin; + +# Domain for process that is not allowed non-init userns capabilities +type test_no_cap_userns_t; +domain_type(test_no_cap_userns_t) +unconfined_runs_test(test_no_cap_userns_t) +typeattribute test_no_cap_userns_t testdomain; +typeattribute test_no_cap_userns_t capusernsdomain; + +# Rules common to both domains. +miscfiles_domain_entry_test_files(capusernsdomain) +corecmd_exec_bin(capusernsdomain) diff --git a/tests/Makefile b/tests/Makefile index 7a9b39c..bf3f946 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -5,7 +5,7 @@ DISTRO=$(shell ./os_detect)    SUBDIRS_COMMON:=domain_trans entrypoint execshare exectrace execute_no_trans fdreceive inherit link mkdir msg open ptrace readlink relabel rename rxdir sem setattr setnice shm sigkill stat sysctl task_create task_setnice task_setscheduler task_getscheduler task_getsid task_getpgid task_setpgid wait file ioctl capable_file capable_net capable_sys   -SUBDIRS:= $(SUBDIRS_COMMON) dyntrans dyntrace bounds nnp mmap unix_socket inet_socket +SUBDIRS:= $(SUBDIRS_COMMON) dyntrans dyntrace bounds nnp mmap unix_socket inet_socket cap_userns    ifeq ($(DISTRO),RHEL4)      SUBDIRS:=$(SUBDIRS_COMMON) diff --git a/tests/cap_userns/Makefile b/tests/cap_userns/Makefile new file mode 100644 index 0000000..27b4676 --- /dev/null +++ b/tests/cap_userns/Makefile @@ -0,0 +1,5 @@ +TARGETS=userns_child_exec + +all: $(TARGETS) +clean: + rm -f $(TARGETS) diff --git a/tests/cap_userns/test b/tests/cap_userns/test new file mode 100755 index 0000000..5842ebd --- /dev/null +++ b/tests/cap_userns/test @@ -0,0 +1,17 @@ +#!/usr/bin/perl + +use Test; +BEGIN { plan tests => 2} + +$basedir = $0;  $basedir =~ s|(.*)/[^/]*|$1|; + +# Verify that test_cap_userns_t can mount proc within its own mount namespace. + +$result = system ("runcon -t test_cap_userns_t -- $basedir/userns_child_exec -p -m -U -M '0 0 1' -G '0 0 1' -- true 2>&1"); +ok($result, 0); + +# Verify that test_no_cap_userns_t cannot mount proc within its own mount namespace. + +$result = system ("runcon -t test_no_cap_userns_t -- $basedir/userns_child_exec -p -m -U -M '0 0 1' -G '0 0 1' -- true 2>&1"); +ok($result); + diff --git a/tests/cap_userns/userns_child_exec.c b/tests/cap_userns/userns_child_exec.c new file mode 100644 index 0000000..26ea357 --- /dev/null +++ b/tests/cap_userns/userns_child_exec.c @@ -0,0 +1,298 @@ +/* Taken from the user_namespaces.7 man page */ + +/* userns_child_exec.c + +   Licensed under GNU General Public License v2 or later + +   Create a child process that executes a shell command in new +   namespace(s); allow UID and GID mappings to be specified when +   creating a user namespace. +*/ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* A simple error-handling function: print an error message based +   on the value in 'errno' and terminate the calling process */ + +#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \ +                        } while (0) + +struct child_args { +    char **argv;        /* Command to be executed by child, with args */ +    int    pipe_fd[2];  /* Pipe used to synchronize parent and child */ +}; + +static int verbose; + +static void +usage(char *pname) +{ +    fprintf(stderr, "Usage: %s [options] cmd [arg...]\n\n", pname); +    fprintf(stderr, "Create a child process that executes a shell " +            "command in a new user namespace,\n" +            "and possibly also other new namespace(s).\n\n"); +    fprintf(stderr, "Options can be:\n\n"); +#define fpe(str) fprintf(stderr, "    %s", str); +    fpe("-i          New IPC namespace\n"); +    fpe("-m          New mount namespace\n"); +    fpe("-n          New network namespace\n"); +    fpe("-p          New PID namespace\n"); +    fpe("-u          New UTS namespace\n"); +    fpe("-U          New user namespace\n"); +    fpe("-M uid_map  Specify UID map for user namespace\n"); +    fpe("-G gid_map  Specify GID map for user namespace\n"); +    fpe("-z          Map user's UID and GID to 0 in user namespace\n"); +    fpe("            (equivalent to: -M '0 1' -G '0 1'\n"); +    fpe("-v          Display verbose messages\n"); +    fpe("\n"); +    fpe("If -z, -M, or -G is specified, -U is required.\n"); +    fpe("It is not permitted to specify both -z and either -M or -G.\n"); +    fpe("\n"); +    fpe("Map strings for -M and -G consist of records of the form:\n"); +    fpe("\n"); +    fpe("    ID-inside-ns   ID-outside-ns   len\n"); +    fpe("\n"); +    fpe("A map string can contain multiple records, separated" +        " by commas;\n"); +    fpe("the commas are replaced by newlines before writing" +        " to map files.\n"); + +    exit(EXIT_FAILURE); +} + +/* Update the mapping file 'map_file', with the value provided in +   'mapping', a string that defines a UID or GID mapping. A UID or +   GID mapping consists of one or more newline-delimited records +   of the form: + +       ID_inside-ns    ID-outside-ns   length + +   Requiring the user to supply a string that contains newlines is +   of course inconvenient for command-line use. Thus, we permit the +   use of commas to delimit records in this string, and replace them +   with newlines before writing the string to the file. */ + +static void +update_map(char *mapping, char *map_file) +{ +    int fd, j; +    size_t map_len;     /* Length of 'mapping' */ + +    /* Replace commas in mapping string with newlines */ + +    map_len = strlen(mapping); +    for (j = 0; j < map_len; j++) +        if (mapping[j] == ',') +            mapping[j] = '\n'; + +    fd = open(map_file, O_RDWR); +    if (fd == -1) { +        fprintf(stderr, "ERROR: open %s: %s\n", map_file, +                strerror(errno)); +        exit(EXIT_FAILURE); +    } + +    if (write(fd, mapping, map_len) != map_len) { +        fprintf(stderr, "ERROR: write %s: %s\n", map_file, +                strerror(errno)); +        exit(EXIT_FAILURE); +    } + +    close(fd); +} + +/* Linux 3.19 made a change in the handling of setgroups(2) and the +   'gid_map' file to address a security issue. The issue allowed +   *unprivileged* users to employ user namespaces in order to drop +   The upshot of the 3.19 changes is that in order to update the +   'gid_maps' file, use of the setgroups() system call in this +   user namespace must first be disabled by writing "deny" to one of +   the /proc/PID/setgroups files for this namespace.  That is the +   purpose of the following function. */ + +static void +proc_setgroups_write(pid_t child_pid, char *str) +{ +    char setgroups_path[PATH_MAX]; +    int fd; + +    snprintf(setgroups_path, PATH_MAX, "/proc/%ld/setgroups", +            (long) child_pid); + +    fd = open(setgroups_path, O_RDWR); +    if (fd == -1) { + +        /* We may be on a system that doesn't support +           /proc/PID/setgroups. In that case, the file won't exist, +           and the system won't impose the restrictions that Linux 3.19 +           added. That's fine: we don't need to do anything in order +           to permit 'gid_map' to be updated. + +           However, if the error from open() was something other than +           the ENOENT error that is expected for that case,  let the +           user know. */ + +        if (errno != ENOENT) +            fprintf(stderr, "ERROR: open %s: %s\n", setgroups_path, +                strerror(errno)); +        return; +    } + +    if (write(fd, str, strlen(str)) == -1) +        fprintf(stderr, "ERROR: write %s: %s\n", setgroups_path, +            strerror(errno)); + +    close(fd); +} + +static int              /* Start function for cloned child */ +childFunc(void *arg) +{ +    struct child_args *args = (struct child_args *) arg; +    char ch; + +    /* Wait until the parent has updated the UID and GID mappings. +       See the comment in main(). We wait for end of file on a +       pipe that will be closed by the parent process once it has +       updated the mappings. */ + +    close(args->pipe_fd[1]);    /* Close our descriptor for the write +                                   end of the pipe so that we see EOF +                                   when parent closes its descriptor */ +    if (read(args->pipe_fd[0], &ch, 1) != 0) { +        fprintf(stderr, +                "Failure in child: read from pipe returned != 0\n"); +        exit(EXIT_FAILURE); +    } + +    /* Execute a shell command */ + +    printf("About to exec %s\n", args->argv[0]); +    execvp(args->argv[0], args->argv); +    errExit("execvp"); +} + +#define STACK_SIZE (1024 * 1024) + +static char child_stack[STACK_SIZE];    /* Space for child's stack */ + +int +main(int argc, char *argv[]) +{ +    int flags, opt, map_zero; +    pid_t child_pid; +    struct child_args args; +    char *uid_map, *gid_map; +    const int MAP_BUF_SIZE = 100; +    char map_buf[MAP_BUF_SIZE]; +    char map_path[PATH_MAX]; + +    /* Parse command-line options. The initial '+' character in +       the final getopt() argument prevents GNU-style permutation +       of command-line options. That's useful, since sometimes +       the 'command' to be executed by this program itself +       has command-line options. We don't want getopt() to treat +       those as options to this program. */ + +    flags = 0; +    verbose = 0; +    gid_map = NULL; +    uid_map = NULL; +    map_zero = 0; +    while ((opt = getopt(argc, argv, "+imnpuUM:G:zv")) != -1) { +        switch (opt) { +        case 'i': flags |= CLONE_NEWIPC;        break; +        case 'm': flags |= CLONE_NEWNS;         break; +        case 'n': flags |= CLONE_NEWNET;        break; +        case 'p': flags |= CLONE_NEWPID;        break; +        case 'u': flags |= CLONE_NEWUTS;        break; +        case 'v': verbose = 1;                  break; +        case 'z': map_zero = 1;                 break; +        case 'M': uid_map = optarg;             break; +        case 'G': gid_map = optarg;             break; +        case 'U': flags |= CLONE_NEWUSER;       break; +        default:  usage(argv[0]); +        } +    } + +    /* -M or -G without -U is nonsensical */ + +    if (((uid_map != NULL || gid_map != NULL || map_zero) && +                !(flags & CLONE_NEWUSER)) || +            (map_zero && (uid_map != NULL || gid_map != NULL))) +        usage(argv[0]); + +    args.argv = &argv[optind]; + +    /* We use a pipe to synchronize the parent and child, in order to +       ensure that the parent sets the UID and GID maps before the child +       calls execve(). This ensures that the child maintains its +       capabilities during the execve() in the common case where we +       want to map the child's effective user ID to 0 in the new user +       namespace. Without this synchronization, the child would lose +       its capabilities if it performed an execve() with nonzero +       user IDs (see the capabilities(7) man page for details of the +       transformation of a process's capabilities during execve()). */ + +    if (pipe(args.pipe_fd) == -1) +        errExit("pipe"); + +    /* Create the child in new namespace(s) */ + +    child_pid = clone(childFunc, child_stack + STACK_SIZE, +                      flags | SIGCHLD, &args); +    if (child_pid == -1) +        errExit("clone"); + +    /* Parent falls through to here */ + +    if (verbose) +        printf("%s: PID of child created by clone() is %ld\n", +                argv[0], (long) child_pid); + +    /* Update the UID and GID maps in the child */ + +    if (uid_map != NULL || map_zero) { +        snprintf(map_path, PATH_MAX, "/proc/%ld/uid_map", +                (long) child_pid); +        if (map_zero) { +            snprintf(map_buf, MAP_BUF_SIZE, "0 %ld 1", (long) getuid()); +            uid_map = map_buf; +        } +        update_map(uid_map, map_path); +    } + +    if (gid_map != NULL || map_zero) { +        proc_setgroups_write(child_pid, "deny"); + +        snprintf(map_path, PATH_MAX, "/proc/%ld/gid_map", +                (long) child_pid); +        if (map_zero) { +            snprintf(map_buf, MAP_BUF_SIZE, "0 %ld 1", (long) getgid()); +            gid_map = map_buf; +        } +        update_map(gid_map, map_path); +    } + +    /* Close the write end of the pipe, to signal to the child that we +       have updated the UID and GID maps */ + +    close(args.pipe_fd[1]); + +    if (waitpid(child_pid, NULL, 0) == -1)      /* Wait for child */ +        errExit("waitpid"); + +    if (verbose) +        printf("%s: terminating\n", argv[0]); + +    exit(EXIT_SUCCESS); +}