How to we find out the pseudoterminal master and slave from each other

pseudoterminalptspty

A pseudoterminal has a pair of master and slave.

How can we find out the master device file from a slave device file (e.g. /etc/pts/3)? I only find /dev/ptmx and /dev/pts/ptmx, but they can't be shared by multiple slaves.

Given one of the processes working on the master and slave, how can we find out the other?
For example, ps provides information about the controlling tty of each process. Can it be helpful?

Thanks.

Best Answer

That is one thing that is harder than it should be.

With newer linux kernels, the index of the slave pty paired with a master can be gathered from the tty-index entry from /proc/PID/fdinfo/FD. See this commit.

With older kernels, the only way you can get that is by attaching with a debugger to a process holding a master pty, and call ptsname(3) (or directly ioctl(TIOCGPTN)) on the file descriptor.

[but both methods will run into issues on systems using multiple devpts mounts, see below]

With that info, you can build a list of master-slave pairings, which will also allow you to look up the master starting up from the slave.

Here is a silly script that should do that; it will first try the tty-index way, and if that doesn't work it will fall back to gdb. For the latter case, it needs a working gdb (not gdb-minimal or another half-broken gdb most distros ship with) and because of its use of gdb, it will be very slow.

For each pty pair, it will print something like:

/dev/pts/1
    1227  3     t/ct_test
        1228  +*      t/ct_test
        1230  +      t/ct_test
/dev/pts/3
    975   9     'sshd: root [priv]' '' '' '' '' '' '' '' ''
    978   14,18,19   'sshd: root@pts/3' '' '' '' '' '' '' ''
        979   -*0,1,2,255   -bash
        1222  1     tiocsti
        1393  -0,1,2   sleep 3600
        1231  +0,2   perl ptys.pl
        1232  +1,2   cut -b1-60

the two sshd processes (pids 975 and 978) have open handles to the master side (one as its 9 fd and the other as its 14, 18 and 19 fds). sleep and -bash have open handles to the slave side as their standard (0,1 and 2) fds. The session leader (bash) is also marked with a *, the processes in the foreground (perl and cut) with a +, and those in the background (less and -bash) with a -.

The t/ct_test processes are using the pty as their controlling terminal without having any fd open to it. tiocsti has an open handle to it without it being its controlling terminal.

Tested on Debian 9 and Fedora 28. Info about the magic numbers it's using can be found in procfs(5) and Documentation/admin-guide/devices.txt in the linux kernel source.


This will fail on any system using chroots or namespace containers; that's not fixable without some changes to the kernel, since there's no reliable way to match the tty field from /proc/PID/stat to a pty, and a fd opened via /dev/ptmx to the corresponding /dev/pts mount. See here for a rant about it.

This will also not link in any fd opened via /dev/tty; the real tty could be worked out by attaching to the process and calling ioctl(fd, TIOCGDEV, &dev), but that will mean another dirty heavy use of gdb, and it will run into the same issues as above with the major, minor numbers being ambiguous for pseudo-tty slaves.

ptys.pl:

my (%pty, %ctty);
for(</proc/*[0-9]*/{fd/*,stat}>){
    if(my ($pid, $fd) = m{/proc/(\d+)/fd/(\d+)}){
        next unless -c $_;
        my $rdev = (stat)[6]; my $maj = $rdev >> 8 & 0xfff;
        if($rdev == 0x502){ # /dev/ptmx or /dev/pts/ptmx
            $pty{ptsname($pid, $fd, readlink $_)}{m}{$pid}{$fd} = 1;
        }elsif($maj >= 136 && $maj <= 143){ # /dev/pts/N
            $pty{readlink $_}{s}{$pid}{$fd} = 1;
        }
    }else{
        my @s = readfile($_) =~ /(?<=\().*(?=\))|[^\s()]+/gs;
        $ctty{$s[6]}{$s[0]} =       # ctty{tty}{pid} =
            ($s[4] == $s[7] ? '+' : '-').   # pgrp == tpgid
            ($s[0] == $s[5] ? '*' : '');    # pid == sid
    }
}
for(sort {length($a)<=>length($b) or $a cmp $b} keys %pty){
    print "$_\n";
    pproc(4, $pty{$_}{m}); pproc(8, $pty{$_}{s}, $ctty{(stat)[6]});
}

sub readfile { local $/; my $h; open $h, '<', shift and <$h> }
sub cmdline {
    join ' ', map { s/'/'\\''/g, $_ = "'$_'" if m{^$|[^\w./+=-]}; $_ }
        readfile("/proc/$_[0]/cmdline") =~ /([^\0]*)\0/g;
}
sub pproc {
    my ($px, $h, $sinfo) = @_;
    exists $$h{$_} or $$h{$_} = {''} for keys %$sinfo;
    return printf "%*s???\n", $px, "" unless $h;
    for my $pid (sort {$a<=>$b} keys %$h){
        printf "%*s%-5d %s%-3s   %s\n", $px, "", $pid, $$sinfo{$pid},
            join(',', sort {$a<=>$b} keys %{$$h{$pid}}),
            cmdline $pid;
    }
}
sub ptsname {
    my ($pid, $fd, $ptmx) = @_;
    return '???' unless defined(my $ptn = getptn($pid, $fd));
    $ptmx =~ m{(.*)(?:/pts)?/ptmx$} ? "$1/pts/$ptn" : "$ptmx ..?? pts/$ptn"
}
sub getptn {
    my ($pid, $fd) = @_;
    return $1 if
        readfile("/proc/$pid/fdinfo/$fd") =~ /^tty-index:\s*(\d+)$/m;
    return gdb_ioctl($pid, $fd, 0x80045430);    # TIOCGPTN
}
sub gdb_ioctl {
    my ($pid, $fd, $ioctl) = @_;
    my $cmd = qq{p (int)ioctl($fd, $ioctl, &errno) ? -1 : errno};
    qx{exec 3>&1; gdb -batch -p $pid -ex '$cmd' 2>&1 >&3 |
            grep -v '/sysdeps/.*No such file or directory' >&2}
        =~ /^\$1 *= *(\d+)$/m ? $1 : undef;
}
Related Question