Login Accounting The lastlog File

 

Chapter 40: Login Accounting
Part 7 โ€“ The lastlog File
๐Ÿ“… Per-User Last Login
๐Ÿ—‚๏ธ Indexed by UID
โšก Direct Seek Access

What Is the lastlog File?

When you log into a Linux system, you often see a message like: “Last login: Wednesday Jun 04 2026 09:30:00 from laptop.local”. This information comes from the lastlog file.

Unlike the wtmp file (which records every login and logout for every user), the lastlog file records only the most recent login for each user. It gets overwritten every time a user logs in, so it always shows just the last session.

Key Terms in This Section

lastlog file struct lastlog ll_time ll_line ll_host UID-indexed lseek _PATH_LASTLOG lastlog(1)

File Location and Path Constant
#include <paths.h>
#include <lastlog.h>

/* Path on Linux */
_PATH_LASTLOG  โ†’  "/var/log/lastlog"

/* View with the lastlog command */
$ lastlog -u username

Like utmp and wtmp, lastlog is normally readable by all users but writable only by privileged processes (root or programs with setgid).

The struct lastlog Definition

Defined in <lastlog.h>. Each record stores the last login time, terminal, and remote host for one user:

#include <lastlog.h>

#define UT_NAMESIZE  32
#define UT_HOSTSIZE  256

struct lastlog {
    time_t ll_time;               /* Time of last login */
    char   ll_line[UT_NAMESIZE];  /* Terminal used (e.g., "tty1", "pts/0") */
    char   ll_host[UT_HOSTSIZE];  /* Remote hostname (empty for local login) */
};

/* Notice what is NOT in this struct: no username, no UID.
 * The user is identified by their position in the file. */

โš ๏ธ Key difference from utmpx: The lastlog struct has NO username or UID field. Instead, records are indexed by UID โ€” the record for UID 1000 is at byte offset 1000 ร— sizeof(struct lastlog) in the file.

UID-Based Indexing โ€” How It Works

The lastlog file is essentially an array of struct lastlog records, where the index is the user’s UID. To find or update a record, you use lseek() to jump directly to the right position:

File Offset UID Content
0 ร— sizeof(struct lastlog) UID 0 (root) root’s last login info
1 ร— sizeof(struct lastlog) UID 1 (daemon) daemon’s last login info (often all zeros)
1000 ร— sizeof(struct lastlog) UID 1000 (ravi) ravi’s last login info
1001 ร— sizeof(struct lastlog) UID 1001 (priya) priya’s last login info

To access a record:

uid_t uid = 1000;   /* ravi's UID */

/* Seek to ravi's record */
off_t offset = (off_t)uid * sizeof(struct lastlog);
lseek(fd, offset, SEEK_SET);

/* Read or write one struct lastlog */
read(fd, &llog, sizeof(struct lastlog));
/* or */
write(fd, &llog, sizeof(struct lastlog));

๐Ÿ’ก Sparse file: The lastlog file is typically a sparse file on Linux. Even if UID 1000 is the only user, the file appears to be 1001 ร— sizeof(struct lastlog) bytes, but only the pages with actual data consume disk space.

Comparison: utmp, wtmp, and lastlog
Property utmp wtmp lastlog
Default path /var/run/utmp /var/log/wtmp /var/log/lastlog
Record type struct utmpx struct utmpx struct lastlog
What it tracks Current logins All login/logout history Last login per user
Write behavior Overwrite on logout Append only Overwrite (update last)
Access method utmpx API utmpx API open/lseek/read/write
Indexed by Sequential scan Sequential scan User UID
Read by command who, w last lastlog

Coding Example 1: View lastlog for Specific Users

Reimplementation of TLPI Listing 40-4 โ€” reads lastlog records for users specified on the command line.

/* view_lastlog.c
 * Display lastlog records for specified users
 * Compile: gcc view_lastlog.c -o view_lastlog
 * Usage: ./view_lastlog ravi priya root
 *
 * Similar to: lastlog -u ravi
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <pwd.h>       /* getpwnam() */
#include <lastlog.h>
#include <paths.h>    /* _PATH_LASTLOG */

