b11b5f
From d178865d3d9940423f4d99360e3dc2fcaf0b2c96 Mon Sep 17 00:00:00 2001
b11b5f
From: =?UTF-8?q?Zbigniew=20J=C4=99drzejewski-Szmek?= <zbyszek@in.waw.pl>
b11b5f
Date: Mon, 28 Nov 2022 12:12:55 +0100
b11b5f
Subject: [PATCH] coredump: do not allow user to access coredumps with changed
b11b5f
 uid/gid/capabilities
b11b5f
b11b5f
When the user starts a program which elevates its permissions via setuid,
b11b5f
setgid, or capabilities set on the file, it may access additional information
b11b5f
which would then be visible in the coredump. We shouldn't make the the coredump
b11b5f
visible to the user in such cases.
b11b5f
b11b5f
Reported-by: Matthias Gerstner <mgerstner@suse.de>
b11b5f
b11b5f
This reads the /proc/<pid>/auxv file and attaches it to the process metadata as
b11b5f
PROC_AUXV. Before the coredump is submitted, it is parsed and if either
b11b5f
at_secure was set (which the kernel will do for processes that are setuid,
b11b5f
setgid, or setcap), or if the effective uid/gid don't match uid/gid, the file
b11b5f
is not made accessible to the user. If we can't access this data, we assume the
b11b5f
file should not be made accessible either. In principle we could also access
b11b5f
the auxv data from a note in the core file, but that is much more complex and
b11b5f
it seems better to use the stand-alone file that is provided by the kernel.
b11b5f
b11b5f
Attaching auxv is both convient for this patch (because this way it's passed
b11b5f
between the stages along with other fields), but I think it makes sense to save
b11b5f
it in general.
b11b5f
b11b5f
We use the information early in the core file to figure out if the program was
b11b5f
32-bit or 64-bit and its endianness. This way we don't need heuristics to guess
b11b5f
whether the format of the auxv structure. This test might reject some cases on
b11b5f
fringe architecutes. But the impact would be limited: we just won't grant the
b11b5f
user permissions to view the coredump file. If people report that we're missing
b11b5f
some cases, we can always enhance this to support more architectures.
b11b5f
b11b5f
I tested auxv parsing on amd64, 32-bit program on amd64, arm64, arm32, and
b11b5f
ppc64el, but not the whole coredump handling.
b11b5f
b11b5f
(cherry picked from commit 3e4d0f6cf99f8677edd6a237382a65bfe758de03)
b11b5f
b11b5f
Resolves: #2155520
b11b5f
---
b11b5f
 src/coredump/coredump.c | 190 ++++++++++++++++++++++++++++++++++++++--
b11b5f
 1 file changed, 182 insertions(+), 8 deletions(-)
b11b5f
b11b5f
diff --git a/src/coredump/coredump.c b/src/coredump/coredump.c
b11b5f
index ebc56d8342..d8acd2d3a7 100644
b11b5f
--- a/src/coredump/coredump.c
b11b5f
+++ b/src/coredump/coredump.c
b11b5f
@@ -4,6 +4,7 @@
b11b5f
 #include <stdio.h>
b11b5f
 #include <stdio_ext.h>
b11b5f
 #include <sys/prctl.h>
b11b5f
+#include <sys/auxv.h>
b11b5f
 #include <sys/xattr.h>
b11b5f
 #include <unistd.h>
b11b5f
 
b11b5f
@@ -88,11 +89,13 @@ enum {
b11b5f
         CONTEXT_COMM,
b11b5f
         CONTEXT_EXE,
b11b5f
         CONTEXT_UNIT,
b11b5f
+        CONTEXT_PROC_AUXV,
b11b5f
         _CONTEXT_MAX
b11b5f
 };
b11b5f
 
b11b5f
 typedef struct Context {
b11b5f
         const char *meta[_CONTEXT_MAX];
b11b5f
+        size_t meta_size[_CONTEXT_MAX];
b11b5f
 } Context;
b11b5f
 
