If you’re writing a program that creates login sessions โ like an SSH daemon, a custom telnet server, or any application that authenticates users โ you are responsible for updating the utmp and wtmp files correctly.
Failing to do this means who, last, and other accounting tools won’t know about your users. Two functions handle the writing: pututxline() for utmp and updwtmpx() for wtmp.
#include <utmpx.h>
struct utmpx *pututxline(const struct utmpx *ut);
/* Returns: pointer to copy of the written record on success,
* NULL on error */
This function writes the given record to the utmp file. It’s smarter than a raw write โ before writing, it calls getutxid() internally to search for an existing record with the same ut_id and compatible ut_type. If found, the existing record is overwritten; otherwise a new record is appended.
๐ก Optimization: If you called getutxid() or getutxline() just before calling pututxline(), the function detects this and skips its internal search โ it uses the current file position directly. This is why the pattern “search then write” is efficient.
โ ๏ธ Important: When pututxline() makes its internal getutxid() call, it does NOT overwrite the static buffer that the public getutx*() functions use. SUSv3 requires this behavior.
#define _GNU_SOURCE
#include <utmpx.h>
void updwtmpx(const char *wtmpx_file, const struct utmpx *ut);
/* Appends the record 'ut' to the file 'wtmpx_file'
* Typically called with _PATH_WTMP as the first argument
* Note: Not specified in SUSv3, but widely available */
Unlike pututxline(), this function always appends โ it never overwrites existing records. This is exactly what you want for wtmp since it must be a permanent audit trail.
Call this function both on login AND on logout with appropriately filled records.
| ut_type | Set to USER_PROCESS |
| ut_user | The login username |
| ut_pid | PID of the login process (getpid()) |
| ut_tv | Current timestamp (gettimeofday() or time()) |
| ut_line | Terminal name without /dev/ (e.g., “pts/0”) |
| ut_id | Suffix of terminal name (e.g., “/0”) |
| ut_host | Remote hostname or IP (for remote logins) |
pututxline(&ut) โ write to utmpThen:
updwtmpx(_PATH_WTMP, &ut) โ append to wtmp| ut_type | Change to DEAD_PROCESS |
| ut_user | Zero out: memset(&ut.ut_user, 0, sizeof(ut.ut_user)) |
| ut_tv | Update to logout time |
Then:
pututxline(&ut) โ overwrites the login record in utmpThen:
updwtmpx(_PATH_WTMP, &ut) โ appends logout record to wtmpThis is a clean, well-commented version of the utmpx_login.c example from TLPI. It simulates a login session by writing records and then cleaning up on logout.
/* utmpx_login.c
* Simulates updating utmp and wtmp for a login/logout session
* Compile: gcc utmpx_login.c -o utmpx_login
* Usage: sudo ./utmpx_login username [sleep-seconds]
* Example: sudo ./utmpx_login ravi 10
*
* IMPORTANT: Requires root/privilege to write to utmp/wtmp
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <utmpx.h>
#include <time.h>
#include <paths.h> /* _PATH_UTMP, _PATH_WTMP */
/*
* fill_login_record() - Initialize a utmpx record for login
*
* devName format: full path like "/dev/pts/7"
* We need:
* ut_line = everything after "/dev/" โ "pts/7"
* ut_id = everything after the 3-char prefix "pts" โ "/7"
* (For /dev/tty1: ut_line="tty1", ut_id="1")
*/
static void fill_login_record(struct utmpx *ut,
const char *username,
const char *devName)
{
memset(ut, 0, sizeof(struct utmpx));
ut->ut_type = USER_PROCESS;
ut->ut_pid = getpid();
/* Stamp with current time */
if (time((time_t *)&ut->ut_tv.tv_sec) == -1) {
perror("time");
exit(EXIT_FAILURE);
}
/* Copy username */
strncpy(ut->ut_user, username, sizeof(ut->ut_user) - 1);
/* /dev/ is 5 chars, terminal prefix (tty/pts/pty) is 3 chars
* So ut_id starts at offset 5+3 = 8 from start of devName */
if (strlen(devName) > 5)
strncpy(ut->ut_line, devName + 5, sizeof(ut->ut_line) - 1);
if (strlen(devName) > 8)
strncpy(ut->ut_id, devName + 8, sizeof(ut->ut_id) - 1);
}
int main(int argc, char *argv[])
{
struct utmpx ut;
char *devName;
int sleep_secs;
if (argc < 2) {
fprintf(stderr, "Usage: %s username [sleep-secs]\n", argv[0]);
exit(EXIT_FAILURE);
}
sleep_secs = (argc > 2) ? atoi(argv[2]) : 10;
/* Get the terminal device name */
devName = ttyname(STDIN_FILENO);
if (devName == NULL) {
perror("ttyname");
fprintf(stderr, "Hint: Run from a real terminal, not a pipe.\n");
exit(EXIT_FAILURE);
}
/* ========== LOGIN ========== */
fill_login_record(&ut, argv[1], devName);
printf("=== LOGIN: Writing records for user '%s' ===\n", argv[1]);
printf(" PID : %d\n", (int)ut.ut_pid);
printf(" Line : %s\n", ut.ut_line);
printf(" ID : %s\n", ut.ut_id);
/* Write to utmp (overwrites if record exists, else appends) */
setutxent();
if (pututxline(&ut) == NULL) {
perror("pututxline (login)");
exit(EXIT_FAILURE);
}
endutxent(); /* Not strictly needed here but good practice */
/* Append login record to wtmp */
updwtmpx(_PATH_WTMP, &ut);
printf("\nSleeping %d seconds. Check utmp with: who\n", sleep_secs);
printf(" or: sudo ./dump_utmpx /var/run/utmp\n\n");
sleep(sleep_secs);
/* ========== LOGOUT ========== */
/* Reuse the same struct โ only change type, user, and time */
ut.ut_type = DEAD_PROCESS;
memset(ut.ut_user, 0, sizeof(ut.ut_user)); /* Zero out username */
time((time_t *)&ut.ut_tv.tv_sec); /* Update to logout time */
printf("=== LOGOUT: Writing DEAD_PROCESS records ===\n");
/* Overwrite the login record in utmp */
setutxent();
if (pututxline(&ut) == NULL) {
perror("pututxline (logout)");
exit(EXIT_FAILURE);
}
/* Append logout record to wtmp */
updwtmpx(_PATH_WTMP, &ut);
endutxent();
printf("Done. Check wtmp with: last %s\n", argv[1]);
return 0;
}
How to test:
# Compile
gcc utmpx_login.c -o utmpx_login
# Run as root (required to write utmp/wtmp)
sudo ./utmpx_login ravi 15 &
# In another terminal, check who is logged in:
who
last ravi
# When the program finishes (after 15 seconds), check again:
last ravi # Shows login and logout time
Shows how to use a signal handler to ensure the utmp record is cleaned up even if the program crashes or is killed.
/* safe_login_session.c
* Demonstrates signal-safe utmp cleanup using a signal handler
* Compile: gcc safe_login_session.c -o safe_login_session
* Usage: sudo ./safe_login_session username
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <utmpx.h>
#include <time.h>
#include <paths.h>
/* Global so the signal handler can access it */
static struct utmpx g_ut;
static volatile int g_logged_in = 0;
/* Signal handler: clean up utmp on SIGTERM/SIGINT */
static void cleanup_handler(int sig)
{
if (!g_logged_in)
return;
/* Write logout record */
g_ut.ut_type = DEAD_PROCESS;
memset(g_ut.ut_user, 0, sizeof(g_ut.ut_user));
time((time_t *)&g_ut.ut_tv.tv_sec);
setutxent();
pututxline(&g_ut);
updwtmpx(_PATH_WTMP, &g_ut);
endutxent();
g_logged_in = 0;
/* Re-raise the signal to get the default behavior (terminate) */
signal(sig, SIG_DFL);
raise(sig);
}
int main(int argc, char *argv[])
{
char *devName;
if (argc < 2) {
fprintf(stderr, "Usage: %s username\n", argv[0]);
exit(EXIT_FAILURE);
}
devName = ttyname(STDIN_FILENO);
if (devName == NULL) {
perror("ttyname");
exit(EXIT_FAILURE);
}
/* Install signal handlers BEFORE writing login record */
signal(SIGTERM, cleanup_handler);
signal(SIGINT, cleanup_handler);
signal(SIGHUP, cleanup_handler);
/* Build and write login record */
memset(&g_ut, 0, sizeof(g_ut));
g_ut.ut_type = USER_PROCESS;
g_ut.ut_pid = getpid();
strncpy(g_ut.ut_user, argv[1], sizeof(g_ut.ut_user) - 1);
if (strlen(devName) > 5)
strncpy(g_ut.ut_line, devName + 5, sizeof(g_ut.ut_line) - 1);
if (strlen(devName) > 8)
strncpy(g_ut.ut_id, devName + 8, sizeof(g_ut.ut_id) - 1);
time((time_t *)&g_ut.ut_tv.tv_sec);
setutxent();
if (pututxline(&g_ut) == NULL) {
perror("pututxline");
exit(EXIT_FAILURE);
}
updwtmpx(_PATH_WTMP, &g_ut);
endutxent();
g_logged_in = 1;
printf("Logged in as '%s'. Press Ctrl+C to logout.\n", argv[1]);
/* Simulate a session */
while (1)
pause(); /* Wait for signal */
return 0;
}
TLPI Exercise 40-3 asks you to implement these three glibc convenience functions. Here is how they work:
/* my_loginout.c
* Implements login(), logout(), and logwtmp() - Exercise 40-3 from TLPI
* These are convenience wrappers around pututxline() and updwtmpx()
*
* man 3 login / man 3 logout / man 3 logwtmp for reference
*
* Compile: gcc my_loginout.c -o my_loginout
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <utmpx.h>
#include <time.h>
#include <paths.h>
/*
* my_login() - Write a USER_PROCESS record to utmp and wtmp
* The caller fills in the utmpx struct; this function handles writing.
*/
void my_login(const struct utmpx *ut)
{
struct utmpx copy = *ut;
/* Ensure ut_type is USER_PROCESS */
copy.ut_type = USER_PROCESS;
/* Stamp with current time if not already set */
if (copy.ut_tv.tv_sec == 0)
time((time_t *)©.ut_tv.tv_sec);
setutxent();
pututxline(©);
endutxent();
updwtmpx(_PATH_WTMP, ©);
}
/*
* my_logout() - Write a DEAD_PROCESS record to utmp
* The 'line' argument is the terminal name (ut_line value).
* Returns 1 on success, 0 if no matching record found.
*/
int my_logout(const char *line)
{
struct utmpx search_ut, *found;
int result = 0;
/* Search for the USER_PROCESS record for this terminal */
memset(&search_ut, 0, sizeof(struct utmpx));
strncpy(search_ut.ut_line, line, sizeof(search_ut.ut_line) - 1);
search_ut.ut_type = USER_PROCESS;
setutxent();
found = getutxline(&search_ut);
if (found != NULL) {
found->ut_type = DEAD_PROCESS;
memset(found->ut_user, 0, sizeof(found->ut_user));
time((time_t *)&found->ut_tv.tv_sec);
pututxline(found);
result = 1;
}
endutxent();
return result;
}
/*
* my_logwtmp() - Append a record to wtmp
* 'line' = terminal name
* 'name' = username (empty string for logout)
* 'host' = remote hostname (empty string for local)
*/
void my_logwtmp(const char *line, const char *name, const char *host)
{
struct utmpx ut;
memset(&ut, 0, sizeof(ut));
ut.ut_type = (name[0] != '\0') ? USER_PROCESS : DEAD_PROCESS;
ut.ut_pid = getpid();
strncpy(ut.ut_line, line, sizeof(ut.ut_line) - 1);
strncpy(ut.ut_user, name, sizeof(ut.ut_user) - 1);
strncpy(ut.ut_host, host, sizeof(ut.ut_host) - 1);
time((time_t *)&ut.ut_tv.tv_sec);
updwtmpx(_PATH_WTMP, &ut);
}
/* Quick test */
int main(void)
{
char *devName = ttyname(STDIN_FILENO);
if (devName == NULL) {
printf("No terminal available.\n");
return 1;
}
/* Build a sample login record */
struct utmpx ut;
memset(&ut, 0, sizeof(ut));
strncpy(ut.ut_user, "testuser", sizeof(ut.ut_user) - 1);
strncpy(ut.ut_line, devName + 5, sizeof(ut.ut_line) - 1);
strncpy(ut.ut_id, devName + 8, sizeof(ut.ut_id) - 1);
ut.ut_pid = getpid();
printf("Testing my_login()...\n");
my_login(&ut);
sleep(2);
printf("Testing my_logout()...\n");
my_logout(ut.ut_line);
printf("Testing my_logwtmp()...\n");
my_logwtmp("pts/9", "audituser", "192.168.1.99");
printf("Done. Check with: last testuser audituser\n");
return 0;
}
The utmp and wtmp files have strict permissions:
$ ls -la /var/run/utmp /var/log/wtmp
-rw-rw-r-- 1 root utmp 1536 /var/run/utmp
-rw-rw-r-- 1 root utmp 8976 /var/log/wtmp
The files are owned by root and the utmp group. Programs like login and sshd that write to these files typically either:
- Run as root (most common)
- Run with the
setgid utmpbit set
โ ๏ธ Security: Never set these files world-writable. An attacker who can write to utmp can fake login records โ for example, making it appear that root is logged in from a remote location to cover their tracks.
Answer: pututxline() writes to the utmp file โ it searches for an existing record to overwrite (or appends if not found). updwtmpx() always appends to the wtmp file, never overwrites. Both should be called for every login and logout event.
Answer: At minimum: ut_type = USER_PROCESS, ut_user (username), ut_tv (timestamp), ut_pid (process ID), ut_id (terminal suffix), and ut_line (terminal name without /dev/). For remote logins, also fill ut_host.
Answer: The stale utmp record stays in the file until the next system reboot. On reboot, init automatically scans utmp and sets ut_type to DEAD_PROCESS for all records that no longer have a corresponding running process. This is why programs should install signal handlers to clean up utmp on crash.
Answer: The utmp file tracks currently active sessions. When a user logs out, we want to update (overwrite) the existing login record with a DEAD_PROCESS record for the same terminal. If pututxline always appended, the file would grow indefinitely and have stale USER_PROCESS records that who would incorrectly show as active users.
Answer: Each terminal (tty1, pts/0, pts/7, etc.) can only have one active login at a time. The ut_line (and its suffix ut_id) uniquely identifies which terminal a record belongs to. When pututxline() searches for an existing record to overwrite, it matches on ut_id. This ensures that only one record per terminal exists in utmp at any time.
