26

I administer a Gentoo Hardened box that uses file capabilities to eliminate most of the need for setuid-root binaries (e.g. /bin/ping has CAP_NET_RAW, etc).

Infact, the only binary I have left is this one:

abraxas ~ # find / -xdev -type f -perm -u=s
/usr/lib64/misc/glibc/pt_chown
abraxas ~ # 

If I remove the setuid bit, or remount my root filesystem nosuid, sshd and GNU Screen stop working, because they call grantpt(3) on their master pesudoterminals and glibc apparently executes this program to chown and chmod the slave pseudoterminal under /dev/pts/, and GNU Screen cares about when this function fails.

The problem is, the manpage for grantpt(3) explicitly states that under Linux, with the devpts filesystem mounted, no such helper binary is required; the kernel will automatically set the UID & GID of the slave to the real UID & GID of the process that opened /dev/ptmx (by calling getpt(3)).

I have written a small example program to demonstrate this:

#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int main(void)
{
    int master;
    char slave[16];
    struct stat slavestat;
    if ((master = getpt()) < 0) {
        fprintf(stderr, "getpt: %m\n");
        return 1;
    }
    printf("Opened a UNIX98 master terminal, fd = %d\n", master);
    /* I am not going to call grantpt() because I am trying to
     * demonstrate that it is not necessary with devpts mounted,
     * the owners and mode will be set automatically by the kernel.
     */
    if (unlockpt(master) < 0) {
        fprintf(stderr, "unlockpt: %m\n");
        return 2;
    }
    memset(slave, 0, sizeof(slave));
    if (ptsname_r(master, slave, sizeof(slave)) < 0) {
        fprintf(stderr, "ptsname: %m\n");
        return 2;
    }
    printf("Device name of slave pseudoterminal: %s\n", slave);
    if (stat(slave, &slavestat) < 0) {
        fprintf(stderr, "stat: %m\n");
        return 3;
    }
    printf("Information for device %s:\n", slave);
    printf("    Owner UID:  %d\n", slavestat.st_uid);
    printf("    Owner GID:  %d\n", slavestat.st_gid);
    printf("    Octal mode: %04o\n", slavestat.st_mode & 00007777);
    return 0;
}

Observe it in action with the setuid bit on the aforementioned program removed:

aaron@abraxas ~ $ id
uid=1000(aaron) gid=100(users) groups=100(users)
aaron@abraxas ~ $ ./ptytest 
Opened a UNIX98 master terminal, fd = 3
Device name of slave pseudoterminal: /dev/pts/17
Information for device /dev/pts/17:
    Owner UID:  1000
    Owner GID:  100
    Octal mode: 0620

I have only a few ideas as to how to work around this problem:

1) Replace the program with a skeleton that simply returns 0.

2) Patch grantpt() in my libc to do nothing.

I can automate both of these, but does anyone have a recommendation for one over the other, or recommendations for how else to solve this?

Once this is solved, I can finally mount -o remount,nosuid /.

