readutmp: Add option to make use of wtmpdb

Bruno Haible via Gnulib discussion list <[email protected]>
Newsgroups gmane.comp.lib.gnulib.bugs
Message-ID <12367356.BaYr0rKQ5T@cagnes>
As mentioned in the thread starting at
  https://lists.gnu.org/archive/html/bug-gnulib/2023-09/msg00064.html
and then again in
  https://lists.gnu.org/archive/html/bug-gnulib/2026-07/msg00145.html
  https://lists.gnu.org/archive/html/bug-gnulib/2026-07/msg00281.html

it would be useful to optionally make use of wtmpdb in the Gnulib module
'readutmp', for the sake of the coreutils programs 'who' and 'users'.

This patch does it: it uses the wtmpdb library when the users asks for
the results of /var/log/wtmp.

Find attached a corresponding patch for coreutils.

Here are the effects on some coreutils program invocations (as seen on
a Devuan 6 system):


$ src/who /var/log/wtmp
bruno    tty7         2026-07-31 23:11 (:0)
bruno    tty7         2026-08-03 20:53 (:0)
→
$ src/who /var/log/wtmp
bruno    tty7         2026-08-03 20:53 (:0)
bruno    tty7         2026-07-31 23:11 (:0)

$ src/who --all /var/log/wtmp
                        2026-07-31 23:11               229 id=si    term=0 exit=0
           system boot  2026-07-31 23:11
           run-level 2  2026-07-31 23:11                   last=S
                        2026-07-31 23:11              1365 id=l2
                        2026-07-31 23:11              1365 id=l2    term=0 exit=0
                        2026-07-31 23:11              1616 id=1
                        2026-07-31 23:11              1617 id=2
                        2026-07-31 23:11              1620 id=5
LOGIN      tty2         2026-07-31 23:11              1617 id=2
LOGIN      tty1         2026-07-31 23:11              1616 id=1
                        2026-07-31 23:11              1618 id=3
LOGIN      tty3         2026-07-31 23:11              1618 id=3
                        2026-07-31 23:11              1619 id=4
LOGIN      tty4         2026-07-31 23:11              1619 id=4
LOGIN      tty5         2026-07-31 23:11              1620 id=5
                        2026-07-31 23:11              1621 id=6
LOGIN      tty6         2026-07-31 23:11              1621 id=6
bruno    + tty7         2026-07-31 23:11 20:40        1644 (:0)
           run-level    2026-08-01 00:45
           run-level 0  2026-08-01 00:45                   last=2
           tty1         2026-08-01 00:45              1616 id=1     term=0 exit=0
           tty2         2026-08-01 00:45              1617 id=2     term=0 exit=0
           tty3         2026-08-01 00:45              1618 id=3     term=0 exit=0
           tty4         2026-08-01 00:45              1619 id=4     term=0 exit=0
           tty5         2026-08-01 00:45              1620 id=5     term=0 exit=0
           tty6         2026-08-01 00:45              1621 id=6     term=0 exit=0
           tty1         2026-08-01 00:45                 0 id=1     term=0 exit=0
           tty2         2026-08-01 00:45                 0 id=2     term=0 exit=0
           tty3         2026-08-01 00:45                 0 id=3     term=0 exit=0
           tty4         2026-08-01 00:45                 0 id=4     term=0 exit=0
           tty5         2026-08-01 00:45                 0 id=5     term=0 exit=0
           tty6         2026-08-01 00:45                 0 id=6     term=0 exit=0
                        2026-08-01 00:45             15341 id=l0
           tty7         2026-08-01 00:45                 0 id=:0    term=0 exit=0
           run-level    2026-08-01 00:45
                        2026-08-03 20:09               227 id=si    term=0 exit=0
           system boot  2026-08-03 20:09
           run-level 2  2026-08-03 20:09                   last=S
                        2026-08-03 20:09              1365 id=l2
                        2026-08-03 20:10              1365 id=l2    term=0 exit=0
                        2026-08-03 20:10              1882 id=1
                        2026-08-03 20:10              1883 id=2
                        2026-08-03 20:10              1885 id=4
LOGIN      tty1         2026-08-03 20:10              1882 id=1
LOGIN      tty2         2026-08-03 20:10              1883 id=2
                        2026-08-03 20:10              1886 id=5
LOGIN      tty5         2026-08-03 20:10              1886 id=5
                        2026-08-03 20:10              1884 id=3
LOGIN      tty4         2026-08-03 20:10              1885 id=4
                        2026-08-03 20:10              1887 id=6
