Index: pam/configure.ac
===================================================================
--- pam.orig/configure.ac
+++ pam/configure.ac
@@ -334,6 +334,9 @@
 esac
 AC_SUBST(LIBDL)
 
+AC_CHECK_LIB(m, log2, LIBM=-lm, LIBM="")
+AC_SUBST(LIBM)
+
 dnl Look for Linux Auditing library - see documentation
 AC_ARG_ENABLE([audit],
         AS_HELP_STRING([--disable-audit],[do not enable audit support]),
Index: pam/modules/pam_time/Makefile.am
===================================================================
--- pam.orig/modules/pam_time/Makefile.am
+++ pam/modules/pam_time/Makefile.am
@@ -23,7 +23,7 @@
 if HAVE_VERSIONING
   AM_LDFLAGS += -Wl,--version-script=$(srcdir)/../modules.map
 endif
-pam_time_la_LIBADD = $(top_builddir)/libpam/libpam.la
+pam_time_la_LIBADD = $(top_builddir)/libpam/libpam.la @LIBM@
 
 securelib_LTLIBRARIES = pam_time.la
 dist_secureconf_DATA = time.conf
@@ -32,3 +32,7 @@
 dist_noinst_DATA = README
 -include $(top_srcdir)/Make.xml.rules
 endif
+
+noinst_HEADERS = time-util.h
+
+pam_time_la_SOURCES = pam_time.c time-util.c
Index: pam/modules/pam_time/pam_time.c
===================================================================
--- pam.orig/modules/pam_time/pam_time.c
+++ pam/modules/pam_time/pam_time.c
@@ -21,7 +21,9 @@
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <fcntl.h>
+#include <math.h>
 #include <netdb.h>
+#include <sys/param.h>
 
 #include <security/_pam_macros.h>
 #include <security/pam_modules.h>
@@ -33,6 +35,8 @@
 #include <libaudit.h>
 #endif
 
+#include "time-util.h"
+
 #define PAM_TIME_BUFLEN        1000
 #define FIELD_SEPARATOR        ';'   /* this is new as of .02 */
 
@@ -48,6 +52,13 @@
 
 typedef enum { AND, OR } operator;
 
+static void cleanup(pam_handle_t *handle UNUSED, void *data, int err UNUSED)
+{
+     if (!data)
+	  return;
+     free(data);
+}
+
 static int
 _pam_parse (const pam_handle_t *pamh, int argc, const char **argv, const char **conffile)
 {
@@ -286,8 +297,9 @@
 
 static int
 logic_field(pam_handle_t *pamh, const void *me, const char *x, int rule,
+	    void *data,
 	    int (*agrees)(pam_handle_t *pamh,
-			      const void *, const char *, int, int))
+			      const void *, const char *, int, int, void *))
 {
      int left=FALSE, right, not=FALSE;
      operator oper=OR;
@@ -302,7 +314,7 @@
 		    not = !not;
 	       else if (isalpha(c) || c == '*' || isdigit(c) || c == '_'
                     || c == '-' || c == '.' || c == '/' || c == ':') {
-		    right = not ^ agrees(pamh, me, x+at, l, rule);
+		    right = not ^ agrees(pamh, me, x+at, l, rule, data);
 		    if (oper == AND)
 			 left &= right;
 		    else
@@ -340,7 +352,7 @@
 
 static int
 is_same(pam_handle_t *pamh UNUSED, const void *A, const char *b,
-	int len, int rule UNUSED)
+	int len, int rule UNUSED, void *data UNUSED)
 {
      int i;
      const char *a;
@@ -405,12 +417,19 @@
 /* take the current date and see if the range "date" passes it */
 static int
 check_time(pam_handle_t *pamh, const void *AT, const char *times,
-	   int len, int rule)
+	   int len, int rule, void *data)
 {
      int not,pass;
      int marked_day, time_start, time_end;
      const TIME *at;
      int i,j=0;
+     struct tm tm;
+     time_t current_time;
+     time_t *time_limit = data;
+
+     current_time = time(NULL);
+     // ignore failures, shouldn't really be possible
+     localtime_r(&current_time, &tm);
 
      at = AT;
      D(("chcking: 0%o/%.4d vs. %s", at->day, at->minute, times));
@@ -503,6 +522,59 @@
 	  }
      }
 
