Controlling TTYs: A Unix Horror Story
Daniel J. Bernstein
Draft 1, 1991-10-06

Introduction

"Normal file access permissions handle security."

— POSIX.1-1988 rationale, B.7.1.1.4

When a user logs into a Unix system, he is assigned a tty which echoes and processes his characters, lets him edit command lines, handles flow control on output, and so on. The user's shell reads and writes from the tty. Messages sent by the write and talk user communication commands are also sent through the tty. In fact, ttys (also called terminals, teletypes, and typewriters) are the focal point of almost all interaction between a user and the system.

Ttys come in two flavors: hardwired ttys and pseudo-ttys. Hardwired ttys are connected directly to a physical device, such as a modem. Pseudo-ttys may be dynamically connected to any process. For instance, the telnetd program links a pseudo-tty with each network connection.

Unlike pipes, ttys have traditionally been assigned a name in the filesystem: /dev/console, for instance, is a hardwired tty, and /dev/ttyp7 is a pseudo-tty. The primary reason for these files is to support user-to-user communication. The system typically assigns ownership of tty files to the current user. That way the user can change his tty modes to control whether write and talk messages are allowed.

Occasionally a program (or more often a script) wants to send messages to the current tty, no matter what redirection has been put on stdout and stderr. (The author defers judgment on whether this is a sensible thing for programs to do.) More often, a user wants to know what the current tty is, to keep track of what he's doing. One obvious solution to both problems is to keep the current tty in an environment variable, like TTY=/dev/ttyp7. Then a tty program can simply echo $TTY, and programs which want to talk to the user can open $TTY.

Unfortunately, Unix never acquired such a simple user-mode solution. Instead each process was assigned a "controlling tty", something which the kernel kept track of and treated specially. That is where the horror story begins.

Controlling TTYs

Processes start without a controlling terminal. When a process without a controlling tty (also called "ctty") opens a tty, it is assigned that ctty, and the ctty is preserved through fork() and exec(). Different Unix systems have various methods of removing the association between a process and its controlling tty. We will return to this subject below.

A special file, /dev/tty, is accessible to all processes at all times. When a process opens /dev/tty, it gets a valid descriptor to its ctty, or ENODEV if it does not have a ctty. The protection on the actual tty file is irrelevant to /dev/tty.

Under BSD 4.2, the controlling tty has almost no effects on job control. BSD 4.3 added some rules in the name of job control security. POSIX went much farther, and formalized controlling ttys into "sessions." Each process is associated with a session, and various ad-hoc session rules are thrown in to complicate job control and signal processing in general. (For instance, a process can only stop if its parent is in the same session but a different process group.) The problem of supporting job control without losing security became an excuse for controlling terminals and sessions.

At this point we can identify six separate forms of access to a tty device, say /dev/ttyp7, by a process:

O
(ownership) access: uid owns /dev/ttyp7
P
(protection) access: uid has permision to open /dev/ttyp7
C
(ctty) access: current ctty is /dev/ttyp7
T
(/dev/tty) access: file descriptor to /dev/tty, pointing to /dev/ttyp7
S
(slave) access: file descriptor to /dev/ttyp7 itself
M
(master) access: file descriptor to /dev/ptyp7 (pseudo-ttys only)

(The names "master" and "slave" are pseudo-tty terminology.) The system lets a process acquire access in several ways: A process with O access can gain P access. A process with P access can gain S access. A process with C access can gain T access. A process with M or S access can gain C access. A process can gain M access (by opening /dev/ptyp7) if no other process has M access.

Under BSD, to shed C access, a process must gain T access (i.e., open /dev/tty) and perform the TIOCNOTTY ioctl. Unfortunately, if any process applies the TIOCEXCL ioctl to the tty, no process will be able to open /dev/tty itself until some process does TIOCNXCL. A program stuck under a tty with TIOCEXCL set has absolutely no reliable way to dissociate itself from that tty. (Perhaps this is not a surprise given that there is also absolutely no reliable way for a process to figure out what tty it is associated with in the first place.)