LOGIN      tty6         2026-08-03 20:10              1887 id=6
LOGIN      tty3         2026-08-03 20:10              1884 id=3
bruno    + tty7         2026-08-03 20:53  old         2099 (:0)
→
$ src/who --all /var/log/wtmp
bruno    + tty7         2026-08-03 20:53 20:41           0 (:0)
LOGIN      tty7         2026-08-03 20:09                 0 id=5
           tty7         2026-08-03 20:53                 0 id=5     term=0 exit=0
           system boot  2026-08-03 20:09
bruno    + tty7         2026-07-31 23:11 20:41           0 (:0)
           tty7         2026-08-01 00:45                 0 id=3     term=0 exit=0
LOGIN      tty7         2026-07-31 23:11                 0 id=2
           tty7         2026-07-31 23:11                 0 id=2     term=0 exit=0
           system boot  2026-07-31 23:11

$ src/who --boot /var/log/wtmp
         system boot  2026-07-31 23:11
         system boot  2026-08-03 20:09
→
$ src/who --boot /var/log/wtmp
         system boot  2026-08-03 20:09
         system boot  2026-07-31 23:11

$ src/users /var/log/wtmp
bruno bruno

(unchanged)



2026-08-04  Bruno Haible  <[email protected]>

	readutmp: Add option to make use of wtmpdb.
	* m4/wtmpdb.m4: New file.
	* m4/readutmp.m4 (gl_READUTMP): Invoke gl_WTMPDB_CHOICE. Define
	READUTMP_USE_WTMPDB. Take it into account for READUTMP_LIB.
	* lib/readutmp.h (WTMPDB_FILE): New macro.
	* lib/readutmp.c: Include <wtmpdb.h>.
	(wtmpdb_time_to_timespec): New function.
	(struct wtmpdb_locals): New type.
	(wtmpdb_callback, read_utmp_from_wtmpdb): New functions.
	(read_utmp): For WTMP_FILE, conditionally use read_utmp_from_wtmpdb.
	* modules/readutmp (Files): Add m4/wtmpdb.m4.
	(Depends-on): Add str_endswith.
	* DEPENDENCIES: Mention libwtmpdb.

diff --git a/DEPENDENCIES b/DEPENDENCIES
index 155101349b..e8276a8b96 100644
--- a/DEPENDENCIES
+++ b/DEPENDENCIES
@@ -416,6 +416,16 @@ For modules 'readutmp' and 'boot-time':
        - On Debian and Debian-based systems: libsystemd-dev,
        - On Red Hat distributions: systemd-devel.
        - Other: https://repology.org/project/systemd/versions
+  * wtmpdb libraries
+    + Recommended if wtmpdb is present on the system.
+      Needed for fetching login, logout, and boot times from
+      /var/log/wtmp.db instead of from the file /var/run/wtmp.
+    + Homepage:
+      https://github.com/thkukuk/wtmpdb
+    + Pre-built package name:
+       - On Debian and Debian-based systems: libwtmpdb-dev,
+       - On Red Hat distributions: --.
+       - Other: https://repology.org/project/wtmpdb/versions
 
 For modules 'terminfo' and 'termcap':
   * GNU ncurses (preferred)
diff --git a/lib/readutmp.c b/lib/readutmp.c
index 6e43b5e956..8e0fdcb0ea 100644
--- a/lib/readutmp.c
+++ b/lib/readutmp.c
@@ -19,6 +19,15 @@
 
 #include <config.h>
 
+#if READUTMP_USE_WTMPDB
+# include <wtmpdb.h>
+/* Kill macro definitions that conflict with <utmp.h> and <utmpx.h>.  */
+# undef EMPTY
+# undef BOOT_TIME
+# undef RUNLEVEL
+# undef USER_PROCESS
+#endif
+
 #include "readutmp.h"
 
 #include <errno.h>
@@ -972,6 +981,166 @@ read_utmp_from_systemd (idx_t *n_entries, STRUCT_UTMP **utmp_buf, int options)
 
 # endif
 
