From patchwork Thu Jun 1 17:44:32 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Adhemerval Zanella Netto X-Patchwork-Id: 70470 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from server2.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 0406A3857735 for ; Thu, 1 Jun 2023 17:45:08 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 0406A3857735 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=sourceware.org; s=default; t=1685641508; bh=V9PQ9Hccj1BrqhjSbjcRX1HrkkNht2ErqxXLP8TeT9M=; h=To:Subject:Date:List-Id:List-Unsubscribe:List-Archive:List-Post: List-Help:List-Subscribe:From:Reply-To:From; b=wUcDNU5mmazPaTHY6c0F56qdxs38QJoR5/K7OogE4PaRu1rDsbU6tYf/JSayCu5Ss tTdIBmXMfqFNSALFTkBkioHzWAOKrKm0gH4iR1wlcPDrMAdm6rVKxuVW8wgsJfYS05 eZOBlU/7Bzq/TzlY3840aSIckZ4My3iB6e6X5894= X-Original-To: libc-alpha@sourceware.org Delivered-To: libc-alpha@sourceware.org Received: from mail-oo1-xc35.google.com (mail-oo1-xc35.google.com [IPv6:2607:f8b0:4864:20::c35]) by sourceware.org (Postfix) with ESMTPS id 56FEC3858404 for ; Thu, 1 Jun 2023 17:44:38 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 56FEC3858404 Received: by mail-oo1-xc35.google.com with SMTP id 006d021491bc7-55554ab909cso811976eaf.2 for ; Thu, 01 Jun 2023 10:44:38 -0700 (PDT) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20221208; t=1685641477; x=1688233477; h=content-transfer-encoding:mime-version:message-id:date:subject:to :from:x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=V9PQ9Hccj1BrqhjSbjcRX1HrkkNht2ErqxXLP8TeT9M=; b=gWYl9Dmr4bi6LjUdThpwQRNB8SM96DIjDd+OUJBxqRc/hG/3ASFADVx0ocIWjEI3Qu lZAjlN1aL1WQVTIR57ZPwcmRf6T/jHunihQ1/lGX1d93OOfMMuZSGa0yBsNqGDqBeSK0 Mvfh+/6bo65Cw+g2b2PhEXdShBhJeVI7z8WFsvC32YuoIfF82RIAEZ1Gmnib77olgWvz 8TITtKVnhMcGXFsjPIhO9CVTqBZJwZv0jdxiODdhyfuSHvvY/s13gPycdNg1ateo4nY6 jVrIcMQI+DOkNJVZ7oeww2hhPj3mQGtAfjtr1OYUXMk8vPNZL4Mz85dB+HYzL7bYRd49 yi/w== X-Gm-Message-State: AC+VfDwNYomIZv9o9/ecpWgme0DoFil2Th0Q3Dce7u38Wu9NJD6+5pal b/35ObwbtOid6Pvv2bnlaAPxYosHbHT3VRaONTWvIw== X-Google-Smtp-Source: ACHHUZ6fl1Qm+EF5bglOWyxx4K2it3owbA226l0/A+sOGXnL3p9MedbfVbZOP0xVb/0ln9Z/W4M1MQ== X-Received: by 2002:a4a:45c7:0:b0:555:991b:b340 with SMTP id y190-20020a4a45c7000000b00555991bb340mr5397283ooa.6.1685641476294; Thu, 01 Jun 2023 10:44:36 -0700 (PDT) Received: from mandiga.. ([2804:1b3:a7c1:4dd5:aab5:9e8c:dfc8:7cd]) by smtp.gmail.com with ESMTPSA id r11-20020a056820038b00b00555a00f3dd4sm7470721ooj.42.2023.06.01.10.44.34 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 01 Jun 2023 10:44:35 -0700 (PDT) To: libc-alpha@sourceware.org Subject: [PATCH] linux: Split tst-ttyname Date: Thu, 1 Jun 2023 14:44:32 -0300 Message-Id: <20230601174432.456518-1-adhemerval.zanella@linaro.org> X-Mailer: git-send-email 2.34.1 MIME-Version: 1.0 X-Spam-Status: No, score=-12.9 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, GIT_PATCH_0, KAM_SHORT, RCVD_IN_DNSWL_NONE, SPF_HELO_NONE, SPF_PASS, TXREP, T_SCC_BODY_TEXT_LINE autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on server2.sourceware.org X-BeenThere: libc-alpha@sourceware.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Libc-alpha mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-Patchwork-Original-From: Adhemerval Zanella via Libc-alpha From: Adhemerval Zanella Netto Reply-To: Adhemerval Zanella Errors-To: libc-alpha-bounces+patchwork=sourceware.org@sourceware.org Sender: "Libc-alpha" The tst-ttyname may fail in container environments while trying to mount the procfs after the unshare (test do_in_chroot_2). It is not clear why exaclty kernel returns EPERM in this case, the container does have CAP_SYS_CHROOT, SELinux/AppArmor is disabled, and there is no seccomp filter. To avoid always reporting the test as FAIL in such scenario, the test that uses new namespaces is moved to a new one and the failure on the mount command is now report as UNSUPPORTED. Checked on x86_64-linux-gnu and aarch64-linux-gnu. Tested-by: Xi Ruoyao --- sysdeps/unix/sysv/linux/Makefile | 1 + sysdeps/unix/sysv/linux/tst-ttyname-common.c | 416 ++++++++++++++ .../unix/sysv/linux/tst-ttyname-namespace.c | 147 +++++ sysdeps/unix/sysv/linux/tst-ttyname.c | 512 +----------------- 4 files changed, 567 insertions(+), 509 deletions(-) create mode 100644 sysdeps/unix/sysv/linux/tst-ttyname-common.c create mode 100644 sysdeps/unix/sysv/linux/tst-ttyname-namespace.c diff --git a/sysdeps/unix/sysv/linux/Makefile b/sysdeps/unix/sysv/linux/Makefile index 594a5dc53e..61b9e4bef0 100644 --- a/sysdeps/unix/sysv/linux/Makefile +++ b/sysdeps/unix/sysv/linux/Makefile @@ -225,6 +225,7 @@ tests += \ tst-tgkill \ tst-timerfd \ tst-ttyname \ + tst-ttyname-namespace \ # tests # process_madvise requires CAP_SYS_ADMIN. diff --git a/sysdeps/unix/sysv/linux/tst-ttyname-common.c b/sysdeps/unix/sysv/linux/tst-ttyname-common.c new file mode 100644 index 0000000000..3f6d8ee944 --- /dev/null +++ b/sysdeps/unix/sysv/linux/tst-ttyname-common.c @@ -0,0 +1,416 @@ +/* Common definitions for ttyname tests. + Copyright (C) 2017-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of the + License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; see the file COPYING.LIB. If + not, see . */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +/* generic utilities */ + +#define VERIFY(expr) \ + do { \ + if (!(expr)) \ + { \ + printf ("error: %s:%d: %s: %m\n", \ + __FILE__, __LINE__, #expr); \ + exit (1); \ + } \ + } while (0) + +static void +touch (const char *path, mode_t mode) +{ + xclose (xopen (path, O_WRONLY|O_CREAT|O_NOCTTY, mode)); +} + +static size_t +trim_prefix (char *str, size_t str_len, const char *prefix) +{ + size_t prefix_len = strlen (prefix); + if (str_len > prefix_len && memcmp (str, prefix, prefix_len) == 0) + { + memmove (str, str + prefix_len, str_len - prefix_len); + return str_len - prefix_len; + } + return str_len; +} + +/* returns a pointer to static storage */ +static char * +proc_fd_readlink (const char *linkname) +{ + static char target[PATH_MAX+1]; + ssize_t target_len = readlink (linkname, target, PATH_MAX); + VERIFY (target_len > 0); + target_len = trim_prefix (target, target_len, "(unreachable)"); + target[target_len] = '\0'; + return target; +} + +/* plain ttyname runner */ + +struct result +{ + const char *name; + int err; +}; + +/* strings in result structure are in static storage */ +static struct result +run_ttyname (int fd) +{ + struct result ret; + errno = 0; + ret.name = ttyname (fd); + ret.err = errno; + return ret; +} + +static bool +eq_ttyname (struct result actual, struct result expected) +{ + char *actual_name, *expected_name; + + if ((actual.err == expected.err) + && (!actual.name == !expected.name) + && (actual.name ? strcmp (actual.name, expected.name) == 0 : true)) + { + if (expected.name) + expected_name = xasprintf ("\"%s\"", expected.name); + else + expected_name = xstrdup ("NULL"); + + printf ("info: ttyname: PASS {name=%s, errno=%d}\n", + expected_name, expected.err); + + free (expected_name); + return true; + } + + if (actual.name) + actual_name = xasprintf ("\"%s\"", actual.name); + else + actual_name = xstrdup ("NULL"); + + if (expected.name) + expected_name = xasprintf ("\"%s\"", expected.name); + else + expected_name = xstrdup ("NULL"); + + printf ("error: ttyname: actual {name=%s, errno=%d} != expected {name=%s, errno=%d}\n", + actual_name, actual.err, + expected_name, expected.err); + + free (actual_name); + free (expected_name); + return false; +} + +/* ttyname_r runner */ + +struct result_r +{ + const char *name; + int ret; + int err; +}; + +/* strings in result structure are in static storage */ +static struct result_r +run_ttyname_r (int fd) +{ + static char buf[TTY_NAME_MAX]; + + struct result_r ret; + errno = 0; + ret.ret = ttyname_r (fd, buf, TTY_NAME_MAX); + ret.err = errno; + if (ret.ret == 0) + ret.name = buf; + else + ret.name = NULL; + return ret; +} + +static bool +eq_ttyname_r (struct result_r actual, struct result_r expected) +{ + char *actual_name, *expected_name; + + if ((actual.err == expected.err) + && (actual.ret == expected.ret) + && (!actual.name == !expected.name) + && (actual.name ? strcmp (actual.name, expected.name) == 0 : true)) + { + if (expected.name) + expected_name = xasprintf ("\"%s\"", expected.name); + else + expected_name = xstrdup ("NULL"); + + printf ("info: ttyname_r: PASS {name=%s, ret=%d, errno=%d}\n", + expected_name, expected.ret, expected.err); + + free (expected_name); + return true; + } + + if (actual.name) + actual_name = xasprintf ("\"%s\"", actual.name); + else + actual_name = xstrdup ("NULL"); + + if (expected.name) + expected_name = xasprintf ("\"%s\"", expected.name); + else + expected_name = xstrdup ("NULL"); + + printf ("error: ttyname_r: actual {name=%s, ret=%d, errno=%d} != expected {name=%s, ret=%d, errno=%d}\n", + actual_name, actual.ret, actual.err, + expected_name, expected.ret, expected.err); + + free (actual_name); + free (expected_name); + return false; +} + +/* combined runner */ + +static bool +doit (int fd, const char *testname, struct result_r expected_r) +{ + struct result expected = {.name=expected_r.name, .err=expected_r.ret}; + bool ret = true; + + printf ("info: testcase: %s\n", testname); + + if (!eq_ttyname (run_ttyname (fd), expected)) + ret = false; + if (!eq_ttyname_r (run_ttyname_r (fd), expected_r)) + ret = false; + + if (!ret) + support_record_failure (); + + return ret; +} + +/* chroot setup */ + +static char *chrootdir; + +static void +prepare (int argc, char **argv) +{ + chrootdir = xasprintf ("%s/tst-ttyname-XXXXXX", test_dir); + if (mkdtemp (chrootdir) == NULL) + FAIL_EXIT1 ("mkdtemp (\"%s\"): %m", chrootdir); + add_temp_file (chrootdir); +} +#define PREPARE prepare + +/* Adjust the file limit so that we have a chance to open PTY. */ +static void +adjust_file_limit (const char *pty) +{ + int number = -1; + if (sscanf (pty, "/dev/pts/%d", &number) != 1 || number < 0) + FAIL_EXIT1 ("invalid PTY name: \"%s\"", pty); + + /* Add a few additional descriptors to cover standard I/O streams + etc. */ + rlim_t desired_limit = number + 10; + + struct rlimit lim; + if (getrlimit (RLIMIT_NOFILE, &lim) != 0) + FAIL_EXIT1 ("getrlimit (RLIMIT_NOFILE): %m"); + if (lim.rlim_cur < desired_limit) + { + printf ("info: adjusting RLIMIT_NOFILE from %llu to %llu\n", + (unsigned long long int) lim.rlim_cur, + (unsigned long long int) desired_limit); + lim.rlim_cur = desired_limit; + if (setrlimit (RLIMIT_NOFILE, &lim) != 0) + printf ("warning: setrlimit (RLIMIT_NOFILE) failed: %m\n"); + } +} + +/* main test */ + +static int +run_chroot_tests (const char *slavename, int slave) +{ + struct stat st; + bool ok = true; + + /* There are 3 groups of tests here. The first group fairly + generically does things known to mess up ttyname, and verifies + that ttyname copes correctly. The remaining groups are + increasingly convoluted, as we target specific parts of ttyname + to try to confuse. */ + + /* Basic tests that it doesn't get confused by multiple devpts + instances. */ + { + VERIFY (stat (slavename, &st) < 0); /* sanity check */ + if (!doit (slave, "no conflict, no match", + (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV})) + ok = false; + VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0); + if (!doit (slave, "no conflict, console", + (struct result_r){.name="/dev/console", .ret=0, .err=0})) + ok = false; + VERIFY (umount ("/dev/console") == 0); + + /* Keep creating PTYs until we we get a name collision. */ + while (true) + { + if (stat (slavename, &st) == 0) + break; + if (posix_openpt (O_RDWR|O_NOCTTY|O_NONBLOCK) < 0) + { + if (errno == ENOSPC || errno == EMFILE || errno == ENFILE) + FAIL_UNSUPPORTED ("cannot re-create PTY \"%s\" in chroot: %m" + " (consider increasing limits)", slavename); + else + FAIL_EXIT1 ("cannot re-create PTY \"%s\" chroot: %m", slavename); + } + } + + if (!doit (slave, "conflict, no match", + (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV})) + ok = false; + VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0); + if (!doit (slave, "conflict, console", + (struct result_r){.name="/dev/console", .ret=0, .err=0})) + ok = false; + VERIFY (umount ("/dev/console") == 0); + } + + /* The first tests kinda assumed that they hit certain code-paths + based on assuming that the readlink target is 'slavename', but + that's not quite always true. They're still a good preliminary + sanity check, so keep them, but let's add tests that make sure + that those code-paths are hit by doing a readlink ourself. */ + { + char *linkname = xasprintf ("/proc/self/fd/%d", slave); + char *target = proc_fd_readlink (linkname); + free (linkname); + /* Depending on how we set up the chroot, the kernel may or may not + trim the leading path to the target (it may give us "/6", + instead of "/dev/pts/6"). We test it both ways (do_in_chroot_1 + and do_in_chroot_2). This test group relies on the target + existing, so guarantee that it does exist by creating it if + necessary. */ + if (stat (target, &st) < 0) + { + VERIFY (errno == ENOENT); + touch (target, 0); + } + + VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0); + VERIFY (mount ("/console", target, NULL, MS_BIND, NULL) == 0); + if (!doit (slave, "with readlink target", + (struct result_r){.name=target, .ret=0, .err=0})) + ok = false; + VERIFY (umount (target) == 0); + VERIFY (umount ("/dev/console") == 0); + + VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0); + VERIFY (mount (slavename, target, NULL, MS_BIND, NULL) == 0); + if (!doit (slave, "with readlink trap; fallback", + (struct result_r){.name="/dev/console", .ret=0, .err=0})) + ok = false; + VERIFY (umount (target) == 0); + VERIFY (umount ("/dev/console") == 0); + + VERIFY (mount (slavename, target, NULL, MS_BIND, NULL) == 0); + if (!doit (slave, "with readlink trap; no fallback", + (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV})) + ok = false; + VERIFY (umount (target) == 0); + } + + /* This test makes sure that everything still works OK if readdir + finds a pseudo-match before and/or after the actual match. Now, + to do that, we need to control that readdir finds the + pseudo-matches before and after the actual match; and there's no + good way to control that order in absence of whitebox testing. + So, just create 3 files, then use opendir/readdir to see what + order they are in, and assign meaning based on that order, not by + name; assigning the first to be a pseudo-match, the second to be + the actual match, and the third to be a pseudo-match. This + assumes that (on tmpfs) ordering within the directory is stable + in the absence of modification, which seems reasonably safe. */ + { + /* since we're testing the fallback search, disable the readlink + happy-path */ + VERIFY (umount2 ("/proc", MNT_DETACH) == 0); + + touch ("/dev/console1", 0); + touch ("/dev/console2", 0); + touch ("/dev/console3", 0); + + char *c[3]; + int ci = 0; + DIR *dirstream = opendir ("/dev"); + VERIFY (dirstream != NULL); + struct dirent *d; + while ((d = readdir (dirstream)) != NULL && ci < 3) + { + if (strcmp (d->d_name, "console1") + && strcmp (d->d_name, "console2") + && strcmp (d->d_name, "console3") ) + continue; + c[ci++] = xasprintf ("/dev/%s", d->d_name); + } + VERIFY (ci == 3); + VERIFY (closedir (dirstream) == 0); + + VERIFY (mount (slavename, c[0], NULL, MS_BIND, NULL) == 0); + VERIFY (mount ("/console", c[1], NULL, MS_BIND, NULL) == 0); + VERIFY (mount (slavename, c[2], NULL, MS_BIND, NULL) == 0); + VERIFY (umount2 ("/dev/pts", MNT_DETACH) == 0); + if (!doit (slave, "with search-path trap", + (struct result_r){.name=c[1], .ret=0, .err=0})) + ok = false; + for (int i = 0; i < 3; i++) + { + VERIFY (umount (c[i]) == 0); + VERIFY (unlink (c[i]) == 0); + free (c[i]); + } + } + + return ok ? 0 : 1; +} + diff --git a/sysdeps/unix/sysv/linux/tst-ttyname-namespace.c b/sysdeps/unix/sysv/linux/tst-ttyname-namespace.c new file mode 100644 index 0000000000..7b74258c10 --- /dev/null +++ b/sysdeps/unix/sysv/linux/tst-ttyname-namespace.c @@ -0,0 +1,147 @@ +/* Tests for ttyname/ttyname_r with namespaces. + Copyright (C) 2017-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of the + License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; see the file COPYING.LIB. If + not, see . */ + +#include +#include +#include + +#include + +#include "tst-ttyname-common.c" + +static int +do_in_chroot_2 (int (*cb)(const char *, int)) +{ + printf ("info: entering chroot 2\n"); + + int pid_pipe[2]; + xpipe (pid_pipe); + int exit_pipe[2]; + xpipe (exit_pipe); + + /* Open the PTS that we'll be testing on. */ + int master; + char *slavename; + VERIFY ((master = posix_openpt (O_RDWR|O_NOCTTY|O_NONBLOCK)) >= 0); + VERIFY ((slavename = ptsname (master))); + VERIFY (unlockpt (master) == 0); + if (strncmp (slavename, "/dev/pts/", 9) != 0) + FAIL_UNSUPPORTED ("slave pseudo-terminal is not under /dev/pts/: %s", + slavename); + adjust_file_limit (slavename); + /* wait until in a new mount ns to open the slave */ + + /* enable `wait`ing on grandchildren */ + VERIFY (prctl (PR_SET_CHILD_SUBREAPER, 1) == 0); + + pid_t pid = xfork (); /* outer child */ + if (pid == 0) + { + xclose (master); + xclose (pid_pipe[0]); + xclose (exit_pipe[1]); + + if (!support_enter_mount_namespace ()) + FAIL_UNSUPPORTED ("could not enter new mount namespace"); + + int slave = xopen (slavename, O_RDWR, 0); + if (!doit (slave, "basic smoketest", + (struct result_r){.name=slavename, .ret=0, .err=0})) + _exit (1); + + VERIFY (mount ("tmpfs", chrootdir, "tmpfs", 0, "mode=755") == 0); + VERIFY (chdir (chrootdir) == 0); + + xmkdir ("proc", 0755); + xmkdir ("dev", 0755); + xmkdir ("dev/pts", 0755); + + VERIFY (mount ("devpts", "dev/pts", "devpts", + MS_NOSUID|MS_NOEXEC, + "newinstance,ptmxmode=0666,mode=620") == 0); + VERIFY (symlink ("pts/ptmx", "dev/ptmx") == 0); + + touch ("console", 0); + touch ("dev/console", 0); + VERIFY (mount (slavename, "console", NULL, MS_BIND, NULL) == 0); + + xchroot ("."); + + if (unshare (CLONE_NEWNS | CLONE_NEWPID) < 0) + FAIL_UNSUPPORTED ("could not enter new PID namespace"); + pid = xfork (); /* inner child */ + if (pid == 0) + { + xclose (pid_pipe[1]); + + /* wait until the outer child has exited */ + char c; + VERIFY (read (exit_pipe[0], &c, 1) == 0); + xclose (exit_pipe[0]); + + if (mount ("proc", "/proc", "proc", + MS_NOSUID|MS_NOEXEC|MS_NODEV, NULL) != 0) + { + /* This happens if we're trying to create a nested container, + like if the build is running under podman, and we lack + priviledges. */ + if (errno == EPERM) + _exit (EXIT_UNSUPPORTED); + else + _exit (EXIT_FAILURE); + } + + char *linkname = xasprintf ("/proc/self/fd/%d", slave); + char *target = proc_fd_readlink (linkname); + VERIFY (strcmp (target, strrchr (slavename, '/')) == 0); + free (linkname); + + _exit (cb (slavename, slave)); + } + + int status; + xwaitpid (pid, &status, 0); + _exit (WEXITSTATUS (status)); + } + xclose (pid_pipe[1]); + xclose (exit_pipe[0]); + xclose (exit_pipe[1]); + + /* wait for the outer child */ + int status; + xwaitpid (pid, &status, 0); + VERIFY (WIFEXITED (status)); + int ret = WEXITSTATUS (status); + if (ret != 0) + FAIL_UNSUPPORTED ("unable to mount /proc on inner child process"); + xclose (master); + + return 0; +} + +static int +do_test (void) +{ + support_become_root (); + + do_in_chroot_2 (run_chroot_tests); + + return 0; +} + +#include diff --git a/sysdeps/unix/sysv/linux/tst-ttyname.c b/sysdeps/unix/sysv/linux/tst-ttyname.c index ef55665fbe..04601777d6 100644 --- a/sysdeps/unix/sysv/linux/tst-ttyname.c +++ b/sysdeps/unix/sysv/linux/tst-ttyname.c @@ -15,256 +15,12 @@ License along with the GNU C Library; see the file COPYING.LIB. If not, see . */ -#include -#include -#include -#include #include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -/* generic utilities */ - -#define VERIFY(expr) \ - do { \ - if (!(expr)) \ - { \ - printf ("error: %s:%d: %s: %m\n", \ - __FILE__, __LINE__, #expr); \ - exit (1); \ - } \ - } while (0) - -static void -touch (const char *path, mode_t mode) -{ - xclose (xopen (path, O_WRONLY|O_CREAT|O_NOCTTY, mode)); -} - -static size_t -trim_prefix (char *str, size_t str_len, const char *prefix) -{ - size_t prefix_len = strlen (prefix); - if (str_len > prefix_len && memcmp (str, prefix, prefix_len) == 0) - { - memmove (str, str + prefix_len, str_len - prefix_len); - return str_len - prefix_len; - } - return str_len; -} - -/* returns a pointer to static storage */ -static char * -proc_fd_readlink (const char *linkname) -{ - static char target[PATH_MAX+1]; - ssize_t target_len = readlink (linkname, target, PATH_MAX); - VERIFY (target_len > 0); - target_len = trim_prefix (target, target_len, "(unreachable)"); - target[target_len] = '\0'; - return target; -} - -/* plain ttyname runner */ - -struct result -{ - const char *name; - int err; -}; - -/* strings in result structure are in static storage */ -static struct result -run_ttyname (int fd) -{ - struct result ret; - errno = 0; - ret.name = ttyname (fd); - ret.err = errno; - return ret; -} - -static bool -eq_ttyname (struct result actual, struct result expected) -{ - char *actual_name, *expected_name; - - if ((actual.err == expected.err) - && (!actual.name == !expected.name) - && (actual.name ? strcmp (actual.name, expected.name) == 0 : true)) - { - if (expected.name) - expected_name = xasprintf ("\"%s\"", expected.name); - else - expected_name = xstrdup ("NULL"); - - printf ("info: ttyname: PASS {name=%s, errno=%d}\n", - expected_name, expected.err); - - free (expected_name); - return true; - } - - if (actual.name) - actual_name = xasprintf ("\"%s\"", actual.name); - else - actual_name = xstrdup ("NULL"); - - if (expected.name) - expected_name = xasprintf ("\"%s\"", expected.name); - else - expected_name = xstrdup ("NULL"); - - printf ("error: ttyname: actual {name=%s, errno=%d} != expected {name=%s, errno=%d}\n", - actual_name, actual.err, - expected_name, expected.err); - - free (actual_name); - free (expected_name); - return false; -} - -/* ttyname_r runner */ - -struct result_r -{ - const char *name; - int ret; - int err; -}; - -/* strings in result structure are in static storage */ -static struct result_r -run_ttyname_r (int fd) -{ - static char buf[TTY_NAME_MAX]; - - struct result_r ret; - errno = 0; - ret.ret = ttyname_r (fd, buf, TTY_NAME_MAX); - ret.err = errno; - if (ret.ret == 0) - ret.name = buf; - else - ret.name = NULL; - return ret; -} - -static bool -eq_ttyname_r (struct result_r actual, struct result_r expected) -{ - char *actual_name, *expected_name; - - if ((actual.err == expected.err) - && (actual.ret == expected.ret) - && (!actual.name == !expected.name) - && (actual.name ? strcmp (actual.name, expected.name) == 0 : true)) - { - if (expected.name) - expected_name = xasprintf ("\"%s\"", expected.name); - else - expected_name = xstrdup ("NULL"); - - printf ("info: ttyname_r: PASS {name=%s, ret=%d, errno=%d}\n", - expected_name, expected.ret, expected.err); - - free (expected_name); - return true; - } - - if (actual.name) - actual_name = xasprintf ("\"%s\"", actual.name); - else - actual_name = xstrdup ("NULL"); - - if (expected.name) - expected_name = xasprintf ("\"%s\"", expected.name); - else - expected_name = xstrdup ("NULL"); - - printf ("error: ttyname_r: actual {name=%s, ret=%d, errno=%d} != expected {name=%s, ret=%d, errno=%d}\n", - actual_name, actual.ret, actual.err, - expected_name, expected.ret, expected.err); - - free (actual_name); - free (expected_name); - return false; -} - -/* combined runner */ - -static bool -doit (int fd, const char *testname, struct result_r expected_r) -{ - struct result expected = {.name=expected_r.name, .err=expected_r.ret}; - bool ret = true; - - printf ("info: testcase: %s\n", testname); - - if (!eq_ttyname (run_ttyname (fd), expected)) - ret = false; - if (!eq_ttyname_r (run_ttyname_r (fd), expected_r)) - ret = false; - - if (!ret) - support_record_failure (); - - return ret; -} - -/* chroot setup */ - -static char *chrootdir; - -static void -prepare (int argc, char **argv) -{ - chrootdir = xasprintf ("%s/tst-ttyname-XXXXXX", test_dir); - if (mkdtemp (chrootdir) == NULL) - FAIL_EXIT1 ("mkdtemp (\"%s\"): %m", chrootdir); - add_temp_file (chrootdir); -} -#define PREPARE prepare - -/* Adjust the file limit so that we have a chance to open PTY. */ -static void -adjust_file_limit (const char *pty) -{ - int number = -1; - if (sscanf (pty, "/dev/pts/%d", &number) != 1 || number < 0) - FAIL_EXIT1 ("invalid PTY name: \"%s\"", pty); - - /* Add a few additional descriptors to cover standard I/O streams - etc. */ - rlim_t desired_limit = number + 10; - - struct rlimit lim; - if (getrlimit (RLIMIT_NOFILE, &lim) != 0) - FAIL_EXIT1 ("getrlimit (RLIMIT_NOFILE): %m"); - if (lim.rlim_cur < desired_limit) - { - printf ("info: adjusting RLIMIT_NOFILE from %llu to %llu\n", - (unsigned long long int) lim.rlim_cur, - (unsigned long long int) desired_limit); - lim.rlim_cur = desired_limit; - if (setrlimit (RLIMIT_NOFILE, &lim) != 0) - printf ("warning: setrlimit (RLIMIT_NOFILE) failed: %m\n"); - } -} +#include "tst-ttyname-common.c" /* These chroot setup functions put the TTY at at "/console" (where it won't be found by ttyname), and create "/dev/console" as an @@ -341,276 +97,14 @@ do_in_chroot_1 (int (*cb)(const char *, int)) return WEXITSTATUS (status); } -static int -do_in_chroot_2 (int (*cb)(const char *, int)) -{ - printf ("info: entering chroot 2\n"); - - int pid_pipe[2]; - xpipe (pid_pipe); - int exit_pipe[2]; - xpipe (exit_pipe); - - /* Open the PTS that we'll be testing on. */ - int master; - char *slavename; - VERIFY ((master = posix_openpt (O_RDWR|O_NOCTTY|O_NONBLOCK)) >= 0); - VERIFY ((slavename = ptsname (master))); - VERIFY (unlockpt (master) == 0); - if (strncmp (slavename, "/dev/pts/", 9) != 0) - FAIL_UNSUPPORTED ("slave pseudo-terminal is not under /dev/pts/: %s", - slavename); - adjust_file_limit (slavename); - /* wait until in a new mount ns to open the slave */ - - /* enable `wait`ing on grandchildren */ - VERIFY (prctl (PR_SET_CHILD_SUBREAPER, 1) == 0); - - pid_t pid = xfork (); /* outer child */ - if (pid == 0) - { - xclose (master); - xclose (pid_pipe[0]); - xclose (exit_pipe[1]); - - if (!support_enter_mount_namespace ()) - FAIL_UNSUPPORTED ("could not enter new mount namespace"); - - int slave = xopen (slavename, O_RDWR, 0); - if (!doit (slave, "basic smoketest", - (struct result_r){.name=slavename, .ret=0, .err=0})) - _exit (1); - - VERIFY (mount ("tmpfs", chrootdir, "tmpfs", 0, "mode=755") == 0); - VERIFY (chdir (chrootdir) == 0); - - xmkdir ("proc", 0755); - xmkdir ("dev", 0755); - xmkdir ("dev/pts", 0755); - - VERIFY (mount ("devpts", "dev/pts", "devpts", - MS_NOSUID|MS_NOEXEC, - "newinstance,ptmxmode=0666,mode=620") == 0); - VERIFY (symlink ("pts/ptmx", "dev/ptmx") == 0); - - touch ("console", 0); - touch ("dev/console", 0); - VERIFY (mount (slavename, "console", NULL, MS_BIND, NULL) == 0); - - xchroot ("."); - - if (unshare (CLONE_NEWNS | CLONE_NEWPID) < 0) - FAIL_UNSUPPORTED ("could not enter new PID namespace"); - pid = xfork (); /* inner child */ - if (pid == 0) - { - xclose (pid_pipe[1]); - - /* wait until the outer child has exited */ - char c; - VERIFY (read (exit_pipe[0], &c, 1) == 0); - xclose (exit_pipe[0]); - - VERIFY (mount ("proc", "/proc", "proc", - MS_NOSUID|MS_NOEXEC|MS_NODEV, NULL) == 0); - - char *linkname = xasprintf ("/proc/self/fd/%d", slave); - char *target = proc_fd_readlink (linkname); - VERIFY (strcmp (target, strrchr (slavename, '/')) == 0); - free (linkname); - - _exit (cb (slavename, slave)); - } - xwrite (pid_pipe[1], &pid, sizeof pid); - _exit (0); - } - xclose (pid_pipe[1]); - xclose (exit_pipe[0]); - xclose (exit_pipe[1]); - - /* wait for the outer child */ - int status; - xwaitpid (pid, &status, 0); - VERIFY (WIFEXITED (status)); - int ret = WEXITSTATUS (status); - if (ret != 0) - return ret; - - /* set 'pid' to the inner child */ - VERIFY (read (pid_pipe[0], &pid, sizeof pid) == sizeof pid); - xclose (pid_pipe[0]); - - /* wait for the inner child */ - xwaitpid (pid, &status, 0); - VERIFY (WIFEXITED (status)); - xclose (master); - return WEXITSTATUS (status); -} - -/* main test */ - -static int -run_chroot_tests (const char *slavename, int slave) -{ - struct stat st; - bool ok = true; - - /* There are 3 groups of tests here. The first group fairly - generically does things known to mess up ttyname, and verifies - that ttyname copes correctly. The remaining groups are - increasingly convoluted, as we target specific parts of ttyname - to try to confuse. */ - - /* Basic tests that it doesn't get confused by multiple devpts - instances. */ - { - VERIFY (stat (slavename, &st) < 0); /* sanity check */ - if (!doit (slave, "no conflict, no match", - (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV})) - ok = false; - VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0); - if (!doit (slave, "no conflict, console", - (struct result_r){.name="/dev/console", .ret=0, .err=0})) - ok = false; - VERIFY (umount ("/dev/console") == 0); - - /* Keep creating PTYs until we we get a name collision. */ - while (true) - { - if (stat (slavename, &st) == 0) - break; - if (posix_openpt (O_RDWR|O_NOCTTY|O_NONBLOCK) < 0) - { - if (errno == ENOSPC || errno == EMFILE || errno == ENFILE) - FAIL_UNSUPPORTED ("cannot re-create PTY \"%s\" in chroot: %m" - " (consider increasing limits)", slavename); - else - FAIL_EXIT1 ("cannot re-create PTY \"%s\" chroot: %m", slavename); - } - } - - if (!doit (slave, "conflict, no match", - (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV})) - ok = false; - VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0); - if (!doit (slave, "conflict, console", - (struct result_r){.name="/dev/console", .ret=0, .err=0})) - ok = false; - VERIFY (umount ("/dev/console") == 0); - } - - /* The first tests kinda assumed that they hit certain code-paths - based on assuming that the readlink target is 'slavename', but - that's not quite always true. They're still a good preliminary - sanity check, so keep them, but let's add tests that make sure - that those code-paths are hit by doing a readlink ourself. */ - { - char *linkname = xasprintf ("/proc/self/fd/%d", slave); - char *target = proc_fd_readlink (linkname); - free (linkname); - /* Depending on how we set up the chroot, the kernel may or may not - trim the leading path to the target (it may give us "/6", - instead of "/dev/pts/6"). We test it both ways (do_in_chroot_1 - and do_in_chroot_2). This test group relies on the target - existing, so guarantee that it does exist by creating it if - necessary. */ - if (stat (target, &st) < 0) - { - VERIFY (errno == ENOENT); - touch (target, 0); - } - - VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0); - VERIFY (mount ("/console", target, NULL, MS_BIND, NULL) == 0); - if (!doit (slave, "with readlink target", - (struct result_r){.name=target, .ret=0, .err=0})) - ok = false; - VERIFY (umount (target) == 0); - VERIFY (umount ("/dev/console") == 0); - - VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0); - VERIFY (mount (slavename, target, NULL, MS_BIND, NULL) == 0); - if (!doit (slave, "with readlink trap; fallback", - (struct result_r){.name="/dev/console", .ret=0, .err=0})) - ok = false; - VERIFY (umount (target) == 0); - VERIFY (umount ("/dev/console") == 0); - - VERIFY (mount (slavename, target, NULL, MS_BIND, NULL) == 0); - if (!doit (slave, "with readlink trap; no fallback", - (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV})) - ok = false; - VERIFY (umount (target) == 0); - } - - /* This test makes sure that everything still works OK if readdir - finds a pseudo-match before and/or after the actual match. Now, - to do that, we need to control that readdir finds the - pseudo-matches before and after the actual match; and there's no - good way to control that order in absence of whitebox testing. - So, just create 3 files, then use opendir/readdir to see what - order they are in, and assign meaning based on that order, not by - name; assigning the first to be a pseudo-match, the second to be - the actual match, and the third to be a pseudo-match. This - assumes that (on tmpfs) ordering within the directory is stable - in the absence of modification, which seems reasonably safe. */ - { - /* since we're testing the fallback search, disable the readlink - happy-path */ - VERIFY (umount2 ("/proc", MNT_DETACH) == 0); - - touch ("/dev/console1", 0); - touch ("/dev/console2", 0); - touch ("/dev/console3", 0); - - char *c[3]; - int ci = 0; - DIR *dirstream = opendir ("/dev"); - VERIFY (dirstream != NULL); - struct dirent *d; - while ((d = readdir (dirstream)) != NULL && ci < 3) - { - if (strcmp (d->d_name, "console1") - && strcmp (d->d_name, "console2") - && strcmp (d->d_name, "console3") ) - continue; - c[ci++] = xasprintf ("/dev/%s", d->d_name); - } - VERIFY (ci == 3); - VERIFY (closedir (dirstream) == 0); - - VERIFY (mount (slavename, c[0], NULL, MS_BIND, NULL) == 0); - VERIFY (mount ("/console", c[1], NULL, MS_BIND, NULL) == 0); - VERIFY (mount (slavename, c[2], NULL, MS_BIND, NULL) == 0); - VERIFY (umount2 ("/dev/pts", MNT_DETACH) == 0); - if (!doit (slave, "with search-path trap", - (struct result_r){.name=c[1], .ret=0, .err=0})) - ok = false; - for (int i = 0; i < 3; i++) - { - VERIFY (umount (c[i]) == 0); - VERIFY (unlink (c[i]) == 0); - free (c[i]); - } - } - - return ok ? 0 : 1; -} - static int do_test (void) { support_become_root (); - int ret1 = do_in_chroot_1 (run_chroot_tests); - if (ret1 == EXIT_UNSUPPORTED) - return ret1; - - int ret2 = do_in_chroot_2 (run_chroot_tests); - if (ret2 == EXIT_UNSUPPORTED) - return ret2; + do_in_chroot_1 (run_chroot_tests); - return ret1 | ret2; + return 0; } #include