[v2,5/5] linux ttyname and ttyname_r: Add tests [BZ #22145]

Message ID 20171102185346.1386-6-lukeshu@parabola.nu
State New, archived
Headers

Commit Message

Luke Shumaker Nov. 2, 2017, 6:53 p.m. UTC
  Add a new tst-ttyname test that includes several named sub-testcases.

This patch is ordered the fixes it tests for are applied (to avoid
breaking `git bisect`), but for reference, here's how each relevent
change so far affected the testcases in this commit:

  |                                 | before  |         | make tests | don't |
  |                                 | 15e9a4f | 15e9a4f | consistent | bail  |
  |---------------------------------+---------+---------+------------+-------|
  | basic smoketest                 | PASS    | PASS    | PASS       | PASS  |
  | no conflict, no match           | PASS[1] | PASS    | PASS       | PASS  |
  | no conflict, console            | PASS    | FAIL!   | FAIL       | PASS! |
  | conflict, no match              | FAIL    | PASS!   | PASS       | PASS  |
  | conflict, console               | FAIL    | FAIL    | FAIL       | PASS! |
  | with readlink target            | PASS    | PASS    | PASS       | PASS  |
  | with readlink trap; fallback    | FAIL    | FAIL    | FAIL       | PASS! |
  | with readlink trap; no fallback | FAIL    | PASS!   | PASS       | PASS  |
  | with search-path trap           | FAIL    | FAIL    | PASS!      | PASS  |
  |---------------------------------+---------+---------+------------+-------|
  |                                 | 4/9     | 5/9     | 6/9        | 9/9   |

  [1]: 15e9a4f introduced a semantic that, under certain failure
       conditions, ttyname sets errno=ENODEV, where previously it
       didn't set errno; it's not quite fair to hold "before 15e9a4f"
       ttyname to those new semantics.  This testcase actually fails,
       but would have passed if we tested for the old the semantics.

Each of the failing tests before 15e9a4f is essentially the same bug:
that it returns a PTY slave with the correct minor device number, but
from the wrong devpts filesystem instance.

"15e9a4f" sought to fix this, but missed several of the cases that can
cause this to happen, and also broke the case where both the erroneous
PTY and the correct PTY exist.

v2:
 - Rearranged commit order
 - Fix code style
 - Expanded commit message
 - Pulled xtouch, become-root, and chroot setup into functions
 - Better tracking of test failures
 - Fixed the "with search-path trap" test case
 - Added test cases for the readlink target directly
 - More readable diagnostic output
 - Test with both shared and private procfs
---
 ChangeLog                             |   4 +
 sysdeps/unix/sysv/linux/Makefile      |   3 +-
 sysdeps/unix/sysv/linux/tst-ttyname.c | 582 ++++++++++++++++++++++++++++++++++
 3 files changed, 588 insertions(+), 1 deletion(-)
 create mode 100644 sysdeps/unix/sysv/linux/tst-ttyname.c
  

Comments

Luke Shumaker Nov. 8, 2017, 4:14 p.m. UTC | #1
On Thu, 02 Nov 2017 14:53:46 -0400,
Luke Shumaker wrote:
> +  /* support_become_root might have put us in a new user namespace;
> +     most filesystems (including tmpfs) don't allow file or directory
> +     creation from a user namespace unless uid and gid maps are set,
> +     even if we have root privileges in the namespace (failing with
> +     EOVERFLOW, since the uid overflows the empty (0-length) uid map).
> +
> +     Also, stat always reports that uid and gid maps are empty, so we
> +     have to try actually reading from them to check if they are
> +     empty. */

Style: two spaces after the trailing period.  This mistake is
consistent throughout the file.

> +    /* Depeding 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").  This test group relies on the target
> +     * existing, so guarantee that (so create a file at "/6" if
> +     * necessary). */

Style: no '*', two spaces after the trailing period.

Andreas Schwab made me aware of these style issues on the previous
commit.
  
Christian Brauner Nov. 8, 2017, 7:43 p.m. UTC | #2
On Thu, Nov 02, 2017 at 02:53:46PM -0400, Luke Shumaker wrote:
> Add a new tst-ttyname test that includes several named sub-testcases.
> 
> This patch is ordered the fixes it tests for are applied (to avoid
> breaking `git bisect`), but for reference, here's how each relevent
> change so far affected the testcases in this commit:
> 
>   |                                 | before  |         | make tests | don't |
>   |                                 | 15e9a4f | 15e9a4f | consistent | bail  |
>   |---------------------------------+---------+---------+------------+-------|
>   | basic smoketest                 | PASS    | PASS    | PASS       | PASS  |
>   | no conflict, no match           | PASS[1] | PASS    | PASS       | PASS  |
>   | no conflict, console            | PASS    | FAIL!   | FAIL       | PASS! |
>   | conflict, no match              | FAIL    | PASS!   | PASS       | PASS  |
>   | conflict, console               | FAIL    | FAIL    | FAIL       | PASS! |
>   | with readlink target            | PASS    | PASS    | PASS       | PASS  |
>   | with readlink trap; fallback    | FAIL    | FAIL    | FAIL       | PASS! |
>   | with readlink trap; no fallback | FAIL    | PASS!   | PASS       | PASS  |
>   | with search-path trap           | FAIL    | FAIL    | PASS!      | PASS  |
>   |---------------------------------+---------+---------+------------+-------|
>   |                                 | 4/9     | 5/9     | 6/9        | 9/9   |
> 
>   [1]: 15e9a4f introduced a semantic that, under certain failure
>        conditions, ttyname sets errno=ENODEV, where previously it
>        didn't set errno; it's not quite fair to hold "before 15e9a4f"
>        ttyname to those new semantics.  This testcase actually fails,
>        but would have passed if we tested for the old the semantics.
> 
> Each of the failing tests before 15e9a4f is essentially the same bug:
> that it returns a PTY slave with the correct minor device number, but
> from the wrong devpts filesystem instance.
> 
> "15e9a4f" sought to fix this, but missed several of the cases that can
> cause this to happen, and also broke the case where both the erroneous
> PTY and the correct PTY exist.
> 
> v2:
>  - Rearranged commit order
>  - Fix code style
>  - Expanded commit message
>  - Pulled xtouch, become-root, and chroot setup into functions
>  - Better tracking of test failures
>  - Fixed the "with search-path trap" test case
>  - Added test cases for the readlink target directly
>  - More readable diagnostic output
>  - Test with both shared and private procfs
> ---
>  ChangeLog                             |   4 +
>  sysdeps/unix/sysv/linux/Makefile      |   3 +-
>  sysdeps/unix/sysv/linux/tst-ttyname.c | 582 ++++++++++++++++++++++++++++++++++
>  3 files changed, 588 insertions(+), 1 deletion(-)
>  create mode 100644 sysdeps/unix/sysv/linux/tst-ttyname.c
> 
> diff --git a/ChangeLog b/ChangeLog
> index b5d214ae6f..1982a498ce 100644
> --- a/ChangeLog
> +++ b/ChangeLog
> @@ -1,5 +1,9 @@
>  2017-11-02  Luke Shumaker  <lukeshu@parabola.nu>
>  
> +	[BZ #22145]
> +	* sysdeps/unix/sysv/linux/tst-ttyname.c: New file.
> +	* sysdeps/unix/sysv/linux/Makefile: Add tst-ttyname to tests.
> +
>  	[BZ #22145]
>  	* sysdeps/unix/sysv/linux/ttyname.c (ttyname):
>  	Defer is_pty check until end.
> diff --git a/sysdeps/unix/sysv/linux/Makefile b/sysdeps/unix/sysv/linux/Makefile
> index 0c8a009b5e..69edb75209 100644
> --- a/sysdeps/unix/sysv/linux/Makefile
> +++ b/sysdeps/unix/sysv/linux/Makefile
> @@ -50,7 +50,8 @@ sysdep_headers += sys/mount.h sys/acct.h sys/sysctl.h \
>  		  bits/siginfo-arch.h bits/siginfo-consts-arch.h
>  
>  tests += tst-clone tst-clone2 tst-clone3 tst-fanotify tst-personality \
> -	 tst-quota tst-sync_file_range test-errno-linux tst-sysconf-iov_max
> +	 tst-quota tst-sync_file_range tst-sysconf-iov_max tst-ttyname \
> +	 test-errno-linux
>  
>  # Generate the list of SYS_* macros for the system calls (__NR_*
>  # macros).  The file syscall-names.list contains all possible system
> diff --git a/sysdeps/unix/sysv/linux/tst-ttyname.c b/sysdeps/unix/sysv/linux/tst-ttyname.c
> new file mode 100644
> index 0000000000..45d356cab4
> --- /dev/null
> +++ b/sysdeps/unix/sysv/linux/tst-ttyname.c
> @@ -0,0 +1,582 @@
> +/* Copyright (C) 2017 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 <http://www.gnu.org/licenses/>.  */
> +
> +#include <dirent.h>
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <limits.h>
> +#include <sched.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/mount.h>
> +#include <sys/prctl.h>
> +#include <sys/stat.h>
> +#include <sys/wait.h>
> +#include <unistd.h>
> +
> +#include <support/check.h>
> +#include <support/namespace.h>
> +#include <support/support.h>
> +#include <support/temp_file.h>
> +#include <support/test-driver.h>
> +#include <support/xunistd.h>
> +
> +/* generic utilities */
> +
> +#define VERIFY(expr)                                                    \
> +  do {                                                                  \
> +    if (!(expr))                                                        \
> +      {                                                                 \
> +        printf ("error: %s:%d: %s: %m\n",                               \
> +                __FILE__, __LINE__, #expr);                             \
> +        exit (1);                                                       \
> +      }                                                                 \
> +  } while (0)
> +
> +static void
> +xtouch (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 *
> +xreadlink (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;
> +}
> +
> +static void
> +become_root_in_mount_ns (void) {
> +  uid_t orig_uid = getuid ();
> +  gid_t orig_gid = getgid ();
> +
> +  support_become_root ();
> +
> +  if (unshare (CLONE_NEWNS) < 0)
> +    FAIL_UNSUPPORTED ("could not enter new mount namespace");
> +
> +  /* support_become_root might have put us in a new user namespace;
> +     most filesystems (including tmpfs) don't allow file or directory
> +     creation from a user namespace unless uid and gid maps are set,
> +     even if we have root privileges in the namespace (failing with
> +     EOVERFLOW, since the uid overflows the empty (0-length) uid map).
> +
> +     Also, stat always reports that uid and gid maps are empty, so we
> +     have to try actually reading from them to check if they are
> +     empty. */
> +  int fd;
> +
> +  if ((fd = open ("/proc/self/uid_map", O_RDWR, 0)) >= 0)
> +    {
> +      char buf;
> +      if (read (fd, &buf, 1) == 0)
> +	{
> +	  char *str = xasprintf ("0 %ld 1\n", (long)orig_uid);
> +	  if (write (fd, str, strlen (str)) < 0)
> +	    FAIL_EXIT1 ("write (uid_map, \"%s\"): %m", str);

I haven't looked at the test code a lot yet but is it generally considered ok to
not cleanup before exit? That's mostly style but still it'd be good to clarify
this. I usually do all the cleanup even when exiting.

> +	  free (str);
> +	}
> +      xclose (fd);
> +    }
> +
> +  /* Oh, but we can't set the gid map until we turn off setgroups. */

That sounds like a discovery you just made. I think - nitpick - a simple:
/* We need to set setgroups to "deny" otherwise we won't be able to write a gid
*  mapping. */

> +  if ((fd = open ("/proc/self/setgroups", O_WRONLY, 0)) >= 0)
> +    {
> +      const char *str = "deny";
> +      if (write (fd, str, strlen (str)) < 0)
> +	FAIL_EXIT1 ("write (setroups, \"%s\"): %m", str);
> +      xclose (fd);
> +    }
> +
> +  if ((fd = open ("/proc/self/gid_map", O_RDWR, 0)) >= 0)
> +    {
> +      char buf;
> +      if (read (fd, &buf, 1) == 0)
> +	{
> +	  char *str = xasprintf ("0 %ld 1\n", (long)orig_gid);
> +	  if (write (fd, str, strlen (str)) < 0)
> +	    FAIL_EXIT1 ("write (gid_map, \"%s\"): %m", str);
> +	  free (str);
> +	}
> +      xclose (fd);
> +    }
> +}
> +
> +/* 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)
> +{
> +  if ((actual.err == expected.err) &&
> +      (!actual.name == !expected.name) &&
> +      (actual.name ? strcmp (actual.name, expected.name) == 0 : true))
> +    {
> +      char *name = expected.name
> +	? xasprintf ("\"%s\"", expected.name)
> +	: strdup ("NULL");
> +      printf ("info:      ttyname: PASS {name=%s, errno=%d}\n",
> +	      name, expected.err);
> +      free (name);
> +      return true;
> +    }
> +
> +  char *actual_name = actual.name
> +    ? xasprintf ("\"%s\"", actual.name)
> +    : strdup ("NULL");

I'd prefer if this would be proper if () branches as this is pretty unreadable.
Also, I'd prefer - even though newer C standards allow this - if new variables
wouldn't be declared mid-stack especially if the function is rather small.

> +  char *expected_name = expected.name
> +    ? xasprintf ("\"%s\"", expected.name)
> +    : strdup ("NULL");

Likewise.

> +  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)
> +{
> +  if ((actual.err == expected.err) &&
> +      (actual.ret == expected.ret) &&
> +      (!actual.name == !expected.name) &&
> +      (actual.name ? strcmp (actual.name, expected.name) == 0 : true))
> +    {
> +      char *name = expected.name
> +	? xasprintf ("\"%s\"", expected.name)
> +	: strdup ("NULL");
> +      printf ("info:      ttyname_r: PASS {name=%s, ret=%d, errno=%d}\n",
> +              name, expected.ret, expected.err);
> +      free (name);
> +      return true;
> +    }
> +
> +  char *actual_name = actual.name
> +    ? xasprintf ("\"%s\"", actual.name)
> +    : strdup ("NULL");

Likewise.

> +  char *expected_name = expected.name
> +    ? xasprintf ("\"%s\"", expected.name)
> +    : strdup ("NULL");

Likewise.

> +  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);
> +
> +  ret = eq_ttyname (run_ttyname (fd), expected) && ret;
> +  ret = eq_ttyname_r (run_ttyname_r (fd), expected_r) && ret;

Imho, this is not very nice. It looks clever but is hard to parse semantically.
I'd prefer if the assignment from eq_ttyname{_r}() would be entangled from the
following "&& ret" check. This is just easier to maintain.

> +
> +  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
> +
> +/* These chroot setup functions put the TTY at at "/console" (where it
> +   won't be found by ttyname), and create "/dev/console" as an
> +   ordinary file.  This way, it's easier to write test-cases that
> +   expect ttyname to fail; test-cases that expect it to succeed need
> +   to explicitly remount it at "/dev/console". */
> +
> +static int
> +do_in_chroot_1 (int (*cb)(const char *, int)) {
> +  printf ("info:  entering chroot 1\n");
> +
> +  /* 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);
> +  int slave = xopen (slavename, O_RDWR, 0);
> +  if (!doit (slave, "basic smoketest",
> +             (struct result_r){.name=slavename, .ret=0, .err=0}))
> +    return 1;
> +
> +  pid_t pid = xfork ();
> +  if (pid == 0)
> +    {
> +      xclose (master);
> +
> +      become_root_in_mount_ns ();
> +
> +      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 ("/proc", "proc", NULL, MS_BIND|MS_REC, NULL) == 0);
> +      VERIFY (mount ("devpts", "dev/pts", "devpts",
> +                     MS_NOSUID|MS_NOEXEC,
> +                     "newinstance,ptmxmode=0666,mode=620") == 0);
> +      VERIFY (symlink ("pts/ptmx", "dev/ptmx") == 0);
> +
> +      xtouch ("console", 0);
> +      xtouch ("dev/console", 0);
> +      VERIFY (mount (slavename, "console", NULL, MS_BIND, NULL) == 0);
> +
> +      xchroot (".");
> +
> +      char *linkname = xasprintf ("/proc/self/fd/%d", slave);
> +      char *target = xreadlink (linkname);
> +      VERIFY (strcmp (target, slavename) == 0);
> +      free (linkname);
> +
> +      _exit (cb (slavename, slave));
> +    }
> +  int status;
> +  xwaitpid (pid, &status, 0);
> +  VERIFY (WIFEXITED (status));
> +  xclose (master);
> +  xclose (slave);
> +  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);
> +  /* wait until in a new mount ns to open the slave */
> +
> +  /* enable `wait`ing on grandchildren */
> +  VERIFY (prctl (PR_SET_CHILD_SUBREAPER, 1) == 0);

For future reference in case you want to avoid this clutch you can also use
clone() with CLONE_PARENT.

> +
> +  pid_t pid = xfork (); /* outer child */
> +  if (pid == 0)
> +    {
> +      xclose (master);
> +      xclose (pid_pipe[0]);
> +      xclose (exit_pipe[1]);
> +
> +      become_root_in_mount_ns ();
> +
> +      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);
> +
> +      xtouch ("console", 0);
> +      xtouch ("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 = xreadlink (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 throws a
> +     wrench into the works in a generic way, and verifies that ttyname

That's using a pretty (for a non-native speaker) specific English idiom. Could
you maybe simply say "creates problems" or something similar.

> +     copes correctly.  The remaining groups are increasingly
> +     convoluted, as we direct that wrench towards specific parts of

Likewise.

> +     the routine, and try to break it in specific ways. */
> +
> +  /* Basic tests that it doesn't get confused by multiple devpts
> +     instances. */
> +  {
> +    VERIFY (stat (slavename, &st) < 0); /* sanity check */
> +    ok = doit (slave, "no conflict, no match",
> +               (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV}) && ok;
> +    VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0);
> +    ok = doit (slave, "no conflict, console",
> +               (struct result_r){.name="/dev/console", .ret=0, .err=0}) && ok;
> +    VERIFY (umount ("/dev/console") == 0);
> +
> +    /* keep creating PTYs until we we get a name collision */
> +    while (stat (slavename, &st) < 0)
> +      posix_openpt (O_RDWR|O_NOCTTY|O_NONBLOCK);
> +    VERIFY (stat (slavename, &st) == 0);
> +
> +    ok = doit (slave, "conflict, no match",
> +               (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV}) && ok;
> +    VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0);
> +    ok = doit (slave, "conflict, console",
> +               (struct result_r){.name="/dev/console", .ret=0, .err=0}) && ok;
> +    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 = xreadlink (linkname);
> +    free (linkname);
> +    /* Depeding 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",

Uhm? I'm sorry, I don't understand. That sounds like a bug we fixed a while back
upstream whereby the kernel recorded the wrong vfs mount for devpts. That's one
scenario where this can happen. In any case the comment should outline when the
kernel can *legitimately* show you the contents of the symlink as /<n> instead
of the correct /dev/pts/<n>. Right now it sounds like this code doesn't
understand how such a scenario can happen but only that it can happen.

> +     * instead of "/dev/pts/6").  This test group relies on the target
> +     * existing, so guarantee that (so create a file at "/6" if
> +     * necessary). */
> +    if (stat (target, &st) < 0)
> +      {
> +        VERIFY (errno == ENOENT);
> +        xtouch (target, 0);
> +      }
> +
> +    VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0);
> +    VERIFY (mount ("/console", target, NULL, MS_BIND, NULL) == 0);
> +    ok = doit (slave, "with readlink target",
> +               (struct result_r){.name=target, .ret=0, .err=0}) && ok;
> +    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);
> +    ok = doit (slave, "with readlink trap; fallback",
> +               (struct result_r){.name="/dev/console", .ret=0, .err=0}) && ok;
> +    VERIFY (umount (target) == 0);
> +    VERIFY (umount ("/dev/console") == 0);
> +
> +    VERIFY (mount (slavename, target, NULL, MS_BIND, NULL) == 0);
> +    ok = doit (slave, "with readlink trap; no fallback",
> +               (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV}) && ok;

Same comment as above: Imho, this is not very nice. It looks clever but is hard
to parse semantically. I'd prefer if the assignment would be entangled from the
following "&& ret" check. This is just easier to maintain.

> +    VERIFY (umount (target) == 0);
> +  }
> +
> +  /* This test is extra-convoluted because we need to control the
> +     order of files within the directory.  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.  This assumes

Why do you want to assign meaning based on order not name? If there's a
legitimate reason please mention it.

> +     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);
> +
> +    xtouch ("/dev/console1", 0);
> +    xtouch ("/dev/console2", 0);
> +    xtouch ("/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);
> +    ok = doit (slave, "with search-path trap",
> +               (struct result_r){.name=c[1], .ret=0, .err=0}) && ok;

Same comment as above: Imho, this is not very nice. It looks clever but is hard
to parse semantically. I'd prefer if the assignment would be entangled from the
following "&& ret" check. This is just easier to maintain.

> +    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)
> +{
> +  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;
> +
> +  return  ret1 | ret2;
> +}
> +
> +#include <support/test-driver.c>
> -- 
> 2.15.0
>
  
Luke Shumaker Nov. 8, 2017, 11:23 p.m. UTC | #3
On Wed, 08 Nov 2017 14:43:07 -0500,
Christian Brauner wrote:
> > +  if ((fd = open ("/proc/self/uid_map", O_RDWR, 0)) >= 0)
> > +    {
> > +      char buf;
> > +      if (read (fd, &buf, 1) == 0)
> > +	{
> > +	  char *str = xasprintf ("0 %ld 1\n", (long)orig_uid);
> > +	  if (write (fd, str, strlen (str)) < 0)
> > +	    FAIL_EXIT1 ("write (uid_map, \"%s\"): %m", str);
> 
> I haven't looked at the test code a lot yet but is it generally considered ok to
> not cleanup before exit? That's mostly style but still it'd be good to clarify
> this. I usually do all the cleanup even when exiting.

Clean up the file system, or clean up memory?  FS cleanup isn't
necessary because we do everything on a tmpfs in a new mount
namespace.  Looking at other tests, it seems to be OK to leave memory
messy when exiting with an error.

> > +	  free (str);
> > +	}
> > +      xclose (fd);
> > +    }
> > +
> > +  /* Oh, but we can't set the gid map until we turn off setgroups. */
> 
> That sounds like a discovery you just made. I think - nitpick - a simple:
> /* We need to set setgroups to "deny" otherwise we won't be able to write a gid
> *  mapping. */

My intent was for it to read as a postscript to the previous comment,
but I'm happy to change it.

> > +    }
> > +
> > +  char *actual_name = actual.name
> > +    ? xasprintf ("\"%s\"", actual.name)
> > +    : strdup ("NULL");
> 
> I'd prefer if this would be proper if () branches as this is pretty unreadable.
> Also, I'd prefer - even though newer C standards allow this - if new variables
> wouldn't be declared mid-stack especially if the function is rather small.

Sure.

> > +  char *expected_name = expected.name
> > +    ? xasprintf ("\"%s\"", expected.name)
> > +    : strdup ("NULL");
> 
> Likewise.

Ack.

> > +    }
> > +
> > +  char *actual_name = actual.name
> > +    ? xasprintf ("\"%s\"", actual.name)
> > +    : strdup ("NULL");
> 
> Likewise.

Ack.

> > +  char *expected_name = expected.name
> > +    ? xasprintf ("\"%s\"", expected.name)
> > +    : strdup ("NULL");
> 
> Likewise.

Ack.

> > +static bool
> > +doit (int fd, const char *testname, struct result_r expected_r) {

(whoops, that brace needs to be on a new line)

> > +  bool ret = true;
> > +
> > +  printf ("info:    testcase: %s\n", testname);
> > +
> > +  ret = eq_ttyname (run_ttyname (fd), expected) && ret;
> > +  ret = eq_ttyname_r (run_ttyname_r (fd), expected_r) && ret;
> 
> Imho, this is not very nice. It looks clever but is hard to parse semantically.
> I'd prefer if the assignment from eq_ttyname{_r}() would be entangled from the
> following "&& ret" check. This is just easier to maintain.

Ok, see below.

> > +  /* enable `wait`ing on grandchildren */
> > +  VERIFY (prctl (PR_SET_CHILD_SUBREAPER, 1) == 0);
> 
> For future reference in case you want to avoid this clutch you can also use
> clone() with CLONE_PARENT.

I wanted to do that, but unfortunately glibc's clone(2) wrapper
requires passing in the stack explicitly, which requires knowing if
the stack grows up or down; and I didn't want to have to deal with
that in this test.

> > +  /* There are 3 groups of tests here.  The first group throws a
> > +     wrench into the works in a generic way, and verifies that ttyname
> 
> That's using a pretty (for a non-native speaker) specific English idiom. Could
> you maybe simply say "creates problems" or something similar.

Ok.

> > +     copes correctly.  The remaining groups are increasingly
> > +     convoluted, as we direct that wrench towards specific parts of
> 
> Likewise.

Ok.

> > +  {
> > +    char *linkname = xasprintf ("/proc/self/fd/%d", slave);
> > +    char *target = xreadlink (linkname);
> > +    free (linkname);
> > +    /* Depeding 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",
> 
> Uhm? I'm sorry, I don't understand. That sounds like a bug we fixed a while back
> upstream whereby the kernel recorded the wrong vfs mount for devpts. That's one
> scenario where this can happen. In any case the comment should outline when the
> kernel can *legitimately* show you the contents of the symlink as /<n> instead
> of the correct /dev/pts/<n>. Right now it sounds like this code doesn't
> understand how such a scenario can happen but only that it can happen.

You're right, this comment should be clearer.  The test has two
chroot-setup routines, and runs this code in each.  One of the setup
routines does it in a way that results in /<n> and the other does it
in a way that results in /dev/pts/<n>; this code doesn't have context
to know which of the setup routines was called.

> 
> > +     * instead of "/dev/pts/6").  This test group relies on the target
> > +     * existing, so guarantee that (so create a file at "/6" if
> > +     * necessary). */
> > +    if (stat (target, &st) < 0)
> > +      {
> > +        VERIFY (errno == ENOENT);
> > +        xtouch (target, 0);
> > +      }
> > +
> > +    VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0);
> > +    VERIFY (mount ("/console", target, NULL, MS_BIND, NULL) == 0);
> > +    ok = doit (slave, "with readlink target",
> > +               (struct result_r){.name=target, .ret=0, .err=0}) && ok;
> > +    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);
> > +    ok = doit (slave, "with readlink trap; fallback",
> > +               (struct result_r){.name="/dev/console", .ret=0, .err=0}) && ok;
> > +    VERIFY (umount (target) == 0);
> > +    VERIFY (umount ("/dev/console") == 0);
> > +
> > +    VERIFY (mount (slavename, target, NULL, MS_BIND, NULL) == 0);
> > +    ok = doit (slave, "with readlink trap; no fallback",
> > +               (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV}) && ok;
> 
> Same comment as above: Imho, this is not very nice. It looks clever but is hard
> to parse semantically. I'd prefer if the assignment would be entangled from the
> following "&& ret" check. This is just easier to maintain.