int main(int argc, char *argv[])
{
    int fd, j;
    struct lastlog llog;
    struct passwd *pwd;
    uid_t uid;
    char timebuf[64];

    if (argc < 2) {
        fprintf(stderr, "Usage: %s username...\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* Open the lastlog file read-only */
    fd = open(_PATH_LASTLOG, O_RDONLY);
    if (fd == -1) {
        perror("open " _PATH_LASTLOG);
        fprintf(stderr, "Hint: May need root. Try: sudo ./view_lastlog\n");
        exit(EXIT_FAILURE);
    }

    printf("%-12s %-10s %-24s %s\n",
           "Username", "Terminal", "Remote Host", "Last Login");
    printf("%-12s %-10s %-24s %s\n",
           "--------", "--------", "-----------", "----------");

    for (j = 1; j < argc; j++) {
        /* Look up the username to get the UID */
        pwd = getpwnam(argv[j]);
        if (pwd == NULL) {
            printf("%-12s  (no such user)\n", argv[j]);
            continue;
        }
        uid = pwd->pw_uid;

        /* Seek to this user's record: offset = UID ร— record_size */
        if (lseek(fd, (off_t)uid * sizeof(struct lastlog), SEEK_SET) == -1) {
            perror("lseek");
            continue;
        }

        /* Read one record */
        if (read(fd, &llog, sizeof(struct lastlog)) <= 0) {
            printf("%-12s  (no lastlog record)\n", argv[j]);
            continue;
        }

        /* Check if user has ever logged in */
        if (llog.ll_time == 0) {
            printf("%-12s %-10s %-24s %s\n",
                   argv[j], "N/A", "N/A", "**Never logged in**");
            continue;
        }

        /* Format the timestamp */
        strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S",
                 localtime(&llog.ll_time));

        printf("%-12s %-10.10s %-24.24s %s\n",
               argv[j],
               llog.ll_line[0] ? llog.ll_line : "N/A",
               llog.ll_host[0] ? llog.ll_host : "local",
               timebuf);
    }

    close(fd);
    return 0;
}

Sample output:

Username     Terminal   Remote Host              Last Login
--------     --------   -----------              ----------
ravi         pts/0      192.168.1.10             2026-06-04 09:30:00
priya        tty1       local                    2026-06-03 14:22:11
root         pts/1      laptop.local             2026-06-05 08:00:55
daemon       N/A        N/A                      **Never logged in**

Coding Example 2: Update lastlog on Login

Shows how a login program should update the lastlog file. This is Exercise 40-2 from TLPI โ€” modify utmpx_login.c to also update lastlog.

/* update_lastlog.c
 * Function to update the lastlog file on login
 * Used in Exercise 40-2 from TLPI
 * Compile: gcc update_lastlog.c -o update_lastlog
 * Usage: sudo ./update_lastlog username terminal [hostname]
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <pwd.h>
#include <lastlog.h>
#include <paths.h>

/*
 * update_lastlog() - Update the lastlog record for a user on login
 *
 * This should be called AFTER writing the utmp/wtmp records,
 * with the current login information.
 *
 * Returns: 0 on success, -1 on error
 */
int update_lastlog(const char *username,
                   const char *terminal,
                   const char *hostname)
{
    struct passwd *pwd;
    struct lastlog llog;
    int fd;
    int ret = 0;

    /* Look up UID for this username */
    pwd = getpwnam(username);
    if (pwd == NULL) {
        fprintf(stderr, "update_lastlog: unknown user '%s'\n", username);
        return -1;
    }

    /* Open lastlog for reading and writing */
    fd = open(_PATH_LASTLOG, O_RDWR);
    if (fd == -1) {
        perror("open lastlog");
        return -1;
    }

    /* Fill in the new record */
    memset(&llog, 0, sizeof(llog));
    llog.ll_time = time(NULL);  /* Current time */
    strncpy(llog.ll_line, terminal, sizeof(llog.ll_line) - 1);
    if (hostname)
        strncpy(llog.ll_host, hostname, sizeof(llog.ll_host) - 1);

    /* Seek to the correct position: UID ร— record_size */
    if (lseek(fd, (off_t)pwd->pw_uid * sizeof(struct lastlog),
              SEEK_SET) == -1) {
        perror("lseek lastlog");
        ret = -1;
    } else {
        /* Write the record (overwrites previous entry) */
        if (write(fd, &llog, sizeof(llog)) != sizeof(llog)) {
            perror("write lastlog");
            ret = -1;
        }
    }

    close(fd);
    return ret;
}

int main(int argc, char *argv[])
{
    const char *username, *terminal, *hostname;

    if (argc < 3) {
        fprintf(stderr, "Usage: %s username terminal [hostname]\n", argv[0]);
        fprintf(stderr, "  Example: %s ravi pts/0 192.168.1.10\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    username = argv[1];
    terminal = argv[2];
    hostname = (argc > 3) ? argv[3] : NULL;

    printf("Updating lastlog for user '%s'...\n", username);

    if (update_lastlog(username, terminal, hostname) == 0) {
        printf("Success! Check with: lastlog -u %s\n", username);
    } else {
        fprintf(stderr, "Failed. Check permissions and username.\n");
        exit(EXIT_FAILURE);
    }

    return 0;
}

Coding Example 3: Dump All lastlog Records

Scans the entire lastlog file and prints all users who have ever logged in, sorted by UID.

/* dump_lastlog.c
 * Dump all lastlog records (similar to running 'lastlog' with no args)
 * Compile: gcc dump_lastlog.c -o dump_lastlog
 * Usage: sudo ./dump_lastlog
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <pwd.h>
#include <sys/stat.h>
#include <lastlog.h>
#include <paths.h>

int main(void)
{
    int fd;
    struct lastlog llog;
    struct stat st;
    struct passwd *pwd;
    char timebuf[64];
    long num_records;
    uid_t uid;
    ssize_t nread;

    /* Open and check size */
    fd = open(_PATH_LASTLOG, O_RDONLY);
    if (fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    if (fstat(fd, &st) == -1) {
        perror("fstat");
        close(fd);
        exit(EXIT_FAILURE);
    }

    num_records = st.st_size / sizeof(struct lastlog);
    printf("File: %s (%ld bytes, %ld possible records)\n\n",
           _PATH_LASTLOG, (long)st.st_size, num_records);

    printf("%-6s %-12s %-10s %-24s %s\n",
           "UID", "Username", "Terminal", "Remote Host", "Last Login");
    printf("%-6s %-12s %-10s %-24s %s\n",
           "---", "--------", "--------", "-----------", "----------");

    /* Read through all records sequentially */
    for (uid = 0; uid < (uid_t)num_records; uid++) {
        nread = read(fd, &llog, sizeof(struct lastlog));
        if (nread <= 0)
            break;

        /* Skip users who never logged in */
        if (llog.ll_time == 0)
            continue;

        /* Look up username for this UID */
        pwd = getpwuid(uid);
        const char *username = pwd ? pwd->pw_name : "(unknown)";

        strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S",
                 localtime(&llog.ll_time));

        printf("%-6d %-12.12s %-10.10s %-24.24s %s\n",
               uid, username,
               llog.ll_line[0] ? llog.ll_line : "N/A",
               llog.ll_host[0] ? llog.ll_host : "local",
               timebuf);
    }

    close(fd);
    return 0;
}

Interview Questions
Q1. How is the lastlog file different from wtmp?

Answer: wtmp is a growing audit trail that records every login and logout for every user. lastlog records only the most recent login for each user, and that record gets overwritten on every login. wtmp uses sequential utmpx records accessed via the utmpx API; lastlog uses fixed-size struct lastlog records indexed directly by UID via lseek.

Q2. How do you find the lastlog record for UID 1500?

Answer: Open /var/log/lastlog and seek to byte offset 1500 ร— sizeof(struct lastlog) using lseek(fd, 1500 * sizeof(struct lastlog), SEEK_SET), then read one struct lastlog.

Q3. Why doesn’t the lastlog structure contain a username or UID field?

Answer: Because the file is indexed by UID โ€” the position of a record in the file implicitly encodes the UID. Record at offset N ร— sizeof(struct lastlog) belongs to UID N. Including UID in the struct would be redundant. This design makes lookups O(1) โ€” you seek directly to the right record without scanning.

Q4. Can the lastlog file be very large? How does Linux handle this efficiently?

Answer: Theoretically, if the highest UID on a system is 60000, the lastlog file would need to be 60001 ร— sizeof(struct lastlog) bytes large. Linux handles this efficiently using sparse files โ€” only the blocks that contain actual login data are allocated on disk. Unused slots (users who never logged in) don’t consume disk space.

Q5. When should a login application update the lastlog file?

Answer: On login only (not on logout). The lastlog is updated at the start of a new login session with the current time, terminal, and remote hostname. This overwrites the previous lastlog entry for that user, so it always reflects the most recent login. Programs like the standard login binary update lastlog, utmp, and wtmp together on login.

Chapter 40 Summary

Linux maintains three login accounting files:

File Purpose Key Function
/var/run/utmp Currently logged-in users pututxline()
/var/log/wtmp All logins and logouts ever updwtmpx()
/var/log/lastlog Last login per user open/lseek/read/write

Any program implementing a login service must update all three files correctly. The C library’s utmpx API handles utmp and wtmp, while lastlog is updated with raw file I/O indexed by UID.

Leave a Reply

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