return to first page linux journal archive
keywordscontents

Linux Programming Hints

Programming the VT Interface, Part 2

In last month's column, I said that I would give a simple screen-locking example that uses the VT, or Virtual Terminal, ioctl()'s that I documented in that column.

by Michael K. Johnson

In case you can't remember or didn't read last month's column, the VT ioctl()'s allow you to specify from a user program what the kernel should do about the virtual terminals, or virtual consoles. (These are essentially the same. For the rest of the column, I will refer to them as virtual consoles, not virtual terminals, for no particular reason).

A program can request that the kernel give it raw scan codes instead of full keystrokes, can tell the kernel that you are going into graphics mode on that terminal, and do many other low-level things. XFree86 uses these ioctl()'s heavily, as does svgalib. The Linux DOS emulator (which is really a BIOS emulator) uses them, and the loadable keymaps program (kbd) uses them.

If you didn't read last month's column, the main content of the column will be included in future man pages to be released by the Linux man page project.

I have written a program called vlock, which is a screen locker which can lock virtual consoles. I don't have space here to reproduce the entire source code, but I will give enough details for you to easily construct your own similar program. Instructions for obtaining a copy via anonymous ftp on the Internet follow the code in this column.

Why?

My original purpose in writing vlock was to demonstrate a use for the VT ioctl()'s that they had really not been designed for, to show their flexibility.

If you are like many Linux users, you may have one or two sessions of X running, and a few console logins active at the save time, and be switching back and forth between them. Perhaps you have been editing a program you have been working on, and don't want your roommates or children to start playing with your files while you go away from your computer for one reason or another, but you really don't feel like logging out and restarting all your sessions.

xlock could solve your problem if you only have an X session that you want to lock, but anyone can still switch to the console even when xlock is running. You need a program that can lock all the sessions at once. Well, maybe you need a program that can lock all the sessions at once...

How?

My first idea for locking the console was to read raw scancodes from the keyboard instead of reading normal characters, and to ignore anything but the scancodes for alphanumeric keys, the shift key, the caps lock key, and the control key, and write a state machine to get keys from that. This would automatically ignore the ALT-Fn keys that are normally used to switch from virtual console to virtual console, so those keypresses would not make the VC switch. Of course, it would be possible that there would be some problems with some national keyboards, but it would mostly work for mostly anyone.

However, that would involve a lot of work, and a lot of testing, and I'm too lazy to do that much work if there is an easy way to do it. (I later realized that there was a serious security problem with this approach as well. I'll let you try to figure out what the flaw is, and I'll explain at the end of this column.)

I then noticed that there are ioctl()'s specifically for telling the kernel to ask first before switching virtual consoles. It is possible for a program to explicitly refuse to let the kernel switch virtual consoles. These ioctl()'s only work on virtual consoles, so first we need to open one of the virtual consoles to perform the ioctl()'s on. The easiest thing to do is this:

if (vfd = open("/dev/console", O_RDWR) < 0) {
  perror("vlock: could not open /dev/console");
  exit (1);
}

/dev/console stands for the current screen. The assumption is that when vlock is run, it will be run on the current virtual console. (It turns out that this assumption does not create a security hole, although it might look to you like it ought to.)

It would also be possible to switch to an unallocated virtual terminal, like X does, which might be preferable in some circumstances. To do this, we could have used the ioctl VT_OPENQRY to find the number of the first available virtual console, opened the appropriate device (/dev/ttynn, where nn is the number returned by VT_OPENQRY), and used VT_ACTIVATE to switch to that virtual console.

It is a lot easier to just open /dev/console.

c = ioctl(vfd, VT_GETMODE, &vtm);
if (c < 0) {
  fprintf(stderr,
        "This tty is not a virtual console.\n");
  is_vt = 0;
} else {
  is_vt = 1;
}

We will treat the VT_GETMODE and VT_SETMODE ioctl()'s like the termios interface: first we get the current settings, then we change the local copy, then we set the kernel's copy to look like the changed local copy.

VT_GETMODE fills a vt_mode structure with the current VT settings. If it returns an error, the program must not be running on a virtual console. vlock does not exit on this error, but it does set the is_vt variable to 0, and it does not try to use any more VT ioctl()'s if the is_vt variable is set to 0.

/* we set SIGUSR{1,2} to point to *_vt() */
sigemptyset(&(sa.sa_mask));
sa.sa_flags = 0;
sa.sa_handler = release_vt;
sigaction(SIGUSR1, &sa, NULL);
sa.sa_handler = acquire_vt;
sigaction(SIGUSR2, &sa, NULL);

