own-tty and usershell

Kragen Sitaker; $Id: usershell.metext,v 1.4 1998/03/22 01:41:26 kragen Exp $

This document lives at http://www.pobox.com/~kragen/sw/usershell.html.

Someone in Linux Gazette asked if they could run a shell on their console from inittab instead of logging in. (Presumably no one they distrust ever sits at their console.)

I thought about it, and I realized that it would be a pretty handy thing for me, too. Maybe I'm not the only one.

I am making available my solution (a little more than 7K).

Briefly, here's how it works.

User's manual

This package consists of two programs, one of which is called `own-tty' and one of which is called `usershell'. `Usershell', given a user and a tty, will run `login' for the user on the tty on the tty. Typical use is something like this:

usershell kragen /dev/tty6 (from the command line, or)

c6:1235:respawn:/usr/sbin/usershell kragen /dev/tty6 (in inittab)

The tty name needs to be a full path.

Usershell calls own-tty to do most of the dirty work. own-tty is just a short C program that gives a process its own tty. You don't have to know about this to use usershell, but you might want to use it for something else, so here's how it's normally called:

own-tty /dev/tty6 /bin/tcsh -tcsh (from the command line, to start a shell, or)

own-tty /dev/tty7 /bin/ls ls -l (to ls -l on /dev/tty7)

This opens /dev/tty6, dups it onto stdin, stdout, and stderr, starts a child process, creates a new pgrp for that child, sets the controlling tty for that child to /dev/tty6, and then executes the command you asked for. When the child process dies, so does the parent.

History

Not having HOWTOs or clear documentation on this stuff, I kind of bashed around until I got something that worked. Here's a story that hopefully approximates the reality of what happened. Most people will probably be bored; that's why I put it last.

First, I tried the obvious thing of starting /bin/sh directly in inittab. This didn't work too well, since /bin/sh ended up attaching to the same console where the first agetty was (or maybe every console? I'm not sure). The next try was /bin/sh with input, output, and error redirected to /dev/tty6.

Well, this sort of worked. Except that signals sent to the terminal I started sh from went to sh, and signals from tty6 did not. Also, /dev/tty seemed to map back to the tty I started it from, not the one I started it on. tcsh said ``Warning: no access to tty (Not a typewriter). Thus no job control in this shell.''.

Well, this was definitely less than satisfactory. So I did straces on tcsh started by hand with redirection and tcsh started from agetty, and puzzled over the results. They didn't make any sense to me; I guess I'm a little dense about these things sometimes.

Well, anyway, so I went and groveled through the kernel source to see why TIOCGPGRP was returning EPERM. I found some concepts that were not familiar to me (session leader? pgrp of a tty?) although I'd heard of them before.

So I decided to try getting and setting the pgrp of the tty. I groveled around some more and found that there was a kernel function called sys_setsid() which would make current->leader 1, which was part of what I wanted; I read the man page for setsid(); I wrote a program that forked, setsid()ed, and then TIOCSCTTYed. It worked.

So, once I had this baby working, I added some exec stuff to the bottom of it and some dup2 stuff to the middle of it so I could run a shell. Lo and behold, tcsh was happy.

I put it in inittab. init was most unhappy. Seems I was trying to TIOCNOTTY before I did much else, thinking it was necessary, but init was starting the process without giving it a tty. Thus, my program would exit immediately with an error.

So I took out the complaining and exiting code for TIOCNOTTY. Then, on a hunch, I took out TIOCNOTTY altogether. It still worked from the command line. So I guess I didn't need TIOCNOTTY at all.

Well, I copied the TIOCNOTTY-less binary to where init would find it. This time it worked OK, but init was still unhappy. Seems the parent process wasn't waiting for its child, so init would run another usershell, and another, and another. All but the first one of 'em would fail, since there was a shell already running, and it owned the tty. (Well, its pgrp owned the tty.) So init complained some more.

I went and stuck in a wait(). init was happy.

Well, then I noticed that the user's shell was ending up in whatever random directory root happened to run `usershell' from. This didn't seem like the right thing to do; rather than add stuff to usershell to look up the user in the password file to get their shell and home dir, I just added the home dir after the shell to the command line. This was kind of silly, but I didn't know what else to do.

Well, this was about it until version 3. After a while, I realized that there was no username on the utmp entry that was created using the usershell method of `exec su "$user" -c "own-tty '$tty' '$shell' -'$shell'"'. So, for version 4 (released later in the day than versions 1, 2, and 3), I read the man pages for login(1) and in.rlogind(8). I realized that `exec own-tty "$tty" /bin/login login -f "$user"' was really a much better method, anyway, so I switched to that. This had several good effects:

Also, I realized that I left that fd to the specified tty open, in addition to the usual fds on 0, 1, and 2. Sloppy me. I fixed it.