]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
rlimit-util: add pid_getrlimit() helper
authorLennart Poettering <lennart@poettering.net>
Fri, 8 Dec 2023 09:25:57 +0000 (10:25 +0100)
committerLuca Boccassi <luca.boccassi@gmail.com>
Thu, 14 Dec 2023 08:31:29 +0000 (08:31 +0000)
This is gets the resource limits off a specified process, and is very
similar to prlimit() with a NULL new_rlimit argument. In fact, it tries
that first. However, it then falls back to use /proc/$PID/limits. Why?
Simply because Linux prohibits access to prlimit() for processes with a
different UID, but /proc/$PID/limits still works.

This is preparation to allow nspawn to run unprivileged.

src/basic/rlimit-util.c
src/basic/rlimit-util.h
src/nspawn/nspawn.c
src/test/test-rlimit-util.c

index c1f0b2b974c7f0d9f4069c7d6682a5d5cf6bab38..a9f7b87f2899617318d816d95a1c920e45b0efac 100644 (file)
@@ -6,11 +6,14 @@
 #include "errno-util.h"
 #include "extract-word.h"
 #include "fd-util.h"
+#include "fileio.h"
 #include "format-util.h"
 #include "macro.h"
 #include "missing_resource.h"
+#include "process-util.h"
 #include "rlimit-util.h"
 #include "string-table.h"