+# if READUTMP_USE_WTMPDB
+
+static int
+wtmpdb_time_to_timespec (const char *value, struct timespec *ts)
+{
+  if (value != NULL)
+    {
+      char *endptr;
+      unsigned long long login_usec = strtoull (value, &endptr, 10);
+      if (endptr != value && *endptr == '\0')
+        {
+          ts->tv_sec = login_usec / 1000000;
+          ts->tv_nsec = (login_usec % 1000000) * 1000;
+          return 0;
+        }
+    }
+  return -1;
+}
+
+struct wtmpdb_locals
+{
+  int options;
+  struct utmp_alloc a;
+};
+
+static int
+wtmpdb_callback (void *data, int argc, char **argv, char **column_names)
+{
+  struct wtmpdb_locals *l = (struct wtmpdb_locals *) data;
+
+  /* column_names  is typically
+     [ "ID", "Type", "User", "Login", "Logout", "TTY", "RemoteHost", "Service" ]
+   */
+
+  int id_index = -1;
+  int type_index = -1;
+  int user_index = -1;
+  int login_index = -1;
+  int logout_index = -1;
+  int tty_index = -1;
+  int remote_host_index = -1;
+  int service_index = -1;
+
+  for (int i = 0; i < argc; i++)
+    {
+      if (streq (column_names[i], "ID"))
+        id_index = i;
+      if (streq (column_names[i], "Type"))
+        type_index = i;
+      if (streq (column_names[i], "User"))
+        user_index = i;
+      if (streq (column_names[i], "Login"))
+        login_index = i;
+      if (streq (column_names[i], "Logout"))
+        logout_index = i;
+      if (streq (column_names[i], "TTY"))
+        tty_index = i;
+      if (streq (column_names[i], "RemoteHost"))
+        remote_host_index = i;
+      if (streq (column_names[i], "Service"))
+        service_index = i;
+    }
+
+  const char *id      = (id_index >= 0 ? argv[id_index] : NULL);
+  const char *type    = (type_index >= 0 ? argv[type_index] : NULL);
+  const char *user    = (user_index >= 0 ? argv[user_index] : NULL);
+  const char *login   = (login_index >= 0 ? argv[login_index] : NULL);
+  const char *logout  = (logout_index >= 0 ? argv[logout_index] : NULL);
+  const char *tty     = (tty_index >= 0 ? argv[tty_index] : NULL);
+  const char *host    = (remote_host_index >= 0 ? argv[remote_host_index] : NULL);
+  const char *service = (service_index >= 0 ? argv[service_index] : NULL);
+
+  if (type != NULL && streq (type, "1" /* BOOT_TIME */))
+    {
+      struct timespec login_ts;
+      if (wtmpdb_time_to_timespec (login, &login_ts) == 0)
+        {
+          l->a = add_utmp (l->a, l->options,
+                           "reboot", strlen ("reboot"),
+                           id, strlen (id),
+                           tty, strlen (tty),
+                           host, strlen (host),
+                           0, BOOT_TIME,
+                           login_ts, 0, 0, 0);
+
+          struct timespec logout_ts;
+          if (wtmpdb_time_to_timespec (logout, &logout_ts) == 0)
+            {
+              l->a = add_utmp (l->a, l->options,
+                               "shutdown", strlen ("shutdown"),
+                               id, strlen (id),
+                               tty, strlen (tty),
+                               host, strlen (host),
+                               0, EMPTY,
+                               logout_ts, 0, 0, 0);
+            }
+        }
+    }
+
+  if (type != NULL && streq (type, "3" /* USER_PROCESS */)
+      && login != NULL)
+    {
+      struct timespec login_ts;
+      if (wtmpdb_time_to_timespec (login, &login_ts) == 0)
+        {
+          short ctype =
+            (service != NULL && str_endswith (service, "-greeter")
+             ? LOGIN_PROCESS
+             : USER_PROCESS);
+          l->a = add_utmp (l->a, l->options,
+                           user, strlen (user),
+                           id, strlen (id),
+                           tty, strlen (tty),
+                           host, strlen (host),
+                           0, ctype,
+                           login_ts, 0, 0, 0);
+
+          struct timespec logout_ts;
+          if (wtmpdb_time_to_timespec (logout, &logout_ts) == 0)
+            {
+              l->a = add_utmp (l->a, l->options,
+                               "", 0,
+                               id, strlen (id),
+                               tty, strlen (tty),
+                               host, strlen (host),
+                               0, DEAD_PROCESS,
+                               logout_ts, 0, 0, 0);
+            }
+        }
+    }
+
+  return 0;
+}
+
+static int
+read_utmp_from_wtmpdb (idx_t *n_entries, STRUCT_UTMP **utmp_buf, int options)
+{
+  /* Fill entries, simulating what a utmp file would contain.  */
+  struct wtmpdb_locals locals = { options, { NULL, 0, 0, 0 } };
+
+  char *err = NULL;
+  int ret = wtmpdb_read_all_v2 (WTMPDB_FILE, wtmpdb_callback, &locals, &err);
+  free (err);
+  if (ret >= 0)
+    {
+      locals.a = finish_utmp (locals.a);
+      if (locals.a.filled > 0)
+        {
+          *n_entries = locals.a.filled;
+          *utmp_buf = locals.a.utmp;
+          return 0;
+        }
+      free (locals.a.utmp);
+    }
+
+  return -1;
+}
+
+# endif
+
 int
 read_utmp (char const *file, idx_t *n_entries, STRUCT_UTMP **utmp_buf,
            int options)