We will arrange in a moment for SIGUSR1 to be sent to the process whenever the kernel is requested to change away from the virtual console the program is running on, and for SIGUSR2 to be sent to the process whenever the kernel is requested to change to the virtual console the program is running on. These requests can be caused by the user pressing ALT-Fn keys or by other programs issuing a VT_ACTIVATE ioctl.

When SIGUSR1 is received, release_vt() is called:

void release_vt(int signo) {
  if (!o_lock_all)
    /* kernel is allowed to switch */
    ioctl(vfd, VT_RELDISP, 1);
  else
    /* kernel is not allowed to switch */
    ioctl(vfd, VT_RELDISP, 0);
}

The variable o_lock_all is set if the user wants to lock all virtual consoles at once. It is not set if the user only wants to lock the current virtual console. VT_RELDISP is used to tell the kernel that the program acknowledges that it has received the signal asking it to relinquish the virtual console, and tells the kernel whether or not it agrees to do so. The third argument is set to 1 to allow the kernel to switch to another virtual console, or set to 0 to prevent the kernel from switching to another virtual console.

When SIGUSR2 is received, acquire_vt() is called:

void acquire_vt(int signo) {
  /* This call is not currently required under Linux,
     but it won't hurt, either... */
  ioctl(vfd, VT_RELDISP, VT_ACKACQ);
}

Linux does not actually require that this be done; it is included for compatibility with SYSV, which does require that it is called. I included it in vlock mainly so that if someone wanted to port vlock to some version of SYSV, there would be one less stumbling block for him or her.

Now that we have set up these signal handlers, we will tell the virtual console manager about them.

We did not want to tell the virtual console manager to route requests to change virtual consoles through these signals until the signals' handlers had been installed, because to do otherwise could cause a small possibility of a bug on very slow machines which are running too many processes at once.

if (is_vt) {
/* Keep a copy around to restore
     at appropriate times */
  ovtm = vtm;
  vtm.mode = VT_PROCESS;
  /* handled by release_vt(): */
  vtm.relsig = SIGUSR1;
  /* handled by acquire_vt(): */
vtm.acqsig = SIGUSR2;
  ioctl(vfd, VT_SETMODE, &vtm);
}

ovtm is another vt_mode structure, like vtm. Setting vtm.mode to VT_PROCESS causes the kernel to ask permission to change virtual consoles. Setting vtm.relsig to SIGUSR1 and vtm.acqsig to SIGUSR2 tells the kernel how to ask permission.

At this point, all that needs to be done is to handle all reasonable signals, so that people can't break in by typing control-c or control-\ or control-break, and to then ask for the user to type in a password and check it against the real password. There is a library function, getpass(), which gets a password from the user without echoing it to the screen.

Unfortunately, this function is broken under at least one shadow password implementation, because signal handlers are not installed correctly, so to make a screen locking program that works with shadow passwords, you either have to fix the shadow password library or write your own version of getpass(). With vlock, I chose to tell people that vlock doesn't work right with shadow passwords without fixing their shadow password library, rather than writing my own version of the function.

Once a correct password has been entered, the program can just exit. This is acceptable under Linux, at least. However, in case this doesn't work with some other SYSV implementations of the VT ioctl()'s, I have included code in vlock to restore everything, including the VT state, to the original settings. That's why I made the copy of vtm called ovtm a few code fragments ago.

Don't re-invent the wheel

Unless you want to, of course. By the time you read this, I will probably have upgraded vlock several times. The newest version of vlock will always be available from the ftp site tsx-11.mit.edu in the directory /pub/linux/sources/usr.bin in a file called vlock-m.n.tar.gz, where m and n are the major and minor version numbers of the release, respectively.

As of this writing, the current version of vlock is 0.6. If you cannot use ftp, but do have Internet e-mail, you may send e-mail to johnsonm@redhat.com and request a copy, and I can send you a uuencoded gzipped tar file containing both sources and a binary for vlock. Also, the Debian distribution of Linux includes vlock.

The Flaw...

Near the beginning of this article I said I would explain the fundamental flaw with trying to lock the virtual consoles by simply capturing all the keys. The problem is that someone could easily log in from the network or a modem or serial terminal and run a program (they would probably have to write it first) which would issue a request to change the virtual console. This program would be a little trickier than it sounds at first, but it is possible to write it. The kernel would honor the ioctl() requesting the change, and the screen-locking program would be defeated.

Loose ends

