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.
#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).
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.
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.
| 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 |
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**
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;
}
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;
}
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.
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.
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.
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.
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.
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.
