Don't abuse su for dropping user privileges.

00 4 * * 1-6 /bin/su adm -c "/usr/lib/acct/runacct 2> /usr/adm/acct/nite/fd2log"

— UNIX System Administration by David Fielder and Bruce H. Hunter, published in 1986

Like M. Fielder's and M. Hunter's 1986 book, one can find many instances in books, on the World Wide Web, in tutorials, and even on manual pages, of abusing su for dropping superuser privileges and running programs with ordinary user privileges — in cron jobs, /etc/rc scripts, init.d scripts, and even from /etc/inittab. They are all wrong.

Don't abuse su for this purpose. It has never in fact been the function of su, and for the past two decades people have been triggering errors with this abusage. Over the past decade or so, this error has gradually become more and more blatant, going from a few ignorable warning messages in obscure log files to systems that fail to function, but it has in fact been there all of this time.

su adds privileges; it does not drop them.

The su command authenticates an additional user (by default the superuser, but it can be anyone) in the same login session, and runs a command (by default a shell program) under the aegis of that user. Thought of in this way, it should be fairly obvious that it is a mechanism for adding privileges, namely adding the privileges of the newly authenticated user to the current login session. If, for example, one is user A at an interactive shell and one runs su B then one has two shells available, one running under the aegis of user A and one running under the aegis of user B, and one has the privileges of both users at one's fingertips. (With job control, switching between the two is a matter of the suspend and fg commands.)

The problem is that, thanks to hugely outdated documentation and outright computer folklore that still circulates, people think of su in terms of raw internal mechanics, rather than in terms of its functional behaviour. su is thought of as looking up accounts in the system password database, calling the setgid() and setuid() system calls to switch its process GIDs and UIDs, and then execvp() to overlay itself with the target program.

M. Fielder and M. Hunter were not wrong in 1986. But, aside from a few hold-out BSDs and BSD-derived systems (such as Android), su hasn't worked this way for roughly two decades, now. On the Unices such as Solaris and AIX, on Linux, and on the other BSDs such as FreeBSD and PC-BSD, su has a completely different mechanism, and what used to hold true of it as a byproduct of its original mechanism no longer holds true now, and hasn't done so for a long while. The 1980s truisms are long gone.

The reason for this is PAM (Pluggable Authentication Modules).

PAM changed everything, over two decades ago.

The su command is a PAM-enabled application with a service name of su. […] The authentication mechanisms used when PAM is enabled depend on the configuration for the su service in /etc/pam.conf. The su command requires /etc/pam.conf entries for the auth, account, password, and session module types. In order for the su command to exhibit a similar behavior through PAM authentication as seen in standard AIX®authentication, the pam_allowroot module must be used as sufficient and called before pam_aix in both the auth and account su service stacks.

— su command, IBM AIX Manuals since at least the turn of the 21st century

In the middle 1990s, with the advent of PAM, commands such as su and login changed drastically, starting with the proprietary Unices and gradually filtering down to Linux and the BSDs. In particular, PAM now controls the actual function of su. All of the authentication behaviour, including the superuser being able to bypass the authentication step, is actually encoded in the series of PAM modules that is defined for the "su" application in PAM. (Witness the /etc/pam.d/su.conf file on a Debian system or the sample /etc/pam.conf content given in the IBM AIX manual for su, for examples.)

PAM itself operates in terms of what it calls "user sessions". Programs such as login and su call the PAM library function pam_open_session() to "open" a login session and something must call the PAM library function pam_close_session() to "close" it. Furthermore, that something must call pam_close_session() with the same security context — the same effective UID and effective GIDs — in which pam_open_session() was originally called. Session close needs the privileges to be able to reverse whatever was done by session open (writing accounting database records, dealing with per-user per-login session directories and services, and so forth). The authenticated user xyrself won't necessarily have those privileges. (It is basic UNIX security that unprivileged users don't have the rights to overwrite their own entries in the accounting database.)

In 2000 and 2001, the GNU world was catching up with the commercial Unices. The GNU su and login programs underwent a series of tweaks as a result of PAM being used on Linux, because PAM broke the old implementation. See Ben Gertzfield's 2000 patches for Debian su. Around the same time, the same changes were made to the FreeBSD su. Witness David J. MacKenzie's 2001 patches to FreeBSD su and login.

