Submitted By: Douglas R. Reno Date: 2022-12-28 Initial Package Version: 251 Origin: Upstream (commit b7641425659243c09473cd8fb3aef2c0d4a3eb9c) Upstream Status: Applied Description: Fixes CVE-2022-4415, a security vulnerability caused by systemd-coredump not respecting fs.suid_dumpable kernel settings. This vulnerability allows for a local infoleak and for privilege escalation. A proof of concept is public, and it retrieves the password hash for the root user. See https://www.openwall.com/lists/oss-security/2022/12/21/3 for more information. This patch is specifically designed for LFS 11.2. diff -Naurp systemd-251.orig/src/basic/io-util.h systemd-251/src/basic/io-util.h --- systemd-251.orig/src/basic/io-util.h 2022-05-21 08:23:41.000000000 -0500 +++ systemd-251/src/basic/io-util.h 2022-12-27 23:40:06.816180599 -0600 @@ -91,7 +91,16 @@ struct iovec_wrapper *iovw_new(void); struct iovec_wrapper *iovw_free(struct iovec_wrapper *iovw); struct iovec_wrapper *iovw_free_free(struct iovec_wrapper *iovw); void iovw_free_contents(struct iovec_wrapper *iovw, bool free_vectors); + int iovw_put(struct iovec_wrapper *iovw, void *data, size_t len); +static inline int iovw_consume(struct iovec_wrapper *iovw, void *data, size_t len) { + /* Move data into iovw or free on error */ + int r = iovw_put(iovw, data, len); + if (r < 0) + free(data); + return r; +} + int iovw_put_string_field(struct iovec_wrapper *iovw, const char *field, const char *value); int iovw_put_string_field_free(struct iovec_wrapper *iovw, const char *field, char *value); void iovw_rebase(struct iovec_wrapper *iovw, char *old, char *new); diff -Naurp systemd-251.orig/src/coredump/coredump.c systemd-251/src/coredump/coredump.c --- systemd-251.orig/src/coredump/coredump.c 2022-05-21 08:23:41.000000000 -0500 +++ systemd-251/src/coredump/coredump.c 2022-12-27 23:46:38.586633853 -0600 @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -105,24 +106,27 @@ enum { META_EXE = _META_MANDATORY_MAX, META_UNIT, + META_PROC_AUXV, _META_MAX }; static const char * const meta_field_names[_META_MAX] = { - [META_ARGV_PID] = "COREDUMP_PID=", - [META_ARGV_UID] = "COREDUMP_UID=", - [META_ARGV_GID] = "COREDUMP_GID=", - [META_ARGV_SIGNAL] = "COREDUMP_SIGNAL=", - [META_ARGV_TIMESTAMP] = "COREDUMP_TIMESTAMP=", - [META_ARGV_RLIMIT] = "COREDUMP_RLIMIT=", - [META_ARGV_HOSTNAME] = "COREDUMP_HOSTNAME=", - [META_COMM] = "COREDUMP_COMM=", - [META_EXE] = "COREDUMP_EXE=", - [META_UNIT] = "COREDUMP_UNIT=", + [META_ARGV_PID] = "COREDUMP_PID=", + [META_ARGV_UID] = "COREDUMP_UID=", + [META_ARGV_GID] = "COREDUMP_GID=", + [META_ARGV_SIGNAL] = "COREDUMP_SIGNAL=", + [META_ARGV_TIMESTAMP] = "COREDUMP_TIMESTAMP=", + [META_ARGV_RLIMIT] = "COREDUMP_RLIMIT=", + [META_ARGV_HOSTNAME] = "COREDUMP_HOSTNAME=", + [META_COMM] = "COREDUMP_COMM=", + [META_EXE] = "COREDUMP_EXE=", + [META_UNIT] = "COREDUMP_UNIT=", + [META_PROC_AUXV] = "COREDUMP_PROC_AUXV=", }; typedef struct Context { const char *meta[_META_MAX]; + size_t meta_size[_META_MAX]; pid_t pid; bool is_pid1; bool is_journald; @@ -137,9 +141,9 @@ typedef enum CoredumpStorage { } CoredumpStorage; static const char* const coredump_storage_table[_COREDUMP_STORAGE_MAX] = { - [COREDUMP_STORAGE_NONE] = "none", + [COREDUMP_STORAGE_NONE] = "none", [COREDUMP_STORAGE_EXTERNAL] = "external", - [COREDUMP_STORAGE_JOURNAL] = "journal", + [COREDUMP_STORAGE_JOURNAL] = "journal", }; DEFINE_PRIVATE_STRING_TABLE_LOOKUP(coredump_storage, CoredumpStorage); @@ -155,13 +159,13 @@ static uint64_t arg_max_use = UINT64_MAX static int parse_config(void) { static const ConfigTableItem items[] = { - { "Coredump", "Storage", config_parse_coredump_storage, 0, &arg_storage }, - { "Coredump", "Compress", config_parse_bool, 0, &arg_compress }, - { "Coredump", "ProcessSizeMax", config_parse_iec_uint64, 0, &arg_process_size_max }, - { "Coredump", "ExternalSizeMax", config_parse_iec_uint64_infinity, 0, &arg_external_size_max }, - { "Coredump", "JournalSizeMax", config_parse_iec_size, 0, &arg_journal_size_max }, - { "Coredump", "KeepFree", config_parse_iec_uint64, 0, &arg_keep_free }, - { "Coredump", "MaxUse", config_parse_iec_uint64, 0, &arg_max_use }, + { "Coredump", "Storage", config_parse_coredump_storage, 0, &arg_storage }, + { "Coredump", "Compress", config_parse_bool, 0, &arg_compress }, + { "Coredump", "ProcessSizeMax", config_parse_iec_uint64, 0, &arg_process_size_max }, + { "Coredump", "ExternalSizeMax", config_parse_iec_uint64_infinity, 0, &arg_external_size_max }, + { "Coredump", "JournalSizeMax", config_parse_iec_size, 0, &arg_journal_size_max }, + { "Coredump", "KeepFree", config_parse_iec_uint64, 0, &arg_keep_free }, + { "Coredump", "MaxUse", config_parse_iec_uint64, 0, &arg_max_use }, {} }; @@ -184,13 +188,16 @@ static uint64_t storage_size_max(void) { return 0; } -static int fix_acl(int fd, uid_t uid) { +static int fix_acl(int fd, uid_t uid, bool allow_user) { + assert(fd >= 0); + assert(uid_is_valid(uid)); #if HAVE_ACL int r; - assert(fd >= 0); - assert(uid_is_valid(uid)); + /* We don't allow users to read coredumps if the uid or capabilities were changed. */ + if (!allow_user) + return 0; if (uid_is_system(uid) || uid_is_dynamic(uid) || uid == UID_NOBODY) return 0; @@ -207,15 +214,15 @@ static int fix_acl(int fd, uid_t uid) { static int fix_xattr(int fd, const Context *context) { static const char * const xattrs[_META_MAX] = { - [META_ARGV_PID] = "user.coredump.pid", - [META_ARGV_UID] = "user.coredump.uid", - [META_ARGV_GID] = "user.coredump.gid", - [META_ARGV_SIGNAL] = "user.coredump.signal", - [META_ARGV_TIMESTAMP] = "user.coredump.timestamp", - [META_ARGV_RLIMIT] = "user.coredump.rlimit", - [META_ARGV_HOSTNAME] = "user.coredump.hostname", - [META_COMM] = "user.coredump.comm", - [META_EXE] = "user.coredump.exe", + [META_ARGV_PID] = "user.coredump.pid", + [META_ARGV_UID] = "user.coredump.uid", + [META_ARGV_GID] = "user.coredump.gid", + [META_ARGV_SIGNAL] = "user.coredump.signal", + [META_ARGV_TIMESTAMP] = "user.coredump.timestamp", + [META_ARGV_RLIMIT] = "user.coredump.rlimit", + [META_ARGV_HOSTNAME] = "user.coredump.hostname", + [META_COMM] = "user.coredump.comm", + [META_EXE] = "user.coredump.exe", }; int r = 0; @@ -250,7 +257,8 @@ static int fix_permissions( const char *filename, const char *target, const Context *context, - uid_t uid) { + uid_t uid, + bool allow_user) { int r; @@ -260,7 +268,7 @@ static int fix_permissions( /* Ignore errors on these */ (void) fchmod(fd, 0640); - (void) fix_acl(fd, uid); + (void) fix_acl(fd, uid, allow_user); (void) fix_xattr(fd, context); r = fsync_full(fd); @@ -330,6 +338,153 @@ static int make_filename(const Context * return 0; } +static int parse_auxv64( + const uint64_t *auxv, + size_t size_bytes, + int *at_secure, + uid_t *uid, + uid_t *euid, + gid_t *gid, + gid_t *egid) { + + assert(auxv || size_bytes == 0); + + if (size_bytes % (2 * sizeof(uint64_t)) != 0) + return log_warning_errno(SYNTHETIC_ERRNO(EIO), "Incomplete auxv structure (%zu bytes).", size_bytes); + + size_t words = size_bytes / sizeof(uint64_t); + + /* Note that we set output variables even on error. */ + + for (size_t i = 0; i + 1 < words; i += 2) + switch (auxv[i]) { + case AT_SECURE: + *at_secure = auxv[i + 1] != 0; + break; + case AT_UID: + *uid = auxv[i + 1]; + break; + case AT_EUID: + *euid = auxv[i + 1]; + break; + case AT_GID: + *gid = auxv[i + 1]; + break; + case AT_EGID: + *egid = auxv[i + 1]; + break; + case AT_NULL: + if (auxv[i + 1] != 0) + goto error; + return 0; + } + error: + return log_warning_errno(SYNTHETIC_ERRNO(ENODATA), + "AT_NULL terminator not found, cannot parse auxv structure."); +} + +static int parse_auxv32( + const uint32_t *auxv, + size_t size_bytes, + int *at_secure, + uid_t *uid, + uid_t *euid, + gid_t *gid, + gid_t *egid) { + + assert(auxv || size_bytes == 0); + + size_t words = size_bytes / sizeof(uint32_t); + + if (size_bytes % (2 * sizeof(uint32_t)) != 0) + return log_warning_errno(SYNTHETIC_ERRNO(EIO), "Incomplete auxv structure (%zu bytes).", size_bytes); + + /* Note that we set output variables even on error. */ + + for (size_t i = 0; i + 1 < words; i += 2) + switch (auxv[i]) { + case AT_SECURE: + *at_secure = auxv[i + 1] != 0; + break; + case AT_UID: + *uid = auxv[i + 1]; + break; + case AT_EUID: + *euid = auxv[i + 1]; + break; + case AT_GID: + *gid = auxv[i + 1]; + break; + case AT_EGID: + *egid = auxv[i + 1]; + break; + case AT_NULL: + if (auxv[i + 1] != 0) + goto error; + return 0; + } + error: + return log_warning_errno(SYNTHETIC_ERRNO(ENODATA), + "AT_NULL terminator not found, cannot parse auxv structure."); +} + +static int grant_user_access(int core_fd, const Context *context) { + int at_secure = -1; + uid_t uid = UID_INVALID, euid = UID_INVALID; + uid_t gid = GID_INVALID, egid = GID_INVALID; + int r; + + assert(core_fd >= 0); + assert(context); + + if (!context->meta[META_PROC_AUXV]) + return log_warning_errno(SYNTHETIC_ERRNO(ENODATA), "No auxv data, not adjusting permissions."); + + uint8_t elf[EI_NIDENT]; + errno = 0; + if (pread(core_fd, &elf, sizeof(elf), 0) != sizeof(elf)) + return log_warning_errno(errno_or_else(EIO), + "Failed to pread from coredump fd: %s", errno != 0 ? strerror_safe(errno) : "Unexpected EOF"); + + if (elf[EI_MAG0] != ELFMAG0 || + elf[EI_MAG1] != ELFMAG1 || + elf[EI_MAG2] != ELFMAG2 || + elf[EI_MAG3] != ELFMAG3 || + elf[EI_VERSION] != EV_CURRENT) + return log_info_errno(SYNTHETIC_ERRNO(EUCLEAN), + "Core file does not have ELF header, not adjusting permissions."); + if (!IN_SET(elf[EI_CLASS], ELFCLASS32, ELFCLASS64) || + !IN_SET(elf[EI_DATA], ELFDATA2LSB, ELFDATA2MSB)) + return log_info_errno(SYNTHETIC_ERRNO(EUCLEAN), + "Core file has strange ELF class, not adjusting permissions."); + + if ((elf[EI_DATA] == ELFDATA2LSB) != (__BYTE_ORDER == __LITTLE_ENDIAN)) + return log_info_errno(SYNTHETIC_ERRNO(EUCLEAN), + "Core file has non-native endianness, not adjusting permissions."); + + if (elf[EI_CLASS] == ELFCLASS64) + r = parse_auxv64((const uint64_t*) context->meta[META_PROC_AUXV], + context->meta_size[META_PROC_AUXV], + &at_secure, &uid, &euid, &gid, &egid); + else + r = parse_auxv32((const uint32_t*) context->meta[META_PROC_AUXV], + context->meta_size[META_PROC_AUXV], + &at_secure, &uid, &euid, &gid, &egid); + if (r < 0) + return r; + + /* We allow access if we got all the data and at_secure is not set and + * the uid/gid matches euid/egid. */ + bool ret = + at_secure == 0 && + uid != UID_INVALID && euid != UID_INVALID && uid == euid && + gid != GID_INVALID && egid != GID_INVALID && gid == egid; + log_debug("Will %s access (uid="UID_FMT " euid="UID_FMT " gid="GID_FMT " egid="GID_FMT " at_secure=%s)", + ret ? "permit" : "restrict", + uid, euid, gid, egid, yes_no(at_secure)); + return ret; +} + static int save_external_coredump( const Context *context, int input_fd, @@ -452,6 +607,8 @@ static int save_external_coredump( context->meta[META_ARGV_PID], context->meta[META_COMM]); truncated = r == 1; + bool allow_user = grant_user_access(fd, context) > 0; + #if HAVE_COMPRESSION if (arg_compress) { _cleanup_(unlink_and_freep) char *tmp_compressed = NULL; @@ -489,7 +646,7 @@ static int save_external_coredump( uncompressed_size += partial_uncompressed_size; } - r = fix_permissions(fd_compressed, tmp_compressed, fn_compressed, context, uid); + r = fix_permissions(fd_compressed, tmp_compressed, fn_compressed, context, uid, allow_user); if (r < 0) return r; @@ -516,7 +673,7 @@ static int save_external_coredump( "SIZE_LIMIT=%zu", max_size, "MESSAGE_ID=" SD_MESSAGE_TRUNCATED_CORE_STR); - r = fix_permissions(fd, tmp, fn, context, uid); + r = fix_permissions(fd, tmp, fn, context, uid, allow_user); if (r < 0) return log_error_errno(r, "Failed to fix permissions and finalize coredump %s into %s: %m", coredump_tmpfile_name(tmp), fn); @@ -764,7 +921,7 @@ static int change_uid_gid(const Context } static int submit_coredump( - Context *context, + const Context *context, struct iovec_wrapper *iovw, int input_fd) { @@ -925,16 +1082,15 @@ static int save_context(Context *context struct iovec *iovec = iovw->iovec + n; for (size_t i = 0; i < ELEMENTSOF(meta_field_names); i++) { - char *p; /* Note that these strings are NUL terminated, because we made sure that a * trailing NUL byte is in the buffer, though not included in the iov_len * count (see process_socket() and gather_pid_metadata_*()) */ assert(((char*) iovec->iov_base)[iovec->iov_len] == 0); - p = startswith(iovec->iov_base, meta_field_names[i]); + const char *p = startswith(iovec->iov_base, meta_field_names[i]); if (p) { - context->meta[i] = p; + context->meta_size[i] = iovec->iov_len - strlen(meta_field_names[i]); count++; break; } @@ -1176,6 +1332,7 @@ static int gather_pid_metadata(struct io uid_t owner_uid; pid_t pid; char *t; + size_t size; const char *p; int r; @@ -1240,13 +1397,26 @@ static int gather_pid_metadata(struct io (void) iovw_put_string_field_free(iovw, "COREDUMP_PROC_LIMITS=", t); p = procfs_file_alloca(pid, "cgroup"); - if (read_full_virtual_file(p, &t, NULL) >=0) + if (read_full_virtual_file(p, &t, NULL) >= 0) (void) iovw_put_string_field_free(iovw, "COREDUMP_PROC_CGROUP=", t); p = procfs_file_alloca(pid, "mountinfo"); - if (read_full_virtual_file(p, &t, NULL) >=0) + if (read_full_virtual_file(p, &t, NULL) >= 0) (void) iovw_put_string_field_free(iovw, "COREDUMP_PROC_MOUNTINFO=", t); + /* We attach /proc/auxv here. ELF coredumps also contain a note for this (NT_AUXV), see elf(5). */ + p = procfs_file_alloca(pid, "auxv"); + if (read_full_virtual_file(p, &t, &size) >= 0) { + char *buf = malloc(strlen("COREDUMP_PROC_AUXV=") + size + 1); + if (buf) { + /* Add a dummy terminator to make save_context() happy. */ + *((uint8_t*) mempcpy(stpcpy(buf, "COREDUMP_PROC_AUXV="), t, size)) = '\0'; + (void) iovw_consume(iovw, buf, size + strlen("COREDUMP_PROC_AUXV=")); + } + + free(t); + } + if (get_process_cwd(pid, &t) >= 0) (void) iovw_put_string_field_free(iovw, "COREDUMP_CWD=", t);