@@ -982,6 +1151,17 @@ read_utmp (char const *file, idx_t *n_entries, STRUCT_UTMP **utmp_buf,
     return read_utmp_from_systemd (n_entries, utmp_buf, options);
 # endif
 
+# if READUTMP_USE_WTMPDB
+  if (streq (file, WTMP_FILE))
+    {
+      /* Imitate reading WTMP_FILE, using wtmpdb APIs.  */
+      int ret = read_utmp_from_wtmpdb (n_entries, utmp_buf, options);
+      if (ret >= 0)
+        return ret;
+      /* Could not read /var/log/wtmp.db.  Fall back to /var/log/wtmp.  */
+    }
+# endif
+
   return read_utmp_from_file (file, n_entries, utmp_buf, options);
 }
 
diff --git a/lib/readutmp.h b/lib/readutmp.h
index badbbc4331..31a27c1dda 100644
--- a/lib/readutmp.h
+++ b/lib/readutmp.h
@@ -246,7 +246,7 @@ struct utmpx32
 #endif
 
 /* Definition of WTMP_FILE.
-   On glibc systems, UTMP_FILE is "/var/log/wtmp".  */
+   On glibc systems, WTMP_FILE is "/var/log/wtmp".  */
 #if !defined WTMP_FILE && defined _PATH_WTMP
 # define WTMP_FILE _PATH_WTMP
 #endif
@@ -258,6 +258,12 @@ struct utmpx32
 # define WTMP_FILE "/etc/wtmp"
 #endif
 
+/* Definition of WTMPDB_FILE.
+   On Debian and Devuan systems, WTMPDB_FILE is "/var/log/wtmp.db".  */
+#ifndef WTMPDB_FILE
+# define WTMPDB_FILE "/var/log/wtmp.db"
+#endif
+
 /* In early versions of Android, <utmp.h> did not define BOOT_TIME or
    LOGIN_PROCESS, only USER_PROCESS.  We need to use the value that is defined
    in newer versions of Android.  */
diff --git a/m4/readutmp.m4 b/m4/readutmp.m4
index 3eeec0d081..787fe4eaa1 100644
--- a/m4/readutmp.m4
+++ b/m4/readutmp.m4
@@ -1,5 +1,5 @@
 # readutmp.m4
-# serial 32
+# serial 33
 dnl Copyright (C) 2002-2026 Free Software Foundation, Inc.
 dnl This file is free software; the Free Software Foundation
 dnl gives unlimited permission to copy and/or distribute it,