The new implementation called fork() and only dropped privileges in the child, retaining a privileged account parent process that could call the PAM "user session" cleanup function once the child exited. (See contemporary accounts such as this one from Ben Collins.) An initial implementation where the PAM "user session" was opened in the parent process and closed in the child was found to be faulty, with bugs such as Debian Bug #195048, Debian Bug #580434, and Debian Bug #599731 a.k.a. Gentoo Bug #246813 because the unprivileged child did not have the access rights to undo all of the session setup that opening the session did in the privileged parent.

For the next decade, bits and pieces kept popping up related to PAM and su. David Z Maze reported GDM not using PAM properly in 2001. In 2004 there was a problem with the SELinux pluggable authentication module. The Freedesktop.org people came late to the party in 2013, ten years after su had been switched to PAM, when pkexec had to be fixed to make the right PAM library calls for closing "user sessions" within the privileged parent process. (pkexec is much the same tool as su, in that it authenticates an additional user into the current login session, except that it uses PolicyKit for authorization and PolicyKit style authentication agents instead of the system account database and a plain password prompt on the terminal.)

PAM thus, years ago, made su unsuitable for use as a dæmon helper tool:

These are in addition to the non-PAM things that make it unsuitable, such as su being sensitive to the target account's choice of interactive login shell, making it impossible to straightforwardly drop privileges with su to a "nologin" unprivileged account — a commonly employed paradigm for dæmons that run under the aegises of dedicated user accounts that shouldn't ever be used for real user login.

Fortunately, the right way to drop privileges in a dæmon had already been around for some several years before the GNU tools changed, being only a couple of years younger than PAM itself, in fact.

The right way to drop privileges.

Create a small wrapper binary with C (e.g. /usr/bin/setid or /bin/setid) to perform basically the following (about 10-20 lines):

  1. takes arguments and one option

  2. first argument is always the userid to change the identity to

  3. the rest of the arguments would be stored as a command.

  4. the option, if present, could toggle whether the command is run through exec or system (default to exec?).

  5. setuid, setgid and initgroups to the specified user

  6. exec or system the command

— Pekka Savola, Debian Bug #55219, 2001-10-27

Writing in 2001 in a Debian Bug discussing a problem where an abuse of su to drop privileges had stopped working, M. Savola was not in fact describing a hypothetical future. He was in fact, possibly unknowingly, describing a tool that had existed for several years at that point. Indeed, by 2001 people were already busy cloning it into their own toolsets.

That tool was setuidgid from Daniel J. Bernstein's daemontools, first released in 1997 (originally under the name setuser in daemontools 0.51). By the start of the 21st century it was already being duplicated into daemontools clones, and today we now have a range of such tools:

All of these tools were specifically designed for dropping privileges from a privileged process, in a simple, self-contained, easily security auditable, and one-way manner. A couple of them explicitly say this on their manual pages. All of these tools do the very thing that su used to do, as an accident of implementation, long ago: take an account name and a command as program arguments; look up the account in the system account database; change the process' UID, GID, and supplementary groups; and chain load the command in the same process.

Some examples of changing an abuse of su into doing things the right way
The wrong wayThe right way
OriginCommand
Fielder and Hunter's book
/bin/su adm -c "/usr/lib/acct/runacct 2> /usr/adm/acct/nite/fd2log"

(shell)

setuidgid adm sh -c "exec /usr/lib/acct/runacct 2> /usr/adm/acct/nite/fd2log"

(execline)

setuidgid adm redirfd -w 2 /usr/adm/acct/nite/fd2log /usr/lib/acct/runacct

(nosh)

setuidgid adm fdredir -w 2 /usr/adm/acct/nite/fd2log /usr/lib/acct/runacct
Daniel Reed's wrapper for FreeCiv
su -s /bin/bash nobody -c "XAUTHORITY=$TMP /usr/local/freeciv/bin/civclient $@" &
XAUTHORITY=$TMP setuidgid nobody /usr/local/bin/civclient $@ &
(This is, of course, an abuse of nobody for running a dæmon, which is also wrong.)

© Copyright 2014,2015 Jonathan de Boyne Pollard. "Moral" rights asserted.
Permission is hereby granted to copy and to distribute this web page in its original, unmodified form as long as its last modification datestamp is preserved.