+     if (pass && !not) {
+	  int numdays = 0;
+	  if (time_start == 0 && time_end == 2400) {
+	       // special case where access is allowed for at least one full
+	       // day, so we have to examine the days until we find one that's
+	       // not allowed (or until we loop around)
+	       for (i = at->day; ; ) {
+		    i <<= 1;
+		    if (i > 0100)
+			 i = 1;
+		    if (! (i & marked_day))
+			 break;
+		    if (i == at->day)
+			 break;
+	       }
+
+	       // access allowed all day, every day...
+	       if (i == at->day)
+		    return (not ^ pass);
+
+	       if (i < at->day)
+		   i <<= 7;
+	       numdays = lrint(log2(i))-lrint((at->day));
+	  } else if (time_end < time_start)
+	       numdays = 1;
+
+	  // if the end time is not today, walk the days 1 at a time to
+	  // increment.  This method avoids problems with both DST and
+	  // end of month / end of year boundaries.
+	  while (numdays > 0) {
+	       tm.tm_hour = 23;
+	       tm.tm_min  = 59;
+	       *time_limit = mktime(&tm);
+	       *time_limit += 60*60; // 00:59 the next day
+	       localtime_r(time_limit, &tm);
+	       numdays--;
+	  }
+
+	  tm.tm_hour = time_end / 100;
+	  tm.tm_min = time_end % 100;
+	  tm.tm_sec = 0;
+	  // tm_hour = 24 is not legal, so get the timestamp for 23:59
+	  // and add 60 seconds
+	  if (time_end == 2400)
+	  {
+	       tm.tm_hour -= 1;
+	       tm.tm_min -= 1;
+	  }
+	  *time_limit = mktime(&tm);
+	  if (time_end == 2400)
+	       *time_limit += 60;
+     }
+
      return (not ^ pass);
 }
 
@@ -515,6 +587,7 @@
      int count=0;
      TIME here_and_now;
      int retval=PAM_SUCCESS;
+     time_t end_time = 0, time_buf = 0;
 
      here_and_now = time_now();                     /* find current time */
      do {
@@ -535,7 +608,7 @@
 	       continue;
 	  }
 
-	  good = logic_field(pamh, service, buffer, count, is_same);
+	  good = logic_field(pamh, service, buffer, count, NULL, is_same);
 	  D(("with service: %s", good ? "passes":"fails" ));
 
 	  /* here we get the terminal name field */
@@ -546,7 +619,7 @@
 			  "%s: malformed rule #%d", file, count);
 	       continue;
 	  }
-	  good &= logic_field(pamh, tty, buffer, count, is_same);
+	  good &= logic_field(pamh, tty, buffer, count, NULL, is_same);
 	  D(("with tty: %s", good ? "passes":"fails" ));
 
 	  /* here we get the username field */
@@ -565,7 +638,7 @@
 	    pam_syslog (pamh, LOG_ERR, "pam_time does not have netgroup support");
 #endif
 	  else
-	    good &= logic_field(pamh, user, buffer, count, is_same);
+	    good &= logic_field(pamh, user, buffer, count, NULL, is_same);
 	  D(("with user: %s", good ? "passes":"fails" ));
 
 	  /* here we get the time field */
@@ -577,7 +650,8 @@
 	       continue;
 	  }
 
-	  intime = logic_field(pamh, &here_and_now, buffer, count, check_time);
+	  intime = logic_field(pamh, &here_and_now, buffer, count, &time_buf,
+	                       check_time);
 	  D(("with time: %s", intime ? "passes":"fails" ));
 
 	  if (good && !intime) {
@@ -587,10 +661,49 @@
 		*/
 	       retval = PAM_PERM_DENIED;
 	  } else {
+               if (good)
+		   end_time = time_buf;
 	       D(("rule passed"));
 	  }
      } while (state != STATE_EOF);
 
