Login Accounting The utmpx Structure and Record Types

 

Chapter 40: Login Accounting
Part 3 โ€“ The utmpx Structure and Record Types
๐Ÿ—๏ธ 9 Record Types
๐Ÿ“ฆ Fixed-Size Records
๐Ÿงฑ Binary File Format

What Is Stored in Each Record?

Both the utmp and wtmp files are binary files made up of fixed-size records. Each record is one struct utmpx. Because the size is fixed, the kernel and C library can quickly seek to any record by position.

Each record captures a snapshot of a login event: who logged in, from where, on which terminal, at what time, and what type of event it was.

Key Terms in This Section

struct utmpx ut_type ut_pid ut_line ut_id ut_user ut_host ut_tv USER_PROCESS DEAD_PROCESS BOOT_TIME

The struct utmpx Definition

This structure is defined in <utmpx.h>. Every record in the utmp and wtmp files has exactly this layout:

#define _GNU_SOURCE  /* Exposes ut_session without __ prefix */
#include <utmpx.h>

/* Helper struct for exit status of a dead process */
struct exit_status {
    short e_termination;  /* Signal that terminated the process */
    short e_exit;         /* Exit status of the process */
};

/* Fixed-size string buffers */
#define __UT_LINESIZE  32   /* max terminal name length */
#define __UT_NAMESIZE  32   /* max username length */
#define __UT_HOSTSIZE  256  /* max hostname length */

struct utmpx {
    short  ut_type;                    /* Type of record (see constants below) */
    pid_t  ut_pid;                     /* PID of the login process */
    char   ut_line[__UT_LINESIZE];     /* Terminal device name (e.g., "tty1", "pts/0") */
    char   ut_id[4];                   /* Terminal suffix (e.g., "1", "/0") */
    char   ut_user[__UT_NAMESIZE];     /* Login username */
    char   ut_host[__UT_HOSTSIZE];     /* Hostname for remote login, or
                                          kernel version for run-level messages */
    struct exit_status ut_exit;        /* Exit status of DEAD_PROCESS
                                          (NOT filled in by init on Linux) */
    long   ut_session;                 /* Session ID (for windowing systems) */
    struct timeval ut_tv;              /* Timestamp when this record was created */
    int32_t ut_addr_v6[4];             /* IP address of remote host:
                                          IPv4 uses ut_addr_v6[0] only;
                                          IPv6 uses all 4 elements */
    char   __unused[20];               /* Reserved for future use */
};

๐Ÿ’ก String fields: All string fields are null-terminated unless the content fills the entire array. For example, a 32-character username with no room for a null terminator won’t have one โ€” be careful when printing these fields. Use %.32s format or similar.

Field-by-Field Explanation
Field Type Meaning Example Value
ut_type short Type of record (login, logout, boot, etc.) USER_PROCESS (7)
ut_pid pid_t PID of the login process (e.g., login, sshd) 1471
ut_line char[32] Terminal device name without “/dev/” prefix “tty1”, “pts/0”
ut_id char[4] Suffix of terminal name after “tty”, “pts”, “pty” “1”, “/0”
ut_user char[32] Login name of user. Zeroed out on logout. “ravi”
ut_host char[256] Remote hostname for SSH/telnet; kernel version for run-level messages “192.168.1.5”
ut_exit exit_status Exit status for DEAD_PROCESS records (not filled by init on Linux) โ€”
ut_session long Session ID โ€” used by windowing systems for terminal windows 1234
ut_tv struct timeval Timestamp of this event (seconds + microseconds) tv_sec=1717569000
ut_addr_v6 int32_t[4] IP of remote host. IPv4 โ†’ [0] only. IPv6 โ†’ all 4. Linux-specific. {0xC0A80105,0,0,0}

Understanding ut_line and ut_id

The ut_line and ut_id fields are derived from the terminal device name. Here is how they relate:

Full Device Path ut_line (after /dev/) ut_id (suffix after tty/pts/pty) Login Type
/dev/tty1 tty1 1 Virtual console
/dev/tty2 tty2 2 Virtual console
/dev/pts/0 pts/0 /0 Pseudoterminal (SSH/xterm)
/dev/pts/7 pts/7 /7 Pseudoterminal (SSH/xterm)

The ut_id is used as a unique key to find and overwrite a specific record in the utmp file when a user logs out from that terminal.

The Nine Record Types (ut_type Values)

The ut_type field tells you what kind of event this record represents. These constants are defined in <utmpx.h>:

