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