+     if (end_time != 0) {
+	  const char *current_limit = NULL;
+	  char *runtime_max_sec = NULL;
+	  time_t curtime = time(NULL);
+	  usec_t timeval = 0, old_timeval = 0;
+
+	  // unlikely, but could happen if we were already close to the limit
+	  if (end_time <= curtime)
+	       return PAM_PERM_DENIED;
+	  timeval = (end_time - curtime) * USEC_PER_SEC;
+
+	  // get current limit, if any
+	  pam_get_data(pamh, "systemd.runtime_max_sec",
+	               (const void **)&current_limit);
+	  if (current_limit) {
+	       retval = parse_time(current_limit, &old_timeval, USEC_PER_SEC);
+	       timeval = MIN(old_timeval, timeval);
+	  }
+	  if (timeval != old_timeval)
+	  {
+	       runtime_max_sec = malloc(FORMAT_TIMESPAN_MAX);
+	       if (!format_timespan(runtime_max_sec, FORMAT_TIMESPAN_MAX,
+		                    timeval, USEC_PER_SEC)) {
+		    free((void *)runtime_max_sec);
+		    return PAM_PERM_DENIED;
+	       }
+	       pam_syslog(pamh, LOG_DEBUG, "user %s session limited to %s",
+	                  user, runtime_max_sec);
+	       retval = pam_set_data(pamh, "systemd.runtime_max_sec",
+				     (void *)runtime_max_sec, cleanup);
+	       if (retval != PAM_SUCCESS) {
+		    free((void *)runtime_max_sec);
+		    return PAM_PERM_DENIED;
+	       }
+	  }
+     }
+
      return retval;
 }
 
