Package: libpam-modules
Version: 1.4.0-10
Severity: important
Control: block 917374 by -1
Control: block 976373 by -1
X-Debbugs-Cc: util-li...@packages.debian.org

The tl;dr version: pam_limits.so in Debian always behaves as though
the set_all option had been given, preventing limits set in entry-point
services (such as sshd, gdm, getty/login, etc.) from taking effect. This
is not the same as upstream, and contrary to the documentation.

I'm reporting this as a separate bug rather than as a follow-up to #917374
or #976373, because those are about specific symptoms and are affected by
other factors, whereas this one can be described in terms of just PAM.

Steps to reproduce
==================

I used a qemu virtual machine, which is probably the most convenient
way to set this up.

Do not have any special settings in /etc/security/limits.conf or
/etc/security/limits.d/* (no non-blank, non-comment lines are present).

Have two login entry-point services that use pam_limits.so. I used sshd
and login (i.e. text-mode login on the virtual console).

Configure an easily recognisable rlimit for each of those two entry-point
services using systemd.resource-control(5). I used RLIMIT_MEMLOCK as my
example:

$ cat /etc/systemd/system/ssh.service.d/override.conf
[Service]
LimitMEMLOCK=123456:234567
$ cat /etc/systemd/system/getty@.service.d/override.conf
[Service]
LimitMEMLOCK=654321:765432
$ sudo systemctl daemon-reload

Edit the PAM stack for *one* of these services to use "pam_limits.so set_all".
Leave the other one with plain "pam_limits.so".

$ grep pam_limits.so /etc/pam.d/login
session    required   pam_limits.so set_all
$ grep pam_limits.so /etc/pam.d/sshd
session    required     pam_limits.so

Restart the system to be 100% sure that all settings have taken effect.

Log in using both the entry-points that use pam_limits.so.

Inspect the limits with `cat /proc/$$/limits` in each resulting shell.

Expected result
===============

For the entry point that uses the set_all keyword (login), I expected my
RLIMIT_MEMLOCK to have been reset to some default value as documented
in the pam_limits(8) man page. Exactly *what* value is up for debate,
but that is out-of-scope for this bug; for the purposes of this bug, the
important thing is that my limit is something other than 654321 bytes.

For the entry point that does not use the set_all keyword (sshd), I
expected my RLIMIT_MEMLOCK to be inherited from the service, as documented
in the pam_limits(8) man page. With my configuration, that would be:

host$ ssh test-vm 'grep locked /proc/$$/limits'
Max locked memory         123456               234567               bytes

Actual result
=============

For both the login/getty session with set_all, and the sshd session
without set_all, I do not have the limit inherited from the service.
Instead, I have:

$ grep locked /proc/$$/limits
Max locked memory         515130880            515130880            bytes

For login/getty, this is as expected, because I asked for this behaviour
with set_all.

For sshd, this is not the expected behaviour.

(This happens to be 12.5% of the RAM of my 4 GiB test VM, due to #976373.)

History
=======

This appears to have been caused by a patch submitted in 2000, originally to
fix #63230 (d/patches-applied/027_pam_limits_better_init_allow_explicit_root).
Unfortunately, this combines several related changes into a single patch,
so inspecting its history isn't straightforward.

In #63230, the bug report was that when using "su -" to transition from
an ordinary user account to a root shell, the ordinary user's rlimits are
inherited by the root shell. The reporter correctly noted that this could
cause problems for the root shell session: in particular, on systems that
use sysvinit, it is conventional to run "/etc/init.d/some-service" from
a random root shell, which would result in some-service inheriting its
rlimits from the ordinary user account that ran "su -", and could result
in unpredictable failures that are hard to debug. The implementation
that they proposed was to raise all limits to RLIM_INFINITY, except for
RLIMIT_CORE, RLIMIT_STACK, RLIMIT_NPROC and RLIMIT_NOFILE, which were
reset to hard-coded values. In modern PAM terms, this is more like the
behaviour of set_all (which had not yet been invented at that time)
than like the default behaviour of pam_limits.so.

More than a decade later, in 2011 (PAM 1.1.4), upstream PAM added
the set_all option. When rebasing the patch onto this new upstream
release in 2014, I think a better implementation would have been to
change privilege-altering PAM users such as su to invoke "pam_limits.so
set_all", then make the module respect the presence or absence of the
set_all option as it does upstream. Instead, the patch was altered
so that it ignores the presence or absence of the set_all option, and
effectively always behaves as though it had been specified.

Impact
======

Resource limits are reset to "defaults" at all entry points (sshd, gdm,
login/getty, IMAP servers, ftp servers, etc.), not just when switching
user context with something like su.

This means that configuring resource limits on a per-entry-point basis
is ineffective: for example, sysadmins cannot give a higher rlimit to
physically-present local users than to ssh users by configuring it in
drop-ins for a display manager such as gdm.service.

It also means that if there is a separate bug where the "default" resource
limits are not determined in the most ideal way, then the impact of
that bug on Debian systems is amplified. In particular this applies to
#917374 and #976373.

Transitional considerations
===========================

To avoid reintroducing #63230, if that is not a desired outcome, it will
be necessary to change /etc/pam.d/su (in the util-linux package) so that
it invokes "pam_limits.so set_all" instead of plain "pam_limits.so". sudo
does not seem to invoke pam_limits.so at all (at the moment), but if it
did, I suspect we would want it to use "pam_limits.so set_all" too. Other
privilege-changing tools like pkexec and calife might also want to use
"pam_limits.so set_all".

Possible implementation
=======================

The attached modification of
027_pam_limits_better_init_allow_explicit_root makes it respect set_all
as documented, and is a possible implementation of a solution. I have not
updated the DEP-3 header, which would be necessary before committing this.

I'm not marking this bug as +patch, because action is needed in other
packages, notably util-linux, before taking this beyond a prototype.

With this patch, I get the expected behaviour:

host$ ssh test-vm 'grep locked /proc/$$/limits'
Max locked memory         123456               234567               bytes

System information
==================

System information below is taken from my test VM. Please excuse the
lack of locales.

Thanks,
    smcv

-- System Information:
Debian Release: bookworm/sid
  APT prefers unstable
  APT policy: (500, 'unstable')
Architecture: amd64 (x86_64)

Kernel: Linux 5.14.0-1-amd64 (SMP w/2 CPU threads)
Locale: LANG=en_GB.utf8, LC_CTYPE=C.UTF-8 (charmap=locale: Cannot set 
LC_MESSAGES to default locale: No such file or directory
locale: Cannot set LC_ALL to default locale: No such file or directory
UTF-8), LANGUAGE not set
Shell: /bin/sh linked to /bin/dash
Init: systemd (via /run/systemd/system)
LSM: AppArmor: enabled

Versions of packages libpam-modules depends on:
ii  debconf [debconf-2.0]  1.5.77
ii  libaudit1              1:3.0.5-1
ii  libc6                  2.32-4
ii  libcrypt1              1:4.4.25-2
ii  libdb5.3               5.3.28+dfsg1-0.8
ii  libnsl2                1.3.0-2
ii  libpam-modules-bin     1.4.0-10
ii  libpam0g               1.4.0-10
ii  libselinux1            3.1-3
ii  libtirpc3              1.3.2-2

libpam-modules recommends no packages.

libpam-modules suggests no packages.

-- debconf information:
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
        LANGUAGE = (unset),
        LC_ALL = (unset),
        LC_CTYPE = "C.UTF-8",
        LANG = "en_GB.utf8"
    are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
locale: Cannot set LC_MESSAGES to default locale: No such file or directory
locale: Cannot set LC_ALL to default locale: No such file or directory
  libpam-modules/deprecate-tally:
  libpam-modules/disable-screensaver:
  libpam-modules/profiles-disabled:
Description: Allow explicit limits for root and reset limits on each session
 When crossing session boundaries (such as when su'ing from one user to
 another), if the target account has no limit specified in limits.conf we
 want to use the default, not the current value configured for the
 source account.
 .
 If /proc/1/limits is unavailable, fall back to a set of hard-coded values
 that shadow the currently known defaults on Linux.
 .
 Also, don't apply wildcard limits to the root account; only apply limits to
 root that reference root by name.
Author: Peter Paluch <pet...@frcatel.fri.utc.sk>,
 Ben Collins <bcoll...@debian.org>,
 Steve Langasek <vor...@debian.org>,
Bug-Debian: http://bugs.debian.org/63230
--- a/modules/pam_limits/pam_limits.c
+++ b/modules/pam_limits/pam_limits.c
@@ -46,6 +46,14 @@
 #include <libaudit.h>
 #endif
 
+#ifndef MLOCK_LIMIT
+#ifdef __FreeBSD_kernel__
+#define MLOCK_LIMIT RLIM_INFINITY
+#else
+#define MLOCK_LIMIT (64*1024)
+#endif
+#endif
+
 /* Module defines */
 #define LINE_LENGTH 1024
 
