Linux – Best Way to Work Around Glibc Problem

glibclinux-kernelSecuritysetuid

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 /.

Best Answer

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().

Related Question