summaryrefslogtreecommitdiffstats
path: root/src/daemon/dumpsocket.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/daemon/dumpsocket.cpp')
-rw-r--r--src/daemon/dumpsocket.cpp573
1 files changed, 573 insertions, 0 deletions
diff --git a/src/daemon/dumpsocket.cpp b/src/daemon/dumpsocket.cpp
new file mode 100644
index 00000000..699a0609
--- /dev/null
+++ b/src/daemon/dumpsocket.cpp
@@ -0,0 +1,573 @@
+/*
+ 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 "dumpsocket.h"
+#include "abrtlib.h"
+#include <glib.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include "debug_dump.h"
+#include "crash_types.h"
+#include "abrt_exception.h"
+#include "hooklib.h"
+#include "strbuf.h"
+
+#define SOCKET_FILE VAR_RUN"/abrt/abrt.socket"
+#define SOCKET_PERMISSION 0666
+
+/* 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)
+
+/* Maximum number of simultaneously opened client connections. */
+#define MAX_CLIENT_COUNT 10
+
+/* Interval between checks of client halt, in seconds. */
+#define CLIENT_CHECK_INTERVAL 10
+
+/* Interval with no data received from client, after which the client is
+ considered halted, in seconds. */
+#define CLIENT_HALT_INTERVAL 10
+
+/* Maximal number of characters read from socket at once. */
+#define INPUT_BUFFER_SIZE 1024
+
+static GIOChannel *channel = NULL;
+static guint channel_cb_id = 0;
+static int client_count = 0;
+
+/* Information about single socket session. */
+struct client
+{
+ /* Client user id */
+ uid_t uid;
+ /* Buffer for incomplete incoming messages. */
+ GByteArray *messagebuf;
+ /* Executable. */
+ char *executable;
+ /* Process ID. */
+ int pid;
+ char *backtrace;
+ /* Python, Ruby etc. */
+ char *analyzer;
+ /* Directory base name: python (or pyhook), ruby etc. */
+ char *basename;
+ /* Crash reason.
+ * Python example: "CCMainWindow.py:1:<module>:ZeroDivisionError:
+ * integer division or modulo by zero"
+ */
+ char *reason;
+ /* Last time some data were received over the socket
+ * from the client.
+ */
+ time_t lastwrite;
+ /* Timer checking client halt. */
+ guint timer_id;
+ /* Client socket callback id. */
+ guint socket_id;
+ /* Client socket channel */
+ GIOChannel *channel;
+};
+
+static gboolean server_socket_cb(GIOChannel *source,
+ GIOCondition condition,
+ gpointer data);
+
+/* Releases all memory that belongs to a client session. */
+static void client_free(struct client *client)
+{
+ /* Delete the uncompleted message if there is some. */
+ g_byte_array_free(client->messagebuf, TRUE);
+ free(client->executable);
+ free(client->backtrace);
+ free(client->analyzer);
+ free(client->basename);
+ free(client->reason);
+ g_source_remove(client->timer_id);
+ g_source_remove(client->socket_id);
+ g_io_channel_unref(client->channel);
+ free(client);
+ --client_count;
+ if (!channel_cb_id)
+ {
+ channel_cb_id = g_io_add_watch(channel,
+ (GIOCondition)(G_IO_IN | G_IO_PRI),
+ (GIOFunc)server_socket_cb,
+ NULL);
+ if (!channel_cb_id)
+ perror_msg_and_die("dumpsocket: Can't add socket watch");
+ }
+}
+
+/* Callback called by glib main loop at regular intervals when
+ some client is connected. */
+static gboolean client_check_cb(gpointer data)
+{
+ struct client *client = (struct client*)data;
+ if (time(NULL) - client->lastwrite > CLIENT_HALT_INTERVAL)
+ {
+ log("dumpsocket: client socket timeout reached, closing connection");
+ client_free(client);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/* Caller is responsible to free() the returned value. */
+static char *giocondition_to_string(GIOCondition condition)
+{
+ struct strbuf *strbuf = strbuf_new();
+ if (condition & G_IO_HUP)
+ strbuf_append_str(strbuf, "G_IO_HUP | ");
+ if (condition & G_IO_ERR)
+ strbuf_append_str(strbuf, "G_IO_ERR | ");
+ if (condition & G_IO_NVAL)
+ strbuf_append_str(strbuf, "G_IO_NVAL | ");
+ if (condition & G_IO_IN)
+ strbuf_append_str(strbuf, "G_IO_IN | ");
+ if (condition & G_IO_OUT)
+ strbuf_append_str(strbuf, "G_IO_OUT | ");
+ if (condition & G_IO_PRI)
+ strbuf_append_str(strbuf, "G_IO_PRI | ");
+ if (strbuf->len == 0)
+ strbuf_append_str(strbuf, "none");
+ else
+ {
+ /* remove the last " | " */
+ strbuf->len -= 3;
+ strbuf->buf[strbuf->len] = '\0';
+ }
+ char *result = strbuf->buf;
+ strbuf_free_nobuf(strbuf);
+ return result;
+}
+
+/* Create a new debug dump from client session.
+ * Caller must ensure that all fields in struct client
+ * are properly filled.
+ */
+static void create_debug_dump(struct client *client)
+{
+ /* Create temp directory with the debug dump.
+ This directory is renamed to final directory name after
+ all files have been stored into it.
+ */
+ char *path = xasprintf(DEBUG_DUMPS_DIR"/%s-%ld-%u.new",
+ client->basename,
+ (long)time(NULL),
+ client->pid);
+ /* No need to check the path length, as all variables used are limited, and dd.Create()
+ fails if the path is too long. */
+
+ CDebugDump dd;
+ try {
+ dd.Create(path, client->uid);
+ } catch (CABRTException &e) {
+ dd.Delete();
+ dd.Close();
+ error_msg_and_die("dumpsocket: Error while creating crash dump %s: %s", path, e.what());
+ }
+
+ dd.SaveText(FILENAME_ANALYZER, client->analyzer);
+ dd.SaveText(FILENAME_EXECUTABLE, client->executable);
+ dd.SaveText(FILENAME_BACKTRACE, client->backtrace);
+ dd.SaveText(FILENAME_REASON, client->reason);
+
+ /* Obtain and save the command line. */
+ char *cmdline = get_cmdline(client->pid); // never NULL
+ dd.SaveText(FILENAME_CMDLINE, cmdline);
+ free(cmdline);
+
+ /* Store id of the user whose application crashed. */
+ char uid_str[sizeof(long) * 3 + 2];
+ sprintf(uid_str, "%lu", (long)client->uid);
+ dd.SaveText(CD_UID, uid_str);
+
+ dd.Close();
+
+ /* Move the completely created debug dump to
+ final directory. */
+ char *newpath = xstrndup(path, strlen(path) - strlen(".new"));
+ if (rename(path, newpath) == 0)
+ strcpy(path, newpath);
+ free(newpath);
+
+ log("dumpsocket: Saved %s crash dump of pid %u to %s",
+ client->analyzer, client->pid, path);
+
+ /* Handle free space checking. */
+ unsigned maxCrashReportsSize = 0;
+ parse_conf(NULL, &maxCrashReportsSize, NULL, NULL);
+ if (maxCrashReportsSize > 0)
+ {
+ check_free_space(maxCrashReportsSize);
+ trim_debug_dumps(maxCrashReportsSize, path);
+ }
+
+ free(path);
+}
+
+/* Checks if a string contains only printable characters. */
+static bool printable_str(const char *str)
+{
+ do {
+ if ((unsigned char)(*str) < ' ' || *str == 0x7f)
+ return false;
+ str++;
+ } while (*str);
+ return true;
+}
+
+/* Checks if a string has certain prefix. */
+static bool starts_with(const char *str, const char *start)
+{
+ return strncmp(str, start, strlen(start)) == 0;
+}
+
+/* @returns
+ * Caller is responsible to call free() on the returned
+ * pointer.
+ * If NULL is returned, string extraction failed.
+ */
+static char *try_to_get_string(const char *message,
+ const char *tag,
+ size_t max_len,
+ bool printable,
+ bool allow_slashes)
+{
+ if (!starts_with(message, tag))
+ return NULL;
+
+ const char *contents = message + strlen(tag);
+ if ((printable && !printable_str(contents)) ||
+ (!allow_slashes && strchr(contents, '/')))
+ {
+ error_msg("dumpsocket: Received %s contains invalid characters -> skipping", tag);
+ return NULL;
+ }
+
+ if (strlen(contents) > max_len)
+ {
+ char *max_len_str = g_format_size_for_display(max_len);
+ error_msg("dumpsocket: Received %s too long -> trimming to %s", tag, max_len_str);
+ g_free(max_len_str);
+ }
+
+ return xstrndup(contents, max_len);
+}
+
+/* Handles a message received from client over socket. */
+static void process_message(struct client *client, const char *message)
+{
+/* @param tag
+ * The message identifier. Message starting with it
+ * is handled by this macro.
+ * @param field
+ * Member in struct client, which should be filled by
+ * the field contents.
+ * @param max_len
+ * Maximum length of the field in bytes.
+ * Exceeding bytes are trimmed.
+ * @param printable
+ * Whether to limit the field contents to ASCII only.
+ * @param allow_slashes
+ * Whether to allow slashes to be a part of input.
+ */
+#define HANDLE_INCOMING_STRING(tag, field, max_len, printable, allow_slashes) \
+ char *field = try_to_get_string(message, tag, max_len, printable, allow_slashes); \
+ if (field) \
+ { \
+ free(client->field); \
+ client->field = field; \
+ return; \
+ }
+
+ HANDLE_INCOMING_STRING("EXECUTABLE=", executable, PATH_MAX, true, true);
+ HANDLE_INCOMING_STRING("BACKTRACE=", backtrace, MAX_BACKTRACE_SIZE, false, true);
+ HANDLE_INCOMING_STRING("BASENAME=", basename, 100, true, false);
+ HANDLE_INCOMING_STRING("ANALYZER=", analyzer, 100, true, true);
+ HANDLE_INCOMING_STRING("REASON=", reason, 512, false, true);
+
+#undef HANDLE_INCOMING_STRING
+
+ /* PID is not handled as a string, we convert it to pid_t. */
+ if (starts_with(message, "PID="))
+ {
+ /* xatou() cannot be used here, because it would
+ * kill whole daemon by non-numeric string.
+ */
+ char *endptr;
+ errno = 0;
+ const char *nptr = message + strlen("PID=");
+ unsigned long number = strtoul(nptr, &endptr, 10);
+ /* pid == 0 is error, the lowest PID is 1. */
+ if (errno || nptr == endptr || *endptr != '\0' || number > UINT_MAX || number == 0)
+ {
+ error_msg("dumpsocket: invalid PID received -> ignoring");
+ return;
+ }
+ client->pid = number;
+ return;
+ }
+
+ /* Creates debug dump if all fields were already provided. */
+ if (starts_with(message, "DONE"))
+ {
+ if (client->pid == 0 ||
+ client->backtrace == NULL ||
+ client->executable == NULL ||
+ client->analyzer == NULL ||
+ client->basename == NULL ||
+ client->reason == NULL)
+ {
+ error_msg("dumpsocket: DONE received, but some data are missing -> ignoring");
+ return;
+ }
+
+ create_debug_dump(client);
+ return;
+ }
+}
+
+/* Callback called by glib main loop when ABRT receives data that have
+ * been written to the socket by some client.
+ */
+static gboolean client_socket_cb(GIOChannel *source,
+ GIOCondition condition,
+ gpointer data)
+{
+ struct client *client = (struct client*)data;
+
+ /* Detailed logging, useful for debugging. */
+ if (g_verbose >= 3)
+ {
+ char *cond = giocondition_to_string(condition);
+ log("dumpsocket: client condition %s", cond);
+ free(cond);
+ }
+
+ /* Handle incoming data. */
+ if (condition & (G_IO_IN | G_IO_PRI))
+ {
+ guint loop = client->messagebuf->len;
+ gsize len;
+ gchar buf[INPUT_BUFFER_SIZE];
+ GError *err = NULL;
+ /* Read data in chunks of size INPUT_BUFFER_SIZE. This allows to limit the number of
+ bytes received (to prevent memory exhaustion). */
+ do {
+ GIOStatus result = g_io_channel_read_chars(source, buf, INPUT_BUFFER_SIZE, &len, &err);
+ if (result == G_IO_STATUS_ERROR)
+ {
+ g_assert(err);
+ error_msg("dumpsocket: Error while reading data from client socket: %s", err->message);
+ g_error_free(err);
+ client_free(client);
+ return FALSE;
+ }
+
+ if (g_verbose >= 3)
+ log("dumpsocket: received %zu bytes of data", len);
+
+ /* Append the incoming data to the message buffer. */
+ g_byte_array_append(client->messagebuf, (guint8*)buf, len);
+
+ if (client->messagebuf->len > MAX_MESSAGE_SIZE)
+ {
+ error_msg("dumpsocket: Message too long.");
+ client_free(client);
+ return FALSE;
+ }
+ } while (len > 0);
+
+ /* Check, if we received a complete message now. */
+ for (; loop < client->messagebuf->len; ++loop)
+ {
+ if (client->messagebuf->data[loop] != '\0')
+ continue;
+
+ VERB3 log("dumpsocket: Processing message: %s",
+ client->messagebuf->data);
+
+ /* Process the message. */
+ process_message(client, (char*)client->messagebuf->data);
+ /* Remove the message including the ending \0 */
+ g_byte_array_remove_range(client->messagebuf, 0, loop + 1);
+ loop = 0;
+ }
+
+ /* Update the last write access time */
+ client->lastwrite = time(NULL);
+ }
+
+ /* Handle socket disconnection.
+ It is important to do it after handling G_IO_IN, because sometimes
+ G_IO_HUP comes together with G_IO_IN. It means that some data arrived
+ and then the socket has been closed.
+ */
+ if (condition & (G_IO_HUP | G_IO_ERR | G_IO_NVAL))
+ {
+ log("dumpsocket: Socket client disconnected");
+ client_free(client);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/* If the status indicates failure, report it. */
+static void check_status(GIOStatus status, GError *err, const char *operation)
+{
+ if (status == G_IO_STATUS_NORMAL)
+ return;
+
+ if (err)
+ {
+ error_msg("dumpsocket: Error while %s: %s", operation, err->message);
+ g_error_free(err);
+ }
+ else
+ error_msg("dumpsocket: Error while %s", operation);
+}
+
+/* Initializes a new client session data structure. */
+static struct client *client_new(int socket)
+{
+ struct client *client = (struct client*)xzalloc(sizeof(struct client));
+
+ /* Get credentials for the socket client. */
+ struct ucred cr;
+ socklen_t crlen = sizeof(struct ucred);
+ if (0 != getsockopt(socket, SOL_SOCKET, SO_PEERCRED, &cr, &crlen))
+ perror_msg_and_die("dumpsocket: Failed to get client uid");
+ if (crlen != sizeof(struct ucred))
+ perror_msg_and_die("dumpsocket: Failed to get client uid (crlen)");
+ client->uid = cr.uid;
+
+ client->messagebuf = g_byte_array_new();
+ client->lastwrite = time(NULL);
+
+ close_on_exec_on(socket);
+
+ /* Create client IO channel. */
+ client->channel = g_io_channel_unix_new(socket);
+ g_io_channel_set_close_on_unref(client->channel, TRUE);
+
+ /* Set nonblocking access. */
+ GError *err = NULL;
+ GIOStatus status = g_io_channel_set_flags(client->channel, G_IO_FLAG_NONBLOCK, &err);
+ check_status(status, err, "setting NONBLOCK flag");
+
+ /* Disable channel encoding to protect binary data. */
+ err = NULL;
+ status = g_io_channel_set_encoding(client->channel, NULL, &err);
+ check_status(status, err, "setting encoding");
+
+ /* Start timer to check the client problems. */
+ client->timer_id = g_timeout_add_seconds(CLIENT_CHECK_INTERVAL, client_check_cb, client);
+ if (!client->timer_id)
+ error_msg_and_die("dumpsocket: Can't add client timer");
+
+ /* Register client callback. */
+ client->socket_id = g_io_add_watch(client->channel,
+ (GIOCondition)(G_IO_IN | G_IO_PRI | G_IO_ERR | G_IO_HUP | G_IO_NVAL),
+ (GIOFunc)client_socket_cb,
+ client);
+ if (!client->socket_id)
+ error_msg_and_die("dumpsocket: Can't add client socket watch");
+
+ ++client_count;
+ return client;
+}
+
+/* Callback called by glib main loop when a client newly opens ABRT's socket. */
+static gboolean server_socket_cb(GIOChannel *source,
+ GIOCondition condition,
+ gpointer data)
+{
+ /* Check the limit for number of simultaneously attached clients. */
+ if (client_count >= MAX_CLIENT_COUNT)
+ {
+ error_msg("dumpsocket: Too many clients, refusing connection.");
+ /* To avoid infinite loop caused by the descriptor in "ready" state,
+ the callback must be disabled.
+ It is added back in client_free(). */
+ g_source_remove(channel_cb_id);
+ channel_cb_id = 0;
+ return TRUE;
+ }
+
+ struct sockaddr_un remote;
+ socklen_t len = sizeof(remote);
+ int socket = accept(g_io_channel_unix_get_fd(source),
+ (struct sockaddr*)&remote, &len);
+ if (socket == -1)
+ {
+ perror_msg("dumpsocket: Server can not accept client");
+ return TRUE;
+ }
+
+ log("dumpsocket: New client connected");
+ client_new(socket);
+ return TRUE;
+}
+
+/* Initializes the dump socket, usually in /var/run directory
+ * (the path depends on compile-time configuration).
+ */
+void dumpsocket_init()
+{
+ struct sockaddr_un local;
+ unlink(SOCKET_FILE); /* not caring about the result */
+ int socketfd = xsocket(AF_UNIX, SOCK_STREAM, 0);
+ close_on_exec_on(socketfd);
+ memset(&local, 0, sizeof(local));
+ local.sun_family = AF_UNIX;
+ strcpy(local.sun_path, SOCKET_FILE);
+ xbind(socketfd, (struct sockaddr*)&local, sizeof(local));
+ xlisten(socketfd, MAX_CLIENT_COUNT);
+
+ if (chmod(SOCKET_FILE, SOCKET_PERMISSION) != 0)
+ perror_msg_and_die("dumpsocket: failed to chmod socket file");
+
+ channel = g_io_channel_unix_new(socketfd);
+ g_io_channel_set_close_on_unref(channel, TRUE);
+ channel_cb_id = g_io_add_watch(channel,
+ (GIOCondition)(G_IO_IN | G_IO_PRI),
+ (GIOFunc)server_socket_cb,
+ NULL);
+ if (!channel_cb_id)
+ perror_msg_and_die("dumpsocket: Can't add socket watch");
+}
+
+/* Releases all resources used by dumpsocket. */
+void dumpsocket_shutdown()
+{
+ /* Set everything to pre-initialization state. */
+ if (channel)
+ {
+ /* This one is for g_io_add_watch. */
+ if (channel_cb_id)
+ g_source_remove(channel_cb_id);
+ /* This one is for g_io_channel_unix_new. */
+ g_io_channel_unref(channel);
+ channel = NULL;
+ }
+}