@@ -83,6 +91,7 @@ struct user_limits_struct {
 
 /* internal data */
 struct pam_limit_s {
+    int root;            /* running as root? */
     int login_limit;     /* the max logins limit */
     int login_limit_def; /* which entry set the login limit */
     int flag_numsyslogins; /* whether to limit logins only for a
@@ -445,9 +454,18 @@ static int init_limits(pam_handle_t *pam
 {
     int i;
     int retval = PAM_SUCCESS;
+    static int mlock_limit = 0;
 
     D(("called."));
 
+    pl->root = 0;
+
+    if (mlock_limit == 0) {
+       mlock_limit = sysconf(_SC_PAGESIZE);
+       if (mlock_limit < MLOCK_LIMIT)
+               mlock_limit = MLOCK_LIMIT;
+    }
+
     for(i = 0; i < RLIM_NLIMITS; i++) {
        int r = getrlimit(i, &pl->limits[i].limit);
        if (r == -1) {
@@ -465,16 +483,68 @@ static int init_limits(pam_handle_t *pam
 #ifdef __linux__
     if (ctrl & PAM_SET_ALL) {
       parse_kernel_limits(pamh, pl, ctrl);
+#endif
 
       for(i = 0; i < RLIM_NLIMITS; i++) {
        if (pl->limits[i].supported &&
            (pl->limits[i].src_soft == LIMITS_DEF_NONE ||
             pl->limits[i].src_hard == LIMITS_DEF_NONE)) {
-         pam_syslog(pamh, LOG_WARNING, "Did not find kernel RLIMIT for %s, 
using PAM default", rlimit2str(i));
+#ifdef __linux__
+           pam_syslog(pamh, LOG_WARNING, "Did not find kernel RLIMIT for %s, 
using PAM default", rlimit2str(i));
+#endif
+           pl->limits[i].src_soft = LIMITS_DEF_DEFAULT;
+           pl->limits[i].src_hard = LIMITS_DEF_DEFAULT;
+           switch(i) {
+               case RLIMIT_CPU:
+               case RLIMIT_FSIZE:
+               case RLIMIT_DATA:
+               case RLIMIT_RSS:
+               case RLIMIT_NPROC:
+#ifdef RLIMIT_AS
+               case RLIMIT_AS:
+#endif
+#ifdef RLIMIT_LOCKS
+               case RLIMIT_LOCKS:
+#endif
+                   pl->limits[i].limit.rlim_cur = RLIM_INFINITY;
+                   pl->limits[i].limit.rlim_max = RLIM_INFINITY;
+                   break;
+               case RLIMIT_MEMLOCK:
+                   pl->limits[i].limit.rlim_cur = mlock_limit;
+                   pl->limits[i].limit.rlim_max = mlock_limit;
+                   break;
+#ifdef RLIMIT_SIGPENDING
+               case RLIMIT_SIGPENDING:
+                   pl->limits[i].limit.rlim_cur = 16382;
+                   pl->limits[i].limit.rlim_max = 16382;
+                   break;
+#endif
+#ifdef RLIMIT_MSGQUEUE
+               case RLIMIT_MSGQUEUE:
+                   pl->limits[i].limit.rlim_cur = 819200;
+                   pl->limits[i].limit.rlim_max = 819200;
+                   break;
+#endif
+               case RLIMIT_CORE:
+                   pl->limits[i].limit.rlim_cur = 0;
+                   pl->limits[i].limit.rlim_max = RLIM_INFINITY;
+                   break;
+               case RLIMIT_STACK:
+                   pl->limits[i].limit.rlim_cur = 8192*1024;
+                   pl->limits[i].limit.rlim_max = RLIM_INFINITY;
+                   break;
+               case RLIMIT_NOFILE:
+                   pl->limits[i].limit.rlim_cur = 1024;
+                   pl->limits[i].limit.rlim_max = 1024;
+                   break;
+               default:
+                   pl->limits[i].src_soft = LIMITS_DEF_NONE;
+                   pl->limits[i].src_hard = LIMITS_DEF_NONE;
+                   break;
+           }
        }
       }
     }
-#endif
 
     errno = 0;
     pl->priority = getpriority (PRIO_PROCESS, 0);
@@ -813,7 +883,7 @@ parse_config_file(pam_handle_t *pamh, co
 
             if (strcmp(uname, domain) == 0) /* this user have a limit */
                 process_limit(pamh, LIMITS_DEF_USER, ltype, item, value, ctrl, 
pl);
-            else if (domain[0]=='@') {
+            else if (domain[0]=='@' && !pl->root) {
                if (ctrl & PAM_DEBUG_ARG) {
                        pam_syslog(pamh, LOG_DEBUG,
                                   "checking if %s is in group %s",
@@ -839,7 +909,7 @@ parse_config_file(pam_handle_t *pamh, co
                            process_limit(pamh, LIMITS_DEF_GROUP, ltype, item, 
value, ctrl,
                                          pl);
                }
-            } else if (domain[0]=='%') {
+            } else if (domain[0]=='%' && !pl->root) {
                if (ctrl & PAM_DEBUG_ARG) {
                        pam_syslog(pamh, LOG_DEBUG,
                                   "checking if %s is in group %s",
@@ -873,7 +943,7 @@ parse_config_file(pam_handle_t *pamh, co
             } else {
                switch(rngtype) {
                    case LIMIT_RANGE_NONE:
-                       if (strcmp(domain, "*") == 0)
+                       if (strcmp(domain, "*") == 0 && !pl->root)
                            process_limit(pamh, LIMITS_DEF_DEFAULT, ltype, 
item, value, ctrl,
                                          pl);
                        break;
@@ -1059,6 +1129,8 @@ pam_sm_open_session (pam_handle_t *pamh,
         return PAM_ABORT;
     }
 
+    if (pwd->pw_uid == 0)
+        pl->root = 1;
     retval = parse_config_file(pamh, pwd->pw_name, pwd->pw_uid, pwd->pw_gid, 
ctrl, pl);
     if (retval == PAM_IGNORE) {
        D(("the configuration file ('%s') has an applicable '<domain> -' 
entry", CONF_FILE));
--- a/modules/pam_limits/limits.conf
+++ b/modules/pam_limits/limits.conf
@@ -11,6 +11,9 @@
 #        - the wildcard *, for default entry
 #        - the wildcard %, can be also used with %group syntax,
 #                 for maxlogin limit
+#        - NOTE: group and wildcard limits are not applied to root.
+#          To apply a limit to the root user, <domain> must be
+#          the literal username root.
 #
 #<type> can have the two values:
 #        - "soft" for enforcing the soft limits
@@ -41,6 +44,7 @@
 #
 
 #*               soft    core            0
+#root            hard    core            100000
 #*               hard    rss             10000
 #@student        hard    nproc           20
 #@faculty        soft    nproc           20
--- a/modules/pam_limits/limits.conf.5.xml
+++ b/modules/pam_limits/limits.conf.5.xml
@@ -96,6 +96,11 @@
               </para>
             </listitem>
           </itemizedlist>
+         <para>
+           <emphasis remap='B'>NOTE:</emphasis> group and wildcard limits are 
not
+           applied to the root user.  To set a limit for the root user, this 
field
+           must contain the literal username <emphasis 
remap='B'>root</emphasis>.
+         </para>
         </listitem>
       </varlistentry>
 
@@ -323,6 +328,7 @@
     </para>
     <programlisting>
 *               soft    core            0
+root            hard    core            100000
 *               hard    nofile          512
 @student        hard    nproc           20
 @faculty        soft    nproc           20
--- a/modules/pam_limits/limits.conf.5
+++ b/modules/pam_limits/limits.conf.5
@@ -145,6 +145,10 @@ a gid specified as
 \fB%:\fR\fI<gid>\fR
 applicable to maxlogins limit only\&. It limits the total number of logins of 
all users that are member of the group with the specified gid\&.
 .RE
+.sp
+\fBNOTE:\fR
+group and wildcard limits are not applied to the root user\&. To set a limit 
for the root user, this field must contain the literal username
+\fBroot\fR\&.
 .RE
 .PP
 \fB<type>\fR
@@ -320,6 +324,7 @@ These are some example lines which might
 .\}
 .nf
 *               soft    core            0
+root            hard    core            100000
 *               hard    nofile          512
 @student        hard    nproc           20
 @faculty        soft    nproc           20
--- a/modules/pam_limits/README
+++ b/modules/pam_limits/README
@@ -56,6 +56,7 @@ These are some example lines which might
 limits.conf.
 
 *               soft    core            0
+root            hard    core            100000
 *               hard    nofile          512
 @student        hard    nproc           20
 @faculty        soft    nproc           20

Reply via email to