/* Copyright (C) 2010 ABRT team 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 "problem_api.h" #include "abrt_glib.h" #include "libabrt.h" /* Maximal length of backtrace. */ #define MAX_BACKTRACE_SIZE (1024*1024) /* Amount of data received from one client for a message before reporting error. */ #define MAX_MESSAGE_SIZE (4*MAX_BACKTRACE_SIZE) /* Maximal number of characters read from socket at once. */ #define INPUT_BUFFER_SIZE (8*1024) /* We exit after this many seconds */ #define TIMEOUT 10 #define ABRT_SERVER_EVENT_ENV "ABRT_SERVER_PID" /* Unix socket in ABRT daemon for creating new dump directories. Why to use socket for creating dump dirs? Security. When a Python script throws unexpected exception, ABRT handler catches it, running as a part of that broken Python application. The application is running with certain SELinux privileges, for example it can not execute other programs, or to create files in /var/cache or anything else required to properly fill a problem directory. Adding these privileges to every application would weaken the security. The most suitable solution is for the Python application to open a socket where ABRT daemon is listening, write all relevant data to that socket, and close it. ABRT daemon handles the rest. ** Protocol Initializing new dump: open /var/run/abrt.socket Providing dump data (hook writes to the socket): MANDATORY ITEMS: -> "PID=" number 0 - PID_MAX (/proc/sys/kernel/pid_max) \0 -> "EXECUTABLE=" string \0 -> "BACKTRACE=" string \0 -> "ANALYZER=" string \0 -> "BASENAME=" string (no slashes) \0 -> "REASON=" string \0 You can send more messages using the same KEY=value format. */ static int g_signal_pipe[2]; static struct ns_ids g_ns_ids; struct waiting_context { GMainLoop *main_loop; const char *dirname; int retcode; enum abrt_daemon_reply { ABRT_CONTINUE, ABRT_INTERRUPT, } reply; }; static unsigned total_bytes_read = 0; static pid_t client_pid = (pid_t)-1L; static uid_t client_uid = (uid_t)-1L; static void handle_signal(int signo) { int save_errno = errno; uint8_t sig_caught = signo; if (write(g_signal_pipe[1], &sig_caught, 1)) /* we ignore result, if () shuts up stupid compiler */; errno = save_errno; } static gboolean handle_signal_pipe_cb(GIOChannel *gio, GIOCondition condition, gpointer user_data) { gsize len = 0; uint8_t signals[2]; for (;;) { GError *error = NULL; GIOStatus stat = g_io_channel_read_chars(gio, (void *)signals, sizeof(signals), &len, NULL); if (stat == G_IO_STATUS_ERROR) error_msg_and_die(_("Can't read from gio channel: '%s'"), error ? error->message : ""); if (stat == G_IO_STATUS_EOF) return FALSE; /* Remove this GLib source */ if (stat == G_IO_STATUS_AGAIN) break; /* G_IO_STATUS_NORMAL */ for (unsigned signo = 0; signo < len; ++signo) { /* we did receive a signal */ struct waiting_context *context = (struct waiting_context *)user_data; log_debug("Got signal %d through signal pipe", signals[signo]); switch (signals[signo]) { case SIGUSR1: context->reply = ABRT_CONTINUE; break; case SIGINT: context->reply = ABRT_INTERRUPT; break; default: { error_msg("Bug - aborting - unsupported signal: %d", signals[signo]); abort(); } } g_main_loop_quit(context->main_loop); return FALSE; /* remove this event */ } } return TRUE; /* "please don't remove this event" */ } /* Remove dump dir */ static int delete_path(const char *dump_dir_name) { /* If doesn't start with "g_settings_dump_location/"... */ if (!dir_is_in_dump_location(dump_dir_name)) { /* Then refuse to operate on it (someone is attacking us??) */ 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, DD_PERM_DAEMONS)) { error_msg("Problem directory '%s' has wrong owner or group", dump_dir_name); return 400; /* */ } struct dump_dir *dd = dd_opendir(dump_dir_name, DD_OPEN_FD_ONLY); if (dd == NULL) { perror_msg("Can't open problem directory '%s'", dump_dir_name); return 400; } if (!dd_accessible_by_uid(dd, client_uid)) { dd_close(dd); if (errno == ENOTDIR) { error_msg("Path '%s' isn't problem directory", dump_dir_name); return 404; /* Not Found */ } error_msg("Problem directory '%s' can't be accessed by user with uid %ld", dump_dir_name, (long)client_uid); return 403; /* Forbidden */ } dd = dd_fdopendir(dd, /*flags:*/ 0); if (dd) { if (dd_delete(dd) != 0) { error_msg("Failed to delete problem directory '%s'", dump_dir_name); dd_close(dd); return 400; } } return 0; /* success */ } static pid_t spawn_event_handler_child(const char *dump_dir_name, const char *event_name, int *fdp) { char *args[9]; args[0] = (char *) LIBEXEC_DIR"/abrt-handle-event"; /* Do not forward ASK_* messages to parent*/ args[1] = (char *) "-i"; args[2] = (char *) "--nice"; args[3] = (char *) "10"; args[4] = (char *) "-e"; args[5] = (char *) event_name; args[6] = (char *) "--"; args[7] = (char *) dump_dir_name; args[8] = NULL; int pipeout[2]; int flags = EXECFLG_INPUT_NUL | EXECFLG_OUTPUT | EXECFLG_QUIET | EXECFLG_ERR2OUT; VERB1 flags &= ~EXECFLG_QUIET; char *env_vec[3]; /* Intercept ASK_* messages in Client API -> don't wait for user response */ env_vec[0] = xstrdup("REPORT_CLIENT_NONINTERACTIVE=1"); env_vec[1] = xasprintf("%s=%d", ABRT_SERVER_EVENT_ENV, getpid()); env_vec[2] = NULL; pid_t child = fork_execv_on_steroids(flags, args, pipeout, env_vec, /*dir:*/ NULL, /*uid(unused):*/ 0); if (fdp) *fdp = pipeout[0]; return child; } static int problem_dump_dir_was_provoked_by_abrt_event(struct dump_dir *dd, char **provoker) { char *env_var = NULL; const int r = dd_get_env_variable(dd, ABRT_SERVER_EVENT_ENV, &env_var); /* Dump directory doesn't contain the environ file */ if (r == -ENOENT) return 0; if (provoker != NULL) *provoker = env_var; else free(env_var); return env_var != NULL; } static int emit_new_problem_signal(gpointer data) { struct waiting_context *context = (struct waiting_context *)data; const size_t wrote = fprintf(stderr, "NEW_PROBLEM_DETECTED: %s\n", context->dirname); fflush(stderr); if (wrote <= 0) { error_msg("Failed to communicate with the daemon"); context->retcode = 503; g_main_loop_quit(context->main_loop); } log_notice("Emitted new problem signal, waiting for SIGUSR1|SIGINT"); return FALSE; } struct response { int code; char *message; }; #define RESPONSE_SETTER(r, c, m) \ do { if (r != NULL) { r->message = m; r->code = c; } else { free(m); }} while (0) #define RESPONSE_RETURN(r, c ,m) \ do { RESPONSE_SETTER(r, c, m); \ return c; } while (0) static int run_post_create(const char *dirname, struct response *resp) { /* If doesn't start with "g_settings_dump_location/"... */ if (!dir_is_in_dump_location(dirname)) { /* Then refuse to operate on it (someone is attacking us??) */ error_msg("Bad problem directory name '%s', should start with: '%s'", dirname, g_settings_dump_location); RESPONSE_RETURN(resp, 400, NULL); } if (!dir_has_correct_permissions(dirname, DD_PERM_EVENTS)) { error_msg("Problem directory '%s' has wrong owner or group", dirname); RESPONSE_RETURN(resp, 400, NULL); } /* Check completness */ { struct dump_dir *dd = dd_opendir(dirname, DD_OPEN_READONLY); char *provoker = NULL; const bool event_dir = dd && problem_dump_dir_was_provoked_by_abrt_event(dd, &provoker); if (event_dir) { if (g_settings_debug_level == 0) { error_msg("Removing problem provoked by ABRT(pid:%s): '%s'", provoker, dirname); dd_delete(dd); } else { char *dumpdir = NULL; char *event = NULL; char *reason = NULL; char *cmdline = NULL; /* Ignore errors */ dd_get_env_variable(dd, "DUMP_DIR", &dumpdir); dd_get_env_variable(dd, "EVENT", &event); reason = dd_load_text(dd, FILENAME_REASON); cmdline = dd_load_text(dd, FILENAME_CMDLINE); error_msg("ABRT_SERVER_PID=%s;DUMP_DIR='%s';EVENT='%s';REASON='%s';CMDLINE='%s'", provoker, dumpdir, event, reason, cmdline); } free(provoker); return 400; } const bool complete = dd && problem_dump_dir_is_complete(dd); dd_close(dd); if (complete) { error_msg("Problem directory '%s' has already been processed", dirname); RESPONSE_RETURN(resp, 403, NULL); } } /* * The post-create event cannot be run concurrently for more problem * directories. The problem is in searching for duplicates process * in case when two concurrently processed directories are duplicates * of each other. Both of the directories are marked as duplicates * of each other and are deleted. */ log_debug("Creating glib main loop"); struct waiting_context context = {0}; context.main_loop = g_main_loop_new(NULL, FALSE); context.dirname = strrchr(dirname, '/') + 1; log_debug("Setting up a signal handler"); /* Set up signal pipe */ xpipe(g_signal_pipe); close_on_exec_on(g_signal_pipe[0]); close_on_exec_on(g_signal_pipe[1]); ndelay_on(g_signal_pipe[0]); ndelay_on(g_signal_pipe[1]); signal(SIGUSR1, handle_signal); signal(SIGINT, handle_signal); GIOChannel *channel_signal = abrt_gio_channel_unix_new(g_signal_pipe[0]); g_io_add_watch(channel_signal, G_IO_IN | G_IO_PRI, handle_signal_pipe_cb, &context); g_idle_add(emit_new_problem_signal, &context); g_main_loop_run(context.main_loop); g_main_loop_unref(context.main_loop); g_io_channel_unref(channel_signal); close(g_signal_pipe[1]); log_notice("Waiting finished"); if (context.retcode != 0) RESPONSE_RETURN(resp, context.retcode, NULL); if (context.reply != ABRT_CONTINUE) /* The only reason for the interruption is removed problem directory */ RESPONSE_RETURN(resp, 413, NULL); /* * The post-create event synchronization done. */ int child_stdout_fd; int child_pid = spawn_event_handler_child(dirname, "post-create", &child_stdout_fd); char *dup_of_dir = NULL; struct strbuf *cmd_output = strbuf_new(); bool child_is_post_create = 1; /* else it is a notify child */ read_child_output: //log_warning("Reading from event fd %d", child_stdout_fd); /* Read streamed data and split lines */ for (;;) { char buf[250]; /* usually we get one line, no need to have big buf */ errno = 0; int r = safe_read(child_stdout_fd, buf, sizeof(buf) - 1); if (r <= 0) break; buf[r] = '\0'; /* split lines in the current buffer */ char *raw = buf; char *newline; while ((newline = strchr(raw, '\n')) != NULL) { *newline = '\0'; strbuf_append_str(cmd_output, raw); char *msg = cmd_output->buf; if (child_is_post_create && prefixcmp(msg, "DUP_OF_DIR: ") == 0 ) { free(dup_of_dir); dup_of_dir = xstrdup(msg + strlen("DUP_OF_DIR: ")); } else log_warning("%s", msg); strbuf_clear(cmd_output); /* jump to next line */ raw = newline + 1; } /* beginning of next line. the line continues by next read */ strbuf_append_str(cmd_output, raw); } /* EOF/error */ /* Wait for child to actually exit, collect status */ int status = 0; if (safe_waitpid(child_pid, &status, 0) <= 0) /* should not happen */ perror_msg("waitpid(%d)", child_pid); /* If it was a "notify[-dup]" event, then we're done */ if (!child_is_post_create) goto ret; /* exit 0 means "this is a good, non-dup dir" */ /* exit with 1 + "DUP_OF_DIR: dir" string => dup */ if (status != 0) { if (WIFSIGNALED(status)) { log_warning("'post-create' on '%s' killed by signal %d", dirname, WTERMSIG(status)); goto delete_bad_dir; } /* else: it is WIFEXITED(status) */ if (!dup_of_dir) { log_warning("'post-create' on '%s' exited with %d", dirname, WEXITSTATUS(status)); goto delete_bad_dir; } } const char *work_dir = (dup_of_dir ? dup_of_dir : dirname); /* Load problem_data (from the *first dir* if this one is a dup) */ struct dump_dir *dd = dd_opendir(work_dir, /*flags:*/ 0); if (!dd) /* dd_opendir already emitted error msg */ goto delete_bad_dir; /* Update count */ char *count_str = dd_load_text_ext(dd, FILENAME_COUNT, DD_FAIL_QUIETLY_ENOENT); unsigned long count = strtoul(count_str, NULL, 10); /* Don't increase crash count if we are working with newly uploaded * directory (remote crash) which already has its crash count set. */ if ((status != 0 && dup_of_dir) || count == 0) { count++; char new_count_str[sizeof(long)*3 + 2]; sprintf(new_count_str, "%lu", count); dd_save_text(dd, FILENAME_COUNT, new_count_str); /* This condition can be simplified to either * (status * != 0 && * dup_of_dir) or (count == 1). But the * chosen form is much more reliable and safe. We must not call * dd_opendir() to locked dd otherwise we go into a deadlock. */ if (strcmp(dd->dd_dirname, dirname) != 0) { /* Update the last occurrence file by the time file of the new problem */ struct dump_dir *new_dd = dd_opendir(dirname, DD_OPEN_READONLY); char *last_ocr = NULL; if (new_dd) { /* TIME must exists in a valid dump directory but we don't want to die * due to broken duplicated dump directory */ last_ocr = dd_load_text_ext(new_dd, FILENAME_TIME, DD_LOAD_TEXT_RETURN_NULL_ON_FAILURE | DD_FAIL_QUIETLY_ENOENT); dd_close(new_dd); } else { /* dd_opendir() already produced a message with good information about failure */ error_msg("Can't read the last occurrence file from the new dump directory."); } if (!last_ocr) { /* the new dump directory may lie in the dump location for some time */ log_warning("Using current time for the last occurrence file which may be incorrect."); time_t t = time(NULL); last_ocr = xasprintf("%lu", (long)t); } dd_save_text(dd, FILENAME_LAST_OCCURRENCE, last_ocr); free(last_ocr); } } /* Reset mode/uig/gid to correct values for all files created by event run */ dd_sanitize_mode_and_owner(dd); dd_close(dd); if (!dup_of_dir) log_notice("New problem directory %s, processing", work_dir); else { log_warning("Deleting problem directory %s (dup of %s)", strrchr(dirname, '/') + 1, strrchr(dup_of_dir, '/') + 1); delete_dump_dir(dirname); } /* Run "notify[-dup]" event */ int fd; child_pid = spawn_event_handler_child( work_dir, (dup_of_dir ? "notify-dup" : "notify"), &fd ); //log_warning("Started notify, fd %d -> %d", fd, child_stdout_fd); xmove_fd(fd, child_stdout_fd); child_is_post_create = 0; if (dup_of_dir) RESPONSE_SETTER(resp, 303, dup_of_dir); else { RESPONSE_SETTER(resp, 200, NULL); free(dup_of_dir); } dup_of_dir = NULL; strbuf_clear(cmd_output); goto read_child_output; delete_bad_dir: log_warning("Deleting problem directory '%s'", dirname); delete_dump_dir(dirname); /* TODO - better code to allow detection on client's side */ RESPONSE_SETTER(resp, 403, NULL); ret: strbuf_free(cmd_output); free(dup_of_dir); close(child_stdout_fd); return 0; } /* Create a new problem directory from client session. * Caller must ensure that all fields in struct client * are properly filled. */ static int create_problem_dir(GHashTable *problem_info, unsigned pid) { /* Exit if free space is less than 1/4 of MaxCrashReportsSize */ if (g_settings_nMaxCrashReportsSize > 0) { if (low_free_space(g_settings_nMaxCrashReportsSize, g_settings_dump_location)) exit(1); } /* Create temp directory with the problem data. * This directory is renamed to final directory name after * all files have been stored into it. */ gchar *dir_basename = g_hash_table_lookup(problem_info, "basename"); if (!dir_basename) dir_basename = g_hash_table_lookup(problem_info, FILENAME_TYPE); char *path = xasprintf("%s/%s-%s-%u.new", g_settings_dump_location, dir_basename, iso_date_string(NULL), pid); /* This item is useless, don't save it */ g_hash_table_remove(problem_info, "basename"); /* No need to check the path length, as all variables used are limited, * and dd_create() fails if the path is too long. */ struct dump_dir *dd = dd_create(path, /*fs owner*/0, DEFAULT_DUMP_DIR_MODE); if (!dd) { error_msg_and_die("Error creating problem directory '%s'", path); } const int proc_dir_fd = open_proc_pid_dir(pid); char *rootdir = NULL; if (proc_dir_fd < 0) { pwarn_msg("Cannot open /proc/%d:", pid); } else if (process_has_own_root_at(proc_dir_fd)) { /* Obtain the root directory path only if process' root directory is * not the same as the init's root directory */ rootdir = get_rootdir_at(proc_dir_fd); } /* Reading data from an arbitrary root directory is not secure. */ if (proc_dir_fd >= 0 && g_settings_explorechroots) { char proc_pid_root[sizeof("/proc/[pid]/root") + sizeof(pid_t) * 3]; const size_t w = snprintf(proc_pid_root, sizeof(proc_pid_root), "/proc/%d/root", pid); assert(sizeof(proc_pid_root) > w); /* Yes, test 'rootdir' but use 'source_filename' because 'rootdir' can * be '/' for a process with own namespace. 'source_filename' is /proc/[pid]/root. */ dd_create_basic_files(dd, client_uid, (rootdir != NULL) ? proc_pid_root : NULL); } else { dd_create_basic_files(dd, client_uid, NULL); } if (proc_dir_fd >= 0) { /* Obtain and save the command line. */ char *cmdline = get_cmdline_at(proc_dir_fd); if (cmdline) { dd_save_text(dd, FILENAME_CMDLINE, cmdline); free(cmdline); } /* Obtain and save the environment variables. */ char *environ = get_environ_at(proc_dir_fd); if (environ) { dd_save_text(dd, FILENAME_ENVIRON, environ); free(environ); } dd_copy_file_at(dd, FILENAME_CGROUP, proc_dir_fd, "cgroup"); dd_copy_file_at(dd, FILENAME_MOUNTINFO, proc_dir_fd, "mountinfo"); FILE *open_fds = dd_open_item_file(dd, FILENAME_OPEN_FDS, O_RDWR); if (open_fds) { if (dump_fd_info_at(proc_dir_fd, open_fds) < 0) dd_delete_item(dd, FILENAME_OPEN_FDS); fclose(open_fds); } const int init_proc_dir_fd = open_proc_pid_dir(1); FILE *namespaces = dd_open_item_file(dd, FILENAME_NAMESPACES, O_RDWR); if (namespaces && init_proc_dir_fd >= 0) { if (dump_namespace_diff_at(init_proc_dir_fd, proc_dir_fd, namespaces) < 0) dd_delete_item(dd, FILENAME_NAMESPACES); } if (init_proc_dir_fd >= 0) close(init_proc_dir_fd); if (namespaces) fclose(namespaces); /* The process's root directory isn't the same as the init's root * directory. */ if (rootdir) { if (strcmp(rootdir, "/") == 0) { /* We are dealing containerized process because root's * directory path is '/' and that means that the process * has mounted its own root. * Seriously, it is possible if the process is running in its * own MOUNT namespaces. */ log_debug("Process %d is considered to be containerized", pid); pid_t container_pid; if (get_pid_of_container_at(proc_dir_fd, &container_pid) == 0) { char *container_cmdline = get_cmdline(container_pid); dd_save_text(dd, FILENAME_CONTAINER_CMDLINE, container_cmdline); free(container_cmdline); } } else { /* We are dealing chrooted process. */ dd_save_text(dd, FILENAME_ROOTDIR, rootdir); } } close(proc_dir_fd); } free(rootdir); /* Store id of the user whose application crashed. */ char uid_str[sizeof(long) * 3 + 2]; sprintf(uid_str, "%lu", (long)client_uid); dd_save_text(dd, FILENAME_UID, uid_str); GHashTableIter iter; gpointer gpkey; gpointer gpvalue; g_hash_table_iter_init(&iter, problem_info); while (g_hash_table_iter_next(&iter, &gpkey, &gpvalue)) { dd_save_text(dd, (gchar *) gpkey, (gchar *) gpvalue); } dd_save_text(dd, FILENAME_ABRT_VERSION, VERSION); dd_close(dd); /* Not needing it anymore */ g_hash_table_destroy(problem_info); /* Move the completely created problem directory * to final directory. */ char *newpath = xstrndup(path, strlen(path) - strlen(".new")); if (rename(path, newpath) == 0) strcpy(path, newpath); free(newpath); log_notice("Saved problem directory of pid %u to '%s'", pid, path); /* We let the peer know that problem dir was created successfully * _before_ we run potentially long-running post-create. */ printf("HTTP/1.1 201 Created\r\n\r\n"); fflush(NULL); /* Closing STDIN_FILENO (abrtd duped the socket to stdin and stdout) and * not-replacing it with something else to let abrt-server die on reading * from invalid stdin - to catch bugs. */ close(STDIN_FILENO); close(STDOUT_FILENO); xdup2(STDERR_FILENO, STDOUT_FILENO); /* paranoia: don't leave stdout fd closed */ /* Trim old problem directories if necessary */ if (g_settings_nMaxCrashReportsSize > 0) { trim_problem_dirs(g_settings_dump_location, g_settings_nMaxCrashReportsSize * (double)(1024*1024), path); } run_post_create(path, NULL); /* free(path); */ exit(0); } static gboolean key_value_ok(gchar *key, gchar *value) { char *i; /* check key, it has to be valid filename and will end up in the * bugzilla */ for (i = key; *i != 0; i++) { if (!isalpha(*i) && (*i != '-') && (*i != '_') && (*i != ' ')) return FALSE; } /* check value of 'basename', it has to be valid non-hidden directory * name */ if (strcmp(key, "basename") == 0 || strcmp(key, FILENAME_TYPE) == 0 ) { if (!str_is_correct_filename(value)) { error_msg("Value of '%s' ('%s') is not a valid directory name", key, value); return FALSE; } } return allowed_new_user_problem_entry(client_uid, key, value); } /* Handles a message received from client over socket. */ static void process_message(GHashTable *problem_info, char *message) { gchar *key, *value; value = strchr(message, '='); if (value) { key = g_ascii_strdown(message, value - message); /* result is malloced */ //TODO: is it ok? it uses g_malloc, not malloc! value++; if (key_value_ok(key, value)) { if (strcmp(key, FILENAME_UID) == 0) { error_msg("Ignoring value of %s, will be determined later", FILENAME_UID); } else { g_hash_table_insert(problem_info, key, xstrdup(value)); /* Prevent freeing key later: */ key = NULL; } } else { /* should use error_msg_and_die() here? */ error_msg("Invalid key or value format: %s", message); } free(key); } else { /* should use error_msg_and_die() here? */ error_msg("Invalid message format: '%s'", message); } } static void die_if_data_is_missing(GHashTable *problem_info) { gboolean missing_data = FALSE; gchar **pstring; static const gchar *const needed[] = { FILENAME_TYPE, FILENAME_REASON, /* FILENAME_BACKTRACE, - ECC errors have no such elements */ /* FILENAME_EXECUTABLE, */ NULL }; for (pstring = (gchar**) needed; *pstring; pstring++) { if (!g_hash_table_lookup(problem_info, *pstring)) { error_msg("Element '%s' is missing", *pstring); missing_data = TRUE; } } if (missing_data) error_msg_and_die("Some data is missing, aborting"); } /* * Takes hash table, looks for key FILENAME_PID and tries to convert its value * to int. */ unsigned convert_pid(GHashTable *problem_info) { long ret; gchar *pid_str = (gchar *) g_hash_table_lookup(problem_info, FILENAME_PID); char *err_pos; if (!pid_str) error_msg_and_die("PID data is missing, aborting"); errno = 0; ret = strtol(pid_str, &err_pos, 10); if (errno || pid_str == err_pos || *err_pos != '\0' || ret > UINT_MAX || ret < 1) error_msg_and_die("Malformed or out-of-range PID number: '%s'", pid_str); return (unsigned) ret; } static int perform_http_xact(struct response *rsp) { /* use free instead of g_free so that we can use xstr* functions from * libreport/lib/xfuncs.c */ GHashTable *problem_info = g_hash_table_new_full(g_str_hash, g_str_equal, free, free); /* Read header */ char *body_start = NULL; char *messagebuf_data = NULL; unsigned messagebuf_len = 0; /* Loop until EOF/error/timeout/end_of_header */ while (1) { messagebuf_data = xrealloc(messagebuf_data, messagebuf_len + INPUT_BUFFER_SIZE); char *p = messagebuf_data + messagebuf_len; int rd = read(STDIN_FILENO, p, INPUT_BUFFER_SIZE); if (rd < 0) { if (errno == EINTR) /* SIGALRM? */ error_msg_and_die("Timed out"); perror_msg_and_die("read"); } if (rd == 0) break; log_debug("Received %u bytes of data", rd); messagebuf_len += rd; total_bytes_read += rd; if (total_bytes_read > MAX_MESSAGE_SIZE) error_msg_and_die("Message is too long, aborting"); /* Check whether we see end of header */ /* Note: we support both [\r]\n\r\n and \n\n */ char *past_end = messagebuf_data + messagebuf_len; if (p > messagebuf_data+1) p -= 2; /* start search from two last bytes in last read - they might be '\n\r' */ while (p < past_end) { p = memchr(p, '\n', past_end - p); if (!p) break; p++; if (p >= past_end) break; if (*p == '\n' || (*p == '\r' && p+1 < past_end && p[1] == '\n') ) { body_start = p + 1 + (*p == '\r'); *p = '\0'; goto found_end_of_header; } } } /* while (read) */ found_end_of_header: ; log_debug("Request: %s", messagebuf_data); /* Sanitize and analyze header. * Header now is in messagebuf_data, NUL terminated string, * with last empty line deleted (by placement of NUL). * \r\n are not (yet) converted to \n, multi-line headers also * not converted. */ /* First line must be "op[http://host]/pathHTTP/n.n". * is exactly one space char. */ if (prefixcmp(messagebuf_data, "DELETE ") == 0) { messagebuf_data += strlen("DELETE "); char *space = strchr(messagebuf_data, ' '); if (!space || prefixcmp(space+1, "HTTP/") != 0) return 400; /* Bad Request */ *space = '\0'; //decode_url(messagebuf_data); %20 => ' ' alarm(0); return delete_path(messagebuf_data); } /* We erroneously used "PUT /" to create new problems. * POST is the correct request in this case: * "PUT /" implies creation or replace of resource named "/"! * Delete PUT in 2014. */ if (prefixcmp(messagebuf_data, "PUT ") != 0 && prefixcmp(messagebuf_data, "POST ") != 0 ) { return 400; /* Bad Request */ } enum { CREATION_NOTIFICATION, CREATION_REQUEST, }; int url_type; char *url = skip_non_whitespace(messagebuf_data) + 1; /* skip "POST " */ if (prefixcmp(url, "/creation_notification ") == 0) url_type = CREATION_NOTIFICATION; else if (prefixcmp(url, "/ ") == 0) url_type = CREATION_REQUEST; else return 400; /* Bad Request */ /* Read body */ if (!body_start) { log_warning("Premature EOF detected, exiting"); return 400; /* Bad Request */ } messagebuf_len -= (body_start - messagebuf_data); memmove(messagebuf_data, body_start, messagebuf_len); log_debug("Body so far: %u bytes, '%s'", messagebuf_len, messagebuf_data); /* Loop until EOF/error/timeout */ while (1) { if (url_type == CREATION_REQUEST) { while (1) { unsigned len = strnlen(messagebuf_data, messagebuf_len); if (len >= messagebuf_len) break; /* messagebuf has at least one NUL - process the line */ process_message(problem_info, messagebuf_data); messagebuf_len -= (len + 1); memmove(messagebuf_data, messagebuf_data + len + 1, messagebuf_len); } } messagebuf_data = xrealloc(messagebuf_data, messagebuf_len + INPUT_BUFFER_SIZE + 1); int rd = read(STDIN_FILENO, messagebuf_data + messagebuf_len, INPUT_BUFFER_SIZE); if (rd < 0) { if (errno == EINTR) /* SIGALRM? */ error_msg_and_die("Timed out"); perror_msg_and_die("read"); } if (rd == 0) break; log_debug("Received %u bytes of data", rd); messagebuf_len += rd; total_bytes_read += rd; if (total_bytes_read > MAX_MESSAGE_SIZE) error_msg_and_die("Message is too long, aborting"); } /* Body received, EOF was seen. Don't let alarm to interrupt after this. */ alarm(0); int ret = 0; if (url_type == CREATION_NOTIFICATION) { if (client_uid != 0) { error_msg("UID=%ld is not authorized to trigger post-create processing", (long)client_uid); ret = 403; /* Forbidden */ goto out; } messagebuf_data[messagebuf_len] = '\0'; return run_post_create(messagebuf_data, rsp); } die_if_data_is_missing(problem_info); /* Save problem dir */ char *executable = g_hash_table_lookup(problem_info, FILENAME_EXECUTABLE); if (executable) { char *last_file = concat_path_file(g_settings_dump_location, "last-via-server"); int repeating_crash = check_recent_crash_file(last_file, executable); free(last_file); if (repeating_crash) /* Only pretend that we saved it */ { error_msg("Not saving repeating crash in '%s'", executable); goto out; /* ret is 0: "success" */ } } #if 0 //TODO: /* At least it should generate local problem identifier UUID */ problem_data_add_basics(problem_info); //...the problem being that problem_info here is not a problem_data_t! #endif unsigned pid = convert_pid(problem_info); struct ns_ids client_ids; if (get_ns_ids(client_pid, &client_ids) < 0) error_msg_and_die("Cannot get peer's Namespaces from /proc/%d/ns", client_pid); if (client_ids.nsi_ids[PROC_NS_ID_PID] != g_ns_ids.nsi_ids[PROC_NS_ID_PID]) { log_notice("Client is running in own PID Namespace, using PID %d instead of %d", client_pid, pid); pid = client_pid; } create_problem_dir(problem_info, pid); /* does not return */ out: g_hash_table_destroy(problem_info); return ret; /* Used as HTTP response code */ } static void dummy_handler(int sig_unused) {} int main(int argc, char **argv) { /* I18n */ setlocale(LC_ALL, ""); #if ENABLE_NLS bindtextdomain(PACKAGE, LOCALEDIR); textdomain(PACKAGE); #endif abrt_init(argv); /* Can't keep these strings/structs static: _() doesn't support that */ const char *program_usage_string = _( "& [options]" ); enum { OPT_v = 1 << 0, OPT_u = 1 << 1, OPT_s = 1 << 2, OPT_p = 1 << 3, }; /* Keep enum above and order of options below in sync! */ struct options program_options[] = { OPT__VERBOSE(&g_verbose), OPT_INTEGER('u', NULL, &client_uid, _("Use NUM as client uid")), OPT_BOOL( 's', NULL, NULL , _("Log to syslog")), OPT_BOOL( 'p', NULL, NULL , _("Add program names to log")), OPT_END() }; unsigned opts = parse_opts(argc, argv, program_options, program_usage_string); export_abrt_envvars(opts & OPT_p); msg_prefix = xasprintf("%s[%u]", g_progname, getpid()); if (opts & OPT_s) { logmode = LOGMODE_JOURNAL; } /* Set up timeout handling */ /* Part 1 - need this to make SIGALRM interrupt syscalls * (as opposed to restarting them): I want read syscall to be interrupted */ struct sigaction sa; /* sa.sa_flags.SA_RESTART bit is clear: make signal interrupt syscalls */ memset(&sa, 0, sizeof(sa)); sa.sa_handler = dummy_handler; /* pity, SIG_DFL won't do */ sigaction(SIGALRM, &sa, NULL); /* Part 2 - set the timeout per se */ alarm(TIMEOUT); /* Get uid of the connected client */ struct ucred cr; socklen_t crlen = sizeof(cr); if (0 != getsockopt(STDIN_FILENO, SOL_SOCKET, SO_PEERCRED, &cr, &crlen)) perror_msg_and_die("getsockopt(SO_PEERCRED)"); if (crlen != sizeof(cr)) error_msg_and_die("%s: bad crlen %d", "getsockopt(SO_PEERCRED)", (int)crlen); if (client_uid == (uid_t)-1L) client_uid = cr.uid; client_pid = cr.pid; pid_t pid = getpid(); if (get_ns_ids(getpid(), &g_ns_ids) < 0) error_msg_and_die("Cannot get own Namespaces from /proc/%d/ns", pid); load_abrt_conf(); struct response rsp = { 0 }; int r = perform_http_xact(&rsp); if (r == 0) r = 200; if (rsp.code == 0) rsp.code = r; free_abrt_conf_data(); printf("HTTP/1.1 %u \r\n\r\n", rsp.code); if (rsp.message != NULL) { printf("%s", rsp.message); fflush(stdout); free(rsp.message); } return (r >= 400); /* Error if 400+ */ }