b11b5f
 typedef enum CoredumpStorage {
b11b5f
@@ -148,8 +151,7 @@ static inline uint64_t storage_size_max(void) {
b11b5f
         return 0;
b11b5f
 }
b11b5f
 
b11b5f
-static int fix_acl(int fd, uid_t uid) {
b11b5f
-
b11b5f
+static int fix_acl(int fd, uid_t uid, bool allow_user) {
b11b5f
 #if HAVE_ACL
b11b5f
         _cleanup_(acl_freep) acl_t acl = NULL;
b11b5f
         acl_entry_t entry;
b11b5f
@@ -157,6 +159,11 @@ static int fix_acl(int fd, uid_t uid) {
b11b5f
         int r;
b11b5f
 
b11b5f
         assert(fd >= 0);
b11b5f
+        assert(uid_is_valid(uid));
b11b5f
+
b11b5f
+        /* We don't allow users to read coredumps if the uid or capabilities were changed. */
b11b5f
+        if (!allow_user)
b11b5f
+                return 0;
b11b5f
 
b11b5f
         if (uid_is_system(uid) || uid_is_dynamic(uid) || uid == UID_NOBODY)
b11b5f
                 return 0;
b11b5f
@@ -235,7 +242,8 @@ static int fix_permissions(
b11b5f
                 const char *filename,
b11b5f
                 const char *target,
b11b5f
                 const Context *context,
b11b5f
-                uid_t uid) {
b11b5f
+                uid_t uid,
b11b5f
+                bool allow_user) {
b11b5f
 
b11b5f
         int r;
b11b5f
 
b11b5f
@@ -245,7 +253,7 @@ static int fix_permissions(
b11b5f
 
b11b5f
         /* Ignore errors on these */
b11b5f
         (void) fchmod(fd, 0640);
b11b5f
-        (void) fix_acl(fd, uid);
b11b5f
+        (void) fix_acl(fd, uid, allow_user);
b11b5f
         (void) fix_xattr(fd, context);
b11b5f
 
b11b5f
         if (fsync(fd) < 0)
b11b5f
@@ -316,6 +324,154 @@ static int make_filename(const Context *context, char **ret) {
b11b5f
         return 0;
b11b5f
 }
b11b5f
 
b11b5f
+static int parse_auxv64(
b11b5f
+                const uint64_t *auxv,
b11b5f
+                size_t size_bytes,
b11b5f
+                int *at_secure,
b11b5f
+                uid_t *uid,
b11b5f
+                uid_t *euid,
b11b5f
+                gid_t *gid,
b11b5f
+                gid_t *egid) {
b11b5f
+
b11b5f
+        assert(auxv || size_bytes == 0);
b11b5f
+
b11b5f
+        if (size_bytes % (2 * sizeof(uint64_t)) != 0)
b11b5f
+                return log_warning_errno(-EIO, "Incomplete auxv structure (%zu bytes).", size_bytes);
b11b5f
+
b11b5f
+        size_t words = size_bytes / sizeof(uint64_t);
b11b5f
+
b11b5f
+        /* Note that we set output variables even on error. */
b11b5f
+
b11b5f
+        for (size_t i = 0; i + 1 < words; i += 2)
b11b5f
+                switch (auxv[i]) {
b11b5f
+                case AT_SECURE:
b11b5f
+                        *at_secure = auxv[i + 1] != 0;
b11b5f
+                        break;
b11b5f
+                case AT_UID:
b11b5f
+                        *uid = auxv[i + 1];
b11b5f
+                        break;
b11b5f
+                case AT_EUID:
b11b5f
+                        *euid = auxv[i + 1];
b11b5f
+                        break;
b11b5f
+                case AT_GID:
b11b5f
+                        *gid = auxv[i + 1];
b11b5f
+                        break;
b11b5f
+                case AT_EGID:
b11b5f
+                        *egid = auxv[i + 1];
b11b5f
+                        break;
b11b5f
+                case AT_NULL:
b11b5f
+                        if (auxv[i + 1] != 0)
b11b5f
+                                goto error;
b11b5f
+                        return 0;
b11b5f
+                }
b11b5f
+ error:
b11b5f
+        return log_warning_errno(-ENODATA,
b11b5f
+                                 "AT_NULL terminator not found, cannot parse auxv structure.");
b11b5f
+}
b11b5f
+
b11b5f
+static int parse_auxv32(
b11b5f
+                const uint32_t *auxv,
b11b5f
+                size_t size_bytes,
b11b5f
+                int *at_secure,
b11b5f
+                uid_t *uid,
b11b5f
+                uid_t *euid,
b11b5f
+                gid_t *gid,
b11b5f
+                gid_t *egid) {
b11b5f
+
b11b5f
+        assert(auxv || size_bytes == 0);
b11b5f
+
b11b5f
+        size_t words = size_bytes / sizeof(uint32_t);
b11b5f
+
b11b5f
+        if (size_bytes % (2 * sizeof(uint32_t)) != 0)
b11b5f
+                return log_warning_errno(-EIO, "Incomplete auxv structure (%zu bytes).", size_bytes);
b11b5f
+
b11b5f
+        /* Note that we set output variables even on error. */
b11b5f
+
b11b5f
+        for (size_t i = 0; i + 1 < words; i += 2)
b11b5f
+                switch (auxv[i]) {
b11b5f
+                case AT_SECURE:
b11b5f
+                        *at_secure = auxv[i + 1] != 0;
b11b5f
+                        break;
b11b5f
+                case AT_UID:
b11b5f
+                        *uid = auxv[i + 1];
b11b5f
+                        break;
b11b5f
+                case AT_EUID:
b11b5f
+                        *euid = auxv[i + 1];
b11b5f
+                        break;
b11b5f
+                case AT_GID:
b11b5f
+                        *gid = auxv[i + 1];
b11b5f
+                        break;
b11b5f
+                case AT_EGID:
b11b5f
+                        *egid = auxv[i + 1];
b11b5f
+                        break;
b11b5f
+                case AT_NULL:
b11b5f
+                        if (auxv[i + 1] != 0)
b11b5f
+                                goto error;
b11b5f
+                        return 0;
b11b5f
+                }
b11b5f
+ error:
b11b5f
+        return log_warning_errno(-ENODATA,
b11b5f
+                                 "AT_NULL terminator not found, cannot parse auxv structure.");
b11b5f
+}
b11b5f
+
b11b5f
+static int grant_user_access(int core_fd, const Context *context) {
b11b5f
+        int at_secure = -1;
b11b5f
+        uid_t uid = UID_INVALID, euid = UID_INVALID;
b11b5f
+        uid_t gid = GID_INVALID, egid = GID_INVALID;
b11b5f
+        int r;
b11b5f
+
b11b5f
+        assert(core_fd >= 0);
b11b5f
+        assert(context);
b11b5f
+
b11b5f
+        if (!context->meta[CONTEXT_PROC_AUXV])
b11b5f
+                return log_warning_errno(-ENODATA, "No auxv data, not adjusting permissions.");
b11b5f
+
b11b5f
+        uint8_t elf[EI_NIDENT];
b11b5f
+        errno = 0;
b11b5f
+        if (pread(core_fd, &elf, sizeof(elf), 0) != sizeof(elf))
b11b5f
+                return log_warning_errno(errno > 0 ? -errno : -EIO,
b11b5f
+                                         "Failed to pread from coredump fd: %s",
b11b5f
+                                         errno > 0 ? STRERROR(errno) : "Unexpected EOF");
b11b5f
+
b11b5f
+        if (elf[EI_MAG0] != ELFMAG0 ||
b11b5f
+            elf[EI_MAG1] != ELFMAG1 ||
b11b5f
+            elf[EI_MAG2] != ELFMAG2 ||
b11b5f
+            elf[EI_MAG3] != ELFMAG3 ||
b11b5f
+            elf[EI_VERSION] != EV_CURRENT)
b11b5f
+                return log_info_errno(-EUCLEAN,
b11b5f
+                                      "Core file does not have ELF header, not adjusting permissions.");
b11b5f
+        if (!IN_SET(elf[EI_CLASS], ELFCLASS32, ELFCLASS64) ||
b11b5f
+            !IN_SET(elf[EI_DATA], ELFDATA2LSB, ELFDATA2MSB))
b11b5f
+                return log_info_errno(-EUCLEAN,
b11b5f
+                                      "Core file has strange ELF class, not adjusting permissions.");
b11b5f
+
b11b5f
+        if ((elf[EI_DATA] == ELFDATA2LSB) != (__BYTE_ORDER == __LITTLE_ENDIAN))
b11b5f
+                return log_info_errno(-EUCLEAN,
b11b5f
+                                      "Core file has non-native endianness, not adjusting permissions.");
b11b5f
+
b11b5f
+        if (elf[EI_CLASS] == ELFCLASS64)
b11b5f
+                r = parse_auxv64((const uint64_t*) context->meta[CONTEXT_PROC_AUXV],
b11b5f
+                                 context->meta_size[CONTEXT_PROC_AUXV],
b11b5f
+                                 &at_secure, &uid, &euid, &gid, &egid);
b11b5f
+        else
b11b5f
+                r = parse_auxv32((const uint32_t*) context->meta[CONTEXT_PROC_AUXV],
b11b5f
+                                 context->meta_size[CONTEXT_PROC_AUXV],
b11b5f
+                                 &at_secure, &uid, &euid, &gid, &egid);
b11b5f
+        if (r < 0)
b11b5f
+                return r;
b11b5f
+
b11b5f
+        /* We allow access if we got all the data and at_secure is not set and
b11b5f
+         * the uid/gid matches euid/egid. */
b11b5f
+        bool ret =
b11b5f
+                at_secure == 0 &&
b11b5f
+                uid != UID_INVALID && euid != UID_INVALID && uid == euid &&
b11b5f
+                gid != GID_INVALID && egid != GID_INVALID && gid == egid;
b11b5f
+        log_debug("Will %s access (uid="UID_FMT " euid="UID_FMT " gid="GID_FMT " egid="GID_FMT " at_secure=%s)",
b11b5f
+                  ret ? "permit" : "restrict",
b11b5f
+                  uid, euid, gid, egid, yes_no(at_secure));
b11b5f
+        return ret;
b11b5f
+}
b11b5f
+
b11b5f
 static int save_external_coredump(
b11b5f
                 const Context *context,
b11b5f
                 int input_fd,
b11b5f
@@ -395,6 +551,8 @@ static int save_external_coredump(
b11b5f
                 goto fail;
b11b5f
         }
b11b5f
 
b11b5f
+        bool allow_user = grant_user_access(fd, context) > 0;
b11b5f
+
b11b5f
 #if HAVE_XZ || HAVE_LZ4
b11b5f
         /* If we will remove the coredump anyway, do not compress. */
b11b5f
         if (arg_compress && !maybe_remove_external_coredump(NULL, st.st_size)) {
b11b5f
@@ -420,7 +578,7 @@ static int save_external_coredump(
b11b5f
                         goto fail_compressed;
b11b5f
                 }
b11b5f
 
b11b5f
-                r = fix_permissions(fd_compressed, tmp_compressed, fn_compressed, context, uid);
b11b5f
+                r = fix_permissions(fd_compressed, tmp_compressed, fn_compressed, context, uid, allow_user);
b11b5f
                 if (r < 0)
b11b5f
                         goto fail_compressed;
b11b5f
 
b11b5f
@@ -443,7 +601,7 @@ static int save_external_coredump(
b11b5f
 uncompressed:
b11b5f
 #endif
b11b5f
 
b11b5f
-        r = fix_permissions(fd, tmp, fn, context, uid);
b11b5f
+        r = fix_permissions(fd, tmp, fn, context, uid, allow_user);
b11b5f
         if (r < 0)
b11b5f
                 goto fail;
b11b5f
 
b11b5f
@@ -842,6 +1000,7 @@ static void map_context_fields(const struct iovec *iovec, Context *context) {
b11b5f
                 [CONTEXT_HOSTNAME] = "COREDUMP_HOSTNAME=",
b11b5f
                 [CONTEXT_COMM] = "COREDUMP_COMM=",
b11b5f
                 [CONTEXT_EXE] = "COREDUMP_EXE=",
b11b5f
+                [CONTEXT_PROC_AUXV] = "COREDUMP_PROC_AUXV=",
b11b5f
         };
b11b5f
 
b11b5f
         unsigned i;
b11b5f
@@ -862,6 +1021,7 @@ static void map_context_fields(const struct iovec *iovec, Context *context) {
b11b5f
                 /* Note that these strings are NUL terminated, because we made sure that a trailing NUL byte is in the
b11b5f
                  * buffer, though not included in the iov_len count. (see below) */
b11b5f
                 context->meta[i] = p;
b11b5f
+                context->meta_size[i] = iovec->iov_len - strlen(context_field_names[i]);
b11b5f
                 break;
b11b5f
         }
b11b5f
 }
b11b5f
@@ -1070,7 +1230,7 @@ static int gather_pid_metadata(
b11b5f
                 char **comm_fallback,
b11b5f
                 struct iovec *iovec, size_t *n_iovec) {
b11b5f
 
b11b5f
-        /* We need 27 empty slots in iovec!
b11b5f
+        /* We need 28 empty slots in iovec!
b11b5f
          *
b11b5f
          * Note that if we fail on oom later on, we do not roll-back changes to the iovec structure. (It remains valid,
b11b5f
          * with the first n_iovec fields initialized.) */
b11b5f
@@ -1078,6 +1238,7 @@ static int gather_pid_metadata(
b11b5f
         uid_t owner_uid;
b11b5f
         pid_t pid;
b11b5f
         char *t;
b11b5f
+        size_t size;
b11b5f
         const char *p;
b11b5f
         int r, signo;
b11b5f
 
b11b5f
@@ -1187,6 +1348,19 @@ static int gather_pid_metadata(
b11b5f
         if (read_full_file(p, &t, NULL) >=0)
b11b5f
                 set_iovec_field_free(iovec, n_iovec, "COREDUMP_PROC_MOUNTINFO=", t);
b11b5f
 
b11b5f
+        /* We attach /proc/auxv here. ELF coredumps also contain a note for this (NT_AUXV), see elf(5). */
b11b5f
+        p = procfs_file_alloca(pid, "auxv");
b11b5f
+        if (read_full_file(p, &t, &size) >= 0) {
b11b5f
+                char *buf = malloc(strlen("COREDUMP_PROC_AUXV=") + size + 1);
b11b5f
+                if (buf) {
b11b5f
+                        /* Add a dummy terminator to make save_context() happy. */
b11b5f
+                        *((uint8_t*) mempcpy(stpcpy(buf, "COREDUMP_PROC_AUXV="), t, size)) = '\0';
b11b5f
+                        iovec[(*n_iovec)++] = IOVEC_MAKE(buf, size + strlen("COREDUMP_PROC_AUXV="));
b11b5f
+                }
b11b5f
+
b11b5f
+                free(t);
b11b5f
+        }
b11b5f
+
b11b5f
         if (get_process_cwd(pid, &t) >= 0)
b11b5f
                 set_iovec_field_free(iovec, n_iovec, "COREDUMP_CWD=", t);
b11b5f
 
b11b5f
@@ -1219,7 +1393,7 @@ static int gather_pid_metadata(
b11b5f
 static int process_kernel(int argc, char* argv[]) {
b11b5f
 
b11b5f
         Context context = {};
b11b5f
-        struct iovec iovec[29 + SUBMIT_COREDUMP_FIELDS];
b11b5f
+        struct iovec iovec[30 + SUBMIT_COREDUMP_FIELDS];
b11b5f
         size_t i, n_iovec, n_to_free = 0;
b11b5f
         int r;
b11b5f