Login Accounting Updating utmp and wtmp for a Login Session

 

Chapter 40: Login Accounting
Part 6 โ€“ Updating utmp and wtmp for a Login Session
โœ๏ธ pututxline()
๐Ÿ“ updwtmpx()
๐Ÿ” Root Required

Writing Login Records

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.

Key Terms in This Section

pututxline() updwtmpx() USER_PROCESS DEAD_PROCESS ut_tv timestamp _PATH_WTMP login record logout record

pututxline() โ€” Write a Record to utmp
#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.

updwtmpx() โ€” Append a Record to wtmp
#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.

Rules for Correct Login/Logout Updates
ON LOGIN โ€” Fill these fields:
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)
Then: pututxline(&ut) โ†’ write to utmp
Then: updwtmpx(_PATH_WTMP, &ut) โ†’ append to wtmp
ON LOGOUT โ€” Modify only these fields:
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
Keep ut_line, ut_id, ut_pid same as login record (used to find the right record)
Then: pututxline(&ut) โ†’ overwrites the login record in utmp
Then: updwtmpx(_PATH_WTMP, &ut) โ†’ appends logout record to wtmp

Coding Example 1: Complete Login/Logout Session Update

This 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

Coding Example 2: Safe utmp Cleanup on Crash

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;
}

Coding Example 3: Implementing login(3), logout(3), logwtmp(3)

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 *)&copy.ut_tv.tv_sec);

    setutxent();
    pututxline(&copy);
    endutxent();

    updwtmpx(_PATH_WTMP, &copy);
}

/*
 * 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;
}

File Permissions and Security

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 utmp bit 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.

Interview Questions
Q1. What is the difference between pututxline() and updwtmpx()?

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.

Q2. What fields must you fill in the utmpx struct for a login record?

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.

Q3. What happens to the utmp record if a program crashes without logging out?

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.

Q4. Why does pututxline() search for an existing record before writing?

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.

Q5. How does the ut_line field act as a unique key in the utmp file?

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.

Leave a Reply

Your email address will not be published. Required fields are marked *