And the TIOCEXCL problem pales beside the number of twists and turns introduced by POSIX. Obviously this is not a simple system to implement, use, understand, or make secure.

Security

Steve Bellovin pointed out years ago ([]), as did this author ([]), that an attacker could abuse controlling ttys to take almost complete control of the system. Put simply, it is very difficult to detect, let alone revoke, whether another user has S, T, or C access to a tty.

BSD 4.2 and its descendants have a vhangup() system call meant to revoke access to a tty. Unfortunately, it doesn't work on most systems, and only revokes S access on the rest. (This author considers vhangup() a joke in very poor taste.)

Several vendors have attempted to fix this problem. The Convex Unix 8.0 documentation, for instance, says that the holes are gone. They're not. Convex insisted that its system was secure until the author sent explicit code showing how to break tty security.

Similarly, after a series of Internet breakins using tty security holes, Sun distributed a patch to their SunOS 4.1 telnetd and rlogind supposedly fixing the problems ([]). (It is worth noting that essentially the same fix appeared in version 3.0 of the author's pty package [] several months before.) Although the patch was enough to temporarily stop the attacks, it did not close C access, and hence was not enough.

Version 4.0 of the author's pty package ([]) includes security tests which detect all possible forms of tty access, and which work on any BSD system. It is obscene that such measures have proven necessary. In the meantime, crackers from the Netherlands and elsewhere have broken into literally thousands of Internet accounts, taking advantage of tty security holes at will.

Simplifications

Under SunOS 4.1, BSD 4.4, and a few other systems, T access has been removed. A process which opens /dev/tty will get a full-fledged descriptor to /dev/ttyp7; there is no longer any distinction between /dev/tty and the real tty file it refers to, except that /dev/tty is always completely unprotected.

Any excuse for controlling terminals or POSIX sessions on the basis of job control security is completely invalid. As pointed out by the author in How to add job control to a Unix system, the job control system can be made much simpler, easier to use, and simultaneously completely secure, with absolutely no help from cttys or POSIX sessions. As suggested above, the controlling tty can be passed in the TTY environment variable. C (and T) access, as well as POSIX sessions, can simply disappear.

(Apparently the disappearance of cttys and POSIX sessions would remove absolutely no functionality, from either the user's or the application's point of view; and of course it would make a much cleaner system and standard if it were officially adopted. The author would greatly love to hear from any proponents of POSIX sessions who can explain what features they provide, especially in light of his new, simple, secure job control system, when in fact vendors, programmers, and users wouldn't care if setsid() became a no-op.)

For backwards compatibility it would be useful to keep a /dev/tty device so that programs do not have to be changed to use $TTY. Applications running under the author's pty program can normally access the tty through descriptor 3, so an easy solution here is to let /dev/tty be a simple driver which dup()s fd 3.

The only other useful function of a controlling terminal — viz., to help the user categorize processes — is easily taken care of at user level. ps can use $TTY for its output. Accounting could be by pid (as it should be) instead of by the marginally useful ac_tty.

We're still left with O and P access — ttys are still in the filesystem. Given fd 3 support, programs which want to talk to the user don't need to open() ttys. The other use of ttys in the filesystem is user-to-user communication, but there are several replacement communication systems which depend on users running their own daemons rather than having a writable tty file. So tty files can disappear. Rather than opening tty files, programs can create ttys with a new system call. Then ttys will be much more like pipes, which may be accessed only through a pair of descriptors.

If these simplifications had been made when Unix was young, before it was used by millions of people, then maybe the controlling terminal horror story would not have lasted so long or caused so much damage. Fortunately, the story will end soon, as in October 1992 the author will distribute code which anyone can use to exploit these holes. Any vendor which wants to stay in business will fix its systems long before then.


© Copyright 1991 Daniel J. Bernstein. Presumably.