The utx.active database (/var/run/utx.active) maintains a list of currently logged-in users; it needs to be updated when a user logs in or out. This file is world-readable (which allows "who" to list logged-in users without requiring suid root).
Since updating the file requires locking it, and this is done via open with O_EXLOCK, it is possible for a user to indefinitely postpone updates to the file by locking the file themselves. Program below can be used to do this (does not require root privileges). While this program is running it will be impossible for any other user (including root) to log in to the system.
The problematic locking code is in pututxline.c, function futx_open(), here:
The example program is as follows:
--- begin ---
int main(int argc, char **argv)
open("/var/run/utx.active", O_EXLOCK | O_RDONLY);
--- end ---
This program runs for 100 seconds during which no other logins will be possible (and logouts will also stall).
In terms of solution, I would recommend either:
(a) making the file not world-readable and making "who" and any other relevant programs setgid to a group with permission to read the file, or
(b) changing the locking mechanism implemented in pututxline.c, so that it locks a separate file which is not world readable and uses that lock to control access to the utx.active file.
Note that GNU libc has a similar issue, but uses an fcntl-based lock with a timeout of 10 seconds. This means that logins can not be completely disabled by the user, but they can prevent the utmp (equivalent to utx.active) database from being updated. I do not recommend this approach.
(a) sort of breaks the libc getutxent APIs for ordinary users.
Happily, the getutxent APIs do not try to use locking to get a consistent snapshot of the file, so (b) should fix the problem just fine.
Created attachment 199629 [details]
Proposed patch (untested)
CC recent committers (and original author) to lib/libc/gen/pututxline.c who may be able to review attachment 199629 [details] by cem@
Instead of going down this road, what are your thoughts on the following?
- Tossing out the use of O_EXLOCK entirely and leave the file writing as it is now,
- Using a single lock file acquisition in pututxline() to serialise write access all data files in one go.
This likely makes the code a bit simpler/lighter, while also improving the sequential consistency guarantees between the data files.
(In reply to Ed Schouten from comment #4)
Sure, that approach seems unobjectionable to me.
Is there any reason to keep this "service" in a flat file? Would it be acceptable to turn it into a directory with one file per user or even require writes to go through a daemon serializing writes and using atomic updates (rename and fsync a temp file)=
(In reply to crest from comment #6)
Sure, we could! The file format/backend used by this API can now be changed to work any way we want. When I implemented utmpx for FreeBSD, the goal was to first see if we could eliminate any direct access to underlying storage, which has now been realised.
That said, to solve this specific issue, there is no need to do anything that drastic.
(In reply to Ed Schouten from comment #7)
On FreeBSD, we guarantee that reader never see torn writes, assuming writer always write single record using one atomic write(2) syscall, and similarly reader uses single atomic read(2) syscall to get the record.
My guess is that if you get rid of stdio(3) use with its buffers, then you can drop O_EXCLOCK and the issue disappears.
No, that's not the case. The code does more than simply write a record into the file at a certain offset. It also reads entries to determine what the offset is at which the record should be placed.
It's insufficient to replace this code by something that doesn't use file locking. There may be race conditions in which you end up overwriting recently created login session entries.