Constant Value Meaning Written by
EMPTY 0 Slot is empty, no valid data โ€”
RUN_LVL 1 System run-level change (boot/shutdown) init
BOOT_TIME 2 System boot time stored in ut_tv init
NEW_TIME 3 New system time after clock adjustment NTP daemon
OLD_TIME 4 Old system time before clock adjustment NTP daemon
INIT_PROCESS 5 Process spawned by init (e.g., getty) init
LOGIN_PROCESS 6 Session leader for a login (e.g., login program) getty/login
USER_PROCESS 7 Active user login session โ€” most common login/sshd
DEAD_PROCESS 8 Process has exited (logout marker) init/login

โš ๏ธ Important: The numeric values (0โ€“8) are significant. Some programs in the Linux source (like agetty) write range checks like ut_type >= INIT_PROCESS && ut_type <= DEAD_PROCESS to identify process-related records (types 5โ€“8).

The Full Login Lifecycle in wtmp

When a user logs into a virtual console, four records appear in the wtmp file in this order:

Step 1
INIT_PROCESS record โ€” written by init when it spawns the getty process
Step 2
LOGIN_PROCESS record โ€” written by getty after it opens the terminal and prompts for username
Step 3
USER_PROCESS record โ€” written by login after validating credentials; ut_user is set to username
Step 4
DEAD_PROCESS record โ€” written by init when it detects the login shell exited (user logged out)

๐Ÿ’ก Race condition note: Some versions of init spawn getty before updating wtmp. This can cause the INIT_PROCESS and LOGIN_PROCESS records to appear in reverse order in the wtmp file.

Coding Example 1: Print All Fields of a utmpx Record

This program reads the utmp file and prints every field of every record โ€” great for understanding what data is actually stored.

/* print_utmpx_fields.c
 * Prints all fields of every record in utmp file
 * Compile: gcc print_utmpx_fields.c -o print_utmpx_fields
 * Note: run as root if permission denied
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <utmpx.h>
#include <time.h>
#include <arpa/inet.h>  /* inet_ntop */
#include <string.h>

const char *ut_type_str(short type)
{
    switch (type) {
        case EMPTY:         return "EMPTY";
        case RUN_LVL:       return "RUN_LVL";
        case BOOT_TIME:     return "BOOT_TIME";
        case NEW_TIME:      return "NEW_TIME";
        case OLD_TIME:      return "OLD_TIME";
        case INIT_PROCESS:  return "INIT_PROCESS";
        case LOGIN_PROCESS: return "LOGIN_PROCESS";
        case USER_PROCESS:  return "USER_PROCESS";
        case DEAD_PROCESS:  return "DEAD_PROCESS";
        default:            return "UNKNOWN";
    }
}

void print_record(const struct utmpx *ut, int num)
{
    char timebuf[64];
    char ipbuf[INET6_ADDRSTRLEN];
    struct tm *tm_info;

    printf("--- Record #%d ---\n", num);
    printf("  ut_type    : %d (%s)\n", ut->ut_type, ut_type_str(ut->ut_type));
    printf("  ut_pid     : %d\n", (int)ut->ut_pid);
    printf("  ut_line    : %.32s\n", ut->ut_line);
    printf("  ut_id      : %.4s\n", ut->ut_id);
    printf("  ut_user    : %.32s\n", ut->ut_user);
    printf("  ut_host    : %.256s\n", ut->ut_host);

    /* Format timestamp */
    tm_info = localtime((time_t *)&ut->ut_tv.tv_sec);
    strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm_info);
    printf("  ut_tv      : %s (+ %ldus)\n", timebuf, (long)ut->ut_tv.tv_usec);

    /* Show IP if present (IPv4 only check) */
    if (ut->ut_addr_v6[0] != 0) {
        struct in_addr addr;
        addr.s_addr = ut->ut_addr_v6[0];
        printf("  ut_addr_v6 : %s (IPv4)\n",
               inet_ntop(AF_INET, &addr, ipbuf, sizeof(ipbuf)));
    }
    printf("  ut_session : %ld\n\n", ut->ut_session);
}

int main(void)
{
    struct utmpx *ut;
    int count = 0;

    printf("sizeof(struct utmpx) = %zu bytes\n\n", sizeof(struct utmpx));

    setutxent();
    while ((ut = getutxent()) != NULL)
        print_record(ut, ++count);
    endutxent();

    printf("Total records: %d\n", count);
    return 0;
}

Coding Example 2: Filter Records by Type

A practical program that filters utmpx records to show only records of a specific type โ€” useful for monitoring system boot time or user logins.