@@ -9,11 +9,13 @@
 AC_DEFUN([gl_READUTMP],
 [
   AC_REQUIRE([gl_SYSTEMD_CHOICE])
+  AC_REQUIRE([gl_WTMPDB_CHOICE])
 
-  dnl Set READUTMP_LIB to '-lsystemd' or '', depending on whether use of
-  dnl systemd APIs is possible and desired (only the systemd login API, here).
+  dnl Set READUTMP_LIB to '-lsystemd' or '-lwtmpdb' or '', depending on whether
+  dnl use of systemd APIs is possible and desired (only the systemd login API,
+  dnl here) and whether use of wtmpdb APIs is possible and desired.
   dnl AC_LIB_LINKFLAGS_BODY would be overkill here, since few people install
-  dnl libsystemd in non-system directories.
+  dnl libsystemd or libwtmpdb in non-system directories.
   READUTMP_LIB=
   if test "$SYSTEMD_CHOICE" = yes; then
     AC_CHECK_HEADER([systemd/sd-login.h])
@@ -42,6 +44,14 @@ AC_DEFUN([gl_READUTMP]
       fi
     fi
   fi
+  if test "$WTMPDB_CHOICE" = yes; then
+    AC_CHECK_HEADER([wtmpdb.h])
+    if test $ac_cv_header_wtmpdb_h = yes; then
+      AC_DEFINE([READUTMP_USE_WTMPDB], [1],
+        [Define if the readutmp module should use the wtmpdb API.])
+      READUTMP_LIB=$READUTMP_LIB${READUTMP_LIB:+ }'-lwtmpdb'
+    fi
+  fi
   AC_SUBST([READUTMP_LIB])
 
   gl_PREREQ_READUTMP_H
diff --git a/m4/wtmpdb.m4 b/m4/wtmpdb.m4
new file mode 100644
index 0000000000..159524ef86
--- /dev/null
+++ b/m4/wtmpdb.m4
@@ -0,0 +1,25 @@
+# wtmpdb.m4
+# serial 1
+dnl Copyright (C) 2026 Free Software Foundation, Inc.
+dnl This file is free software; the Free Software Foundation
+dnl gives unlimited permission to copy and/or distribute it,
+dnl with or without modifications, as long as this notice is preserved.
+dnl This file is offered as-is, without any warranty.
+
+# Sets WTMPDB_CHOICE to 'yes' or 'no', depending on the preferred use of
+# wtmpdb APIs.
+AC_DEFUN([gl_WTMPDB_CHOICE],
+[
+  AC_MSG_CHECKING([whether to use wtmpdb APIs])
+  AC_ARG_WITH([wtmpdb],
+    [  --with-wtmpdb           use wtmpdb APIs],
+    [WTMPDB_CHOICE="$withval"],
+    [WTMPDB_CHOICE=no])
+  AC_MSG_RESULT([$WTMPDB_CHOICE])
+  AC_SUBST([WTMPDB_CHOICE])
+])
+
+# Pre-built package name for the libwtmpdb library:
+# - On Debian and Debian-based systems: libwtmpdb-dev,
+# - On Red Hat distributions: --.
+# - Other: https://repology.org/project/wtmpdb/versions
diff --git a/modules/readutmp b/modules/readutmp
index f74c533f78..0ce43d8972 100644
--- a/modules/readutmp
+++ b/modules/readutmp
@@ -7,6 +7,7 @@ lib/readutmp.c
 lib/boot-time-aux.h
 m4/readutmp.m4
 m4/systemd.m4
+m4/wtmpdb.m4
 
 Depends-on:
 extensions
@@ -19,6 +20,7 @@ stdint-h
 memeq
 streq
 strnlen
+str_endswith
 time-h
 unlocked-io-internal
 xalloc
0001-maint-Document-with-wtmpdb-option.patch (text/x-patch, 1.7 KB)
From 90b2857cd6bf9ec273d06f157f0eb60e769d0dc9 Mon Sep 17 00:00:00 2001
From: Bruno Haible <[email protected]>
Date: Tue, 4 Aug 2026 17:17:27 +0200
Subject: [PATCH] maint: Document --with-wtmpdb option

* DEPENDENCIES: Mention wtmpdb.
* NEWS: Mention the effects of --with-wtmpdb.
---
 DEPENDENCIES | 12 ++++++++++++
 NEWS         |  4 ++++
 2 files changed, 16 insertions(+)

diff --git a/DEPENDENCIES b/DEPENDENCIES
index b501ffe4a..4dfd6c0c8 100644
--- a/DEPENDENCIES
+++ b/DEPENDENCIES
@@ -15,6 +15,18 @@
     - On Red Hat distributions: systemd-devel.
     - Other: https://repology.org/project/systemd/versions
 
+* wtmpdb libraries
+  + Recommended if wtmpdb is present on the system.
+    Needed for fetching login, logout, and boot times from
+    /var/log/wtmp.db instead of from the file /var/run/wtmp.
+    Related 'configure' option: --with-wtmpdb.
+  + Homepage:
+    https://github.com/thkukuk/wtmpdb
+  + Pre-built package name:
+     - On Debian and Debian-based systems: libwtmpdb-dev,
+     - On Red Hat distributions: --.
+     - Other: https://repology.org/project/wtmpdb/versions
+
 * libselinux
   + Mandatory on Linux systems that support SELinux.
     Needed so that the file utilities respect the security contexts,
diff --git a/NEWS b/NEWS
index ae34bab0d..002722f1f 100644
--- a/NEWS
+++ b/NEWS
@@ -82,6 +82,10 @@ GNU coreutils NEWS                                    -*- outline -*-
 
 ** Improvements
 
+  When built with the configure option '--with-wtmpdb', invocations of
+  'who /var/log/wtmp' and 'users /var/log/wtmp' use the wtmpdb database
+  instead of the file /var/log/wtmp.  This makes them Y2038-safe.
+
   'df', 'du', 'ls', 'od', 'pr', and 'sort' now escape invalid arguments in error
   messages for options expecting an integer.
 
-- 
2.53.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.