/
githubmirror
/
libreport
Обзор
Документация
Войти
/
githubmirror
/
libreport
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/lib/dump_dir.c
2 605 строк
75 KB
Michal Srb
Add support for unpacking coredumps
04 фев 2024, 19:56
04 фев 2024, 19:56
0f85350
Код
Авторство
О чём код?
/* Copyright (C) 2009 Zdenek Prikryl (zprikryl@redhat.com) Copyright (C) 2009 RedHat inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include <sys/utsname.h> #include <archive.h> #include <archive_entry.h> #include <glib-unix.h> #include "internal_libreport.h" // Locking logic: // // The directory is locked by creating a symlink named .lock inside it, // whose value (where it "points to") is the pid of locking process. // We use symlink, not an ordinary file, because symlink creation // is an atomic operation. // // There are two cases where after .lock creation, we might discover // that directory is not really free: // * another process just created new directory, but didn't manage // to lock it before us. // * another process is deleting the directory, and we managed to sneak in // and create .lock after it deleted all files (including .lock) // but before it rmdir'ed the empty directory. // // Both these cases are detected by the fact that file named "time" // is not present (it must be present in any valid dump dir). // If after locking the dir we don't see time file, we remove the lock // at once and back off. What happens in concurrent processes // we interfered with? // * "create new dump dir" process just re-tries locking. // * "delete dump dir" process just retries rmdir. // // There is another case when we don't find time file: // when the directory is not really a *dump* dir - user gave us // an ordinary directory name by mistake. // We detect it by bailing out of "lock, check time file; sleep // and retry if it doesn't exist" loop using a counter. // // To make locking work reliably, it's important to set timeouts // correctly. For example, dd_create should retry locking // its newly-created directory much faster than dd_opendir // tries to lock the directory it tries to open. // How long to sleep between "symlink fails with EEXIST, // readlink fails with ENOENT" tries. Someone just unlocked the dir. // We never bail out in this case, we retry forever. // The value can be really small: #define SYMLINK_RETRY_USLEEP (10*1000) // How long to sleep when lock file with valid pid is seen by dd_opendir // (we are waiting for other process to unlock or die): #define WAIT_FOR_OTHER_PROCESS_USLEEP (500*1000) // How long to sleep when lock file with valid pid is seen by dd_create // (some idiot jumped the gun and locked the dir we just created). // Must not be the same as WAIT_FOR_OTHER_PROCESS_USLEEP (we depend on this) // and should be small (we have the priority in locking, this is OUR dir): #define CREATE_LOCK_USLEEP (10*1000) // How long to sleep after we locked a dir, found no time file // (either we are racing with someone, or it's not a dump dir) // and unlocked it; // and after how many tries to give up and declare it's not a dump dir: #define NO_TIME_FILE_USLEEP (50*1000) #define NO_TIME_FILE_COUNT 10 // How long to sleep after we unlocked an empty dir, but then rmdir failed // (some idiot jumped the gun and locked the dir we are deleting); // and after how many tries to give up: #define RMDIR_FAIL_USLEEP (10*1000) #define RMDIR_FAIL_COUNT 50 // A sub-directory of a dump directory where the meta-data such as owner are // stored. The meta-data directory must have same owner, group and mode as its // parent dump directory. It is not a fatal error, if the meta-data directory // does not exist (backward compatibility). #define META_DATA_DIR_NAME ".libreport" #define META_DATA_FILE_OWNER "owner" enum { /* Try to create meta-data dir if it does not exist */ DD_MD_GET_CREATE = 1 << 0, }; // a little trick to copy read bits from file mode to exec bit of dir mode // * mode of dump directory is in the form of 640 (no X) because we create // files more often then we play with directories // * so if we want to get real mode of the directory we have to copy the read // bits #define DD_MODE_TO_DIR_MODE(mode) ((mode) | (((mode) & 0444) >> 2)) /* Owner of trusted elements */ uid_t dd_g_super_user_uid = 0; /* Group of new dump directories */ gid_t dd_g_fs_group_gid = (gid_t)-1; 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); 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); static bool isdigit_str(const char *str) { do { if (*str < '0' || *str > '9') return false; str++; } while (*str); return true; } static bool exist_file_dir_at(int dir_fd, const char *name) { struct stat buf; if (fstatat(dir_fd, name, &buf, AT_SYMLINK_NOFOLLOW) == 0) { if (S_ISDIR(buf.st_mode) || S_ISREG(buf.st_mode)) { return true; } } return false; } /* A valid dump dir element name is correct filename and is not a name of any * internal file or directory. */ #define dd_validate_element_name(name) \ (libreport_str_is_correct_filename(name) && (strcmp(META_DATA_DIR_NAME, name) != 0)) /* nobody user should not own any file */ static int get_no_owner_uid(uid_t *uid) { struct passwd *pw = getpwnam("nobody"); if (pw == NULL) { perror_msg("can't get nobody's uid"); if (errno == 0) return -ENOENT; return -errno; } *uid = pw->pw_uid; return 0; } /* 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 *filename) { if (strchr(filename, '/')) { error_msg("Path must be file name without directory: '%s'", filename); return -EFAULT; } static char reopen_buf[sizeof("/proc/self/fd/") + 3*sizeof(int) + 1]; int path_fd = openat(dir_fd, filename, O_PATH | O_NOFOLLOW); if (path_fd < 0) return -errno; struct stat path_sb; int r = fstat(path_fd, &path_sb); if (r < 0) { perror_msg("stat"); close(path_fd); return -EINVAL; } 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)", (unsigned long)path_sb.st_nlink); close(path_fd); return -EINVAL; } 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; } static int read_number_from_file_at(int dir_fd, const char *filename, const char *typename, size_t typesz, unsigned long long min, unsigned long long max, unsigned long long *value) { const int fd = secure_openat_read(dir_fd, filename); if (fd < 0) { log_info("Can't open '%s'", filename); return fd; } int ret = 0; /* - libreport_xmalloc_read() does not count '\0' Byte * - count on sign * - count on '\n' */ const size_t max_size = typesz * 3 + 2; /* allow to read one extra Byte to be able to identify longer text */ size_t total_read = max_size + 1; char *const value_buf = libreport_xmalloc_read(fd, &total_read); /* Just reading, so no need to check the returned value. */ if (value_buf == NULL) { log_info("Can't read from '%s'", filename); ret = -EBADFD; goto finito; } if (total_read >= max_size) { log_info("File '%s' is too long to be valid %s " "(max size %u)", filename, typename, (int)sizeof(value_buf)); ret = -EMSGSIZE; goto finito; } /* Our tools don't put trailing newline into one line files, * but we allow such format too: */ if (total_read != 0 && value_buf[total_read - 1] == '\n') --total_read; value_buf[total_read] = '\0'; const int neg = (value_buf[0] == '-'); /* if min equals 0, then we shall be to converting an unsigned number */ if (neg && 0 == min) { log_info("File '%s' contains a negative number ('%s')", filename, value_buf); ret = -ERANGE; goto finito; } errno = 0; /* To distinguish success/failure after call */ char *endptr; const unsigned long long res = strtoull(value_buf, &endptr, /* base */ 10); /* Check for various possible errors */ if (errno == ERANGE && (res == 0 || res == ULLONG_MAX)) { log_info("File '%s' contains a number out-of-range of %s " "('%s')", filename, typename, value_buf); ret = -ERANGE; goto finito; } if ( (errno != 0 && res == 0) || (*endptr != '\0') || endptr == value_buf ) { log_info("File '%s' doesn't contain valid %s " "('%s')", filename, typename, value_buf); ret = -EINVAL; goto finito; } if ((neg ? res < min : res > max)) { log_info("File '%s' contains a number ('%s') %s of %s", filename, value_buf, neg ? "lower than minimum" : "greater than maximum", typename); ret = -ERANGE; goto finito; } *value = res; finito: close(fd); g_free(value_buf); /* If we got here, strtoll() successfully parsed a number */ return ret; } /* 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_at(int dir_fd, const char *filename) { /* Note that on some architectures (x32) time_t is "long long" */ const long long MAX_TIME_T = (1ULL << (sizeof(time_t)*8 - 1)) - 1; unsigned long long value = (time_t)-1; read_number_from_file_at(dir_fd, filename, "unix time stamp", sizeof(time_t), 0, MAX_TIME_T, &value); return (time_t)value; } /* Return values: * -1: error (in this case, errno is 0 if error message is already logged) * 0: failed to lock (someone else has it locked) * 1: success */ int create_symlink_lockfile_at(int dir_fd, const char* lock_file, const char* pid) { while (symlinkat(pid, dir_fd, lock_file) != 0) { if (errno != EEXIST) { if (errno != ENOENT && errno != ENOTDIR && errno != EACCES) { perror_msg("Can't create lock file '%s'", lock_file); errno = 0; } return -1; } char pid_buf[sizeof(pid_t)*3 + 4]; ssize_t r = readlinkat(dir_fd, lock_file, pid_buf, sizeof(pid_buf) - 1); if (r < 0) { if (errno == ENOENT) { /* Looks like lock_file was deleted */ usleep(SYMLINK_RETRY_USLEEP); /* avoid CPU eating loop */ continue; } perror_msg("Can't read lock file '%s'", lock_file); errno = 0; return -1; } pid_buf[r] = '\0'; if (strcmp(pid_buf, pid) == 0) { log_warning("Lock file '%s' is already locked by us", lock_file); errno = EALREADY; return 0; } if (isdigit_str(pid_buf)) { char pid_str[sizeof("/proc/") + sizeof(pid_buf)]; snprintf(pid_str, sizeof(pid_str), "/proc/%s", pid_buf); if (access(pid_str, F_OK) == 0) { log_warning("Lock file '%s' is locked by process %s", lock_file, pid_buf); return 0; } log_warning("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 (unlinkat(dir_fd, lock_file, /*only files*/0) != 0 && errno != ENOENT) { perror_msg("Can't remove stale lock file '%s'", lock_file); errno = 0; return -1; } } log_info("Locked '%s'", lock_file); 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) { dd->dd_time = parse_time_file_at(dd->dd_fd, FILENAME_TIME); if (dd->dd_time < 0) { log_debug("Missing file: "FILENAME_TIME); return FILENAME_TIME; } /* Do not warn about missing 'type' file in non-verbose modes. * * Handling of FILENAME_TYPE should be consistent with handling of * FILENAME_TIME in the function parse_time_file_at() where the missing * file message is printed by log_info() (in a verbose mode). */ int load_flags = DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE; if (libreport_g_verbose < 2) load_flags |= DD_FAIL_QUIETLY_ENOENT; dd->dd_type = load_text_file_at(dd->dd_fd, FILENAME_TYPE, load_flags); if (!dd->dd_type || (strlen(dd->dd_type) == 0)) { log_debug("Missing or empty file: "FILENAME_TYPE); return FILENAME_TYPE; } return NULL; } static int dd_lock(struct dump_dir *dd, unsigned sleep_usec, int flags) { if (dd->locked) error_msg_and_die("Locking bug on '%s'", dd->dd_dirname); char pid_buf[sizeof(long)*3 + 2]; snprintf(pid_buf, sizeof(pid_buf), "%lu", (long)getpid()); unsigned count = NO_TIME_FILE_COUNT; retry: while (1) { int r = create_symlink_lockfile_at(dd->dd_fd, ".lock", pid_buf); if (r < 0) return r; /* error */ if (r > 0 || errno == EALREADY) break; /* locked successfully */ if (flags & DD_DONT_WAIT_FOR_LOCK) { errno = EAGAIN; return -1; } /* Other process has the lock, wait for it to go away */ usleep(sleep_usec); } /* Reset errno to 0 only if errno is EALREADY (used by * create_symlink_lockfile() to signal that the dump directory is already * locked by us) */ if (!(dd->owns_lock = (errno != EALREADY))) errno = 0; /* Are we called by dd_opendir (as opposed to dd_create)? */ if (sleep_usec == WAIT_FOR_OTHER_PROCESS_USLEEP) /* yes */ { const char *missing_file = dd_check(dd); /* some of the required files don't exist. We managed to lock the directory * which was just created by somebody else, or is almost deleted * by delete_file_dir. * Unlock and back off. */ if (missing_file) { if (dd->owns_lock) libreport_xunlinkat(dd->dd_fd, ".lock", /*only files*/0); log_notice("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" */ return -1; } usleep(NO_TIME_FILE_USLEEP); goto retry; } } dd->locked = true; return 0; } static void dd_unlock(struct dump_dir *dd) { if (dd->locked) { if (dd->owns_lock) libreport_xunlinkat(dd->dd_fd, ".lock", /*only files*/0); dd->owns_lock = 0; dd->locked = 0; log_info("Unlocked '%s/.lock'", dd->dd_dirname); } } static inline struct dump_dir *dd_init(void) { struct dump_dir* dd = g_new0(struct dump_dir, 1); dd->dd_time = (time_t)-1; dd->dd_fd = -1; dd->dd_md_fd = -1; return dd; } int dd_exist(const struct dump_dir *dd, const char *name) { if (!dd_validate_element_name(name)) error_msg_and_die("Cannot test existence. '%s' is not a valid file name", name); const int ret = exist_file_dir_at(dd->dd_fd, name); return ret; } static void dd_close_meta_data_dir(struct dump_dir *dd) { if (dd->dd_md_fd < 0) return; close(dd->dd_md_fd); dd->dd_md_fd = -1; } void dd_close(struct dump_dir *dd) { if (!dd) return; dd_unlock(dd); if (dd->dd_fd >= 0) close(dd->dd_fd); dd_close_meta_data_dir(dd); dd_clear_next_file(dd); g_free(dd->dd_type); g_free(dd->dd_dirname); g_free(dd); } static int dd_create_subdir(int dd_fd, const char *dirname, uid_t dd_uid, gid_t dd_gid, mode_t dd_mode) { if (mkdirat(dd_fd, dirname, dd_mode) < 0) { perror_msg("Can't create directory '%s'", dirname); return -1; } int dd_md_fd = openat(dd_fd, dirname, O_DIRECTORY | O_NOFOLLOW); if (dd_md_fd < 0) { perror_msg("Can't open newly created directory '%s'", dirname); goto fail_open; } if (dd_uid != (uid_t)-1) { if (fchown(dd_md_fd, dd_uid, dd_gid) != 0) { perror_msg("Can't change owner and group of '%s'", dirname); goto fail_modify; } } /* mkdir's mode (above) can be affected by umask, fix it */ if (fchmod(dd_md_fd, dd_mode) == -1) { perror_msg("Can't change mode of '%s'", dirname); goto fail_modify; } return dd_md_fd; fail_modify: close(dd_md_fd); fail_open: if (unlinkat(dd_fd, dirname, AT_REMOVEDIR) < 0) perror_msg("Fialed to unlink '%s' while cleaning up after failure", dirname); return -1; } /* Opens the meta-data directory, checks its file system attributes and returns * its file descriptor. * * The meta-data directory must have the same file system attributes as the * parent dump directory in order to avoid unexpected situations and detects * program errors (it is an error to modify bits of the dump directory and * forgot to update the meta-data directory). * * Keep on mind that the old dump directories might miss the meta-data directory * so the return value -ENOENT does not necessarily need to be fatal. */ static int dd_open_meta_data_dir(struct dump_dir *dd) { int md_dir_fd = openat(dd->dd_fd, META_DATA_DIR_NAME, O_DIRECTORY | O_NOFOLLOW); if (md_dir_fd < 0) { md_dir_fd = -errno; /* ENOENT is not critical */ if (errno != ENOENT) log_warning("Can't open meta-data '"META_DATA_DIR_NAME"'"); else log_info("The dump dir doesn't contain '"META_DATA_DIR_NAME"'"); goto finito; } struct stat md_sb; if (fstat(md_dir_fd, &md_sb) < 0) { log_debug("Can't stat '"META_DATA_DIR_NAME"'"); goto fail; } /* Test only permission bits, ignore SUID, SGID, etc. */ const mode_t md_mode = md_sb.st_mode & 0777; const mode_t dd_mode = DD_MODE_TO_DIR_MODE(dd->mode); if ( md_sb.st_uid != dd->dd_uid || md_sb.st_gid != dd->dd_gid || md_mode != dd_mode) { log_debug("'"META_DATA_DIR_NAME"' has different attributes than the dump dir, '%d'='%d', '%d'='%d', %o = %o", md_sb.st_uid, dd->dd_uid, md_sb.st_gid, dd->dd_gid, md_mode, dd_mode); goto fail; } finito: return md_dir_fd; fail: close(md_dir_fd); return -EINVAL; } /* Returns a file descriptor to the meta-data directory. Can be configured to * create the directory if it does not exist. * * This function enables lazy initialization of the meta-data directory. */ static int dd_get_meta_data_dir_fd(struct dump_dir *dd, int flags) { if (dd->dd_md_fd < 0) { dd->dd_md_fd = dd_open_meta_data_dir(dd); if ( dd->dd_md_fd == -ENOENT && (flags & DD_MD_GET_CREATE)) { dd->dd_md_fd = dd_create_subdir(dd->dd_fd, META_DATA_DIR_NAME, dd->dd_uid, dd->dd_gid, DD_MODE_TO_DIR_MODE(dd->mode)); } } return dd->dd_md_fd; } /* Tries to safely overwrite the existing file. * * The functions writes the new value to a temporary file and if the temporary * file is successfully created, then moves the tmp file to the old file name. * * If the meta-data directory does not exist, the function will try to create * it. */ static int dd_meta_data_save_text(struct dump_dir *dd, const char *name, const char *data) { if (!dd->locked) error_msg_and_die("dump_dir is not opened"); /* bug */ if (!libreport_str_is_correct_filename(name)) error_msg_and_die("Cannot save meta-data. '%s' is not a valid file name", name); int dd_md_fd = dd_get_meta_data_dir_fd(dd, DD_MD_GET_CREATE); if (dd_md_fd < 0) { error_msg("Can't save meta-data: '%s'", name); return dd_md_fd; } g_autofree char *tmp_name = g_strdup_printf("~%s.tmp", name); int ret = -1; if (!save_binary_file_at(dd_md_fd, tmp_name, data, strlen(data), dd->dd_uid, dd->dd_gid, dd->mode)) return ret; /* man 2 rename * * If newpath already exists it will be atomically replaced (subject to a * few conditions; see ERRORS below), so that there is no point at which * another process attempting to access newpath will find it missing. */ if (renameat(dd_md_fd, tmp_name, dd_md_fd, name)) { ret = -errno; perror_msg("Failed to move temporary file '%s' to '%s'", tmp_name, name); return ret; } ret = 0; return ret; } int dd_set_owner(struct dump_dir *dd, uid_t owner) { /* I was tempted to use the keyword static, but we should have reentracy * always on mind. Who knows! */ char long_str[sizeof(long) * 3 + 2]; if (owner == (uid_t)-1) owner = dd->dd_uid; snprintf(long_str, sizeof(long_str), "%li", (long)owner); const int ret = dd_meta_data_save_text(dd, META_DATA_FILE_OWNER, long_str); if (ret < 0) error_msg("The dump dir owner wasn't set to '%s'", long_str); return ret; } int dd_set_no_owner(struct dump_dir *dd) { uid_t no_owner_uid = (uid_t)-1; int ret = get_no_owner_uid(&no_owner_uid); if (ret < 0) return ret; return dd_set_owner(dd, no_owner_uid); } uid_t dd_get_owner(struct dump_dir *dd) { static const long long MAX_UID_T = (1ULL << (sizeof(uid_t)*8 - 1)) - 1; int dd_md_fd = dd_get_meta_data_dir_fd(dd, /*no create*/0); if (dd_md_fd < 0) { errno = 0; log_info("No meta-data, using fs owner."); return dd->dd_uid; } unsigned long long owner = 0; int ret = read_number_from_file_at(dd_md_fd, META_DATA_FILE_OWNER, "UID", sizeof(uid_t), 0, MAX_UID_T, &owner); if (ret < 0) { if (ret != -ENOENT) { errno = -ret; return (uid_t)-1; } log_info("No meta-data 'owner', using fs owner."); owner = dd->dd_uid; } errno = 0; return (uid_t)owner; } time_t dd_get_first_occurrence(struct dump_dir *dd) { if (dd->dd_time == (time_t)-1) { log_debug("The dump directory wasn't opened"); errno = ENODATA; } return dd->dd_time; } time_t dd_get_last_occurrence(struct dump_dir *dd) { const time_t first_occurrence = dd_get_first_occurrence(dd); if (first_occurrence == (time_t)-1) return -1; /* Do not touch errno in case of errors, because this function claims * to set errno only to ENODATA */ const int old_errno = errno; time_t last_occurrence = parse_time_file_at(dd->dd_fd, FILENAME_LAST_OCCURRENCE); if (last_occurrence < first_occurrence) { log_info("Adapting '%s' to monotonic time (+%lds)", FILENAME_LAST_OCCURRENCE, first_occurrence - last_occurrence); last_occurrence = first_occurrence; } else if (last_occurrence < 0) last_occurrence = first_occurrence; errno = old_errno; return last_occurrence; } /* A helper function useful for traversing directories. * * DIR* d opendir(dir_fd); ... closedir(d); closes also dir_fd but we want to * keep it opened. */ static int fdreopen(int dir_fd, DIR **d) { int opendir_fd = dup(dir_fd); if (opendir_fd < 0) { perror_msg("dup(dir_fd)"); return -EBADFD; } lseek(opendir_fd, 0, SEEK_SET); *d = fdopendir(opendir_fd); if (!*d) { int ret = -errno; close(opendir_fd); perror_msg("fdopendir(dir_fd)"); return ret; } /* 'opendir_fd' will be closed with 'd' */ return 0; } /* A macro for going through the entries of a directory referenced as a file * descriptor. * * Usage: * * FOREACH_REGULAR_FILE_AS_FD_AT_BEGIN(dir_fd) * { * printf("Short name '%s'", dent->d_name); * printf("File descriptor %d", fd); * } * FOREACH_REGULAR_FILE_AS_FD_AT_END */ #define FOREACH_REGULAR_FILE_AS_FD_AT_BEGIN(dir_fd) \ DIR *d; \ struct dirent *dent; \ if (fdreopen(dir_fd, &d) < 0) return -1; \ while ((dent = readdir(d)) != NULL) \ { \ if (libreport_dot_or_dotdot(dent->d_name)) continue; \ int fd = secure_openat_read(dirfd(d), dent->d_name); \ if (fd >= 0) \ { #define FOREACH_REGULAR_FILE_AS_FD_AT_END \ close(fd); \ } \ } \ closedir(d); /* Sets attributes of the meta-data directory and its contents to the same * attributes of the parent dump directory. */ static int dd_sanitize_mode_meta_data(struct dump_dir *dd) { if (!dd->locked) error_msg_and_die("%s: dump_dir is not opened", __func__); /* bug */ int dd_md_fd = dd_get_meta_data_dir_fd(dd, /*no create*/0); if (dd_md_fd < 0) return 0; int res = fchmod(dd_md_fd, DD_MODE_TO_DIR_MODE(dd->mode)); if (res < 0) { perror_msg("Failed to chmod meta-data sub-dir"); return res; } FOREACH_REGULAR_FILE_AS_FD_AT_BEGIN(dd_md_fd) { log_debug("chmoding %s", dent->d_name); res = fchmod(fd, dd->mode); if (res) { perror_msg("fchmod('%s')", dent->d_name); break; } } FOREACH_REGULAR_FILE_AS_FD_AT_END return 0; } /* Sets owner and group of the meta-data directory and its contents to the same * attributes of the parent dump directory. */ static int dd_chown_meta_data(struct dump_dir *dd, uid_t uid, gid_t gid) { if (!dd->locked) error_msg_and_die("%s: dump_dir is not opened", __func__); /* bug */ int dd_md_fd = dd_get_meta_data_dir_fd(dd, /*no create*/0); if (dd_md_fd < 0) return 0; int res = fchown(dd_md_fd, uid, gid); if (res < 0) { perror_msg("Failed to chown meta-data sub-dir"); return res; } FOREACH_REGULAR_FILE_AS_FD_AT_BEGIN(dd_md_fd) { log_debug("%s: chowning %s", __func__, dent->d_name); res = fchown(fd, uid, gid); if (res) { perror_msg("fchown('%s')", dent->d_name); break; } } FOREACH_REGULAR_FILE_AS_FD_AT_END return res; } static char* rm_trailing_slashes(const char *dir) { unsigned len = strlen(dir); while (len != 0 && dir[len-1] == '/') len--; return g_strndup(dir, len); } static struct dump_dir *dd_do_open(struct dump_dir *dd, const char *dir, int flags) { if (dir != NULL) { dd->dd_dirname = rm_trailing_slashes(dir); /* dd_do_open validates dd_fd */ dd->dd_fd = open(dd->dd_dirname, O_DIRECTORY | O_NOFOLLOW); struct stat stat_buf; 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); /* We want to have dd_uid and dd_gid always initialized. But we have to * initialize it in the way which does not prevent non-privileged user * from saving data in their dump directories. * * Non-privileged users are not allowed to change the group to * 'abrt' so we have to use their GID. * * If the caller is super-user, we have to use dd's fs owner and fs * group, because he can do everything and the data must be readable by * the real owner. * * We always use fs uid, because non-privileged users must own the * directory and super-user must use fs owner. */ dd->dd_uid = stat_buf.st_uid; /* We use fs group only if the caller is super-user, because we want to * make sure non-privileged users can modify elements (libreport call * chown(dd_uid, dd_gid) after modifying an element) and the modified * elements do not have super-user's group. */ dd->dd_gid = getegid(); if (geteuid() == 0) dd->dd_gid = stat_buf.st_gid; if ((flags & DD_OPEN_FD_ONLY)) { dd->dd_md_fd = dd_open_meta_data_dir(dd); return dd; } } errno = 0; if (dd_lock(dd, WAIT_FOR_OTHER_PROCESS_USLEEP, flags) < 0) { if (errno == EISDIR) { /* EISDIR: dd_lock can lock the dir, but it sees no time file there, * even after it retried many times. It must be an ordinary directory! * * Without this check, e.g. abrt-action-print happily prints any current * directory when run without arguments, because its option -d DIR * defaults to "."! */ error_msg("'%s' is not a problem directory", dd->dd_dirname); goto fail_with_close; } if (errno == EAGAIN && (flags & DD_DONT_WAIT_FOR_LOCK)) { log_debug("Can't access locked directory '%s'", dd->dd_dirname); goto fail_with_close; } if (!(flags & DD_OPEN_READONLY)) { log_debug("'%s' can't be opened for writing", dd->dd_dirname); goto fail_with_close; } if (errno != EACCES) { VERB3 perror_msg("failed to lock dump directory '%s'", dd->dd_dirname); goto fail_with_close; } /* Directory is not writable. If it seems to be readable, * return "read only" dd, not NULL * * Does the directory have 'r' flag? */ if (faccessat(dd->dd_fd, ".", R_OK, AT_SYMLINK_NOFOLLOW) != 0) { VERB3 perror_msg("failed to lock dump directory '%s'", dd->dd_dirname); goto fail_with_close; } /* dd_check prints out good log messages */ if(dd_check(dd) != NULL) goto fail_with_close; /* The dd is opened in READONLY moded, continue.*/ } return dd; cant_access: if (errno == ENOENT || errno == ENOTDIR) { if (!(flags & DD_FAIL_QUIETLY_ENOENT)) error_msg("'%s' does not exist", dd->dd_dirname); } else { if (!(flags & DD_FAIL_QUIETLY_EACCES)) perror_msg("Can't access '%s'", dd->dd_dirname); } fail_with_close: dd_close(dd); return NULL; } struct dump_dir *dd_fdopendir(struct dump_dir *dd, int flags) { if ((flags & DD_OPEN_FD_ONLY)) error_msg_and_die("the passed flags must not contain DD_OPEN_FD_ONLY"); if (dd->dd_fd < 0) error_msg_and_die("the dump directory was not initialized yet"); if (dd->locked) error_msg_and_die("the dump directory is already locked"); return dd_do_open(dd, NULL, flags); } struct dump_dir *dd_opendir(const char *dir, int flags) { struct dump_dir *dd = dd_init(); return dd_do_open(dd, dir, flags); } /* Create a fresh empty debug dump dir which is owned bu the calling user. If * you want to create the directory with meaningful ownership you should * consider using dd_create() function or you can modify the ownership * afterwards by calling dd_reset_ownership() function. * * ABRT owns dump dir: * We should not allow users to write new files or write into existing ones, * but they should be able to read them. * * We set dir's gid to passwd(uid)->pw_gid parameter, and we set uid to * abrt's user id. We do not allow write access to group. We can't set dir's * uid to crashed applications's user uid because owner can modify dir's * mode and ownership. * * Advantages: * Safeness * * Disadvantages: * This approach leads to stealing of directories because events requires * write access to a dump directory and events are run under non root (abrt) * user while reporting. * * This approach allows group members to see crashes of other members. * Institutions like schools uses one common group for all students. * * User owns dump dir: * We grant ownership of dump directories to the user (read/write access). * * We set set dir's uid to crashed applications's user uid, and we set gid to * abrt's group id. We allow write access to group because we want to allow * abrt binaries to process dump directories. * * Advantages: * No disadvantages from the previous approach * * Disadvantages: * In order to protect the system dump directories must be saved on * noncritical filesystem (e.g. /tmp or /var/tmp). * * * @param uid * Crashed application's User Id * * We currently have only three callers: * kernel oops hook: uid -> not saved, so everyone can steal and work with it * this hook runs under 0:0 * * ccpp hook: uid=uid of crashed user's binary * this hook runs under 0:0 * * create_dump_dir_from_problem_data() function: * Currently known callers: * abrt server: uid=uid of user's executable * this runs under 0:0 * - clinets: python hook, ruby hook * abrt dbus: uid=uid of user's executable * this runs under 0:0 * - clients: setroubleshootd, abrt python */ struct dump_dir *dd_create_skeleton(const char *dir, uid_t uid, mode_t mode, int flags) { mode_t dir_mode = DD_MODE_TO_DIR_MODE(mode); struct dump_dir *dd = dd_init(); dd->mode = mode; /* Unlike dd_opendir, can't use realpath: the directory doesn't exist yet, * realpath will always return NULL. We don't really have to: * dd_opendir(".") makes sense, dd_create(".") does not. */ dir = dd->dd_dirname = rm_trailing_slashes(dir); const char *last_component = strrchr(dir, '/'); if (last_component) last_component++; else last_component = dir; if (libreport_dot_or_dotdot(last_component)) { /* dd_create("."), dd_create(".."), dd_create("dir/."), * dd_create("dir/..") and similar are madness, refuse them. */ error_msg("Bad dir name '%s'", dir); goto fail; } /* Was creating it with mode 0700 and user as the owner, but this allows * the user to replace any file in the directory, changing security-sensitive data * (e.g. "uid", "analyzer", "executable") */ int r; if ((flags & DD_CREATE_PARENTS)) r = g_mkdir_with_parents(dd->dd_dirname, dir_mode); else r = mkdir(dd->dd_dirname, dir_mode); if (r != 0) { perror_msg("Can't create directory '%s'", dir); goto fail; } dd->dd_fd = open(dd->dd_dirname, O_DIRECTORY | O_NOFOLLOW); if (dd->dd_fd < 0) { 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 (fchmod(dd->dd_fd, dir_mode) == -1) { perror_msg("Can't change mode of '%s'", dir); goto fail; } /* Initiliaze dd_uid and dd_gid to sane values which reflect the reality. */ dd->dd_uid = stat_sb.st_uid; dd->dd_gid = stat_sb.st_gid; /* Create META-DATA directory with real fs attributes which must be changed * in dd_reset_ownership(), when populating of a new dump directory is * done. * * It allows daemons to create a dump directory, populate the directory as * root and then switch the ownership to the real user. */ dd->dd_md_fd = dd_create_subdir(dd->dd_fd, META_DATA_DIR_NAME, dd->dd_uid, dd->dd_gid, dir_mode); if (dd->dd_md_fd < 0) { error_msg("Can't create meta-data directory"); goto fail; } if (dd_set_owner(dd, dd->dd_uid) < 0) { log_debug("Failed to initialized 'owner'"); goto fail; } if (uid != (uid_t)-1L) { dd->dd_uid = 0; dd->dd_gid = 0; #if DUMP_DIR_OWNED_BY_USER > 0 /* Check crashed application's uid */ struct passwd *pw = getpwuid(uid); if (pw) dd->dd_uid = pw->pw_uid; else error_msg("User %lu does not exist, using uid 0", (long)uid); if (dd_g_fs_group_gid == (uid_t)-1) { /* Get ABRT's group gid */ struct group *gr = getgrnam("abrt"); if (gr) dd->dd_gid = gr->gr_gid; else error_msg("Group 'abrt' does not exist, using gid 0"); } else dd->dd_gid = dd_g_fs_group_gid; #else /* Get ABRT's user uid */ struct passwd *pw = getpwnam("abrt"); if (pw) dd->dd_uid = pw->pw_uid; else error_msg("User 'abrt' does not exist, using uid 0"); /* Get crashed application's gid */ pw = getpwuid(uid); if (pw) dd->dd_gid = pw->pw_gid; else error_msg("User %lu does not exist, using gid 0", (long)uid); #endif } /* Initialize dd_time to some sane value */ dd->dd_time = time(NULL); 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) { if (!dd->locked) error_msg_and_die("dump_dir is not opened"); /* bug */ 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, (long)dd->dd_uid, (long)dd->dd_gid); } if (dd_chown_meta_data(dd, dd->dd_uid, dd->dd_gid) != 0) error_msg("Failed to reset ownership of meta-data"); /* We ignore failures above, so we will ignore failures here too. * The meta-data owner already exist (created by dd_create_skeleton). */ dd_set_owner(dd, dd->dd_uid); return r; } /* Calls dd_create_skeleton() and dd_reset_ownership(). */ struct dump_dir *dd_create(const char *dir, uid_t uid, mode_t mode) { struct dump_dir *dd = dd_create_skeleton(dir, uid, mode, DD_CREATE_PARENTS); if (dd == NULL) return NULL; /* ignore results */ dd_reset_ownership(dd); return dd; } void dd_create_basic_files(struct dump_dir *dd, uid_t uid, const char *chroot_dir) { char long_str[sizeof(long) * 3 + 2]; const time_t t = parse_time_file_at(dd->dd_fd, FILENAME_TIME); if (t < 0) { sprintf(long_str, "%lu", (long)dd->dd_time); /* first occurrence */ dd_save_text(dd, FILENAME_TIME, long_str); /* last occurrence */ dd_save_text(dd, FILENAME_LAST_OCCURRENCE, long_str); } else { dd->dd_time = t; } /* it doesn't make sense to create the uid file if uid == -1 */ /* and 'owner' is set since dd_create_skeleton */ if (uid != (uid_t)-1L) { /* Failure is not a problem here, because we still have the fs * attributes and there is only a little chance that the old value * gets lost. */ dd_set_owner(dd, uid); snprintf(long_str, sizeof(long_str), "%li", (long)uid); dd_save_text(dd, FILENAME_UID, long_str); } struct utsname buf; uname(&buf); /* never fails */ /* Check if files already exist in dumpdir as they might have * more relevant information about the problem */ if (!dd_exist(dd, FILENAME_KERNEL)) dd_save_text(dd, FILENAME_KERNEL, buf.release); if (!dd_exist(dd, FILENAME_ARCHITECTURE)) dd_save_text(dd, FILENAME_ARCHITECTURE, buf.machine); if (!dd_exist(dd, FILENAME_HOSTNAME)) dd_save_text(dd, FILENAME_HOSTNAME, buf.nodename); char *release = load_text_file("/etc/os-release", DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE | DD_OPEN_FOLLOW); if (release) { dd_save_text(dd, FILENAME_OS_INFO, release); g_clear_pointer(&release, g_free); } if (chroot_dir) copy_file_from_chroot(dd, FILENAME_OS_INFO_IN_ROOTDIR, chroot_dir, "/etc/os-release"); /* if release exists in dumpdir don't create it, but don't warn * if it doesn't * i.e: anaconda doesn't have /etc/{fedora,redhat}-release and trying to load it * results in errors: rhbz#725857 */ release = dd_load_text_ext(dd, FILENAME_OS_RELEASE, DD_FAIL_QUIETLY_ENOENT | DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE); if (!release) { release = load_text_file("/etc/system-release", DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE | DD_OPEN_FOLLOW); if (!release) release = load_text_file("/etc/redhat-release", DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE | DD_OPEN_FOLLOW); if (!release) release = load_text_file("/etc/SuSE-release", DD_OPEN_FOLLOW); char *newline = strchr(release, '\n'); if (newline) *newline = '\0'; dd_save_text(dd, FILENAME_OS_RELEASE, release); if (chroot_dir) copy_file_from_chroot(dd, FILENAME_OS_RELEASE_IN_ROOTDIR, chroot_dir, "/etc/system-release"); } g_free(release); } void dd_sanitize_mode_and_owner(struct dump_dir *dd) { /* Don't sanitize if we aren't run under root: * we assume that during file creation (by whatever means, * even by "hostname >file" in abrt_event.conf) * normal umask-based mode setting takes care of correct mode, * and uid:gid is, of course, set to user's uid and gid. * * For root operating on /var/spool/abrt/USERS_PROBLEM, this isn't true: * "hostname >file", for example, would create file OWNED BY ROOT! * This routine resets mode and uid:gid for all such files. */ if (dd->dd_uid == (uid_t)-1) return; if (!dd->locked) error_msg_and_die("dump_dir is not opened"); /* bug */ dd_init_next_file(dd); char *short_name; while (dd_get_next_file(dd, &short_name, /*full_name*/ NULL)) { /* 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: g_free(short_name); } /* No need to check return value, the functions print good messages. * There are two approaches for handling errors in libreport: * - print out a warning message and keep status quo * - terminate the process */ dd_sanitize_mode_meta_data(dd); dd_chown_meta_data(dd, dd->dd_uid, dd->dd_gid); } static int delete_file_dir(int dir_fd, bool skip_lock_file) { DIR *d; int ret = fdreopen(dir_fd, &d); if (ret < 0) { /* The caller expects us to error out only if the directory * still exists (not deleted). If directory * *doesn't exist*, return 0 and clear errno. */ if (ret == -ENOENT || ret == -ENOTDIR) { errno = 0; return 0; } return -1; } bool unlink_lock_file = false; struct dirent *dent; while ((dent = readdir(d)) != NULL) { if (libreport_dot_or_dotdot(dent->d_name)) continue; if (skip_lock_file && strcmp(dent->d_name, ".lock") == 0) { unlink_lock_file = true; continue; } if (unlinkat(dir_fd, dent->d_name, /*only files*/0) == -1 && errno != ENOENT) { int err = 0; if (errno == EISDIR) { errno = 0; 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'", dent->d_name); closedir(d); return -1; } } } /* 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) libreport_xunlinkat(dir_fd, ".lock", /*only files*/0); closedir(d); return 0; } static int dd_delete_meta_data(struct dump_dir *dd) { if (!dd->locked) { error_msg("Can't remove meta-data of unlocked problem directory %s", dd->dd_dirname); return -1; } int dd_md_fd = dd_get_meta_data_dir_fd(dd, /*no create*/0); if (dd_md_fd < 0) return 0; if (delete_file_dir(dd_md_fd, /*skip_lock_file:*/ true) != 0) { perror_msg("Can't remove meta-data from '"META_DATA_DIR_NAME"'"); return -2; } dd_close_meta_data_dir(dd); if (unlinkat(dd->dd_fd, META_DATA_DIR_NAME, AT_REMOVEDIR)) { perror_msg("Can't remove meta-data directory '"META_DATA_DIR_NAME"'"); return -3; } return 0; } int dd_delete(struct dump_dir *dd) { int retval; retval = 0; if (!dd->locked) { error_msg("unlocked problem directory %s cannot be deleted", dd->dd_dirname); retval = -1; goto close; } if (dd_delete_meta_data(dd) != 0) { retval = -2; goto close; } if (delete_file_dir(dd->dd_fd, /*skip_lock_file:*/ true) != 0) { perror_msg("Can't remove contents of directory '%s'", dd->dd_dirname); retval = -2; goto close; } 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); retval = -3; } dd->locked = 0; /* delete_file_dir already removed .lock */ close: dd_close(dd); return retval; } int dd_chown(struct dump_dir *dd, uid_t new_uid) { if (!dd->locked) error_msg_and_die("dump_dir is not opened"); /* bug */ struct stat statbuf; if (fstat(dd->dd_fd, &statbuf) != 0) { perror_msg("stat('%s')", dd->dd_dirname); return 1; } struct passwd *pw = getpwuid(new_uid); if (!pw) { error_msg("UID %ld is not found in user database", (long)new_uid); return 1; } #if DUMP_DIR_OWNED_BY_USER > 0 uid_t owners_uid = pw->pw_uid; gid_t groups_gid = statbuf.st_gid; #else uid_t owners_uid = statbuf.st_uid; gid_t groups_gid = pw->pw_gid; #endif int chown_res = fchown(dd->dd_fd, owners_uid, groups_gid); if (chown_res) perror_msg("fchown('%s')", dd->dd_dirname); else { dd_init_next_file(dd); char *short_name = NULL; while (chown_res == 0 && dd_get_next_file(dd, &short_name, /*full_name*/ NULL)) { /* 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); close(fd); if (chown_res) { perror_msg("fchownat('%s')", short_name); break; } next: g_clear_pointer(&short_name, g_free); } } if (chown_res == 0) chown_res = dd_chown_meta_data(dd, owners_uid, groups_gid); if (chown_res == 0) { dd->dd_uid = owners_uid; dd->dd_gid = groups_gid; } if (chown_res == 0) chown_res = dd_set_owner(dd, (long)dd->dd_uid); return chown_res; } static char *load_text_from_file_descriptor(int fd, const char *path, int flags) { if (fd == -1) { if (!(flags & DD_FAIL_QUIETLY_ENOENT)) perror_msg("Can't open file '%s' for reading", path); return (flags & DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE ? NULL : g_strdup("")); } /* Why? Because half a million read syscalls of one byte each isn't fun. * FILE-based IO buffers reads. */ FILE *fp = fdopen(fd, "r"); if (!fp) libreport_die_out_of_memory(); GString *buf_content = g_string_new(NULL); int oneline = 0; int ch; while ((ch = fgetc(fp)) != EOF) { //TODO? \r -> \n? //TODO? strip trailing spaces/tabs? if (ch == '\n') oneline = (oneline << 1) | 1; if (ch == '\0') ch = ' '; if (isspace(ch) || ch >= ' ') /* used !iscntrl, but it failed on unicode */ g_string_append_c(buf_content, ch); } fclose(fp); /* this also closes fd */ char last = oneline != 0 ? buf_content->str[buf_content->len - 1] : 0; if (last == '\n') { /* If file contains exactly one '\n' and it is at the end, remove it. * This enables users to use simple "echo blah >file" in order to create * short string items in dump dirs. */ if (oneline == 1) buf_content->str[--buf_content->len] = '\0'; } else /* last != '\n' */ { /* Last line is unterminated, fix it */ /* Cases: */ /* oneline=0: "qwe" - DONT fix this! */ /* oneline=1: "qwe\nrty" - two lines in fact */ /* oneline>1: "qwe\nrty\uio" */ if (oneline >= 1) g_string_append_c(buf_content, '\n'); } return g_string_free(buf_content, FALSE); } 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); } 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) { g_autofree char *chrooted_name = g_build_filename(chroot_dir ? chroot_dir : "", file_path, NULL); g_autofree char *data = load_text_file(chrooted_name, DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE | DD_OPEN_FOLLOW); if (data) dd_save_text(dd, name, data); } static int create_new_file_at(int dir_fd, int omode, const char *name, uid_t uid, gid_t gid, mode_t mode) { assert(name[0] != '/'); assert(omode == O_WRONLY || omode == O_RDWR); /* the mode is set by the caller, see dd_create() for security analysis */ unlinkat(dir_fd, name, /*remove only files*/0); int fd = openat(dir_fd, name, omode | O_EXCL | O_CREAT | O_NOFOLLOW, mode); if (fd < 0) { perror_msg("Can't open file '%s' for writing", name); return -1; } if ((uid != (uid_t)-1L) && (fchown(fd, uid, gid) == -1)) { perror_msg("Can't change '%s' ownership to %lu:%lu", name, (long)uid, (long)gid); close(fd); return -1; } /* O_CREAT in the open() call above causes that the permissions of the * created file are (mode & ~umask) * * This is true only if we did create file. We are not sure we created it * in this case - it may exist already. */ if (fchmod(fd, mode) == -1) { perror_msg("Can't change mode of '%s'", name); close(fd); return -1; } return fd; } 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) { const int fd = create_new_file_at(dir_fd, O_WRONLY, name, uid, gid, mode); if (fd < 0) goto fail; const unsigned r = libreport_full_write(fd, data, size); close(fd); if (r != size) goto fail; return true; fail: error_msg("Can't save file '%s'", name); return false; } char* dd_load_text_ext(const struct dump_dir *dd, const char *name, unsigned flags) { // if (!dd->locked) // error_msg_and_die("dump_dir is not opened"); /* bug */ if (!dd_validate_element_name(name)) { error_msg("Cannot load text. '%s' is not a valid file name", name); if ((flags & DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE)) return NULL; libreport_xfunc_die(); } /* Compat with old abrt dumps. Remove in abrt-2.1 */ if (strcmp(name, "release") == 0) name = FILENAME_OS_RELEASE; return load_text_file_at(dd->dd_fd, name, flags); } char* dd_load_text(const struct dump_dir *dd, const char *name) { return dd_load_text_ext(dd, name, /*flags:*/ 0); } int dd_load_int32(const struct dump_dir *dd, const char *name, int32_t *value) { unsigned long long parsed = 0; const int ret = read_number_from_file_at(dd->dd_fd, name, "int32_t", sizeof(int32_t), INT32_MIN, INT32_MAX, &parsed); if (ret == 0) *value = (int32_t)parsed; return ret; } int dd_load_uint32(const struct dump_dir *dd, const char *name, uint32_t *value) { unsigned long long parsed = 0; const int ret = read_number_from_file_at(dd->dd_fd, name, "uint32_t", sizeof(uint32_t), 0, UINT32_MAX, &parsed); if (ret == 0) *value = (uint32_t)parsed; return ret; } int dd_load_int64(const struct dump_dir *dd, const char *name, int64_t *value) { unsigned long long parsed = 0; const int ret = read_number_from_file_at(dd->dd_fd, name, "int64_t", sizeof(int64_t), INT64_MIN, INT64_MAX, &parsed); if (ret == 0) *value = (int64_t)parsed; return ret; } int dd_load_uint64(const struct dump_dir *dd, const char *name, uint64_t *value) { unsigned long long parsed = 0; const int ret = read_number_from_file_at(dd->dd_fd, name, "uint64_t", sizeof(uint64_t), 0, UINT64_MAX, &parsed); if (ret == 0) *value = (uint64_t)parsed; return ret; } int dd_get_env_variable(struct dump_dir *dd, const char *name, char **value) { const int fd = openat(dd->dd_fd, FILENAME_ENVIRON, O_RDONLY | O_NOFOLLOW); if (fd < 0) return -errno; const int r = libreport_get_env_variable_ext(fd, '\n', name, value); close(fd); return r; } void dd_save_text(struct dump_dir *dd, const char *name, const char *data) { if (!dd->locked) error_msg_and_die("dump_dir is not opened"); /* bug */ if (!dd_validate_element_name(name)) error_msg_and_die("Cannot save text. '%s' is not a valid file name", name); 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) { if (!dd->locked) error_msg_and_die("dump_dir is not opened"); /* bug */ if (!dd_validate_element_name(name)) error_msg_and_die("Cannot save binary. '%s' is not a valid file name", name); save_binary_file_at(dd->dd_fd, name, data, size, dd->dd_uid, dd->dd_gid, dd->mode); } int dd_item_stat(struct dump_dir *dd, const char *name, struct stat *statbuf) { if (!dd_validate_element_name(name)) return -EINVAL; int r = fstatat(dd->dd_fd, name, statbuf, AT_SYMLINK_NOFOLLOW); if (r != 0) return -errno; if (!S_ISREG(statbuf->st_mode)) return -EMEDIUMTYPE; return 0; } long dd_get_item_size(struct dump_dir *dd, const char *name) { long size = -1; struct stat statbuf; int r = dd_item_stat(dd, name, &statbuf); const char *error = NULL; if (r == 0) size = statbuf.st_size; else if (r == -ENOENT) size = 0; else if (r == -EINVAL) error = "Is an invalid item name"; else if (r == -EMEDIUMTYPE) error = "Is not a regular file"; else error = strerror(errno); if (error != NULL) error_msg("Cannot get item size ('%s'): %s", name, error); return size; } int dd_delete_item(struct dump_dir *dd, const char *name) { if (!dd->locked) error_msg_and_die("dump_dir is not opened"); /* bug */ if (!dd_validate_element_name(name)) { error_msg("Cannot delete item. '%s' is not a valid file name", name); return -EINVAL; } 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'", name); } return res; } int dd_open_item(struct dump_dir *dd, const char *name, int flag) { if (!dd_validate_element_name(name)) { error_msg("Cannot open item as FD. '%s' is not a valid file name", name); return -EINVAL; } if (flag == O_RDONLY) return openat(dd->dd_fd, name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (!dd->locked) error_msg_and_die("dump_dir is not locked"); /* bug */ if (flag == O_RDWR) return create_new_file_at(dd->dd_fd, O_RDWR, name, dd->dd_uid, dd->dd_gid, dd->mode); error_msg("invalid open item flag"); return -ENOTSUP; } FILE *dd_open_item_file(struct dump_dir *dd, const char *name, int flag) { const int item_fd = dd_open_item(dd, name, flag); if (item_fd < 0) return NULL; const char *mode = flag == O_RDONLY ? "r" : "w+"; return fdopen(item_fd, mode); } static int _dd_get_next_file_dent(struct dump_dir *dd, struct dirent **dent) { if (dd->next_dir == NULL) return 0; while ((*dent = readdir(dd->next_dir)) != NULL) { if (libreport_is_regular_file_at(*dent, dd->dd_fd)) return 1; } dd_clear_next_file(dd); return 0; } int dd_get_items_count(struct dump_dir *dd) { int retval = 0; if (dd_init_next_file(dd) == NULL) return -EIO; while (dd_get_next_file(dd, NULL, NULL)) { /* Check for integer overflow. */ if (retval == INT_MAX) { dd_clear_next_file(dd); return -E2BIG; } ++retval; } return retval; } off_t dd_compute_size(struct dump_dir *dd, int flags) { off_t retval = 0; if (dd_init_next_file(dd) == NULL) return -EIO; struct stat statbuf; struct dirent *dent; while (_dd_get_next_file_dent(dd, &dent)) { if (fstatat(dd->dd_fd, dent->d_name, &statbuf, AT_SYMLINK_NOFOLLOW) != 0) { retval = -errno; goto finito; } retval += statbuf.st_size; /* Check overflow */ if (retval < 0) { retval = -E2BIG; goto finito; } } finito: dd_clear_next_file(dd); return retval; } 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; } dd_clear_next_file(dd); lseek(opendir_fd, 0, SEEK_SET); dd->next_dir = fdopendir(opendir_fd); if (!dd->next_dir) { error_msg("Can't open directory '%s'", dd->dd_dirname); close(opendir_fd); } return dd->next_dir; } void dd_clear_next_file(struct dump_dir *dd) { if (dd->next_dir == NULL) return; closedir(dd->next_dir); dd->next_dir = NULL; } int dd_get_next_file(struct dump_dir *dd, char **short_name, char **full_name) { struct dirent *dent; if (0 == _dd_get_next_file_dent(dd, &dent)) return 0; if (short_name) *short_name = g_strdup(dent->d_name); if (full_name) *full_name = g_build_filename(dd->dd_dirname ? dd->dd_dirname : "", dent->d_name, NULL); return 1; } /* reported_to handling */ void libreport_add_reported_to(struct dump_dir *dd, const char *line) { if (!dd->locked) error_msg_and_die("dump_dir is not opened"); /* bug */ g_autofree char *reported_to = dd_load_text_ext(dd, FILENAME_REPORTED_TO, DD_FAIL_QUIETLY_ENOENT | DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE); if (libreport_add_reported_to_data(&reported_to, line)) dd_save_text(dd, FILENAME_REPORTED_TO, reported_to); } void libreport_add_reported_to_entry(struct dump_dir *dd, struct report_result *result) { if (!dd->locked) error_msg_and_die("dump_dir is not opened"); /* bug */ g_autofree char *reported_to = dd_load_text_ext(dd, FILENAME_REPORTED_TO, DD_FAIL_QUIETLY_ENOENT | DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE); if (libreport_add_reported_to_entry_data(&reported_to, result)) dd_save_text(dd, FILENAME_REPORTED_TO, reported_to); } report_result_t *libreport_find_in_reported_to(struct dump_dir *dd, const char *report_label) { g_autofree char *reported_to = dd_load_text_ext(dd, FILENAME_REPORTED_TO, DD_FAIL_QUIETLY_ENOENT | DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE); if (!reported_to) return NULL; report_result_t *result = libreport_find_in_reported_to_data(reported_to, report_label); return result; } GList *libreport_read_entire_reported_to(struct dump_dir *dd) { g_autofree char *reported_to = dd_load_text_ext(dd, FILENAME_REPORTED_TO, DD_FAIL_QUIETLY_ENOENT | DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE); if (!reported_to) return NULL; GList *result = libreport_read_entire_reported_to_data(reported_to); return result; } /* reported_to handling end */ int dd_rename(struct dump_dir *dd, const char *new_path) { if (!dd->locked) { error_msg("unlocked problem directory %s cannot be renamed", dd->dd_dirname); return -1; } /* Keeps the opened file descriptor valid */ int res = rename(dd->dd_dirname, new_path); if (res == 0) { g_free(dd->dd_dirname); dd->dd_dirname = rm_trailing_slashes(new_path); } return res; } /* Utility function */ void delete_dump_dir(const char *dirname) { struct dump_dir *dd = dd_opendir(dirname, /*flags:*/ 0); if (dd) { dd_delete(dd); } } bool libreport_uid_in_group(uid_t uid, gid_t gid) { char **tmp; struct passwd *pwd = getpwuid(uid); if (!pwd) return FALSE; if (pwd->pw_gid == gid) return TRUE; struct group *grp = getgrgid(gid); if (!(grp && grp->gr_mem)) return FALSE; for (tmp = grp->gr_mem; *tmp != NULL; tmp++) { if (g_strcmp0(*tmp, pwd->pw_name) == 0) { log_debug("user %s belongs to group: %s", pwd->pw_name, grp->gr_name); return TRUE; } } log_info("user %s DOESN'T belong to group: %s", pwd->pw_name, grp->gr_name); return FALSE; } int dd_stat_for_uid(struct dump_dir *dd, uid_t uid) { int ddstat = 0; bool uid_test; if (uid == dd_g_super_user_uid) { log_debug("directory accessible by super-user"); ddstat |= DD_STAT_ACCESSIBLE_BY_UID; } #define DD_OWNER_FLAGS (DD_STAT_ACCESSIBLE_BY_UID | DD_STAT_OWNED_BY_UID) if (dd->dd_uid == dd_g_super_user_uid) { log_debug("directory owned by super-user: checking meta-data"); const uid_t owner = dd_get_owner(dd); if (errno != 0) goto fsattributes; if (owner == uid) { log_debug("meta-data: %ld uid owns directory", (long)uid); ddstat |= DD_OWNER_FLAGS; goto finito; } uid_t no_owner_uid = (uid_t)-1; int ret = get_no_owner_uid(&no_owner_uid); if ( ret >= 0 && owner == no_owner_uid) { log_debug("meta-data: directory is accessible by %ld uid", (long)uid); ddstat |= DD_STAT_ACCESSIBLE_BY_UID; ddstat |= DD_STAT_NO_OWNER; } goto finito; } fsattributes: if (dd->mode & S_IROTH) { log_debug("directory is accessible by %ld uid", (long)uid); ddstat |= DD_STAT_ACCESSIBLE_BY_UID; } #if DUMP_DIR_OWNED_BY_USER > 0 uid_test = uid == dd->dd_uid; #else uid_test = libreport_uid_in_group(uid, dd->dd_gid); #endif if (uid_test) { log_debug("fs attributes: %ld uid owns directory", (long)uid); ddstat |= DD_OWNER_FLAGS; } #undef DD_OWNER_FLAGS finito: log_debug("UID=%d, %s: %o", uid, dd->dd_dirname, ddstat); return ddstat; } int dump_dir_stat_for_uid(const char *dirname, uid_t uid) { struct dump_dir *dd = dd_opendir(dirname, DD_OPEN_FD_ONLY); if (dd == NULL) return -1; int r = dd_stat_for_uid(dd, uid); dd_close(dd); return r; } int dd_accessible_by_uid(struct dump_dir *dd, uid_t uid) { int ddstat = dd_stat_for_uid(dd, uid); if (ddstat >= 0) return ddstat & DD_STAT_ACCESSIBLE_BY_UID; VERB3 pwarn_msg("can't determine accessibility for %ld uid", (long)uid); return 0; } int dump_dir_accessible_by_uid(const char *dirname, uid_t uid) { int ddstat = dump_dir_stat_for_uid(dirname, uid); if (ddstat >= 0) return ddstat & DD_STAT_ACCESSIBLE_BY_UID; VERB3 pwarn_msg("can't determine accessibility of '%s' by %ld uid", dirname, (long)uid); return 0; } int dd_mark_as_notreportable(struct dump_dir *dd, const char *reason) { if (!dd->locked) { error_msg("dump_dir is not locked for writing"); return -1; } dd_save_text(dd, FILENAME_NOT_REPORTABLE, reason); return 0; } int dd_copy_file(struct dump_dir *dd, const char *name, const char *source_path) { if (!dd_validate_element_name(name)) error_msg_and_die("Cannot test existence. '%s' is not a valid file name", name); log_debug("copying '%s' to '%s' at '%s'", source_path, name, dd->dd_dirname); unlinkat(dd->dd_fd, name, /*remove only files*/0); off_t copied = libreport_copy_file_ext_at(source_path, dd->dd_fd, name, DEFAULT_DUMP_DIR_MODE, dd->dd_uid, dd->dd_gid, O_RDONLY, O_WRONLY | O_TRUNC | O_EXCL | O_CREAT); if (copied < 0) error_msg("Can't copy %s to %s at '%s'", source_path, name, dd->dd_dirname); else log_debug("copied %li bytes", (unsigned long)copied); return copied < 0; } int dd_copy_file_at(struct dump_dir *dd, const char *name, int src_dir_fd, const char *src_name) { if (!dd_validate_element_name(name)) error_msg_and_die("Cannot test existence. '%s' is not a valid file name", name); log_debug("copying file '%s' to element '%s' at '%s'", src_name, name, dd->dd_dirname); unlinkat(dd->dd_fd, name, /*remove only files*/0); off_t copied = libreport_copy_file_ext_2at(src_dir_fd, src_name, dd->dd_fd, name, DEFAULT_DUMP_DIR_MODE, dd->dd_uid, dd->dd_gid, O_RDONLY, O_WRONLY | O_TRUNC | O_EXCL | O_CREAT); if (copied < 0) error_msg("Can't copy file '%s' to element '%s' at '%s'", src_name, name, dd->dd_dirname); else log_debug("copied %li bytes", (unsigned long)copied); return copied < 0; } int dd_copy_file_unpack(struct dump_dir *dd, const char *name, const char *source_path) { if (!dd_validate_element_name(name)) error_msg_and_die("Cannot test existence. '%s' is not a valid file name", name); log_debug("unpacking '%s' to '%s' at '%s'", source_path, name, dd->dd_dirname); unlinkat(dd->dd_fd, name, /*remove only files*/0); off_t copied = libreport_decompress_file_ext_at(source_path, dd->dd_fd, name, DEFAULT_DUMP_DIR_MODE, dd->dd_uid, dd->dd_gid, O_RDONLY, O_WRONLY | O_TRUNC | O_EXCL | O_CREAT); if (copied != 0) error_msg("Can't copy %s to %s at '%s'", source_path, name, dd->dd_dirname); else log_debug("unpackaged file '%s'", source_path); return copied < 0; } int dd_unpack_coredump(struct dump_dir *dd, const char *coredump_archive_filename) { if (dd_exist(dd, FILENAME_COREDUMP)) return 0; if (!coredump_archive_filename || !dd_exist(dd, coredump_archive_filename)) { error_msg("Coredump archive file missing in the problem directory '%s'", dd->dd_dirname); return -1; } const gchar *parts[] = {dd->dd_dirname, coredump_archive_filename, NULL}; g_autofree const char *coredump_archive_path = g_build_filenamev((gchar **) parts); if (dd_copy_file_unpack(dd, FILENAME_COREDUMP, coredump_archive_path)) return -1; return 0; } /* flags - for future needs */ int dd_create_archive(struct dump_dir *dd, const char *archive_name, const_string_vector_const_ptr_t exclude_elements, int flags) { if (!g_str_has_suffix(archive_name, ".tar.gz")) return -ENOSYS; if (access(archive_name, F_OK) == 0) return -EEXIST; int result = 0; /* Create tar writer object */ struct archive *a; struct archive_entry *entry = NULL; struct stat st; int archive_fd = -1; char buff[8192]; int len; int fd; int r = 0; unsigned max_tries = 10; //because why not unsigned tries = 0; a = archive_write_new(); if (a == NULL) { result = -1; log_warning(_("Failed to allocate and initialize archive object")); goto finito; } r = archive_write_add_filter_gzip(a); if (r == ARCHIVE_FATAL) { result = -archive_errno(a); log_warning(_("%s"), archive_error_string(a)); return result; } r = archive_write_set_format_pax_restricted(a); if (r != ARCHIVE_OK) { result = -archive_errno(a); log_warning(_("%s"), archive_error_string(a)); return result; } archive_fd = open(archive_name, O_CREAT|O_WRONLY|O_EXCL, S_IRUSR|S_IWUSR); if (archive_fd == -1) return -errno; r = archive_write_open_fd(a, archive_fd); if (r == ARCHIVE_FATAL) { result = -archive_errno(a); log_warning(_("%s"), archive_error_string(a)); return result; } /* Write data to the tarball */ dd_init_next_file(dd); char *short_name, *full_name; entry = archive_entry_new(); if (entry == NULL) { result = -1; log_warning(_("Failed to allocate and initialize archive entry object")); return result; } while (dd_get_next_file(dd, &short_name, &full_name)) { if (!(exclude_elements && libreport_is_in_string_list(short_name, exclude_elements))) { stat(full_name, &st); archive_entry_set_pathname(entry, short_name); archive_entry_copy_stat(entry, &st); archive_entry_set_filetype(entry, AE_IFREG); archive_entry_set_perm(entry, 0644); retry: r = archive_write_header(a, entry); switch (r) { case ARCHIVE_WARN: log_warning(_("%s"), archive_error_string(a)); case ARCHIVE_OK: break; case ARCHIVE_RETRY: if (++tries < max_tries) { log_warning(_("Failed to write to archive, retrying...")); goto retry; } case ARCHIVE_FATAL: log_warning(_("Failed to write to archive: %s"), (tries == max_tries ? "too many attempts" : archive_error_string(a))); result = (tries == max_tries ? -1 : -archive_errno(a)); goto finito; } archive_entry_clear(entry); fd = open(full_name, O_RDONLY); if (fd == -1) { result = -errno; log_warning(_("Failed to add file to archive: %s"), full_name); goto finito; } len = read(fd, buff, sizeof (buff)); while (len > 0) { archive_write_data(a, buff, len); len = read(fd, buff, sizeof (buff)); } close(fd); } g_clear_pointer(&short_name, g_free); g_clear_pointer(&full_name, g_free); } /*while*/ finito: archive_entry_free(entry); archive_write_close(a); r = close(archive_fd); if (r == -1) { log_warning(_("Failed to close archive")); return -errno; } archive_write_free(a); return result; } off_t dd_copy_fd(struct dump_dir *dd, const char *name, int fd, int copy_flags, off_t maxsize) { if (!dd_validate_element_name(name)) error_msg_and_die("Cannot test existence. '%s' is not a valid file name", name); log_debug("Saving data from file descriptor %d to '%s' at '%s'", fd, name, dd->dd_dirname); unlinkat(dd->dd_fd, name, /*remove only files*/0); off_t read = libreport_copyfd_ext_at(fd, dd->dd_fd, name, DEFAULT_DUMP_DIR_MODE, dd->dd_uid, dd->dd_gid, O_WRONLY | O_CREAT | O_EXCL, copy_flags, maxsize); if (read < 0) { error_msg("Can't copy file descriptor %d to %s at '%s'", fd, name, dd->dd_dirname); /* Destroy the file to get rid of empty files and files with invalid owners */ unlinkat(dd->dd_fd, name, /*remove only files*/0); } else if (read > maxsize) log_debug("Saved %lu Bytes (read %lu Bytes)", (unsigned long)maxsize, (unsigned long)read); else log_debug("Saved %lu Bytes", (unsigned long)read); return read; }