Aaron Jones
  • 259
  • 2
  • 6
  • While I await a response, I went with approach 1, and sshd and GNU Screen still work. – Aaron Jones Mar 16 '13 at 17:37
  • What exactly are the programs that fail? Perhaps _they_ are broken and check not for the `pty` (as they should) but for the program? – vonbrand Mar 16 '13 at 22:03
  • Any program that does not have CAP_CHOWN and CAP_FOWNER, calls grantpt(), and the helper binary is not started with EUID == 0, will have a non-zero return code for grantpt(), and programs SHOULD abort PTS creation when this happens, as per ptmx(4). – Aaron Jones Mar 16 '13 at 22:21
  • I eventually patched the glibc source to immediately return 0 in grantpt() and immediately return 0 in pt_chown's main(). Everything works great without the need for a setuid-root binary now. – Aaron Jones Mar 16 '13 at 22:22
  • 3
    That "solution" gives me the willies... in the best case, it papers over a bug, it probably creates a new bug, in the worst case it creates a serious security vulnerability. Please take this up with the glibc developers. – vonbrand Mar 16 '13 at 22:25
  • The helper binary is simply not required for Linux when devpts is mounted, because the kernel will change the mode & owner UID/GID when the process calls getpt(). This is exactly what the helper binary also does, but it causes grantpt() to return a non-zero exit code if the chown() or chmod() fail. This is undesirable because when the root filesystem is mounted nosuid, people without capabilities to perform chown() or chmod() on arbitrary files cannot create pseudoterminals without an error code, and thus cannot use things like GNU Screen. This does not create a vulnerability or a bug. – Aaron Jones Mar 16 '13 at 22:26
  • You will notice that at `line 270` of `pty.c` in version `4.0.3` of GNU Screen, it will abort if `grantpt()` returns a non-zero result. This is exactly what will happen if an unprivileged user attempts to use GNU Screen with a `nosuid` root filesystem, because `grantpt()` calls the helper binary regardless of whether `devpts` is mounted, and the helper binary will return a non-zero result if its EUID is not 0. – Aaron Jones Mar 16 '13 at 22:50
  • 3
    Then report this to the glibc people. – vonbrand Mar 16 '13 at 22:55
  • Any reason why you can't/don't want to give CAP_CHOWN to the helper app? – peterph Mar 26 '13 at 00:09
  • That would not make a difference, you would have to modify the glibc sources (where the helper app is contained) anyway, to remove the check for EUID == 0. – Aaron Jones Mar 26 '13 at 21:09
  • 3) Patch grantpt() to return success if the permissions are already as they should be? If that's not what it's doing... – frostschutz Apr 13 '13 at 01:17

1 Answers1

2

If your glibc is reasonably current, and devpts is set up correctly, there should be no need to invoke the pt_chown helper at all.

You might be running into a known/potential issue removing setuid-root from pt_chown.

grantpt() supported devfs from glibc-2.7, changes were made in glibc-2.11 though so that rather than explicitly checking for DEVFS_SUPER_MAGIC, it checks instead to see if it needs to do any work before attempting chown() or falling back to invoking pt_chown.

From glibc-2.17/sysdeps/unix/grantpt.c

  ...
  uid_t uid = __getuid ();
  if (st.st_uid != uid)
    {
       if (__chown (buf, uid, st.st_gid) < 0)
       goto helper;
    }
  ...

A similar stanza is used to check the gid and the permissions. The catch is that the uid, gid and mode must match expectations (you, tty, and exactly 620; confirm with /usr/libexec/pt_chown --help). If not, chown() (which would require capabilities CAP_CHOWN, CAP_FOWNER of the calling binary/process) is attempted, and if that fails, the pt_chown external helper (which must be setuid-root) is attempted. In order for pt_chown to be able to use capabilities it (and hence your glibc) must have been compiled with HAVE_LIBCAP. However, it seems that pt_chown is (as of glibc-2.17, and as you noted though you haven't stated the version) hard-coded to want geteuid()==0 regardless of HAVE_LIBCAP, relevant code from glibc-2.17/login/programs/pt_chown.c:

  ...
  if (argc == 1 && euid == 0)
    {
#ifdef HAVE_LIBCAP
  /* Drop privileges.  */
     if (uid != euid)
  ...
#endif
    /* Normal invocation of this program is with no arguments and
       with privileges.  */
    return do_pt_chown ();
  }
...
  /* Check if we are properly installed.  */
  if (euid != 0)
    error (FAIL_EXEC, 0, gettext ("needs to be installed setuid `root'"));

(Expecting geteuid()==0 before attempting to use capabilities doesn't seem to be really in the spirit of capabilities, I'd go with logging a bug on this one.)

A potential workaround might be to give CAP_CHOWN, CAP_FOWNER to the affected programs, but I really don't recommend that since you cannot restrict that to ptys of course.

If that doesn't help you solve it, patching sshd and screen is slightly less disagreeable than patching glibc. Since the problem lies within glibc though, a cleaner approach would be selective use of DLL injection to implement a dummy grantpt().

mr.spuratic
  • 9,721
  • 26
  • 41
  • "Since the problem lies within glibc though, a cleaner approach would be selective use of DLL injection to implement a dummy grantpt()." -- Brilliant. Why did I not think of that? Thanks. :) – Aaron Jones Apr 26 '13 at 03:33