The old AT&T System 5 mechanism for pseudo-terminal slave devices was that they were ordinary persistent character device nodes under /dev. There was a multiplexor master device at /dev/ptmx. The old 4.3BSD mechanism for pseudo-terminal devices had parallel pairs of ordinary persistent master and slave device nodes under /dev.
In both cases, this meant that the slave device files retained their last ownership and permissions after last file descriptor closure. Hence the evolution of the grantpt() function to fix up the ownership and permissions of the slave device file after a (re-used) pseudo-terminal had been (re-)allocated.
This in turn meant that there was a window when a program was setting up a re-used pseudo-terminal between the open() and the grantpt() where whoever had owned the slave device beforehand could sneak in and open it as well, potentially gaining access to someone else's terminal. Hence the idea of pseudo-terminal slave character devices starting in a locked state where they could not be opened and being unlocked by unlockpt() after the grantpt() had been successfully performed.
Over the years, it turned out that this was unnecessary.
Nowadays, the slave device files are not persistent, because the kernel makes and destroys things in /dev itself. The act of opening the master device either resets the slave device permissions and ownership, or outright creates the slave device file afresh (in the latter case with the slave device file disappearing again when all open file descriptors are closed), in either case atomically in the same system call.
- On OpenBSD, this is part of the
PTMGET I/O control's functionality on the /dev/ptm device. /dev is still a disc volume, and the kernel internally issues the relevant calls to create new device nodes there and reset their ownerships and permissions.
- On FreeBSD, this is done by the
posix_openpt() system call. /dev is not a disc volume at all. It is a devfs filesystem. It contains no "multiplexor" device nor master device files, because posix_openpt() is an outright system call, not an wrapped ioctl() on an open file descriptor. Slave devices appear in the devfs filesystem under its pts/ directory.
The kernel thus ensures that they have the right permissions and ownership ab initio, and there is no window of opportunity where they have stale ones. Thus the grantpt() and unlockpt() library functions are essentially no-ops, whose sole remaining functionality is to check their passed file descriptor and set EINVAL if it isn't the master side of a pseudo-terminal, because programs might be doing daft things like passing non-pseudo-terminal file descriptors to these functions and expecting them to return errors.
For a while on Linux, pseudo-terminal slave devices were persistent device nodes. The GNU C library's grantpt() wasn't a system call. Rather, it forked and executed a set-UID helper program named pt_chown, much to the dismay of the no set-UID executables crowd. (grantpt() has to allow an unprivileged user to change the ownership and permissions of a special device file that it does not necessarily own, remember.) So there was still the window of opportunity, and Linux still had to maintain a lock for unlockpt().
Its "new" devpts filesystem (where "new" means introduced quite a few years ago, now) almost permits the same way of doing things as on FreeBSD with devfs, however. There are some differences.
- There is still a "multiplexor" device.
- In the older "new"
devpts system, this was a ptmx device in a different devtmpfs filesystem, with the devpts filesystem containing only the automatically created/destroyed slave device files. Conventionally the setup was /dev/ptmx and an accompanying devpts mount at /dev/pts.
- But Linux people wanted to have multiple wholly independent instances of the
devpts filesystem, for containers and the like, and it turned out to be quite hard synchronizing the (correct) two filesystems when there were many devtmpfs and devpts filesystems. So in the newer "new" devpts system all of the devices, multiplexor and slave, are in the one filesystem. For backwards compatibility, the default was for the new ptmx node to be inaccessible unless one set a new ptmxmode mount option.
- In the even newer still "new"
devpts the ptmx device file in the devpts filesystem is now the primary multiplexor, and the ptmx in the devtmpfs is either a shim provided by the kernel that tries to mimic a symbolic link, a bind mount, or a plain old actual symbolic link to pts/ptmx.
- The kernel does not always set up the ownership and permissions as
grantpt() should. Setting the wrong mount options, either a gid other than the tty GID or a mode other than 0620, triggers fallback behaviour in the GNU C library. In order to reduce grantpt() to a no-operation in the GNU C library as desired, the kernel must not assign the group of the opening process (i.e. there must be an explicit gid setting), the group assigned must be the tty group, and the mode of newly created slave devices must be exactly 0620.
Not switching on /dev/pts/ptmx by default and the GNU C library not wholly reducing grantpt() to a no-op are both because the kernel and the C library are not maintained in lockstep. Each had to operate with older versions of the other. Linux still had to provide an older /dev/ptmx. The GNU C library still has to fall back to running pt_chown if there's not a new devpts filesystem with the correct mount options in place.
The window of opportunity thus still exists for unlockpt() to guard against on Linux, if the devpts mount options are wrong and the GNU C library consequently has to fall back to actually doing something in grantpt().
Further reading