CVE-2015-3150
Description
A race condition in ABRT's D-Bus API allows local attackers to delete or chown arbitrary files via crafted problem directory paths.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A race condition in ABRT's D-Bus API allows local attackers to delete or chown arbitrary files via crafted problem directory paths.
Vulnerability
A race condition exists in the D-Bus service abrt-dbus of the Automatic Bug Reporting Tool (ABRT) versions prior to the fixes introduced in commits 7814554 [1], b7f8bd2 [2], 6e811d7 [3], and 1951e72 [4]. The ChownProblemDir, DeleteElement, and DeleteProblem methods accept a problem_dir argument but did not properly validate that the supplied path is a safe subdirectory of the ABRT dump location. Specifically, the validation only checked for a path prefix and for "/." substrings, which can be bypassed by a malicious path that still resolves to an arbitrary location (e.g., using symlinks or hard links). A local attacker can exploit a time-of-check time-of-use (TOCTOU) race window between the path validation and the actual file operation, because the directory could be replaced with a symlink after validation passes but before the operation is performed. The affected versions include ABRT packages shipped in various Linux distributions (e.g., Red Hat Enterprise Linux 6 and 7, Fedora) before the patches were applied.
Exploitation
An attacker must be a local user who can interact with the abrt-dbus D-Bus service. The attacker crafts a path that passes the initial prefix and substring checks, such as a path inside /var/spool/abrt/ that contains a symlink pointing outside that directory. By repeatedly invoking one of the vulnerable methods while rapidly swapping the target directory with a symlink to an arbitrary file or directory, the attacker wins a race condition: the validation sees the intended directory, but the subsequent operation (chown, unlink, or deletion) follows the symlink and acts on the attacker-chosen target. No authentication beyond local system access is needed; the D-Bus service runs as root and performs the operation with root privileges.
Impact
A successful exploit allows the local attacker to delete arbitrary files or change their ownership to the attacker's UID, depending on which method is called. This can lead to privilege escalation (e.g., overwriting a setuid binary or critical system file), denial of service (by deleting key files), or information disclosure (by changing ownership to read sensitive files). The attacker gains a root-level file operation with full system compromise potential.
Mitigation
The vulnerabilities are fixed by the following commits: 7814554 [1] (uses file descriptor-based operations to avoid races), b7f8bd2 [2] (adds permission checks on dump directories), 6e811d7 [3] (hardens path validation using dir_is_in_dump_location), and 1951e72 [4] (introduces safe file operations with secure_openat_read and xunlinkat). Users should update to a version of ABRT containing these patches (e.g., versions after 2.1.12 for related releases). Red Hat published updates via RHSA-2015:1083 and similar. No known workarounds exist; the service must be patched or, if ABRT is not needed, administrators can disable the abrt-dbus service.
AI Insight generated on May 24, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2- ABRT/ABRTv5Range: before 1951e7282043dfe1268d492aea056b554baedb75
Patches
47814554e0827dbus: avoid race-conditions in tests for dum dir availability
2 files changed · +71 −10
src/dbus/abrt-dbus.c+58 −8 modified@@ -245,7 +245,15 @@ static struct dump_dir *open_directory_for_modification_of_element( } } - if (!dump_dir_accessible_by_uid(problem_id, caller_uid)) + int dir_fd = dd_openfd(problem_id); + if (dir_fd < 0) + { + perror_msg("can't open problem directory '%s'", problem_id); + return_InvalidProblemDir_error(invocation, problem_id); + return NULL; + } + + if (!fdump_dir_accessible_by_uid(dir_fd, caller_uid)) { if (errno == ENOTDIR) { @@ -260,10 +268,11 @@ static struct dump_dir *open_directory_for_modification_of_element( _("Not Authorized")); } + close(dir_fd); return NULL; } - struct dump_dir *dd = dd_opendir(problem_id, /* flags : */ 0); + struct dump_dir *dd = dd_fdopendir(dir_fd, problem_id, /* flags : */ 0); if (!dd) { /* This should not happen because of the access check above */ log_notice("Can't access the problem '%s' for modification", problem_id); @@ -429,7 +438,15 @@ static void handle_method_call(GDBusConnection *connection, return; } - int ddstat = dump_dir_stat_for_uid(problem_dir, caller_uid); + int dir_fd = dd_openfd(problem_dir); + if (dir_fd < 0) + { + perror_msg("can't open problem directory '%s'", problem_dir); + return_InvalidProblemDir_error(invocation, problem_dir); + return; + } + + int ddstat = fdump_dir_stat_for_uid(dir_fd, caller_uid); if (ddstat < 0) { if (errno == ENOTDIR) @@ -443,13 +460,15 @@ static void handle_method_call(GDBusConnection *connection, return_InvalidProblemDir_error(invocation, problem_dir); + close(dir_fd); return; } if (ddstat & DD_STAT_OWNED_BY_UID) { //caller seems to be in group with access to this dir, so no action needed log_notice("caller has access to the requested directory %s", problem_dir); g_dbus_method_invocation_return_value(invocation, NULL); + close(dir_fd); return; } @@ -460,10 +479,11 @@ static void handle_method_call(GDBusConnection *connection, g_dbus_method_invocation_return_dbus_error(invocation, "org.freedesktop.problems.AuthFailure", _("Not Authorized")); + close(dir_fd); return; } - struct dump_dir *dd = dd_opendir(problem_dir, DD_OPEN_READONLY | DD_FAIL_QUIETLY_EACCES); + struct dump_dir *dd = dd_fdopendir(dir_fd, problem_dir, DD_OPEN_READONLY | DD_FAIL_QUIETLY_EACCES); if (!dd) { return_InvalidProblemDir_error(invocation, problem_dir); @@ -497,12 +517,21 @@ static void handle_method_call(GDBusConnection *connection, return; } - if (!dump_dir_accessible_by_uid(problem_dir, caller_uid)) + int dir_fd = dd_openfd(problem_dir); + if (dir_fd < 0) + { + perror_msg("can't open problem directory '%s'", problem_dir); + return_InvalidProblemDir_error(invocation, problem_dir); + return; + } + + if (!fdump_dir_accessible_by_uid(dir_fd, caller_uid)) { if (errno == ENOTDIR) { log_notice("Requested directory does not exist '%s'", problem_dir); return_InvalidProblemDir_error(invocation, problem_dir); + close(dir_fd); return; } @@ -512,11 +541,12 @@ static void handle_method_call(GDBusConnection *connection, g_dbus_method_invocation_return_dbus_error(invocation, "org.freedesktop.problems.AuthFailure", _("Not Authorized")); + close(dir_fd); return; } } - struct dump_dir *dd = dd_opendir(problem_dir, DD_OPEN_READONLY | DD_FAIL_QUIETLY_EACCES); + struct dump_dir *dd = dd_fdopendir(dir_fd, problem_dir, DD_OPEN_READONLY | DD_FAIL_QUIETLY_EACCES); if (!dd) { return_InvalidProblemDir_error(invocation, problem_dir); @@ -677,20 +707,40 @@ static void handle_method_call(GDBusConnection *connection, for (GList *l = problem_dirs; l; l = l->next) { const char *dir_name = (const char*)l->data; - if (!dump_dir_accessible_by_uid(dir_name, caller_uid)) + + int dir_fd = dd_openfd(dir_name); + if (dir_fd < 0) + { + perror_msg("can't open problem directory '%s'", dir_name); + return_InvalidProblemDir_error(invocation, dir_name); + return; + } + + if (!fdump_dir_accessible_by_uid(dir_fd, caller_uid)) { if (errno == ENOTDIR) { log_notice("Requested directory does not exist '%s'", dir_name); + close(dir_fd); continue; } if (polkit_check_authorization_dname(caller, "org.freedesktop.problems.getall") != PolkitYes) { // if user didn't provide correct credentials, just move to the next dir + close(dir_fd); continue; } } - delete_dump_dir(dir_name); + + struct dump_dir *dd = dd_fdopendir(dir_fd, dir_name, /*flags:*/ 0); + if (dd) + { + if (dd_delete(dd) != 0) + { + error_msg("Failed to delete problem directory '%s'", dir_name); + dd_close(dd); + } + } } g_dbus_method_invocation_return_value(invocation, NULL);
src/lib/problem_api.c+13 −2 modified@@ -46,22 +46,33 @@ int for_each_problem_in_dir(const char *path, continue; /* skip "." and ".." */ char *full_name = concat_path_file(path, dent->d_name); - if (caller_uid == -1 || dump_dir_accessible_by_uid(full_name, caller_uid)) + + int dir_fd = dd_openfd(full_name); + if (dir_fd < 0) + { + VERB2 perror_msg("can't open problem directory '%s'", full_name); + continue; + } + + if (caller_uid == -1 || fdump_dir_accessible_by_uid(dir_fd, caller_uid)) { /* Silently ignore *any* errors, not only EACCES. * We saw "lock file is locked by process PID" error * when we raced with wizard. */ int sv_logmode = logmode; logmode = 0; - struct dump_dir *dd = dd_opendir(full_name, DD_OPEN_READONLY | DD_FAIL_QUIETLY_EACCES | DD_DONT_WAIT_FOR_LOCK); + struct dump_dir *dd = dd_fdopendir(dir_fd, full_name, DD_OPEN_READONLY | DD_FAIL_QUIETLY_EACCES | DD_DONT_WAIT_FOR_LOCK); logmode = sv_logmode; if (dd) { brk = callback ? callback(dd, arg) : 0; dd_close(dd); } } + else + close(dir_fd); + free(full_name); if (brk) break;
1951e7282043lib: fix races in dump directory handling code
5 files changed · +275 −181
src/include/dump_dir.h+7 −0 modified@@ -34,6 +34,12 @@ extern "C" { /* Utility function */ int create_symlink_lockfile(const char *filename, const char *pid_str); +int create_symlink_lockfile_at(int dir_fd, const char *filename, const char *pid_str); + +/* Opens filename for reading relatively to a directory represented by dir_fd. + * The function fails if the file is symbolic link, directory or hard link. + */ +int secure_openat_read(int dir_fd, const char *filename); enum { DD_FAIL_QUIETLY_ENOENT = (1 << 0), @@ -57,6 +63,7 @@ struct dump_dir { mode_t mode; time_t dd_time; char *dd_type; + int dd_fd; }; void dd_close(struct dump_dir *dd);
src/include/internal_libreport.h+4 −0 modified@@ -406,6 +406,8 @@ int xopen3(const char *pathname, int flags, int mode); int xopen(const char *pathname, int flags); #define xunlink libreport_xunlink void xunlink(const char *pathname); +#define xunlinkat libreport_xunlinkat +void xunlinkat(int dir_fd, const char *pathname, int flags); /* Just testing dent->d_type == DT_REG is wrong: some filesystems * do not report the type, they report DT_UNKNOWN for every dirent @@ -415,6 +417,8 @@ void xunlink(const char *pathname); */ #define is_regular_file libreport_is_regular_file int is_regular_file(struct dirent *dent, const char *dirname); +#define is_regular_file_at libreport_is_regular_file_at +int is_regular_file_at(struct dirent *dent, int dir_fd); #define dot_or_dotdot libreport_dot_or_dotdot bool dot_or_dotdot(const char *filename);
src/lib/dump_dir.c+243 −174 modified@@ -85,6 +85,7 @@ static char *load_text_file(const char *path, unsigned flags); +static char *load_text_file_at(int dir_fd, const char *name, unsigned flags); static void copy_file_from_chroot(struct dump_dir* dd, const char *name, const char *chroot_dir, const char *file_path); @@ -98,10 +99,10 @@ static bool isdigit_str(const char *str) return true; } -static bool exist_file_dir(const char *path) +static bool exist_file_dir_at(int dir_fd, const char *name) { struct stat buf; - if (stat(path, &buf) == 0) + if (fstatat(dir_fd, name, &buf, AT_SYMLINK_NOFOLLOW) == 0) { if (S_ISDIR(buf.st_mode) || S_ISREG(buf.st_mode)) { @@ -111,15 +112,61 @@ static bool exist_file_dir(const char *path) return false; } +/* Opens the file in the three following steps: + * 1. open the file with O_PATH (get a file descriptor for operations with + * inode) and O_NOFOLLOW (do not dereference symbolick links) + * 2. stat the resulting file descriptor and fail if the opened file is not a + * regular file or if the number of links is greater than 1 (that means that + * the inode has more names (hard links)) + * 3. "re-open" the file descriptor retrieved in the first step with O_RDONLY + * by opening /proc/self/fd/$fd (then close the former file descriptor and + * return the new one). + */ +int secure_openat_read(int dir_fd, const char *pathname) +{ + static char reopen_buf[sizeof("/proc/self/fd/") + 3*sizeof(int) + 1]; + + int path_fd = openat(dir_fd, pathname, O_PATH | O_NOFOLLOW); + if (path_fd < 0) + return -1; + + struct stat path_sb; + int r = fstat(path_fd, &path_sb); + if (r < 0) + { + perror_msg("stat"); + close(path_fd); + return -1; + } + + if (!S_ISREG(path_sb.st_mode) || path_sb.st_nlink > 1) + { + log_notice("Path isn't a regular file or has more links (%lu)", path_sb.st_nlink); + errno = EINVAL; + close(path_fd); + return -1; + } + + if (snprintf(reopen_buf, sizeof(reopen_buf), "/proc/self/fd/%d", path_fd) >= sizeof(reopen_buf)) { + error_msg("BUG: too long path to a file descriptor"); + abort(); + } + + const int fd = open(reopen_buf, O_RDONLY); + close(path_fd); + + return fd; +} + /* Returns value less than 0 if the file is not readable or * if the file doesn't contain valid unixt time stamp. * * Any possible failure will be logged. */ -static time_t parse_time_file(const char *filename) +static time_t parse_time_file_at(int dir_fd, const char *filename) { /* Open input file, and parse it. */ - int fd = open(filename, O_RDONLY | O_NOFOLLOW); + int fd = secure_openat_read(dir_fd, filename); if (fd < 0) { VERB2 pwarn_msg("Can't open '%s'", filename); @@ -183,9 +230,9 @@ static time_t parse_time_file(const char *filename) * 0: failed to lock (someone else has it locked) * 1: success */ -int create_symlink_lockfile(const char* lock_file, const char* pid) +int create_symlink_lockfile_at(int dir_fd, const char* lock_file, const char* pid) { - while (symlink(pid, lock_file) != 0) + while (symlinkat(pid, dir_fd, lock_file) != 0) { if (errno != EEXIST) { @@ -198,7 +245,7 @@ int create_symlink_lockfile(const char* lock_file, const char* pid) } char pid_buf[sizeof(pid_t)*3 + 4]; - ssize_t r = readlink(lock_file, pid_buf, sizeof(pid_buf) - 1); + ssize_t r = readlinkat(dir_fd, lock_file, pid_buf, sizeof(pid_buf) - 1); if (r < 0) { if (errno == ENOENT) @@ -230,7 +277,7 @@ int create_symlink_lockfile(const char* lock_file, const char* pid) log("Lock file '%s' was locked by process %s, but it crashed?", lock_file, pid_buf); } /* The file may be deleted by now by other process. Ignore ENOENT */ - if (unlink(lock_file) != 0 && errno != ENOENT) + if (unlinkat(dir_fd, lock_file, /*only files*/0) != 0 && errno != ENOENT) { perror_msg("Can't remove stale lock file '%s'", lock_file); errno = 0; @@ -242,21 +289,21 @@ int create_symlink_lockfile(const char* lock_file, const char* pid) return 1; } +int create_symlink_lockfile(const char *filename, const char *pid_str) +{ + return create_symlink_lockfile_at(AT_FDCWD, filename, pid_str); +} + static const char *dd_check(struct dump_dir *dd) { - unsigned dirname_len = strlen(dd->dd_dirname); - char filename_buf[FILENAME_MAX+1]; - strcpy(filename_buf, dd->dd_dirname); - strcpy(filename_buf + dirname_len, "/"FILENAME_TIME); - dd->dd_time = parse_time_file(filename_buf); + dd->dd_time = parse_time_file_at(dd->dd_fd, FILENAME_TIME); if (dd->dd_time < 0) { log_warning("Missing file: "FILENAME_TIME); return FILENAME_TIME; } - strcpy(filename_buf + dirname_len, "/"FILENAME_TYPE); - dd->dd_type = load_text_file(filename_buf, DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE); + dd->dd_type = load_text_file_at(dd->dd_fd, FILENAME_TYPE, DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE); if (!dd->dd_type || (strlen(dd->dd_type) == 0)) { log_warning("Missing or empty file: "FILENAME_TYPE); @@ -274,17 +321,12 @@ static int dd_lock(struct dump_dir *dd, unsigned sleep_usec, int flags) char pid_buf[sizeof(long)*3 + 2]; snprintf(pid_buf, sizeof(pid_buf), "%lu", (long)getpid()); - unsigned dirname_len = strlen(dd->dd_dirname); - char lock_buf[dirname_len + sizeof("/.lock")]; - strcpy(lock_buf, dd->dd_dirname); - strcpy(lock_buf + dirname_len, "/.lock"); - unsigned count = NO_TIME_FILE_COUNT; retry: while (1) { - int r = create_symlink_lockfile(lock_buf, pid_buf); + int r = create_symlink_lockfile_at(dd->dd_fd, ".lock", pid_buf); if (r < 0) return r; /* error */ if (r > 0) @@ -304,8 +346,8 @@ static int dd_lock(struct dump_dir *dd, unsigned sleep_usec, int flags) */ if (missing_file) { - xunlink(lock_buf); - log_warning("Unlocked '%s' (no or corrupted '%s' file)", lock_buf, missing_file); + xunlinkat(dd->dd_fd, ".lock", /*only files*/0); + log_warning("Unlocked '%s' (no or corrupted '%s' file)", dd->dd_dirname, missing_file); if (--count == 0 || flags & DD_DONT_WAIT_FOR_LOCK) { errno = EISDIR; /* "this is an ordinary dir, not dump dir" */ @@ -326,31 +368,26 @@ static void dd_unlock(struct dump_dir *dd) { dd->locked = 0; - unsigned dirname_len = strlen(dd->dd_dirname); - char lock_buf[dirname_len + sizeof("/.lock")]; - strcpy(lock_buf, dd->dd_dirname); - strcpy(lock_buf + dirname_len, "/.lock"); - xunlink(lock_buf); + xunlinkat(dd->dd_fd, ".lock", /*only files*/0); - log_info("Unlocked '%s'", lock_buf); + log_info("Unlocked '%s/.lock'", dd->dd_dirname); } } static inline struct dump_dir *dd_init(void) { struct dump_dir* dd = (struct dump_dir*)xzalloc(sizeof(struct dump_dir)); dd->dd_time = -1; + dd->dd_fd = -1; return dd; } -int dd_exist(const struct dump_dir *dd, const char *path) +int dd_exist(const struct dump_dir *dd, const char *name) { - if (!str_is_correct_filename(path)) - error_msg_and_die("Cannot test existence. '%s' is not a valid file name", path); + if (!str_is_correct_filename(name)) + error_msg_and_die("Cannot test existence. '%s' is not a valid file name", name); - char *full_path = concat_path_file(dd->dd_dirname, path); - int ret = exist_file_dir(full_path); - free(full_path); + const int ret = exist_file_dir_at(dd->dd_fd, name); return ret; } @@ -360,6 +397,7 @@ void dd_close(struct dump_dir *dd) return; dd_unlock(dd); + close(dd->dd_fd); if (dd->next_dir) { closedir(dd->next_dir); @@ -384,10 +422,13 @@ struct dump_dir *dd_opendir(const char *dir, int flags) struct dump_dir *dd = dd_init(); dir = dd->dd_dirname = rm_trailing_slashes(dir); - + dd->dd_fd = open(dir, O_DIRECTORY | O_NOFOLLOW); struct stat stat_buf; - if (stat(dir, &stat_buf) != 0) + if (dd->dd_fd < 0) goto cant_access; + if (fstat(dd->dd_fd, &stat_buf) != 0) + goto cant_access; + /* & 0666 should remove the executable bit */ dd->mode = (stat_buf.st_mode & 0666); @@ -397,11 +438,12 @@ struct dump_dir *dd_opendir(const char *dir, int flags) if ((flags & DD_OPEN_READONLY) && errno == EACCES) { /* Directory is not writable. If it seems to be readable, - * return "read only" dd, not NULL */ - if (stat(dir, &stat_buf) == 0 - && S_ISDIR(stat_buf.st_mode) - && access(dir, R_OK) == 0 - ) { + * return "read only" dd, not NULL + * + * Does the directory have 'x' flag? + */ + if (faccessat(dd->dd_fd, ".", R_OK, AT_SYMLINK_NOFOLLOW) == 0) + { if(dd_check(dd) != NULL) { dd_close(dd); @@ -444,10 +486,9 @@ struct dump_dir *dd_opendir(const char *dir, int flags) if (geteuid() == 0) { /* In case caller would want to create more files, he'll need uid:gid */ - struct stat stat_buf; - if (stat(dir, &stat_buf) != 0 || !S_ISDIR(stat_buf.st_mode)) + if (fstat(dd->dd_fd, &stat_buf) != 0) { - error_msg("Can't stat '%s', or it is not a directory", dir); + error_msg("Can't stat '%s'", dir); dd_close(dd); return NULL; } @@ -542,8 +583,7 @@ struct dump_dir *dd_create_skeleton(const char *dir, uid_t uid, mode_t mode, int * dd_create("dir/..") and similar are madness, refuse them. */ error_msg("Bad dir name '%s'", dir); - dd_close(dd); - return NULL; + goto fail; } /* Was creating it with mode 0700 and user as the owner, but this allows @@ -559,22 +599,31 @@ struct dump_dir *dd_create_skeleton(const char *dir, uid_t uid, mode_t mode, int if (r != 0) { perror_msg("Can't create directory '%s'", dir); - dd_close(dd); - return NULL; + goto fail; } - if (dd_lock(dd, CREATE_LOCK_USLEEP, /*flags:*/ 0) < 0) + dd->dd_fd = open(dd->dd_dirname, O_DIRECTORY | O_NOFOLLOW); + if (dd->dd_fd < 0) { - dd_close(dd); - return NULL; + perror_msg("Can't open newly created directory '%s'", dir); + goto fail; } + struct stat stat_sb; + if (fstat(dd->dd_fd, &stat_sb) < 0) + { + perror_msg("stat(%s)", dd->dd_dirname); + goto fail; + } + + if (dd_lock(dd, CREATE_LOCK_USLEEP, /*flags:*/ 0) < 0) + goto fail; + /* mkdir's mode (above) can be affected by umask, fix it */ - if (chmod(dir, dir_mode) == -1) + if (fchmod(dd->dd_fd, dir_mode) == -1) { perror_msg("Can't change mode of '%s'", dir); - dd_close(dd); - return NULL; + goto fail; } dd->dd_uid = (uid_t)-1L; @@ -616,14 +665,18 @@ struct dump_dir *dd_create_skeleton(const char *dir, uid_t uid, mode_t mode, int } return dd; + +fail: + dd_close(dd); + return NULL; } /* Resets ownership of the given directory to UID and GID according to values * in dd_create_skeleton(). */ int dd_reset_ownership(struct dump_dir *dd) { - const int r =lchown(dd->dd_dirname, dd->dd_uid, dd->dd_gid); + const int r = fchown(dd->dd_fd, dd->dd_uid, dd->dd_gid); if (r < 0) { perror_msg("Can't change '%s' ownership to %lu:%lu", dd->dd_dirname, @@ -740,59 +793,39 @@ void dd_sanitize_mode_and_owner(struct dump_dir *dd) if (!dd->locked) error_msg_and_die("dump_dir is not opened"); /* bug */ - DIR *d = opendir(dd->dd_dirname); - if (!d) - return; - - struct dirent *dent; - while ((dent = readdir(d)) != NULL) + dd_init_next_file(dd); + char *short_name; + while (dd_get_next_file(dd, &short_name, /*full_name*/ NULL)) { - if (dent->d_name[0] == '.') /* ".lock", ".", ".."? skip */ - continue; - char *full_path = concat_path_file(dd->dd_dirname, dent->d_name); - struct stat statbuf; - if (lstat(full_path, &statbuf) == 0 && S_ISREG(statbuf.st_mode)) - { - if ((statbuf.st_mode & 0777) != dd->mode) - { - /* We open the file only for fchmod() - * - * We use fchmod() because chmod() changes the permissions of - * the file specified whose pathname is given in path, which - * is dereferenced if it is a symbolic link. - */ - int fd = open(full_path, O_RDONLY | O_NOFOLLOW, dd->mode); - if (fd >= 0) - { - if (fchmod(fd, dd->mode) != 0) - { - perror_msg("Can't change '%s' mode to 0%o", full_path, - (unsigned)dd->mode); - } - close(fd); - } - else - { - perror_msg("Can't open regular file '%s'", full_path); - } - } - if (statbuf.st_uid != dd->dd_uid || statbuf.st_gid != dd->dd_gid) - { - if (lchown(full_path, dd->dd_uid, dd->dd_gid) != 0) - { - perror_msg("Can't change '%s' ownership to %lu:%lu", full_path, - (long)dd->dd_uid, (long)dd->dd_gid); - } - } - } - free(full_path); + /* The current process has to have read access at least */ + int fd = secure_openat_read(dd->dd_fd, short_name); + if (fd < 0) + goto next; + + if (fchmod(fd, dd->mode) != 0) + perror_msg("Can't change '%s/%s' mode to 0%o", dd->dd_dirname, short_name, + (unsigned)dd->mode); + + if (fchown(fd, dd->dd_uid, dd->dd_gid) != 0) + perror_msg("Can't change '%s/%s' ownership to %lu:%lu", dd->dd_dirname, short_name, + (long)dd->dd_uid, (long)dd->dd_gid); + + close(fd); +next: + free(short_name); } - closedir(d); } -static int delete_file_dir(const char *dir, bool skip_lock_file) +static int delete_file_dir(int dir_fd, bool skip_lock_file) { - DIR *d = opendir(dir); + int opendir_fd = dup(dir_fd); + if (opendir_fd < 0) + { + perror_msg("delete_file_dir: dup(dir_fd)"); + return -1; + } + + DIR *d = fdopendir(opendir_fd); if (!d) { /* The caller expects us to error out only if the directory @@ -818,56 +851,45 @@ static int delete_file_dir(const char *dir, bool skip_lock_file) unlink_lock_file = true; continue; } - char *full_path = concat_path_file(dir, dent->d_name); - if (unlink(full_path) == -1 && errno != ENOENT) + if (unlinkat(dir_fd, dent->d_name, /*only files*/0) == -1 && errno != ENOENT) { int err = 0; if (errno == EISDIR) { errno = 0; - err = delete_file_dir(full_path, /*skip_lock_file:*/ false); + int subdir_fd = openat(dir_fd, dent->d_name, O_DIRECTORY); + if (subdir_fd < 0) + { + perror_msg("Can't open sub-dir'%s'", dent->d_name); + closedir(d); + return -1; + } + else + { + err = delete_file_dir(subdir_fd, /*skip_lock_file:*/ false); + close(subdir_fd); + if (err == 0) + unlinkat(dir_fd, dent->d_name, AT_REMOVEDIR); + } } if (errno || err) { - perror_msg("Can't remove '%s'", full_path); - free(full_path); + perror_msg("Can't remove '%s'", dent->d_name); closedir(d); return -1; } } - free(full_path); } - closedir(d); /* Here we know for sure that all files/subdirs we found via readdir * were deleted successfully. If rmdir below fails, we assume someone * is racing with us and created a new file. */ if (unlink_lock_file) - { - char *full_path = concat_path_file(dir, ".lock"); - xunlink(full_path); - free(full_path); - - unsigned cnt = RMDIR_FAIL_COUNT; - do { - if (rmdir(dir) == 0) - return 0; - /* Someone locked the dir after unlink, but before rmdir. - * This "someone" must be dd_lock(). - * It detects this (by seeing that there is no time file) - * and backs off at once. So we need to just retry rmdir, - * with minimal sleep. - */ - usleep(RMDIR_FAIL_USLEEP); - } while (--cnt != 0); - } + xunlinkat(dir_fd, ".lock", /*only files*/0); - int r = rmdir(dir); - if (r) - perror_msg("Can't remove directory '%s'", dir); - return r; + return 0; } int dd_delete(struct dump_dir *dd) @@ -878,10 +900,34 @@ int dd_delete(struct dump_dir *dd) return -1; } - int r = delete_file_dir(dd->dd_dirname, /*skip_lock_file:*/ true); + if (delete_file_dir(dd->dd_fd, /*skip_lock_file:*/ true) != 0) + { + perror_msg("Can't remove contents of directory '%s'", dd->dd_dirname); + return -2; + } + + unsigned cnt = RMDIR_FAIL_COUNT; + do { + if (rmdir(dd->dd_dirname) == 0) + break; + /* Someone locked the dir after unlink, but before rmdir. + * This "someone" must be dd_lock(). + * It detects this (by seeing that there is no time file) + * and backs off at once. So we need to just retry rmdir, + * with minimal sleep. + */ + usleep(RMDIR_FAIL_USLEEP); + } while (--cnt != 0); + + if (cnt == 0) + { + perror_msg("Can't remove directory '%s'", dd->dd_dirname); + return -3; + } + dd->locked = 0; /* delete_file_dir already removed .lock */ dd_close(dd); - return r; + return 0; } int dd_chown(struct dump_dir *dd, uid_t new_uid) @@ -911,29 +957,37 @@ int dd_chown(struct dump_dir *dd, uid_t new_uid) gid_t groups_gid = pw->pw_gid; #endif - int chown_res = lchown(dd->dd_dirname, owners_uid, groups_gid); + int chown_res = fchown(dd->dd_fd, owners_uid, groups_gid); if (chown_res) - perror_msg("lchown('%s')", dd->dd_dirname); + perror_msg("fchown('%s')", dd->dd_dirname); else { dd_init_next_file(dd); - char *full_name; - while (chown_res == 0 && dd_get_next_file(dd, /*short_name*/ NULL, &full_name)) + char *short_name; + while (chown_res == 0 && dd_get_next_file(dd, &short_name, /*full_name*/ NULL)) { - log_debug("chowning %s", full_name); - chown_res = lchown(full_name, owners_uid, groups_gid); + /* The current process has to have read access at least */ + int fd = secure_openat_read(dd->dd_fd, short_name); + if (fd < 0) + goto next; + + log_debug("chowning %s", short_name); + + chown_res = fchown(fd, owners_uid, groups_gid); if (chown_res) - perror_msg("lchown('%s')", full_name); - free(full_name); + perror_msg("fchownat('%s')", short_name); + + close(fd); +next: + free(short_name); } } return chown_res; } -static char *load_text_file(const char *path, unsigned flags) +static char *load_text_from_file_descriptor(int fd, const char *path, int flags) { - int fd = open(path, O_RDONLY | ((flags & DD_OPEN_FOLLOW) ? 0 : O_NOFOLLOW)); if (fd == -1) { if (!(flags & DD_FAIL_QUIETLY_ENOENT)) @@ -988,6 +1042,20 @@ static char *load_text_file(const char *path, unsigned flags) return strbuf_free_nobuf(buf_content); } +static char *load_text_file_at(int dir_fd, const char *name, unsigned flags) +{ + assert(name[0] != '/'); + + const int fd = openat(dir_fd, name, O_RDONLY | ((flags & DD_OPEN_FOLLOW) ? 0 : O_NOFOLLOW)); + return load_text_from_file_descriptor(fd, name, flags); +} + +static char *load_text_file(const char *path, unsigned flags) +{ + const int fd = open(path, O_RDONLY | ((flags & DD_OPEN_FOLLOW) ? 0 : O_NOFOLLOW)); + return load_text_from_file_descriptor(fd, path, flags); +} + static void copy_file_from_chroot(struct dump_dir* dd, const char *name, const char *chroot_dir, const char *file_path) { char *chrooted_name = concat_path_file(chroot_dir, file_path); @@ -1001,22 +1069,26 @@ static void copy_file_from_chroot(struct dump_dir* dd, const char *name, const c } } -static bool save_binary_file(const char *path, const char* data, unsigned size, uid_t uid, gid_t gid, mode_t mode) +static bool save_binary_file_at(int dir_fd, const char *name, const char* data, unsigned size, uid_t uid, gid_t gid, mode_t mode) { + assert(name[0] != '/'); + /* the mode is set by the caller, see dd_create() for security analysis */ - unlink(path); - int fd = open(path, O_WRONLY | O_TRUNC | O_CREAT | O_NOFOLLOW, mode); + unlinkat(dir_fd, name, /*remove only files*/0); + int fd = openat(dir_fd, name, O_WRONLY | O_EXCL | O_CREAT | O_NOFOLLOW, mode); if (fd < 0) { - perror_msg("Can't open file '%s'", path); + perror_msg("Can't open file '%s'", name); return false; } if (uid != (uid_t)-1L) { if (fchown(fd, uid, gid) == -1) { - perror_msg("Can't change '%s' ownership to %lu:%lu", path, (long)uid, (long)gid); + perror_msg("Can't change '%s' ownership to %lu:%lu", name, (long)uid, (long)gid); + close(fd); + return false; } } @@ -1028,14 +1100,16 @@ static bool save_binary_file(const char *path, const char* data, unsigned size, */ if (fchmod(fd, mode) == -1) { - perror_msg("Can't change mode of '%s'", path); + perror_msg("Can't change mode of '%s'", name); + close(fd); + return false; } unsigned r = full_write(fd, data, size); close(fd); if (r != size) { - error_msg("Can't save file '%s'", path); + error_msg("Can't save file '%s'", name); return false; } @@ -1058,11 +1132,7 @@ char* dd_load_text_ext(const struct dump_dir *dd, const char *name, unsigned fla if (strcmp(name, "release") == 0) name = FILENAME_OS_RELEASE; - char *full_path = concat_path_file(dd->dd_dirname, name); - char *ret = load_text_file(full_path, flags); - free(full_path); - - return ret; + return load_text_file_at(dd->dd_fd, name, flags); } char* dd_load_text(const struct dump_dir *dd, const char *name) @@ -1078,9 +1148,7 @@ void dd_save_text(struct dump_dir *dd, const char *name, const char *data) if (!str_is_correct_filename(name)) error_msg_and_die("Cannot save text. '%s' is not a valid file name", name); - char *full_path = concat_path_file(dd->dd_dirname, name); - save_binary_file(full_path, data, strlen(data), dd->dd_uid, dd->dd_gid, dd->mode); - free(full_path); + save_binary_file_at(dd->dd_fd, name, data, strlen(data), dd->dd_uid, dd->dd_gid, dd->mode); } void dd_save_binary(struct dump_dir* dd, const char* name, const char* data, unsigned size) @@ -1091,9 +1159,7 @@ void dd_save_binary(struct dump_dir* dd, const char* name, const char* data, uns if (!str_is_correct_filename(name)) error_msg_and_die("Cannot save binary. '%s' is not a valid file name", name); - char *full_path = concat_path_file(dd->dd_dirname, name); - save_binary_file(full_path, data, size, dd->dd_uid, dd->dd_gid, dd->mode); - free(full_path); + save_binary_file_at(dd->dd_fd, name, data, size, dd->dd_uid, dd->dd_gid, dd->mode); } long dd_get_item_size(struct dump_dir *dd, const char *name) @@ -1102,21 +1168,19 @@ long dd_get_item_size(struct dump_dir *dd, const char *name) error_msg_and_die("Cannot get item size. '%s' is not a valid file name", name); long size = -1; - char *iname = concat_path_file(dd->dd_dirname, name); struct stat statbuf; + int r = fstatat(dd->dd_fd, name, &statbuf, AT_SYMLINK_NOFOLLOW); - if (lstat(iname, &statbuf) == 0 && S_ISREG(statbuf.st_mode)) + if (r == 0 && S_ISREG(statbuf.st_mode)) size = statbuf.st_size; else { if (errno == ENOENT) size = 0; else - perror_msg("Can't get size of file '%s'", iname); + perror_msg("Can't get size of file '%s'", name); } - free(iname); - return size; } @@ -1128,30 +1192,34 @@ int dd_delete_item(struct dump_dir *dd, const char *name) if (!str_is_correct_filename(name)) error_msg_and_die("Cannot delete item. '%s' is not a valid file name", name); - char *path = concat_path_file(dd->dd_dirname, name); - int res = unlink(path); + int res = unlinkat(dd->dd_fd, name, /*only files*/0); if (res < 0) { if (errno == ENOENT) errno = res = 0; else - perror_msg("Can't delete file '%s'", path); + perror_msg("Can't delete file '%s'", name); } - free(path); return res; } DIR *dd_init_next_file(struct dump_dir *dd) { // if (!dd->locked) // error_msg_and_die("dump_dir is not opened"); /* bug */ + int opendir_fd = dup(dd->dd_fd); + if (opendir_fd < 0) + { + perror_msg("dd_init_next_file: dup(dd_fd)"); + return NULL; + } if (dd->next_dir) closedir(dd->next_dir); - dd->next_dir = opendir(dd->dd_dirname); + dd->next_dir = fdopendir(opendir_fd); if (!dd->next_dir) { error_msg("Can't open directory '%s'", dd->dd_dirname); @@ -1168,7 +1236,7 @@ int dd_get_next_file(struct dump_dir *dd, char **short_name, char **full_name) struct dirent *dent; while ((dent = readdir(dd->next_dir)) != NULL) { - if (is_regular_file(dent, dd->dd_dirname)) + if (is_regular_file_at(dent, dd->dd_fd)) { if (short_name) *short_name = xstrdup(dent->d_name); @@ -1233,6 +1301,7 @@ int dd_rename(struct dump_dir *dd, const char *new_path) return -1; } + /* Keeps the opened file descriptor valid */ int res = rename(dd->dd_dirname, new_path); if (res == 0) {
src/lib/problem_data.c+3 −3 modified@@ -279,14 +279,14 @@ static const char *const always_text_files[] = { FILENAME_OS_RELEASE, NULL }; -static char* is_text_file(const char *name, ssize_t *sz) +static char* is_text_file_at(int dir_fd, const char *name, ssize_t *sz) { /* We were using magic.h API to check for file being text, but it thinks * that file containing just "0" is not text (!!) * So, we do it ourself. */ - int fd = open(name, O_RDONLY); + int fd = secure_openat_read(dir_fd, name); if (fd < 0) return NULL; /* it's not text (because it does not exist! :) */ @@ -399,7 +399,7 @@ void problem_data_load_from_dump_dir(problem_data_t *problem_data, struct dump_d } ssize_t sz = 4*1024; - char *text = is_text_file(full_name, &sz); + char *text = is_text_file_at(dd->dd_fd, short_name, &sz); if (!text || text == HUGE_TEXT) { int flag = !text ? CD_FLAG_BIN : (CD_FLAG_BIN+CD_FLAG_BIGTXT);
src/lib/xfuncs.c+18 −4 modified@@ -331,6 +331,12 @@ int xopen(const char *pathname, int flags) return xopen3(pathname, flags, 0666); } +void xunlinkat(int dir_fd, const char *pathname, int flags) +{ + if (unlinkat(dir_fd, pathname, flags)) + perror_msg_and_die("Can't remove file '%s'", pathname); +} + void xunlink(const char *pathname) { if (unlink(pathname)) @@ -359,21 +365,29 @@ int open_or_warn(const char *pathname, int flags) * do not report the type, they report DT_UNKNOWN for every dirent * (and this is not a bug in filesystem, this is allowed by standards). */ -int is_regular_file(struct dirent *dent, const char *dirname) +int is_regular_file_at(struct dirent *dent, int dir_fd) { if (dent->d_type == DT_REG) return 1; if (dent->d_type != DT_UNKNOWN) return 0; - char *fullname = xasprintf("%s/%s", dirname, dent->d_name); struct stat statbuf; - int r = lstat(fullname, &statbuf); - free(fullname); + int r = fstatat(dir_fd, dent->d_name, &statbuf, AT_SYMLINK_NOFOLLOW); return r == 0 && S_ISREG(statbuf.st_mode); } +int is_regular_file(struct dirent *dent, const char *dirname) +{ + int dir_fd = open(dirname, O_DIRECTORY); + if (dir_fd < 0) + return 0; + int r = is_regular_file_at(dent, dir_fd); + close(dir_fd); + return r; +} + /* Is it "." or ".."? */ /* abrtlib candidate */ bool dot_or_dotdot(const char *filename)
6e811d78e271dbus: process only valid sub-directories of the dump location
1 file changed · +26 −10
src/dbus/abrt-dbus.c+26 −10 modified@@ -132,18 +132,34 @@ static uid_t get_caller_uid(GDBusConnection *connection, GDBusMethodInvocation * return caller_uid; } -static bool allowed_problem_dir(const char *dir_name) +bool allowed_problem_dir(const char *dir_name) { -//HACK HACK HACK! Disabled for now until we fix clients (abrt-gui) to not pass /home/user/.cache/abrt/spool + if (!dir_is_in_dump_location(dir_name)) + { + error_msg("Bad problem directory name '%s', should start with: '%s'", dir_name, g_settings_dump_location); + return false; + } + + /* We cannot test correct permissions yet because we still need to chown + * dump directories before reporting and Chowing changes the file owner to + * the reporter, which causes this test to fail and prevents users from + * getting problem data after reporting it. + * + * Fortunately, libreport has been hardened against hard link and symbolic + * link attacks and refuses to work with such files, so this test isn't + * really necessary, however, we will use it once we get rid of the + * chowning files. + * + * abrt-server refuses to run post-create on directories that have + * incorrect owner (not "root:(abrt|root)"), incorrect permissions (other + * bits are not 0) and are complete (post-create finished). So, there is no + * way to run security sensitive event scripts (post-create) on crafted + * problem directories. + */ #if 0 - unsigned len = strlen(g_settings_dump_location); - - /* If doesn't start with "g_settings_dump_location[/]"... */ - if (strncmp(dir_name, g_settings_dump_location, len) != 0 - || (dir_name[len] != '/' && dir_name[len] != '\0') - /* or contains "/." anywhere (-> might contain ".." component) */ - || strstr(dir_name + len, "/.") - ) { + if (!dir_has_correct_permissions(dir_name)) + { + error_msg("Problem directory '%s' isn't owned by root:abrt or others are not restricted from access", dir_name); return false; } #endif
b7f8bd20b7fblib: add functions validating dump dir
6 files changed · +158 −33
src/daemon/abrt-server.c+10 −32 modified@@ -76,20 +76,6 @@ static unsigned total_bytes_read = 0; static uid_t client_uid = (uid_t)-1L; -static bool dir_is_in_dump_location(const char *dump_dir_name) -{ - unsigned len = strlen(g_settings_dump_location); - - if (strncmp(dump_dir_name, g_settings_dump_location, len) == 0 - && dump_dir_name[len] == '/' - /* must not contain "/." anywhere (IOW: disallow ".." component) */ - && !strstr(dump_dir_name + len, "/.") - ) { - return 1; - } - return 0; -} - /* Remove dump dir */ static int delete_path(const char *dump_dir_name) { @@ -100,6 +86,11 @@ static int delete_path(const char *dump_dir_name) error_msg("Bad problem directory name '%s', should start with: '%s'", dump_dir_name, g_settings_dump_location); return 400; /* Bad Request */ } + if (!dir_has_correct_permissions(dump_dir_name)) + { + error_msg("Problem directory '%s' isn't owned by root:abrt or others are not restricted from access", dump_dir_name); + return 400; /* */ + } if (!dump_dir_accessible_by_uid(dump_dir_name, client_uid)) { if (errno == ENOTDIR) @@ -154,26 +145,13 @@ static int run_post_create(const char *dirname) error_msg("Bad problem directory name '%s', should start with: '%s'", dirname, g_settings_dump_location); return 400; /* Bad Request */ } + if (!dir_has_correct_permissions(dirname)) + { + error_msg("Problem directory '%s' isn't owned by root:abrt or others are not restricted from access", dirname); + return 400; /* */ + } if (g_settings_privatereports) { - struct stat statbuf; - if (lstat(dirname, &statbuf) != 0 || !S_ISDIR(statbuf.st_mode)) - { - error_msg("Path '%s' isn't directory", dirname); - return 404; /* Not Found */ - } - /* Get ABRT's group gid */ - struct group *gr = getgrnam("abrt"); - if (!gr) - { - error_msg("Group 'abrt' does not exist"); - return 500; - } - if (statbuf.st_uid != 0 || !(statbuf.st_gid == 0 || statbuf.st_gid == gr->gr_gid) || statbuf.st_mode & 07) - { - error_msg("Problem directory '%s' isn't owned by root:abrt or others are not restricted from access", dirname); - return 403; - } struct dump_dir *dd = dd_opendir(dirname, DD_OPEN_READONLY); const bool complete = dd && problem_dump_dir_is_complete(dd); dd_close(dd);
src/include/libabrt.h+4 −0 modified@@ -47,6 +47,10 @@ char *run_unstrip_n(const char *dump_dir_name, unsigned timeout_sec); #define get_backtrace abrt_get_backtrace char *get_backtrace(const char *dump_dir_name, unsigned timeout_sec, const char *debuginfo_dirs); +#define dir_is_in_dump_location abrt_dir_is_in_dump_location +bool dir_is_in_dump_location(const char *dir_name); +#define dir_has_correct_permissions abrt_dir_has_correct_permissions +bool dir_has_correct_permissions(const char *dir_name); #define g_settings_nMaxCrashReportsSize abrt_g_settings_nMaxCrashReportsSize extern unsigned int g_settings_nMaxCrashReportsSize;
src/lib/hooklib.c+56 −0 modified@@ -427,3 +427,59 @@ char* problem_data_save(problem_data_t *pd) log_info("problem id: '%s'", problem_id); return problem_id; } + +bool dir_is_in_dump_location(const char *dir_name) +{ + unsigned len = strlen(g_settings_dump_location); + + /* The path must start with "g_settings_dump_location" */ + if (strncmp(dir_name, g_settings_dump_location, len) != 0) + { + log_debug("Bad parent directory: '%s' not in '%s'", g_settings_dump_location, dir_name); + return false; + } + + /* and must be a sub-directory of the g_settings_dump_location dir */ + const char *base_name = dir_name + len; + while (*base_name && *base_name == '/') + ++base_name; + + if (*(base_name - 1) != '/' || !str_is_correct_filename(base_name)) + { + log_debug("Invalid dump directory name: '%s'", base_name); + return false; + } + + /* and we are sure it is a directory */ + struct stat sb; + if (lstat(dir_name, &sb) < 0) + { + VERB2 perror_msg("stat('%s')", dir_name); + return errno== ENOENT; + } + + return S_ISDIR(sb.st_mode); +} + +bool dir_has_correct_permissions(const char *dir_name) +{ + if (g_settings_privatereports) + { + struct stat statbuf; + if (lstat(dir_name, &statbuf) != 0 || !S_ISDIR(statbuf.st_mode)) + { + error_msg("Path '%s' isn't directory", dir_name); + return false; + } + /* Get ABRT's group gid */ + struct group *gr = getgrnam("abrt"); + if (!gr) + { + error_msg("Group 'abrt' does not exist"); + return false; + } + if (statbuf.st_uid != 0 || !(statbuf.st_gid == 0 || statbuf.st_gid == gr->gr_gid) || statbuf.st_mode & 07) + return false; + } + return true; +}
tests/hooklib.at+85 −0 added@@ -0,0 +1,85 @@ +# -*- Autotest -*- + +AT_BANNER([hooklib]) + +AT_TESTFUN([dir_is_in_dump_location], +[[ +#include "libabrt.h" +#include <assert.h> + +void test(char *name, bool expected) +{ + if (dir_is_in_dump_location(name) != expected) + { + fprintf(stderr, "Bad: %s", name); + abort(); + } + + free(name); +} + +int main(void) +{ + g_verbose = 3; + load_abrt_conf(); + + g_verbose = 3; + + char *name; + + assert(dir_is_in_dump_location("/") == false); + + asprintf(&name, "%s", g_settings_dump_location); + test(name, false); + + asprintf(&name, "%s..evil", g_settings_dump_location); + test(name, false); + + asprintf(&name, "%s/", g_settings_dump_location); + test(name, false); + + asprintf(&name, "%s///", g_settings_dump_location); + test(name, false); + + asprintf(&name, "%s/.", g_settings_dump_location); + test(name, false); + + asprintf(&name, "%s///.", g_settings_dump_location); + test(name, false); + + asprintf(&name, "%s/./", g_settings_dump_location); + test(name, false); + + asprintf(&name, "%s/.///", g_settings_dump_location); + test(name, false); + + asprintf(&name, "%s/..", g_settings_dump_location); + test(name, false); + + asprintf(&name, "%s///..", g_settings_dump_location); + test(name, false); + + asprintf(&name, "%s/../", g_settings_dump_location); + test(name, false); + + asprintf(&name, "%s/..///", g_settings_dump_location); + test(name, false); + + asprintf(&name, "%s/good/../../../evil", g_settings_dump_location); + test(name, false); + + asprintf(&name, "%s/good..still", g_settings_dump_location); + test(name, true); + + asprintf(&name, "%s/good.new", g_settings_dump_location); + test(name, true); + + asprintf(&name, "%s/.meta", g_settings_dump_location); + test(name, true); + + asprintf(&name, "%s/..data", g_settings_dump_location); + test(name, true); + + return 0; +} +]])
tests/Makefile.am+2 −1 modified@@ -29,7 +29,8 @@ TESTSUITE_AT = \ testsuite.at \ pyhook.at \ koops-parser.at \ - ignored_problems.at + ignored_problems.at \ + hooklib.at EXTRA_DIST += $(TESTSUITE_AT) TESTSUITE = $(srcdir)/testsuite
tests/testsuite.at+1 −0 modified@@ -4,3 +4,4 @@ m4_include([koops-parser.at]) m4_include([pyhook.at]) m4_include([ignored_problems.at]) +m4_include([hooklib.at])
Vulnerability mechanics
Root cause
"Time-of-check-time-of-use (TOCTOU) race condition: the access check and subsequent file operations used the path string rather than a pinned file descriptor, allowing an attacker to replace the directory with a symlink between the check and the use."
Attack vector
A local attacker can invoke the `ChownProblemDir`, `DeleteElement`, or `DeleteProblem` D-Bus methods exposed by `abrt-dbus`, passing a path that initially passes the `dump_dir_accessible_by_uid()` check (e.g., a legitimate problem directory the user owns). During the race window between the access check and the subsequent `dd_opendir()` or `delete_dump_dir()` call, the attacker replaces the directory with a symlink pointing to an arbitrary file or directory on the system. Because the old code operated on the path string rather than a pinned file descriptor, the privileged `abrt-dbus` process would follow the symlink and delete or change ownership of the attacker-chosen target [CWE-367]. The attacker must be a local user who can create and remove problem directories under the dump location and has the ability to time the replacement.
Affected code
The vulnerability resides in the D-Bus service `abrt-dbus` (`src/dbus/abrt-dbus.c`) and the underlying `libreport` dump directory library (`src/lib/dump_dir.c`). The D-Bus methods `ChownProblemDir`, `DeleteElement`, and `DeleteProblem` accepted a user-supplied problem directory path and performed access checks using `dump_dir_accessible_by_uid()` on the path string, which is subject to a TOCTOU race. The library functions `dd_opendir()`, `dd_delete()`, and `dd_chown()` also operated on path strings without holding a file descriptor to the directory, allowing a race window where the directory could be replaced with a symlink to an arbitrary path.
What the fix does
The patches eliminate the TOCTOU race by switching from path-based operations to file-descriptor-based operations. In `abrt-dbus` [patch_id=2247018], the code now calls `dd_openfd()` to obtain a file descriptor for the problem directory before performing any access check, then uses `fdump_dir_accessible_by_uid()` and `dd_fdopendir()` on that same descriptor — ensuring the check and the use operate on the same directory inode. In `libreport` [patch_id=2247020], the `dump_dir` struct gains a `dd_fd` field, and all internal file operations (`dd_lock`, `dd_delete`, `dd_chown`, `dd_save_text`, etc.) are converted to use `*at()` family functions (e.g., `openat`, `unlinkat`, `fchown`, `fchmod`) relative to that pinned directory fd. Additionally, `secure_openat_read()` is introduced to guard against hard links and symlinks when opening files within the dump directory. The `allowed_problem_dir()` function [patch_id=2247017] now validates that the path is a direct subdirectory of the dump location via `dir_is_in_dump_location()`, and `dir_has_correct_permissions()` [patch_id=2247019] enforces ownership and permission checks on the dump directory before operations proceed.
Preconditions
- authAttacker must be a local user on the system where abrt-dbus is running
- inputAttacker must be able to create and remove problem directories under the ABRT dump location
- inputAttacker must be able to time a symlink replacement between the access check and the file operation (race window)
Generated on May 24, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- bugzilla.redhat.com/show_bug.cgimitrex_refsource_MISC
- github.com/abrt/abrt/commit/6e811d78e2719988ae291181f5b133af32ce62d8mitrex_refsource_MISC
- github.com/abrt/abrt/commit/7814554e0827ece778ca88fd90832bd4d05520b1mitrex_refsource_MISC
- github.com/abrt/abrt/commit/b7f8bd20b7fb5b72f003ae3fa647c1d75f4218b7mitrex_refsource_MISC
- github.com/abrt/libreport/commit/1951e7282043dfe1268d492aea056b554baedb75mitrex_refsource_MISC
News mentions
0No linked articles in our index yet.