/* filter_by_type.c
 * Filter utmpx records by type
 * Compile: gcc filter_by_type.c -o filter_by_type
 * Usage: ./filter_by_type [type_number]
 *   type 2 = BOOT_TIME, type 7 = USER_PROCESS, type 8 = DEAD_PROCESS
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <utmpx.h>
#include <time.h>
#include <paths.h>

int main(int argc, char *argv[])
{
    struct utmpx *ut;
    int target_type = USER_PROCESS; /* Default: show logged-in users */
    int found = 0;
    char timebuf[64];

    if (argc > 1)
        target_type = atoi(argv[1]);

    printf("Filtering records with ut_type = %d\n\n", target_type);

    /* Read from wtmp to see historical data too */
    if (utmpxname(_PATH_WTMP) == -1) {
        perror("utmpxname");
        exit(EXIT_FAILURE);
    }

    setutxent();

    while ((ut = getutxent()) != NULL) {
        if (ut->ut_type != target_type)
            continue;

        strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S",
                 localtime((time_t *)&ut->ut_tv.tv_sec));

        printf("  user=%-12.12s line=%-10.10s pid=%5d  time=%s\n",
               ut->ut_user, ut->ut_line, (int)ut->ut_pid, timebuf);
        found++;
    }

    endutxent();

    if (found == 0)
        printf("  No records found with ut_type = %d\n", target_type);
    else
        printf("\nFound %d record(s).\n", found);

    return 0;
}

Run examples:

./filter_by_type 2   # Show BOOT_TIME records (when system was booted)
./filter_by_type 7   # Show USER_PROCESS records (who is/was logged in)
./filter_by_type 8   # Show DEAD_PROCESS records (logout events)

Coding Example 3: Find Last System Boot Time

Reads the wtmp file and searches for the most recent BOOT_TIME record to find when the system was last rebooted โ€” similar to the uptime command.

/* find_boot_time.c
 * Find last system boot time from wtmp
 * Compile: gcc find_boot_time.c -o find_boot_time
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <utmpx.h>
#include <time.h>
#include <paths.h>
#include <string.h>

int main(void)
{
    struct utmpx *ut;
    struct utmpx last_boot;
    int found = 0;
    char timebuf[64];
    double uptime_seconds;
    time_t now;

    /* We need to scan wtmp for BOOT_TIME records */
    if (utmpxname(_PATH_WTMP) == -1) {
        perror("utmpxname");
        exit(EXIT_FAILURE);
    }

    memset(&last_boot, 0, sizeof(last_boot));
    setutxent();

    /* Scan all records โ€” keep the latest BOOT_TIME */
    while ((ut = getutxent()) != NULL) {
        if (ut->ut_type == BOOT_TIME) {
            last_boot = *ut;  /* Copy the struct */
            found = 1;
        }
    }
    endutxent();

    if (!found) {
        printf("No BOOT_TIME record found in %s\n", _PATH_WTMP);
        return 1;
    }

    strftime(timebuf, sizeof(timebuf), "%A, %d %B %Y %H:%M:%S",
             localtime((time_t *)&last_boot.ut_tv.tv_sec));

    printf("Last system boot: %s\n", timebuf);

    /* Calculate approximate uptime */
    time(&now);
    uptime_seconds = difftime(now, (time_t)last_boot.ut_tv.tv_sec);
    long days    = (long)(uptime_seconds / 86400);
    long hours   = (long)((uptime_seconds - days * 86400) / 3600);
    long minutes = (long)((uptime_seconds - days * 86400
                           - hours * 3600) / 60);

    printf("System uptime   : %ld days, %ld hours, %ld minutes\n",
           days, hours, minutes);

    return 0;
}

Interview Questions
Q1. What is the difference between ut_line and ut_id?

Answer: ut_line contains the full terminal device filename without the “/dev/” prefix (e.g., “tty1”, “pts/0”). ut_id contains only the suffix after “tty”, “pts”, or “pty” (e.g., “1”, “/0”). ut_id is used as a compact unique key to find a specific record in the utmp file.

Q2. How does the system record user logout in the utmp file?

Answer: On logout, the existing utmp record for that terminal is overwritten with a new record where ut_type is set to DEAD_PROCESS and ut_user is zeroed out. The ut_line and ut_id remain the same to identify which terminal it belongs to.

Q3. What are BOOT_TIME records used for?

Answer: BOOT_TIME records store the system boot time in the ut_tv field. Tools like uptime and who -b read the BOOT_TIME record from the utmp file to determine when the system was last started.

Q4. Why is it important that record type values have a specific numeric order?

Answer: Some programs use range checks like ut_type >= INIT_PROCESS && ut_type <= DEAD_PROCESS (i.e., values 5โ€“8) to identify all process-related records. This works correctly because the constants are assigned values in ascending order. Changing these numeric values would break such programs.

Q5. What does ut_host contain for a remote SSH login vs a local login?

Answer: For a remote SSH login, ut_host contains the hostname or IP address of the remote machine. For a local console login, ut_host is typically empty. For run-level records, it contains the kernel version string.

Leave a Reply

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