I am finding out that many Unix programmers are a little confused about signals. This is understandable, because there are at least three standards for using signals. [Purists, please don't tell me that there are actually more; I'm trying to keep things relatively simple here. A much more detailed explanation, which is historically correct to the best of my knowledge, can be found in Advanced Programming in the Unix Environment, by W. Richard Stevens, in chapter 10, Signals.] Though I have mentioned the differences in signals before, in issue one, I will explain more explicitly here.

The original signals were unreliable. The signal() function was used to install a signal handler that was good for one invocation of the signal, and once the signal handler had been invoked once, the signal handler would uninstall. So you would install your signal handler like this:

signal(SIGUSR1, signal_handling_function);

and then you would implement your signal handling function like this:

void signal_handling_function(int signo) {
  signal(SIGUSR1, signal_handling_function);
  /* Do whatever the signal handling
     function is supposed to do... */
}

The problem with taking this approach is that occasionally a second signal would arrive in between the time that the kernel uninstalled the signal handler and the time that the signal handler re-installed itself.

The problem with not taking this approach is that signal handlers need to be reentrant.

Unfortunately, as reliable signals were introduced, BSD revised signal() to not get uninstalled when it was called, while SYSV left signal() the way it was. There is more to the story, but it only gets more confusing.

Fortunately, there is absolutely no need to be confused. There is no need to use signal() at all. Don't use it: to do so (without knowing all the details about the signal() function on all different versions of Unix) is to write non-portable code.

POSIX defines an alternate interface that is the same on all POSIX-compliant platforms. This interface is called sigaction, and is more powerful and flexible than either version of signal(). [sigaction is derived from the first BSD implementation of reliable signals, so code which uses sigaction will not only be portable to all POSIX platforms, but to pre-POSIX BSD systems as well.] Unfortunately, it is a little more complex, but you can write your own signal management wrapper functions to get exactly the kind of signals you need. Here is an example:

typedef void signal_handler(int);
signal_handler *
my_signal(int signo, signal_handler *func, int oneshot) {
 struct sigaction sact, osact;
  sigemptyset(&sact.sa_mask);
  sact.sa_handler = func;
  if (oneshot) {
    sact.sa_flags = SA_ONESHOT;
  } else {
    sact.sa_flags = 0;
  }
  if (sigaction(signo, &sact, &osact) < 0) {
    return (SIG_ERR);
  } else {
    return (oact.sa_handler);
  }
}

This is not perfect, but it creates an interface to sigaction that is as convenient as signal() but will have the same semantics no matter what system it is compiled on, unlike signal().

It works like signal(), except that it takes a third argument. That third argument determines whether the signal handler remains installed when it is called or if it is uninstalled as soon as it is called.

There are two normal reasons to have a signal handler automatically uninstalled. The first is if the signal handler is not reentrant--if it is not safe to run the signal handler again until while it is already being run. The second is for those times when you really only want to catch one instance of a signal, for example SIGALRM.

You may have noticed the call to sigemptyset() in the code above. It is important for it to be there, but I have not yet mentioned it. It turns out that it is possible for a sigaction signal handler to mask out certain signals while it is being run. Perhaps the most common occurrence of this is in signal handlers that are not reentrant. These signal handlers can set their sa_mask to keep from being called again while they are being invoked, by using code like this:

sigemptyset(&sact.sa_mask);
sigaddset(&sact.sa_mask, SIGFOO);
sact.sa_handler = signal_handler;
sact.sa_flags = 0;
if (sigaction(SIGFOO, &sact, &osact) < 0) {
  do_signal_error(SIGFOO);
}

This will allow you to use a non-reentrant signal handler for SIGFOO. Of course, this code will have to be altered slightly to fit into your application. You will at least have to use a real signal name instead of SIGFOO...

If you are interested in doing more with signals, look up the sigaction() function in a modern Unix programming book or manual, and also read up on "signal sets", which may be found under the following functions; sigemptyset(), sigfillset(), sigaddset(), sigdelset(), sigismember(), sigprocmask(), sigpending(), sigsetjmp(), siglongjmp() and sigsuspend(). These provide very fine-tuneable support for all sorts of fancy signal work, which I will not try to cover this month.

Please send e-mail to johnsonm@redhat.com or send paper mail to Programming Tips, Linux Journal, P.O. Box 85867, Seattle, WA 98145-1867, if you have any suggestions or comments about this column. I'd like to know what you have found useful so far.

If there are any undocumented Linux features that you would like to see covered, I'll look at them. I may write a column, if there is enough interest. I'd also like to have guest columnists write for Linux Programming Hints.