+#include "strv.h"
 #include "time-util.h"
 
 int setrlimit_closest(int resource, const struct rlimit *rlim) {
@@ -426,3 +429,116 @@ int rlimit_nofile_safe(void) {
 
         return 1;
 }
+
+int pid_getrlimit(pid_t pid, int resource, struct rlimit *ret) {
+
+        static const char * const prefix_table[_RLIMIT_MAX] = {
+                [RLIMIT_CPU]        = "Max cpu time",
+                [RLIMIT_FSIZE]      = "Max file size",
+                [RLIMIT_DATA]       = "Max data size",
+                [RLIMIT_STACK]      = "Max stack size",
+                [RLIMIT_CORE]       = "Max core file size",
+                [RLIMIT_RSS]        = "Max resident set",
+                [RLIMIT_NPROC]      = "Max processes",
+                [RLIMIT_NOFILE]     = "Max open files",
+                [RLIMIT_MEMLOCK]    = "Max locked memory",
+                [RLIMIT_AS]         = "Max address space",
+                [RLIMIT_LOCKS]      = "Max file locks",
+                [RLIMIT_SIGPENDING] = "Max pending signals",
+                [RLIMIT_MSGQUEUE]   = "Max msgqueue size",
+                [RLIMIT_NICE]       = "Max nice priority",
+                [RLIMIT_RTPRIO]     = "Max realtime priority",
+                [RLIMIT_RTTIME]     = "Max realtime timeout",
+        };
+
+        int r;
+
+        assert(resource >= 0);
+        assert(resource < _RLIMIT_MAX);
+        assert(pid >= 0);
+        assert(ret);
+
+        if (pid == 0 || pid == getpid_cached())
+                return RET_NERRNO(getrlimit(resource, ret));
+
+        r = RET_NERRNO(prlimit(pid, resource, /* new_limit= */ NULL, ret));
+        if (!ERRNO_IS_NEG_PRIVILEGE(r))
+                return r;
+
+        /* We don't have access? Then try to go via /proc/$PID/limits. Weirdly that's world readable in
+         * contrast to querying the data via prlimit() */
+
+        const char *p = procfs_file_alloca(pid, "limits");
+        _cleanup_free_ char *limits = NULL;
+
+        r = read_full_virtual_file(p, &limits, NULL);
+        if (r < 0)
+                return -EPERM; /* propagate original permission error if we can't access the limits file */
+
+        _cleanup_strv_free_ char **l = NULL;
+        l = strv_split(limits, "\n");
+        if (!l)
+                return -ENOMEM;
+
+        STRV_FOREACH(i, strv_skip(l, 1)) {
+                _cleanup_free_ char *soft = NULL, *hard = NULL;
+                uint64_t sv, hv;
+                const char *e;
+
+                e = startswith(*i, prefix_table[resource]);
+                if (!e)
+                        continue;
+
+                if (*e != ' ')
+                        continue;
+
+                e += strspn(e, WHITESPACE);
+
+                size_t n;
+                n = strcspn(e, WHITESPACE);
+                if (n == 0)
+                        continue;
+
+                soft = strndup(e, n);
+                if (!soft)
+                        return -ENOMEM;
+
+                e += n;
+                if (*e != ' ')
+                        continue;
+
+                e += strspn(e, WHITESPACE);
+                n = strcspn(e, WHITESPACE);
+                if (n == 0)
+                        continue;
+
+                hard = strndup(e, n);
+                if (!hard)
+                        return -ENOMEM;
+
+                if (streq(soft, "unlimited"))
+                        sv = RLIM_INFINITY;
+                else {
+                        r = safe_atou64(soft, &sv);
+                        if (r < 0)
+                                return r;
+                }
+
+                if (streq(hard, "unlimited"))
+                        hv = RLIM_INFINITY;
+                else {
+                        r = safe_atou64(hard, &hv);
+                        if (r < 0)
+                                return r;
+                }
+
+                *ret = (struct rlimit) {
+                        .rlim_cur = sv,
+                        .rlim_max = hv,
+                };
+
+                return 0;
+        }
+
+        return -ENOTRECOVERABLE;
+}
index 202c3fdc449b82da150b7357f02e4523e9bad370..afc1a1f26223988d1f108d389bb99806b035f148 100644 (file)
@@ -25,3 +25,5 @@ void rlimit_free_all(struct rlimit **rl);
 
 int rlimit_nofile_bump(int limit);
 int rlimit_nofile_safe(void);
+
+int pid_getrlimit(pid_t pid, int resource, struct rlimit *ret);
index 287b6c18feff340b6a146ba9a2106645410e4f82..1f79d75eef24233a038b351155af08af62430d31 100644 (file)
@@ -5286,7 +5286,7 @@ static int initialize_rlimits(void) {
                  * don't read the other limits from PID 1 but prefer the static table above. */
         };
 
-        int rl;
+        int rl, r;
 
         for (rl = 0; rl < _RLIMIT_MAX; rl++) {
                 /* Let's only fill in what the user hasn't explicitly configured anyway */
@@ -5297,8 +5297,9 @@ static int initialize_rlimits(void) {
                         if (IN_SET(rl, RLIMIT_NPROC, RLIMIT_SIGPENDING)) {
                                 /* For these two let's read the limits off PID 1. See above for an explanation. */
 
-                                if (prlimit(1, rl, NULL, &buffer) < 0)
-                                        return log_error_errno(errno, "Failed to read resource limit RLIMIT_%s of PID 1: %m", rlimit_to_string(rl));
+                                r = pid_getrlimit(1, rl, &buffer);
+                                if (r < 0)
+                                        return log_error_errno(r, "Failed to read resource limit RLIMIT_%s of PID 1: %m", rlimit_to_string(rl));
 
                                 v = &buffer;
                         } else if (rl == RLIMIT_NOFILE) {
index 86d0c045555224d3feecaee4f4f8822551fda605..e14f27df7935c75312274972bfd118b679fdd901 100644 (file)
@@ -1,15 +1,20 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
 #include <sys/resource.h>
+#if HAVE_VALGRIND_VALGRIND_H
+#include <valgrind/valgrind.h>
+#endif
 
 #include "alloc-util.h"
 #include "capability-util.h"
 #include "macro.h"
 #include "missing_resource.h"
+#include "process-util.h"
 #include "rlimit-util.h"
 #include "string-util.h"
 #include "tests.h"
 #include "time-util.h"
+#include "user-util.h"
 
 static void test_rlimit_parse_format_one(int resource, const char *string, rlim_t soft, rlim_t hard, int ret, const char *formatted) {
         _cleanup_free_ char *f = NULL;
@@ -136,4 +141,45 @@ TEST(setrlimit) {
         assert_se(old.rlim_max == new.rlim_max);
 }
 
+TEST(pid_getrlimit) {
+        int r;
+
+        /* We fork off a child and read the parent's resource limit from there (i.e. our own), and compare
+         * with what getrlimit() gives us */
+
+        for (int resource = 0; resource < _RLIMIT_MAX; resource++) {
+                struct rlimit direct;
+
+                assert_se(getrlimit(resource, &direct) >= 0);
+
+                /* We fork off a child so that getrlimit() doesn't work anymore */
+                r = safe_fork("(getrlimit)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL|FORK_LOG|FORK_WAIT, /* ret_pid= */ NULL);
+                assert_se(r >= 0);
+
+                if (r == 0) {
+                        struct rlimit indirect;
+                        /* child */
+
+                        /* Drop privs, so that prlimit() doesn't work anymore */
+                        (void) setresgid(GID_NOBODY, GID_NOBODY, GID_NOBODY);
+                        (void) setresuid(UID_NOBODY, UID_NOBODY, UID_NOBODY);
+
+                        assert_se(pid_getrlimit(getppid(), resource, &indirect) >= 0);
+
+#if HAVE_VALGRIND_VALGRIND_H
+                        /* Valgrind fakes some changes in RLIMIT_NOFILE getrlimit() returns, work around that */
+                        if (RUNNING_ON_VALGRIND && resource == RLIMIT_NOFILE) {
+                                log_info("Skipping pid_getrlimit() check for RLIMIT_NOFILE, running in valgrind");
+                                _exit(EXIT_SUCCESS);
+                        }
+#endif
+
+                        assert_se(direct.rlim_cur == indirect.rlim_cur);
+                        assert_se(direct.rlim_max == indirect.rlim_max);
+
+                        _exit(EXIT_SUCCESS);
+                }
+        }
+}
+
 DEFINE_TEST_MAIN(LOG_INFO);