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

Reply via email to