Index: pam/modules/pam_time/time-util.c
===================================================================
--- /dev/null
+++ pam/modules/pam_time/time-util.c
@@ -0,0 +1,314 @@
+/*
+ *
+ * Copyright (c) 2012-2015 Lennart Poettering,
+ *               2014-2023 Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>,
+ *               2022-2023 Yu Watanabe
+ *
+ * pam_session_timelimit is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * pam_session_timelimit is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/param.h>
+
+#include "time-util.h"
+
+/* What is interpreted as whitespace? */
+#define WHITESPACE          " \t\n\r"
+#define DIGITS              "0123456789"
+
+#define strneq(a, b, n) (strncmp((a), (b), (n)) == 0)
+
+#define ELEMENTSOF(x)                                                   \
+        (__builtin_choose_expr(                                         \
+                !__builtin_types_compatible_p(typeof(x), typeof(&*(x))), \
+                sizeof(x)/sizeof((x)[0]),                               \
+                ((void)0)))
+
+
+static const char *startswith(const char *s, const char *prefix) {
+	size_t l;
+
+	assert(s);
+	assert(prefix);
+
+	l = strlen(prefix);
+	if (!strneq(s, prefix, l))
+		return NULL;
+
+	return s + l;
+}
+
+
+static const char* extract_multiplier(const char *p, usec_t *ret) {
+        static const struct {
+                const char *suffix;
+                usec_t usec;
+        } table[] = {
+                { "seconds", USEC_PER_SEC    },
+                { "second",  USEC_PER_SEC    },
+                { "sec",     USEC_PER_SEC    },
+                { "s",       USEC_PER_SEC    },
+                { "minutes", USEC_PER_MINUTE },
+                { "minute",  USEC_PER_MINUTE },
+                { "min",     USEC_PER_MINUTE },
+                { "months",  USEC_PER_MONTH  },
+                { "month",   USEC_PER_MONTH  },
+                { "M",       USEC_PER_MONTH  },
+                { "msec",    USEC_PER_MSEC   },
+                { "ms",      USEC_PER_MSEC   },
+                { "m",       USEC_PER_MINUTE },
+                { "hours",   USEC_PER_HOUR   },
+                { "hour",    USEC_PER_HOUR   },
+                { "hr",      USEC_PER_HOUR   },
+                { "h",       USEC_PER_HOUR   },
+                { "days",    USEC_PER_DAY    },
+                { "day",     USEC_PER_DAY    },
+                { "d",       USEC_PER_DAY    },
+                { "weeks",   USEC_PER_WEEK   },
+                { "week",    USEC_PER_WEEK   },
+                { "w",       USEC_PER_WEEK   },
+                { "years",   USEC_PER_YEAR   },
+                { "year",    USEC_PER_YEAR   },
+                { "y",       USEC_PER_YEAR   },
+                { "usec",    1ULL            },
+                { "us",      1ULL            },
+                { "µs",      1ULL            },
+        };
+
+        assert(p);
+        assert(ret);
+
+        for (size_t i = 0; i < ELEMENTSOF(table); i++) {
+                const char *e;
+
+                e = startswith(p, table[i].suffix);
+                if (e) {
+                        *ret = table[i].usec;
+                        return e;
+                }
+        }
+
+        return p;
+}
+
+
+int parse_time(const char *t, usec_t *usec, usec_t default_unit) {
+        const char *p, *s;
+        usec_t r = 0;
+        bool something = false;
+
+        assert(t);
+        assert(default_unit > 0);
+
+        p = t;
+
+        p += strspn(p, WHITESPACE);
+        s = startswith(p, "infinity");
+        if (s) {
+                s += strspn(s, WHITESPACE);
+                if (*s != 0)
+                        return -EINVAL;
+
+                if (usec)
+                        *usec = USEC_INFINITY;
+                return 0;
+        }
+
+        for (;;) {
+                usec_t multiplier = default_unit, k;
+                long long l;
+                char *e;
+
+                p += strspn(p, WHITESPACE);
+
+                if (*p == 0) {
+                        if (!something)
+                                return -EINVAL;
+
+                        break;
+                }
+
+                if (*p == '-') /* Don't allow "-0" */
+                        return -ERANGE;
+
+                errno = 0;
+                l = strtoll(p, &e, 10);
+                if (errno > 0)
+                        return -errno;
+                if (l < 0)
+                        return -ERANGE;
+
+                if (*e == '.') {
+                        p = e + 1;
+                        p += strspn(p, DIGITS);
+                } else if (e == p)
+                        return -EINVAL;
+                else
+                        p = e;
+
+                s = extract_multiplier(p + strspn(p, WHITESPACE), &multiplier);
+                if (s == p && *s != '\0')
+                        /* Don't allow '12.34.56', but accept '12.34 .56' or '12.34s.56' */
+                        return -EINVAL;
+
+                p = s;
+
+                if ((usec_t) l >= USEC_INFINITY / multiplier)
+                        return -ERANGE;
+
+                k = (usec_t) l * multiplier;
+                if (k >= USEC_INFINITY - r)
+                        return -ERANGE;
+
+                r += k;
+
+                something = true;
+
+                if (*e == '.') {
+                        usec_t m = multiplier / 10;
+                        const char *b;
+
+                        for (b = e + 1; *b >= '0' && *b <= '9'; b++, m /= 10) {
+                                k = (usec_t) (*b - '0') * m;
+                                if (k >= USEC_INFINITY - r)
+                                        return -ERANGE;
+
+                                r += k;
+                        }
+
+                        /* Don't allow "0.-0", "3.+1", "3. 1", "3.sec" or "3.hoge" */
+                        if (b == e + 1)
+                                return -EINVAL;
+                }
+        }
+
+        if (usec)
+                *usec = r;
+        return 0;
+}
+
+
+char* format_timespan(char *buf, size_t l, usec_t t, usec_t accuracy) {
+        static const struct {
+                const char *suffix;
+                usec_t usec;
+        } table[] = {
+                { "y",     USEC_PER_YEAR   },
+                { "month", USEC_PER_MONTH  },
+                { "w",     USEC_PER_WEEK   },
+                { "d",     USEC_PER_DAY    },
+                { "h",     USEC_PER_HOUR   },
+                { "min",   USEC_PER_MINUTE },
+                { "s",     USEC_PER_SEC    },
+                { "ms",    USEC_PER_MSEC   },
+                { "us",    1               },
+        };
+
+        char *p = buf;
+        bool something = false;
+
+	if (!p)
+		return NULL;
+
+        assert(l > 0);
+
+        if (t == USEC_INFINITY) {
+                strncpy(p, "infinity", l-1);
+                p[l-1] = 0;
+                return p;
+        }
+
+        if (t <= 0) {
+                strncpy(p, "0", l-1);
+                p[l-1] = 0;
+                return p;
+        }
+
+        /* The result of this function can be parsed with parse_sec */
+
+        for (size_t i = 0; i < ELEMENTSOF(table); i++) {
+                int k = 0;
+                size_t n;
+                bool done = false;
+                usec_t a, b;
+
+                if (t <= 0)
+                        break;
+
+                if (t < accuracy && something)
+                        break;
+
+                if (t < table[i].usec)
+                        continue;
+
+                if (l <= 1)
+                        break;
+
+                a = t / table[i].usec;
+                b = t % table[i].usec;
+
+                /* Let's see if we should shows this in dot notation */
+                if (t < USEC_PER_MINUTE && b > 0) {
+                        signed char j = 0;
+
+                        for (usec_t cc = table[i].usec; cc > 1; cc /= 10)
+                                j++;
+
+                        for (usec_t cc = accuracy; cc > 1; cc /= 10) {
+                                b /= 10;
+                                j--;
+                        }
+
+                        if (j > 0) {
+                                k = snprintf(p, l,
+                                             "%s"USEC_FMT".%0*"PRI_USEC"%s",
+                                             p > buf ? " " : "",
+                                             a,
+                                             j,
+                                             b,
+                                             table[i].suffix);
+
+                                t = 0;
+                                done = true;
+                        }
+                }
+
+                /* No? Then let's show it normally */
+                if (!done) {
+                        k = snprintf(p, l,
+                                     "%s"USEC_FMT"%s",
+                                     p > buf ? " " : "",
+                                     a,
+                                     table[i].suffix);
+
+                        t = b;
+                }
+
+                n = MIN((size_t) k, l-1);
+
+                l -= n;
+                p += n;
+
+                something = true;
+        }
+
+        *p = 0;
+
+        return buf;
+}
Index: pam/modules/pam_time/time-util.h
===================================================================
--- /dev/null
+++ pam/modules/pam_time/time-util.h
@@ -0,0 +1,24 @@
+#include <inttypes.h>
+
+typedef uint64_t usec_t;
+
+#define PRI_USEC PRIu64
+#define USEC_FMT "%" PRI_USEC
+
+#define USEC_INFINITY ((usec_t) UINT64_MAX)
+
+#define USEC_PER_SEC  ((usec_t) 1000000ULL)
+#define USEC_PER_MSEC ((usec_t) 1000ULL)
+
+#define USEC_PER_MINUTE ((usec_t) (60ULL*USEC_PER_SEC))
+#define USEC_PER_HOUR ((usec_t) (60ULL*USEC_PER_MINUTE))
+#define USEC_PER_DAY ((usec_t) (24ULL*USEC_PER_HOUR))
+#define USEC_PER_WEEK ((usec_t) (7ULL*USEC_PER_DAY))
+#define USEC_PER_MONTH ((usec_t) (2629800ULL*USEC_PER_SEC))
+#define USEC_PER_YEAR ((usec_t) (31557600ULL*USEC_PER_SEC))
+
+#define FORMAT_TIMESPAN_MAX 64U
+
+int parse_time(const char *t, usec_t *ret, usec_t default_unit);
+char* format_timespan(char *buf, size_t l, usec_t t, usec_t accuracy)
+	__attribute__((__warn_unused_result__));