How would you rather it be written?

    static void
    doit (..., bool *ok)
    {
      ...

      if (!eq_ttyname (run_ttyname (fd), expected))
        *ok = false;
      if (!eq_ttyname_r (run_ttyname_r (fd), expected_r))
        *ok = false;
    }
  
    doit(..., &ok);

?

> > +    VERIFY (umount (target) == 0);
> > +  }
> > +
> > +  /* This test is extra-convoluted because we need to control the
> > +     order of files within the directory.  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.  This assumes
> 
> Why do you want to assign meaning based on order not name? If there's a
> legitimate reason please mention it.

Reason: for this test "we need to control the order of files within
the directory."

Conceptually, the test is making sure that an pseudo-match from the
wrong devpts that readdir() finds before it finds the real match
doesn't mess anything up.

To test that, we need to set up the pseudo-match and the actual match
such that readdir finds the pseudo-match first.  There's no good way
to control the order that readdir will find the files in (maybe I
should look in to the whitebox testing infrastructure?).  So we create
two files, and whichever one readdir finds first, we assign that one
to be the pseudo-match; and whichever one readdir finds second, we
assign that one to be the actual match.

(We also test that readdir finding a pseudo-match after the actual
match doesn't mess anything up either, hence 3 files instead of 2.)

> > +    ok = doit (slave, "with search-path trap",
> > +               (struct result_r){.name=c[1], .ret=0, .err=0}) && ok;
> 
> Same comment as above: Imho, this is not very nice. It looks clever but is hard
> to parse semantically. I'd prefer if the assignment would be entangled from the
> following "&& ret" check. This is just easier to maintain.

See above.
  
Luke Shumaker Nov. 8, 2017, 11:43 p.m. UTC | #4
On Wed, 08 Nov 2017 18:23:49 -0500,
Luke Shumaker wrote:
> > > +	    FAIL_EXIT1 ("write (uid_map, \"%s\"): %m", str);
> > 
> > I haven't looked at the test code a lot yet but is it generally considered ok to
> > not cleanup before exit? That's mostly style but still it'd be good to clarify
> > this. I usually do all the cleanup even when exiting.
> 
> Clean up the file system, or clean up memory?  FS cleanup isn't
> necessary because we do everything on a tmpfs in a new mount
> namespace.  Looking at other tests, it seems to be OK to leave memory
> messy when exiting with an error.

(if it weren't OK to leave the memory a mess, then the x* functions in
support/ wouldn't be calling FAIL_EXIT1 behind your back)

> > > +    ok = doit (slave, "with readlink trap; no fallback",
> > > +               (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV}) && ok;
> > 
> > Same comment as above: Imho, this is not very nice. It looks clever but is hard
> > to parse semantically. I'd prefer if the assignment would be entangled from the
> > following "&& ret" check. This is just easier to maintain.
> 
> How would you rather it be written?
> 
>     static void
>     doit (..., bool *ok)
>     {
>       ...
> 
>       if (!eq_ttyname (run_ttyname (fd), expected))
>         *ok = false;
>       if (!eq_ttyname_r (run_ttyname_r (fd), expected_r))
>         *ok = false;
>     }
>   
>     doit(..., &ok);
> 
> ?

Disregard that, I was being silly,

    if (!doit (...))
      ok = false;

doesn't add too much noise.
  

Patch

diff --git a/ChangeLog b/ChangeLog
index b5d214ae6f..1982a498ce 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,9 @@ 
 2017-11-02  Luke Shumaker  <lukeshu@parabola.nu>
 
+	[BZ #22145]
+	* sysdeps/unix/sysv/linux/tst-ttyname.c: New file.
+	* sysdeps/unix/sysv/linux/Makefile: Add tst-ttyname to tests.
+
 	[BZ #22145]
 	* sysdeps/unix/sysv/linux/ttyname.c (ttyname):
 	Defer is_pty check until end.
diff --git a/sysdeps/unix/sysv/linux/Makefile b/sysdeps/unix/sysv/linux/Makefile
index 0c8a009b5e..69edb75209 100644
--- a/sysdeps/unix/sysv/linux/Makefile
+++ b/sysdeps/unix/sysv/linux/Makefile
@@ -50,7 +50,8 @@  sysdep_headers += sys/mount.h sys/acct.h sys/sysctl.h \
 		  bits/siginfo-arch.h bits/siginfo-consts-arch.h
 
 tests += tst-clone tst-clone2 tst-clone3 tst-fanotify tst-personality \
-	 tst-quota tst-sync_file_range test-errno-linux tst-sysconf-iov_max
+	 tst-quota tst-sync_file_range tst-sysconf-iov_max tst-ttyname \
+	 test-errno-linux
 
 # Generate the list of SYS_* macros for the system calls (__NR_*
 # macros).  The file syscall-names.list contains all possible system
diff --git a/sysdeps/unix/sysv/linux/tst-ttyname.c b/sysdeps/unix/sysv/linux/tst-ttyname.c
new file mode 100644
index 0000000000..45d356cab4
--- /dev/null
+++ b/sysdeps/unix/sysv/linux/tst-ttyname.c
@@ -0,0 +1,582 @@ 
+/* Copyright (C) 2017 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 <http://www.gnu.org/licenses/>.  */
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <support/check.h>
+#include <support/namespace.h>
+#include <support/support.h>
+#include <support/temp_file.h>
+#include <support/test-driver.h>
+#include <support/xunistd.h>
+
+/* generic utilities */
+
+#define VERIFY(expr)                                                    \
+  do {                                                                  \
+    if (!(expr))                                                        \
+      {                                                                 \
+        printf ("error: %s:%d: %s: %m\n",                               \
+                __FILE__, __LINE__, #expr);                             \
+        exit (1);                                                       \
+      }                                                                 \
+  } while (0)
+
+static void
+xtouch (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 *
+xreadlink (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;
+}
+
+static void
+become_root_in_mount_ns (void) {
+  uid_t orig_uid = getuid ();
+  gid_t orig_gid = getgid ();
+
+  support_become_root ();
+
+  if (unshare (CLONE_NEWNS) < 0)
+    FAIL_UNSUPPORTED ("could not enter new mount namespace");
+
+  /* support_become_root might have put us in a new user namespace;
+     most filesystems (including tmpfs) don't allow file or directory
+     creation from a user namespace unless uid and gid maps are set,
+     even if we have root privileges in the namespace (failing with
+     EOVERFLOW, since the uid overflows the empty (0-length) uid map).
+
+     Also, stat always reports that uid and gid maps are empty, so we
+     have to try actually reading from them to check if they are
+     empty. */
+  int fd;
+
+  if ((fd = open ("/proc/self/uid_map", O_RDWR, 0)) >= 0)
+    {
+      char buf;
+      if (read (fd, &buf, 1) == 0)
+	{
+	  char *str = xasprintf ("0 %ld 1\n", (long)orig_uid);
+	  if (write (fd, str, strlen (str)) < 0)
+	    FAIL_EXIT1 ("write (uid_map, \"%s\"): %m", str);
+	  free (str);
+	}
+      xclose (fd);
+    }
+
+  /* Oh, but we can't set the gid map until we turn off setgroups. */
+  if ((fd = open ("/proc/self/setgroups", O_WRONLY, 0)) >= 0)
+    {
+      const char *str = "deny";
+      if (write (fd, str, strlen (str)) < 0)
+	FAIL_EXIT1 ("write (setroups, \"%s\"): %m", str);
+      xclose (fd);
+    }
+
+  if ((fd = open ("/proc/self/gid_map", O_RDWR, 0)) >= 0)
+    {
+      char buf;
+      if (read (fd, &buf, 1) == 0)
+	{
+	  char *str = xasprintf ("0 %ld 1\n", (long)orig_gid);
+	  if (write (fd, str, strlen (str)) < 0)
+	    FAIL_EXIT1 ("write (gid_map, \"%s\"): %m", str);
+	  free (str);
+	}
+      xclose (fd);
+    }
+}
+
+/* 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)
+{
+  if ((actual.err == expected.err) &&
+      (!actual.name == !expected.name) &&
+      (actual.name ? strcmp (actual.name, expected.name) == 0 : true))
+    {
+      char *name = expected.name
+	? xasprintf ("\"%s\"", expected.name)
+	: strdup ("NULL");
+      printf ("info:      ttyname: PASS {name=%s, errno=%d}\n",
+	      name, expected.err);
+      free (name);
+      return true;
+    }
+
+  char *actual_name = actual.name
+    ? xasprintf ("\"%s\"", actual.name)
+    : strdup ("NULL");
+  char *expected_name = expected.name
+    ? xasprintf ("\"%s\"", expected.name)
+    : strdup ("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)
+{
+  if ((actual.err == expected.err) &&
+      (actual.ret == expected.ret) &&
+      (!actual.name == !expected.name) &&
+      (actual.name ? strcmp (actual.name, expected.name) == 0 : true))
+    {
+      char *name = expected.name
+	? xasprintf ("\"%s\"", expected.name)
+	: strdup ("NULL");
+      printf ("info:      ttyname_r: PASS {name=%s, ret=%d, errno=%d}\n",
+              name, expected.ret, expected.err);
+      free (name);
+      return true;
+    }
+
+  char *actual_name = actual.name
+    ? xasprintf ("\"%s\"", actual.name)
+    : strdup ("NULL");
+  char *expected_name = expected.name
+    ? xasprintf ("\"%s\"", expected.name)
+    : strdup ("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);
+
+  ret = eq_ttyname (run_ttyname (fd), expected) && ret;
+  ret = eq_ttyname_r (run_ttyname_r (fd), expected_r) && ret;
+
+  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
+
+/* These chroot setup functions put the TTY at at "/console" (where it
+   won't be found by ttyname), and create "/dev/console" as an
+   ordinary file.  This way, it's easier to write test-cases that
+   expect ttyname to fail; test-cases that expect it to succeed need
+   to explicitly remount it at "/dev/console". */
+
+static int
+do_in_chroot_1 (int (*cb)(const char *, int)) {
+  printf ("info:  entering chroot 1\n");
+
+  /* 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);
+  int slave = xopen (slavename, O_RDWR, 0);
+  if (!doit (slave, "basic smoketest",
+             (struct result_r){.name=slavename, .ret=0, .err=0}))
+    return 1;
+
+  pid_t pid = xfork ();
+  if (pid == 0)
+    {
+      xclose (master);
+
+      become_root_in_mount_ns ();
+
+      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 ("/proc", "proc", NULL, MS_BIND|MS_REC, NULL) == 0);
+      VERIFY (mount ("devpts", "dev/pts", "devpts",
+                     MS_NOSUID|MS_NOEXEC,
+                     "newinstance,ptmxmode=0666,mode=620") == 0);
+      VERIFY (symlink ("pts/ptmx", "dev/ptmx") == 0);
+
+      xtouch ("console", 0);
+      xtouch ("dev/console", 0);
+      VERIFY (mount (slavename, "console", NULL, MS_BIND, NULL) == 0);
+
+      xchroot (".");
+
+      char *linkname = xasprintf ("/proc/self/fd/%d", slave);
+      char *target = xreadlink (linkname);
+      VERIFY (strcmp (target, slavename) == 0);
+      free (linkname);
+
+      _exit (cb (slavename, slave));
+    }
+  int status;
+  xwaitpid (pid, &status, 0);
+  VERIFY (WIFEXITED (status));
+  xclose (master);
+  xclose (slave);
+  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);
+  /* 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]);
+
+      become_root_in_mount_ns ();
+
+      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);
+
+      xtouch ("console", 0);
+      xtouch ("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 = xreadlink (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 throws a
+     wrench into the works in a generic way, and verifies that ttyname
+     copes correctly.  The remaining groups are increasingly
+     convoluted, as we direct that wrench towards specific parts of
+     the routine, and try to break it in specific ways. */
+
+  /* Basic tests that it doesn't get confused by multiple devpts
+     instances. */
+  {
+    VERIFY (stat (slavename, &st) < 0); /* sanity check */
+    ok = doit (slave, "no conflict, no match",
+               (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV}) && ok;
+    VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0);
+    ok = doit (slave, "no conflict, console",
+               (struct result_r){.name="/dev/console", .ret=0, .err=0}) && ok;
+    VERIFY (umount ("/dev/console") == 0);
+
+    /* keep creating PTYs until we we get a name collision */
+    while (stat (slavename, &st) < 0)
+      posix_openpt (O_RDWR|O_NOCTTY|O_NONBLOCK);
+    VERIFY (stat (slavename, &st) == 0);
+
+    ok = doit (slave, "conflict, no match",
+               (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV}) && ok;
+    VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0);
+    ok = doit (slave, "conflict, console",
+               (struct result_r){.name="/dev/console", .ret=0, .err=0}) && ok;
+    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 = xreadlink (linkname);
+    free (linkname);
+    /* Depeding 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").  This test group relies on the target
+     * existing, so guarantee that (so create a file at "/6" if
+     * necessary). */
+    if (stat (target, &st) < 0)
+      {
+        VERIFY (errno == ENOENT);
+        xtouch (target, 0);
+      }
+
+    VERIFY (mount ("/console", "/dev/console", NULL, MS_BIND, NULL) == 0);
+    VERIFY (mount ("/console", target, NULL, MS_BIND, NULL) == 0);
+    ok = doit (slave, "with readlink target",
+               (struct result_r){.name=target, .ret=0, .err=0}) && ok;
+    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);
+    ok = doit (slave, "with readlink trap; fallback",
+               (struct result_r){.name="/dev/console", .ret=0, .err=0}) && ok;
+    VERIFY (umount (target) == 0);
+    VERIFY (umount ("/dev/console") == 0);
+
+    VERIFY (mount (slavename, target, NULL, MS_BIND, NULL) == 0);
+    ok = doit (slave, "with readlink trap; no fallback",
+               (struct result_r){.name=NULL, .ret=ENODEV, .err=ENODEV}) && ok;
+    VERIFY (umount (target) == 0);
+  }
+
+  /* This test is extra-convoluted because we need to control the
+     order of files within the directory.  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.  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);
+
+    xtouch ("/dev/console1", 0);
+    xtouch ("/dev/console2", 0);
+    xtouch ("/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);
+    ok = doit (slave, "with search-path trap",
+               (struct result_r){.name=c[1], .ret=0, .err=0}) && ok;
+    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)
+{
+  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;
+
+  return  ret1 | ret2;
+}
+
+#include <support/test-driver.c>