diff options
Diffstat (limited to 'src/lib/dump_dir.c')
-rw-r--r-- | src/lib/dump_dir.c | 468 |
1 files changed, 345 insertions, 123 deletions
diff --git a/src/lib/dump_dir.c b/src/lib/dump_dir.c index 19f86072..a84e2814 100644 --- a/src/lib/dump_dir.c +++ b/src/lib/dump_dir.c @@ -1,6 +1,4 @@ /* - DebugDump.cpp - Copyright (C) 2009 Zdenek Prikryl (zprikryl@redhat.com) Copyright (C) 2009 RedHat inc. @@ -22,11 +20,70 @@ #include "abrtlib.h" #include "strbuf.h" -// TODO: +// 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. // -// Perhaps dd_opendir should do some sanity checking like -// "if there is no "uid" file in the directory, it's not a crash dump", -// and fail. +// 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 + static char *load_text_file(const char *path, unsigned flags); @@ -53,12 +110,21 @@ static bool exist_file_dir(const char *path) return false; } -static bool get_and_set_lock(const char* lock_file, const char* pid) +/* Return values: + * -1: error + * 0: failed to lock (someone else has it locked) + * 1: success + */ +static int get_and_set_lock(const char* lock_file, const char* pid) { while (symlink(pid, lock_file) != 0) { if (errno != EEXIST) - perror_msg_and_die("Can't create lock file '%s'", lock_file); + { + if (errno != ENOENT && errno != ENOTDIR) + perror_msg("Can't create lock file '%s'", lock_file); + return -1; + } char pid_buf[sizeof(pid_t)*3 + 4]; ssize_t r = readlink(lock_file, pid_buf, sizeof(pid_buf) - 1); @@ -67,54 +133,94 @@ static bool get_and_set_lock(const char* lock_file, const char* pid) if (errno == ENOENT) { /* Looks like lock_file was deleted */ - usleep(10 * 1000); /* avoid CPU eating loop */ + usleep(SYMLINK_RETRY_USLEEP); /* avoid CPU eating loop */ continue; } - perror_msg_and_die("Can't read lock file '%s'", lock_file); + perror_msg("Can't read lock file '%s'", lock_file); + return -1; } pid_buf[r] = '\0'; if (strcmp(pid_buf, pid) == 0) { log("Lock file '%s' is already locked by us", lock_file); - return false; + return 0; } if (isdigit_str(pid_buf)) { - char pid_str[sizeof("/proc/") + strlen(pid_buf)]; + char pid_str[sizeof("/proc/") + sizeof(pid_buf)]; sprintf(pid_str, "/proc/%s", pid_buf); if (access(pid_str, F_OK) == 0) { log("Lock file '%s' is locked by process %s", lock_file, pid_buf); - return false; + return 0; } 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) { - perror_msg_and_die("Can't remove stale lock file '%s'", lock_file); + perror_msg("Can't remove stale lock file '%s'", lock_file); + return -1; } } VERB1 log("Locked '%s'", lock_file); - return true; + return 1; } -static void dd_lock(struct dump_dir *dd) +static int dd_lock(struct dump_dir *dd, unsigned sleep_usec) { if (dd->locked) - error_msg_and_die("Locking bug on '%s'", dd->dd_dir); - - char lock_buf[strlen(dd->dd_dir) + sizeof(".lock")]; - sprintf(lock_buf, "%s.lock", dd->dd_dir); + error_msg_and_die("Locking bug on '%s'", dd->dd_dirname); char pid_buf[sizeof(long)*3 + 2]; sprintf(pid_buf, "%lu", (long)getpid()); - while ((dd->locked = get_and_set_lock(lock_buf, pid_buf)) != true) + + 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) { - sleep(1); /* was 0.5 seconds */ + int r = get_and_set_lock(lock_buf, pid_buf); + if (r < 0) + return r; /* error */ + if (r > 0) + break; /* locked successfully */ + /* Other process has the lock, wait for it to go away */ + usleep(sleep_usec); } + + /* Are we called by dd_opendir (as opposed to dd_create)? */ + if (sleep_usec == WAIT_FOR_OTHER_PROCESS_USLEEP) /* yes */ + { + strcpy(lock_buf + dirname_len, "/time"); + if (access(lock_buf, F_OK) != 0) + { + /* time file doesn'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. + */ + strcpy(lock_buf + dirname_len, "/.lock"); + xunlink(lock_buf); + VERB1 log("Unlocked '%s' (no time file)", lock_buf); + if (--count == 0) + { + 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) @@ -122,9 +228,13 @@ static void dd_unlock(struct dump_dir *dd) if (dd->locked) { dd->locked = 0; - char lock_buf[strlen(dd->dd_dir) + sizeof(".lock")]; - sprintf(lock_buf, "%s.lock", dd->dd_dir); + + 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); + VERB1 log("Unlocked '%s'", lock_buf); } } @@ -136,7 +246,7 @@ static inline struct dump_dir *dd_init(void) int dd_exist(struct dump_dir *dd, const char *path) { - char *full_path = concat_path_file(dd->dd_dir, path); + char *full_path = concat_path_file(dd->dd_dirname, path); int ret = exist_file_dir(full_path); free(full_path); return ret; @@ -154,7 +264,7 @@ void dd_close(struct dump_dir *dd) /* free(dd->next_dir); - WRONG! */ } - free(dd->dd_dir); + free(dd->dd_dirname); free(dd); } @@ -170,52 +280,68 @@ struct dump_dir *dd_opendir(const char *dir, int flags) { struct dump_dir *dd = dd_init(); - /* Used to use rm_trailing_slashes(dir) here, but with dir = "." - * or "..", or if the last component is a symlink, - * then lock file is created in the wrong place. - * IOW: this breaks locking. - */ - dd->dd_dir = realpath(dir, NULL); - if (!dd->dd_dir) - { - if (!(flags & DD_FAIL_QUIETLY)) - error_msg("'%s' does not exist", dir); - dd_close(dd); - return NULL; - } - dir = dd->dd_dir; - - dd_lock(dd); + dir = dd->dd_dirname = rm_trailing_slashes(dir); - struct stat stat_buf; - if (stat(dir, &stat_buf) != 0 || !S_ISDIR(stat_buf.st_mode)) + errno = 0; + if (dd_lock(dd, WAIT_FOR_OTHER_PROCESS_USLEEP) < 0) { - if (!(flags & DD_FAIL_QUIETLY)) - error_msg("'%s' does not exist", dir); + if ((flags & DD_OPEN_READONLY) && errno == EACCES) + { + /* Directory is not writable. If it seems to be readable, + * return "read only" dd, not NULL */ + struct stat stat_buf; + if (stat(dir, &stat_buf) == 0 + && S_ISDIR(stat_buf.st_mode) + && access(dir, R_OK) == 0 + ) { + return dd; + } + } + 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 "."! + */ + /*if (!(flags & DD_FAIL_QUIETLY))... - no, DD_FAIL_QUIETLY only means + * "it's ok if it doesn exist", not "ok if contents is bogus"! + */ + error_msg("'%s' is not a crash dump directory", dir); + dd_close(dd); + return NULL; + } + + if (errno == ENOENT || errno == ENOTDIR) + { + if (!(flags & DD_FAIL_QUIETLY)) + error_msg("'%s' does not exist", dir); + } + else + { + perror_msg("Can't access '%s'", dir); + } dd_close(dd); return NULL; } - /* In case caller would want to create more files, he'll need uid:gid */ - dd->dd_uid = stat_buf.st_uid; - dd->dd_gid = stat_buf.st_gid; - - /* Without this check, e.g. abrt-action-print happily prints any current - * directory when run without arguments, because its option -d DIR - * defaults to "."! Let's require that at least some crash dump dir - * specific files exist before we declare open successful: - */ - char *name = concat_path_file(dir, FILENAME_ANALYZER); - int bad = (lstat(name, &stat_buf) != 0 || !S_ISREG(stat_buf.st_mode)); - free(name); - if (bad) + dd->dd_uid = (uid_t)-1L; + dd->dd_gid = (gid_t)-1L; + if (geteuid() == 0) { - /*if (!(flags & DD_FAIL_QUIETLY))... - no, DD_FAIL_QUIETLY only means - * "it's ok if it doesn exist", not "ok if contents is bogus"! - */ - error_msg("'%s' is not a crash dump directory", dir); - dd_close(dd); - return NULL; + /* 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 (!(flags & DD_FAIL_QUIETLY)) + error_msg("'%s' does not exist", dir); + dd_close(dd); + return NULL; + } + dd->dd_uid = stat_buf.st_uid; + dd->dd_gid = stat_buf.st_gid; } return dd; @@ -248,7 +374,7 @@ struct dump_dir *dd_create(const char *dir, uid_t uid) * realpath will always return NULL. We don't really have to: * dd_opendir(".") makes sense, dd_create(".") does not. */ - dir = dd->dd_dir = rm_trailing_slashes(dir); + dir = dd->dd_dirname = rm_trailing_slashes(dir); const char *last_component = strrchr(dir, '/'); if (last_component) @@ -265,15 +391,38 @@ struct dump_dir *dd_create(const char *dir, uid_t uid) return NULL; } - dd_lock(dd); - + bool created_parents = false; + try_again: /* 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") */ if (mkdir(dir, 0750) == -1) { - perror_msg("Can't create dir '%s'", dir); + int err = errno; + if (!created_parents && errno == ENOENT) + { + char *p = dd->dd_dirname + 1; + while ((p = strchr(p, '/')) != NULL) + { + *p = '\0'; + int r = (mkdir(dd->dd_dirname, 0755) == 0 || errno == EEXIST); + *p++ = '/'; + if (!r) + goto report_err; + } + created_parents = true; + goto try_again; + } + report_err: + errno = err; + perror_msg("Can't create directory '%s'", dir); + dd_close(dd); + return NULL; + } + + if (dd_lock(dd, CREATE_LOCK_USLEEP) < 0) + { dd_close(dd); return NULL; } @@ -281,35 +430,51 @@ struct dump_dir *dd_create(const char *dir, uid_t uid) /* mkdir's mode (above) can be affected by umask, fix it */ if (chmod(dir, 0750) == -1) { - perror_msg("Can't change mode of '%s'", dir); + perror_msg("can't change mode of '%s'", dir); dd_close(dd); return NULL; } - /* Get ABRT's user id */ - /*dd->dd_uid = 0; - dd_init did this already */ - 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 group id */ - /*dd->dd_gid = 0; - dd_init did this already */ - pw = getpwuid(uid); - if (pw) - dd->dd_gid = pw->pw_gid; - else - error_msg("User %lu does not exist, using gid 0", (long)uid); - - if (chown(dir, dd->dd_uid, dd->dd_gid) == -1) + dd->dd_uid = (uid_t)-1L; + dd->dd_gid = (gid_t)-1L; + if (uid != (uid_t)-1L) { - perror_msg("Can't change '%s' ownership to %lu:%lu", dir, - (long)dd->dd_uid, (long)dd->dd_gid); + /* Get ABRT's user id */ + dd->dd_uid = 0; + 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 group id */ + /*dd->dd_gid = 0; - dd_init did this already */ + pw = getpwuid(uid); + if (pw) + dd->dd_gid = pw->pw_gid; + else + error_msg("User %lu does not exist, using gid 0", (long)uid); + + if (chown(dir, dd->dd_uid, dd->dd_gid) == -1) + { + perror_msg("can't change '%s' ownership to %lu:%lu", dir, + (long)dd->dd_uid, (long)dd->dd_gid); + } } + return dd; +} + +void dd_create_basic_files(struct dump_dir *dd, uid_t uid) +{ char long_str[sizeof(long) * 3 + 2]; + time_t t = time(NULL); + sprintf(long_str, "%lu", (long)t); + dd_save_text(dd, FILENAME_TIME, long_str); + + if (uid == (uid_t)-1) + uid = getuid(); sprintf(long_str, "%lu", (long)uid); dd_save_text(dd, FILENAME_UID, long_str); @@ -317,54 +482,102 @@ struct dump_dir *dd_create(const char *dir, uid_t uid) uname(&buf); /* never fails */ dd_save_text(dd, FILENAME_KERNEL, buf.release); dd_save_text(dd, FILENAME_ARCHITECTURE, buf.machine); - char *release = load_text_file("/etc/redhat-release", /*flags:*/ 0); - strchrnul(release, '\n')[0] = '\0'; - dd_save_text(dd, FILENAME_RELEASE, release); - free(release); + dd_save_text(dd, FILENAME_HOSTNAME, buf.nodename); - time_t t = time(NULL); - sprintf(long_str, "%lu", (long)t); - dd_save_text(dd, FILENAME_TIME, long_str); - - return dd; + char *release = load_text_file("/etc/system-release", + DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE); + if (!release) + release = load_text_file("/etc/redhat-release", /*flags:*/ 0); + dd_save_text(dd, FILENAME_OS_RELEASE, release); + free(release); } -static void delete_file_dir(const char *dir) +static int delete_file_dir(const char *dir, bool skip_lock_file) { DIR *d = opendir(dir); if (!d) - return; + { + /* 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 (errno == ENOENT || errno == ENOTDIR) + { + errno = 0; + return 0; + } + return -1; + } + bool unlink_lock_file = false; struct dirent *dent; while ((dent = readdir(d)) != NULL) { if (dot_or_dotdot(dent->d_name)) continue; + if (skip_lock_file && strcmp(dent->d_name, ".lock") == 0) + { + unlink_lock_file = true; + continue; + } char *full_path = concat_path_file(dir, dent->d_name); if (unlink(full_path) == -1 && errno != ENOENT) { - if (errno != EISDIR) + int err = 0; + if (errno == EISDIR) + { + errno = 0; + err = delete_file_dir(full_path, /*skip_lock_file:*/ false); + } + if (errno || err) { - error_msg("Can't remove '%s'", full_path); + perror_msg("Can't remove '%s'", full_path); free(full_path); closedir(d); - return; + return -1; } - delete_file_dir(full_path); } free(full_path); } closedir(d); - if (rmdir(dir) == -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) { - error_msg("Can't remove dir '%s'", dir); + 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); } + + int r = rmdir(dir); + if (r) + perror_msg("Can't remove directory '%s'", dir); + return r; } -void dd_delete(struct dump_dir *dd) +int dd_delete(struct dump_dir *dd) { - delete_file_dir(dd->dd_dir); + int r = delete_file_dir(dd->dd_dirname, /*skip_lock_file:*/ true); + dd->locked = 0; /* delete_file_dir already removed .lock */ dd_close(dd); + return r; } static char *load_text_file(const char *path, unsigned flags) @@ -411,10 +624,15 @@ static bool save_binary_file(const char *path, const char* data, unsigned size, perror_msg("Can't open file '%s'", path); return false; } - if (fchown(fd, uid, gid) == -1) + + if (uid != (uid_t)-1L) { - perror_msg("can't change '%s' ownership to %lu:%lu", path, (long)uid, (long)gid); + if (fchown(fd, uid, gid) == -1) + { + perror_msg("can't change '%s' ownership to %lu:%lu", path, (long)uid, (long)gid); + } } + unsigned r = full_write(fd, data, size); close(fd); if (r != size) @@ -428,10 +646,14 @@ static bool save_binary_file(const char *path, const char* data, unsigned size, 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->locked) +// error_msg_and_die("dump_dir is not opened"); /* bug */ - char *full_path = concat_path_file(dd->dd_dir, name); + /* Compat with old abrt dumps. Remove in abrt-2.1 */ + 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); @@ -448,7 +670,7 @@ 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 */ - char *full_path = concat_path_file(dd->dd_dir, 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); free(full_path); } @@ -458,23 +680,23 @@ void dd_save_binary(struct dump_dir* dd, const char* name, const char* data, uns if (!dd->locked) error_msg_and_die("dump_dir is not opened"); /* bug */ - char *full_path = concat_path_file(dd->dd_dir, name); + char *full_path = concat_path_file(dd->dd_dirname, name); save_binary_file(full_path, data, size, dd->dd_uid, dd->dd_gid); free(full_path); } DIR *dd_init_next_file(struct dump_dir *dd) { - if (!dd->locked) - error_msg_and_die("dump_dir is not opened"); /* bug */ +// if (!dd->locked) +// error_msg_and_die("dump_dir is not opened"); /* bug */ if (dd->next_dir) closedir(dd->next_dir); - dd->next_dir = opendir(dd->dd_dir); + dd->next_dir = opendir(dd->dd_dirname); if (!dd->next_dir) { - error_msg("Can't open dir '%s'", dd->dd_dir); + error_msg("Can't open directory '%s'", dd->dd_dirname); } return dd->next_dir; @@ -488,12 +710,12 @@ 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_dir)) + if (is_regular_file(dent, dd->dd_dirname)) { if (short_name) *short_name = xstrdup(dent->d_name); if (full_name) - *full_name = concat_path_file(dd->dd_dir, dent->d_name); + *full_name = concat_path_file(dd->dd_dirname, dent->d_name); return 1; } } @@ -504,9 +726,9 @@ int dd_get_next_file(struct dump_dir *dd, char **short_name, char **full_name) } /* Utility function */ -void delete_crash_dump_dir(const char *dd_dir) +void delete_dump_dir(const char *dirname) { - struct dump_dir *dd = dd_opendir(dd_dir, /*flags:*/ 0); + struct dump_dir *dd = dd_opendir(dirname, /*flags:*/ 0); if (dd) { dd_delete(dd); |