From 159d6e507eab08796670d7266b000e55122cdebd Mon Sep 17 00:00:00 2001 From: Christian Hergert Date: Sun, 3 Nov 2024 10:41:44 -0800 Subject: [PATCH 16/33] sysprofd: add support for unwinding without frame pointers This provides a new sysprof-live-unwinder subprocess that runs as root to allow accessing all processes on the system via /proc/$pid/. It is spawned by sysprofd with various perf event FDs and a FD to write captures to. Ideally the capture_fd is something that will naturally error if the client application crashes (such as a socketpair() having the peer close). This is not enforced but encouraged. Additionally, an event_fd is used to allow the client application to signal the live-unwinder to exit. Unwinding is performed by looking at the modules loaded into the target pid and using libdwfl to access DWARF/CFI/etc state machinery. Stack data does not touch the disk as it exists in a mmap buffer from perf and is then translated into a callchain and sent to the Sysprof client. Unwinding occurs as normal post-mortem though is improved through the use of debuginfod to locate the appropriate symbols. --- meson.build | 3 +- src/meson.build | 3 + src/sysprof-live-unwinder/main.c | 752 ++++++++++++++++++ src/sysprof-live-unwinder/meson.build | 19 + .../sysprof-live-process.c | 505 ++++++++++++ .../sysprof-live-process.h | 50 ++ .../sysprof-live-unwinder.c | 427 ++++++++++ .../sysprof-live-unwinder.h | 79 ++ src/sysprof-live-unwinder/tests/meson.build | 35 + .../tests/test-live-unwinder.c | 391 +++++++++ src/sysprofd/ipc-unwinder-impl.c | 247 ++++++ src/sysprofd/ipc-unwinder-impl.h | 34 + src/sysprofd/meson.build | 10 +- src/sysprofd/org.gnome.Sysprof3.Unwinder.xml | 23 + src/sysprofd/sysprofd.c | 6 +- 15 files changed, 2580 insertions(+), 4 deletions(-) create mode 100644 src/sysprof-live-unwinder/main.c create mode 100644 src/sysprof-live-unwinder/meson.build create mode 100644 src/sysprof-live-unwinder/sysprof-live-process.c create mode 100644 src/sysprof-live-unwinder/sysprof-live-process.h create mode 100644 src/sysprof-live-unwinder/sysprof-live-unwinder.c create mode 100644 src/sysprof-live-unwinder/sysprof-live-unwinder.h create mode 100644 src/sysprof-live-unwinder/tests/meson.build create mode 100644 src/sysprof-live-unwinder/tests/test-live-unwinder.c create mode 100644 src/sysprofd/ipc-unwinder-impl.c create mode 100644 src/sysprofd/ipc-unwinder-impl.h create mode 100644 src/sysprofd/org.gnome.Sysprof3.Unwinder.xml diff --git a/meson.build b/meson.build index 81e97072..9c37ebc4 100644 --- a/meson.build +++ b/meson.build @@ -39,12 +39,13 @@ need_glib = (need_gtk or get_option('tools') or get_option('tests')) need_libsysprof = (need_gtk or + get_option('sysprofd') == 'bundled' or get_option('libsysprof') or get_option('examples') or get_option('tools') or get_option('tests')) -dex_req = '0.6' +dex_req = '0.9' glib_req = '2.76.0' gtk_req = '4.15' polkit_req = '0.105' diff --git a/src/meson.build b/src/meson.build index f3e0c61d..9432755e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -11,6 +11,8 @@ sysprof_version_conf.set('MINOR_VERSION', sysprof_version[1]) sysprof_version_conf.set('MICRO_VERSION', 0) sysprof_version_conf.set('VERSION', meson.project_version()) +pkglibexecdir = join_paths(get_option('prefix'), get_option('libexecdir')) + subdir('libsysprof-capture') if need_libsysprof @@ -20,6 +22,7 @@ endif if get_option('sysprofd') == 'bundled' subdir('sysprofd') + subdir('sysprof-live-unwinder') endif if get_option('gtk') diff --git a/src/sysprof-live-unwinder/main.c b/src/sysprof-live-unwinder/main.c new file mode 100644 index 00000000..9e2733ae --- /dev/null +++ b/src/sysprof-live-unwinder/main.c @@ -0,0 +1,752 @@ +/* + * main.c + * + * Copyright 2024 Christian Hergert + * + * 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 3 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, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include + +#include +#include + +#include +#include + +#include + +#include "sysprof-live-unwinder.h" +#include "sysprof-perf-event-stream-private.h" + +#define CAPTURE_BUFFER_SIZE (4096*16) +#define N_PAGES 32 + +#define DUMP_BYTES(_n, _b, _l) \ + G_STMT_START { \ + GString *str, *astr; \ + gsize _i; \ + g_log(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, \ + " %s = %p [%d]", #_n, _b, (gint)_l); \ + str = g_string_sized_new (80); \ + astr = g_string_sized_new (16); \ + for (_i = 0; _i < _l; _i++) \ + { \ + if ((_i % 16) == 0) \ + g_string_append_printf (str, "%06x: ", (guint)_i); \ + g_string_append_printf (str, " %02x", _b[_i]); \ + \ + if (g_ascii_isprint(_b[_i])) \ + g_string_append_printf (astr, " %c", _b[_i]); \ + else \ + g_string_append (astr, " ."); \ + \ + if ((_i % 16) == 15) \ + { \ + g_log (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, \ + "%s %s", str->str, astr->str); \ + str->str[0] = str->len = 0; \ + astr->str[0] = astr->len = 0; \ + } \ + else if ((_i % 16) == 7) \ + { \ + g_string_append (str, " "); \ + g_string_append (astr, " "); \ + } \ + } \ + \ + if (_i != 16) \ + g_log (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, \ + "%-56s %s", str->str, astr->str); \ + \ + g_string_free (str, TRUE); \ + g_string_free (astr, TRUE); \ + } G_STMT_END + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SysprofCaptureWriter, sysprof_capture_writer_unref) + +typedef struct _PerfSource +{ + GSource gsource; + + SysprofLiveUnwinder *unwinder; + SysprofCaptureWriter *writer; + + guint64 map_size; + struct perf_event_mmap_page *map; + guint8 *map_data; + const guint8 *map_data_end; + guint64 map_data_size; + guint64 tail; + + guint8 *buffer; + + int cpu; + GPid self_pid; + + guint stack_size; +} PerfSource; + +typedef struct _PerfFDArg +{ + int fd; + int cpu; +} PerfFDArg; + +SYSPROF_ALIGNED_BEGIN(8); +typedef struct _StackRegs +{ + guint64 abi; + guint64 registers[0]; +} StackRegs +SYSPROF_ALIGNED_END(8); + +SYSPROF_ALIGNED_BEGIN(8); +typedef struct _StackUser +{ + guint64 size; + guint8 data[0]; +} StackUser +SYSPROF_ALIGNED_END(8); + +static GArray *all_perf_fds; + +static inline void +realign (gsize *pos, + gsize align) +{ + *pos = (*pos + align - 1) & ~(align - 1); +} + +static void +handle_event (PerfSource *source, + const SysprofPerfEvent *event) +{ + gsize offset; + gint64 time; + + g_assert (source != NULL); + g_assert (event != NULL); + + switch (event->header.type) + { + case PERF_RECORD_COMM: + offset = strlen (event->comm.comm) + 1; + realign (&offset, sizeof (guint64)); + offset += sizeof (GPid) + sizeof (GPid); + memcpy (&time, event->comm.comm + offset, sizeof time); + + if (event->comm.pid == event->comm.tid) + { + sysprof_live_unwinder_seen_process (source->unwinder, + time, + source->cpu, + event->comm.pid, + event->comm.comm); + } + + break; + + case PERF_RECORD_EXIT: + /* Ignore fork exits for now */ + if (event->exit.tid != event->exit.pid) + break; + + sysprof_live_unwinder_process_exited (source->unwinder, + event->exit.time, + source->cpu, + event->exit.pid); + + break; + + case PERF_RECORD_FORK: + sysprof_live_unwinder_process_forked (source->unwinder, + event->fork.time, + source->cpu, + event->fork.ptid, + event->fork.tid); + break; + + case PERF_RECORD_LOST: + { + char message[64]; + g_snprintf (message, sizeof message, + "Lost %"G_GUINT64_FORMAT" samples", + event->lost.lost); + sysprof_capture_writer_add_log (source->writer, + SYSPROF_CAPTURE_CURRENT_TIME, + source->cpu, + -1, + G_LOG_LEVEL_CRITICAL, "Sampler", message); + break; + } + + case PERF_RECORD_MMAP: + offset = strlen (event->mmap.filename) + 1; + realign (&offset, sizeof (guint64)); + offset += sizeof (GPid) + sizeof (GPid); + memcpy (&time, event->mmap.filename + offset, sizeof time); + + sysprof_live_unwinder_track_mmap (source->unwinder, + time, + source->cpu, + event->mmap.pid, + event->mmap.addr, + event->mmap.addr + event->mmap.len, + event->mmap.pgoff, + 0, + event->mmap.filename, + NULL); + + break; + + case PERF_RECORD_MMAP2: + offset = strlen (event->mmap2.filename) + 1; + realign (&offset, sizeof (guint64)); + offset += sizeof (GPid) + sizeof (GPid); + memcpy (&time, event->mmap2.filename + offset, sizeof time); + + if ((event->header.misc & PERF_RECORD_MISC_MMAP_BUILD_ID) != 0) + { + char build_id[G_N_ELEMENTS (event->mmap2.build_id) * 2 + 1]; + guint len = MIN (G_N_ELEMENTS (event->mmap2.build_id), event->mmap2.build_id_size); + + for (guint i = 0; i < len; i++) + g_snprintf (&build_id[len*2], 3, "%02x", event->mmap2.build_id[i]); + build_id[len*2] = 0; + + sysprof_live_unwinder_track_mmap (source->unwinder, + time, + source->cpu, + event->mmap2.pid, + event->mmap2.addr, + event->mmap2.addr + event->mmap2.len, + event->mmap2.pgoff, + event->mmap2.ino, + event->mmap2.filename, + build_id); + } + else + { + sysprof_live_unwinder_track_mmap (source->unwinder, + time, + source->cpu, + event->mmap2.pid, + event->mmap2.addr, + event->mmap2.addr + event->mmap2.len, + event->mmap2.pgoff, + event->mmap2.ino, + event->mmap2.filename, + NULL); + } + break; + + case PERF_RECORD_READ: + break; + + case PERF_RECORD_SAMPLE: + { + const guint8 *endptr = (const guint8 *)event + event->header.size; + const guint64 *ips = event->callchain.ips; + int n_ips = event->callchain.n_ips; + guint64 trace[3]; + + /* We always expect PERF_RECORD_SAMPLE to contain a callchain because + * we need that even if we sample the stack for user-space unwinding. + * Otherwise we lose the blended stack trace. + */ + if (n_ips == 0) + { + if (event->callchain.header.misc & PERF_RECORD_MISC_KERNEL) + { + trace[0] = PERF_CONTEXT_KERNEL; + trace[1] = event->callchain.ip; + trace[2] = PERF_CONTEXT_USER; + + ips = trace; + n_ips = 3; + } + else + { + trace[0] = PERF_CONTEXT_USER; + trace[1] = event->callchain.ip; + + ips = trace; + n_ips = 2; + } + } + + if (source->stack_size && source->stack_size < event->header.size) + { + guint64 dyn_size = *((const guint64 *)endptr - 1); + const StackUser *stack_user = (const StackUser *)(endptr - sizeof (guint64) - source->stack_size - sizeof (StackUser)); + const StackRegs *stack_regs = (const StackRegs *)&event->callchain.ips[event->callchain.n_ips]; + guint n_regs = ((const guint8 *)stack_user - (const guint8 *)stack_regs->registers) / sizeof (guint64); + +#if 0 + g_print ("n_ips=%u stack_size=%ld dyn_size=%ld abi=%ld n_regs=%d\n", + n_ips, stack_user->size, dyn_size, stack_regs->abi, n_regs); +#endif + + sysprof_live_unwinder_process_sampled_with_stack (source->unwinder, + event->callchain.time, + source->cpu, + event->callchain.pid, + event->callchain.tid, + ips, + n_ips, + stack_user->data, + stack_user->size, + dyn_size, + stack_regs->abi, + stack_regs->registers, + n_regs); + + break; + } + + sysprof_live_unwinder_process_sampled (source->unwinder, + event->callchain.time, + source->cpu, + event->callchain.pid, + event->callchain.tid, + ips, + n_ips); + + break; + } + + case PERF_RECORD_THROTTLE: + case PERF_RECORD_UNTHROTTLE: + default: + break; + } +} + +static gboolean +perf_source_prepare (GSource *gsource, + int *timeout) +{ + *timeout = 50; + return FALSE; +} + +static gboolean +perf_source_check (GSource *gsource) +{ + PerfSource *source = (PerfSource *)gsource; + guint64 head; + guint64 tail; + + atomic_thread_fence (memory_order_acquire); + + tail = source->tail; + head = source->map->data_head; + + if (head < tail) + tail = head; + + return (head - tail) >= sizeof (struct perf_event_header); +} + +static gboolean +perf_source_dispatch (GSource *gsource, + GSourceFunc callback, + gpointer user_data) +{ + PerfSource *source = (PerfSource *)gsource; + guint64 n_bytes = source->map_data_size; + guint64 mask = n_bytes - 1; + guint64 head; + guint64 tail; + guint us = 0; + guint them = 0; + + g_assert (source != NULL); + + tail = source->tail; + head = source->map->data_head; + + atomic_thread_fence (memory_order_acquire); + + if (head < tail) + tail = head; + + while ((head - tail) >= sizeof (struct perf_event_header)) + { + const SysprofPerfEvent *event; + struct perf_event_header *header; + gboolean is_self = FALSE; + + /* Note that: + * + * - perf events are a multiple of 64 bits + * - the perf event header is 64 bits + * - the data area is a multiple of 64 bits + * + * which means there will always be space for one header, which means we + * can safely dereference the size field. + */ + header = (struct perf_event_header *)(gpointer)(source->map_data + (tail & mask)); + + if (header->size > head - tail) + { + /* The kernel did not generate a complete event. + * I don't think that can happen, but we may as well + * be paranoid. + */ + g_warn_if_reached (); + break; + } + + if (source->map_data + (tail & mask) + header->size > source->map_data_end) + { + guint8 *b = source->buffer; + gint n_before; + gint n_after; + + n_after = (tail & mask) + header->size - n_bytes; + n_before = header->size - n_after; + + memcpy (b, source->map_data + (tail & mask), n_before); + memcpy (b + n_before, source->map_data, n_after); + + header = (struct perf_event_header *)(gpointer)b; + } + + event = (SysprofPerfEvent *)header; + + switch (event->header.type) + { + default: + case PERF_RECORD_COMM: + case PERF_RECORD_EXIT: + case PERF_RECORD_FORK: + case PERF_RECORD_MMAP: + case PERF_RECORD_MMAP2: + break; + + case PERF_RECORD_SAMPLE: + is_self = event->callchain.pid == source->self_pid; + break; + + case PERF_RECORD_READ: + case PERF_RECORD_THROTTLE: + case PERF_RECORD_UNTHROTTLE: + goto skip_callback; + + case PERF_RECORD_LOST: + break; + } + + handle_event (source, event); + + us += is_self; + them += !is_self; + + skip_callback: + tail += header->size; + } + + source->tail = tail; + + atomic_thread_fence (memory_order_seq_cst); + + source->map->data_tail = tail; + + sysprof_capture_writer_flush (source->writer); + + return G_SOURCE_CONTINUE; +} + +static void +perf_source_finalize (GSource *gsource) +{ + PerfSource *source = (PerfSource *)gsource; + + if (source->map != NULL && + (gpointer)source->map != MAP_FAILED) + munmap ((gpointer)source->map, source->map_size); + + g_clear_pointer (&source->buffer, g_free); + g_clear_pointer (&source->writer, sysprof_capture_writer_unref); + g_clear_object (&source->unwinder); + + source->map = NULL; + source->map_data = NULL; + source->map_data_end = NULL; + source->map_data_size = 0; + source->map_size = 0; + source->tail = 0; +} + +static const GSourceFuncs source_funcs = { + .prepare = perf_source_prepare, + .check = perf_source_check, + .dispatch = perf_source_dispatch, + .finalize = perf_source_finalize, +}; + +static gboolean +perf_source_init (PerfSource *source, + int fd, + SysprofLiveUnwinder *unwinder, + SysprofCaptureWriter *writer, + int cpu, + int stack_size, + GError **error) +{ + gsize map_size; + guint8 *map; + + g_assert (source != NULL); + g_assert (writer != NULL); + g_assert (SYSPROF_IS_LIVE_UNWINDER (unwinder)); + g_assert (fd > STDERR_FILENO); + + map_size = N_PAGES * sysprof_getpagesize () + sysprof_getpagesize (); + map = mmap (NULL, map_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); + + if ((gpointer)map == MAP_FAILED) + { + int errsv = errno; + g_set_error_literal (error, + G_FILE_ERROR, + g_file_error_from_errno (errsv), + g_strerror (errsv)); + return FALSE; + } + + source->writer = sysprof_capture_writer_ref (writer); + source->unwinder = g_object_ref (unwinder); + source->buffer = g_malloc (map_size); + source->map_size = map_size; + source->map = (gpointer)map; + source->map_data = map + sysprof_getpagesize (); + source->map_data_size = N_PAGES * sysprof_getpagesize (); + source->map_data_end = source->map_data + source->map_data_size; + source->tail = 0; + source->self_pid = getpid (); + source->cpu = cpu; + source->stack_size = stack_size; + + g_source_add_unix_fd ((GSource *)source, fd, G_IO_IN); + + return TRUE; +} + +static GSource * +perf_source_new (int perf_fd, + SysprofLiveUnwinder *unwinder, + SysprofCaptureWriter *writer, + int cpu, + int stack_size, + GError **error) +{ + GSource *source; + + if (perf_fd <= STDERR_FILENO) + { + g_set_error (error, + G_FILE_ERROR, + G_FILE_ERROR_BADF, + "Invalid file-descriptor for perf event stream"); + return NULL; + } + + source = g_source_new ((GSourceFuncs *)&source_funcs, sizeof (PerfSource)); + g_source_set_static_name (source, "[perf-event-stream]"); + g_source_set_priority (source, G_PRIORITY_HIGH); + + if (!perf_source_init ((PerfSource *)source, perf_fd, unwinder, writer, cpu, stack_size, error)) + g_clear_pointer (&source, g_source_unref); + + return source; +} + +static void +clear_perf_fd (gpointer data) +{ + PerfFDArg *arg = data; + + if (arg->fd > STDERR_FILENO) + { + close (arg->fd); + arg->fd = -1; + } +} + +static gboolean +perf_fd_callback (const char *option_name, + const char *option_value, + gpointer data, + GError **error) +{ + PerfFDArg arg = {-1, -1}; + + if (sscanf (option_value, "%d:%d", &arg.fd, &arg.cpu) < 1) + { + g_set_error (error, + G_FILE_ERROR, + G_FILE_ERROR_BADF, + "--perf-fd must be in the format FD or FD:CPU_NUMBER"); + return FALSE; + } + + if (arg.fd <= STDERR_FILENO) + { + g_set_error (error, + G_FILE_ERROR, + G_FILE_ERROR_BADF, + "--perf-fd must be >= %d", + STDERR_FILENO); + return FALSE; + } + + g_array_append_val (all_perf_fds, arg); + + return TRUE; +} + +static void +bump_to_max_fd_limit (void) +{ + struct rlimit limit; + + if (getrlimit (RLIMIT_NOFILE, &limit) == 0) + { + limit.rlim_cur = limit.rlim_max; + + if (setrlimit (RLIMIT_NOFILE, &limit) != 0) + g_warning ("Failed to set FD limit to %"G_GSSIZE_FORMAT"", + (gssize)limit.rlim_max); + else + g_debug ("Set RLIMIT_NOFILE to %"G_GSSIZE_FORMAT"", + (gssize)limit.rlim_max); + } +} + +static gboolean +exit_callback (gpointer user_data) +{ + g_main_loop_quit (user_data); + return G_SOURCE_REMOVE; +} + +int +main (int argc, + char *argv[]) +{ + g_autoptr(SysprofCaptureWriter) writer = NULL; + g_autoptr(SysprofLiveUnwinder) unwinder = NULL; + g_autoptr(GOptionContext) context = NULL; + g_autoptr(GMainLoop) main_loop = NULL; + g_autoptr(GError) error = NULL; + g_autofd int capture_fd = -1; + g_autofd int kallsyms_fd = -1; + g_autofd int event_fd = -1; + int stack_size = 0; + const GOptionEntry entries[] = { + { "perf-fd", 0, 0, G_OPTION_ARG_CALLBACK, perf_fd_callback, "A file-descriptor to the perf event stream", "FD[:CPU]" }, + { "capture-fd", 0, 0, G_OPTION_ARG_INT, &capture_fd, "A file-descriptor to the sysprof capture", "FD" }, + { "event-fd", 0, 0, G_OPTION_ARG_INT, &event_fd, "A file-descriptor to an event-fd used to notify unwinder should exit", "FD" }, + { "kallsyms", 'k', 0, G_OPTION_ARG_INT, &kallsyms_fd, "Bundle kallsyms provided from passed FD", "FD" }, + { "stack-size", 's', 0, G_OPTION_ARG_INT, &stack_size, "Size of stacks being recorded", "STACK_SIZE" }, + { 0 } + }; + + main_loop = g_main_loop_new (NULL, FALSE); + + all_perf_fds = g_array_new (FALSE, FALSE, sizeof (PerfFDArg)); + g_array_set_clear_func (all_perf_fds, clear_perf_fd); + + context = g_option_context_new ("- translate perf event stream to sysprof"); + g_option_context_add_main_entries (context, entries, NULL); + g_option_context_set_summary (context, "\ + This tool is used by sysprofd to process incoming perf events that\n\ + include a copy of the stack and register state to the Sysprof capture\n\ + format.\n\ +\n\ + It should be provided two file-descriptors. One is for the perf-event\n\ + stream and one is for the Sysprof capture writer.\n\ +\n\ + Events that are not related to stack traces will also be passed along to\n\ + to the capture in the standard Sysprof capture format. That includes mmap\n\ + events, process information, and more.\n\ +\n\ +Examples:\n\ +\n\ + # FD 3 contains perf_event stream for CPU 1\n\ + sysprof-translate --perf-fd=3:1 --capture-fd=4"); + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("%s\n", error->message); + return EXIT_FAILURE; + } + + if (capture_fd <= STDERR_FILENO) + { + g_printerr ("--capture-fd must be > %d\n", STDERR_FILENO); + return EXIT_FAILURE; + } + + writer = sysprof_capture_writer_new_from_fd (g_steal_fd (&capture_fd), CAPTURE_BUFFER_SIZE); + + if (all_perf_fds->len == 0) + { + g_printerr ("You must secify at least one --perf-fd\n"); + return EXIT_FAILURE; + } + + bump_to_max_fd_limit (); + + unwinder = sysprof_live_unwinder_new (writer, g_steal_fd (&kallsyms_fd)); + + for (guint i = 0; i < all_perf_fds->len; i++) + { + const PerfFDArg *arg = &g_array_index (all_perf_fds, PerfFDArg, i); + g_autoptr(GSource) perf_source = NULL; + + if (!(perf_source = perf_source_new (arg->fd, unwinder, writer, arg->cpu, stack_size, &error))) + { + g_printerr ("Failed to initialize perf event stream: %s\n", + error->message); + return EXIT_FAILURE; + } + + g_source_attach (perf_source, NULL); + } + + if (event_fd != -1) + { + g_autoptr(GSource) exit_source = g_unix_fd_source_new (event_fd, G_IO_IN); + + g_source_set_callback (exit_source, + exit_callback, + g_main_loop_ref (main_loop), + (GDestroyNotify) g_main_loop_unref); + g_source_attach (exit_source, NULL); + } + + g_main_loop_run (main_loop); + + g_clear_pointer (&all_perf_fds, g_array_unref); + + return EXIT_SUCCESS; +} diff --git a/src/sysprof-live-unwinder/meson.build b/src/sysprof-live-unwinder/meson.build new file mode 100644 index 00000000..8cef7106 --- /dev/null +++ b/src/sysprof-live-unwinder/meson.build @@ -0,0 +1,19 @@ +sysprof_live_unwinder_deps = [ + libsysprof_static_dep, + dependency('libdw'), +] + +sysprof_live_unwinder_sources = [ + 'sysprof-live-process.c', + 'sysprof-live-unwinder.c', + 'main.c', +] + +sysprof_live_unwinder = executable('sysprof-live-unwinder', sysprof_live_unwinder_sources, + dependencies: sysprof_live_unwinder_deps, + c_args: release_flags, + install: true, + install_dir: pkglibexecdir, +) + +subdir('tests') diff --git a/src/sysprof-live-unwinder/sysprof-live-process.c b/src/sysprof-live-unwinder/sysprof-live-process.c new file mode 100644 index 00000000..7932048b --- /dev/null +++ b/src/sysprof-live-unwinder/sysprof-live-process.c @@ -0,0 +1,505 @@ +/* + * sysprof-live-pid.c + * + * Copyright 2024 Christian Hergert + * + * 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 3 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, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include +#include + +#include +#include + +/* Workaround for linux/fcntl.h also including the + * flock definitions in addition to libc. + */ +#ifndef _LINUX_FCNTL_H +# define _LINUX_FCNTL_H +# include +# undef _LINUX_FCNTL_H +#else +# include +#endif + +#include + +#include +#include +#include + +#include + +#include + +#include "sysprof-live-process.h" + +typedef struct _SysprofLiveProcess +{ + Dwfl_Callbacks callbacks; + Dwfl *dwfl; + Elf *elf; + char *root; + GPid pid; + int fd; +} SysprofLiveProcess; + +typedef struct _SysprofUnwinder +{ + SysprofLiveProcess *process; + const guint8 *stack; + gsize stack_len; + const guint64 *registers; + guint n_registers; + guint64 *addresses; + guint addresses_capacity; + guint addresses_len; + guint64 abi; + guint64 sp; + guint64 pc; + guint64 base_address; + GPid pid; + GPid tid; +} SysprofUnwinder; + +static SysprofUnwinder *current_unwinder; + +static inline GPid +sysprof_unwinder_next_thread (Dwfl *dwfl, + void *user_data, + void **thread_argp) +{ + SysprofUnwinder *unwinder = current_unwinder; + + if (*thread_argp == NULL) + { + *thread_argp = unwinder; + return unwinder->tid; + } + + return 0; +} + +static inline bool +sysprof_unwinder_get_thread (Dwfl *dwfl, + pid_t tid, + void *user_data, + void **thread_argp) +{ + SysprofUnwinder *unwinder = current_unwinder; + + if (unwinder->tid == tid) + { + *thread_argp = unwinder; + return TRUE; + } + + return FALSE; +} + +static inline bool +copy_word (const guint8 *data, + Dwarf_Word *result, + guint64 abi) +{ + if (abi == PERF_SAMPLE_REGS_ABI_64) + memcpy (result, data, sizeof (guint64)); + else if (abi == PERF_SAMPLE_REGS_ABI_32) + memcpy (result, data, sizeof (guint32)); + else + g_assert_not_reached (); + + return TRUE; +} + +static inline bool +sysprof_unwinder_memory_read (Dwfl *dwfl, + Dwarf_Addr addr, + Dwarf_Word *result, + void *user_data) +{ + SysprofUnwinder *unwinder = current_unwinder; + + if (addr < unwinder->base_address || addr - unwinder->base_address >= unwinder->stack_len) + { + Dwfl_Module *module = NULL; + Elf_Data *data = NULL; + Elf_Scn *section = NULL; + Dwarf_Addr bias; + + if (!(module = dwfl_addrmodule (dwfl, addr)) || + !(section = dwfl_module_address_section (module, &addr, &bias)) || + !(data = elf_getdata (section, NULL))) + return FALSE; + + if (data->d_buf && data->d_size > addr) + return copy_word ((guint8 *)data->d_buf + addr, result, unwinder->abi); + + return FALSE; + } + + return copy_word (&unwinder->stack[addr - unwinder->base_address], result, unwinder->abi); +} + +static inline bool +sysprof_unwinder_set_initial_registers (Dwfl_Thread *thread, + void *user_data) +{ + SysprofUnwinder *unwinder = current_unwinder; + + dwfl_thread_state_register_pc (thread, unwinder->pc); + + if (unwinder->abi == PERF_SAMPLE_REGS_ABI_64) + { + static const int regs_x86_64[] = {0, 3, 2, 1, 4, 5, 6, 7/*sp*/, 9, 10, 11, 12, 13, 14, 15, 16, 8/*ip*/}; + + for (int i = 0; i < G_N_ELEMENTS (regs_x86_64); i++) + { + int j = regs_x86_64[i]; + + dwfl_thread_state_registers (thread, i, 1, &unwinder->registers[j]); + } + } + else if (unwinder->abi == PERF_SAMPLE_REGS_ABI_32) + { + static const int regs_i386[] = {0, 2, 3, 1, 7/*sp*/, 6, 4, 5, 8/*ip*/}; + + for (int i = 0; i < G_N_ELEMENTS (regs_i386); i++) + { + int j = regs_i386[i]; + + dwfl_thread_state_registers (thread, i, 1, &unwinder->registers[j]); + } + } + + return TRUE; +} + +static const Dwfl_Thread_Callbacks thread_callbacks = { + sysprof_unwinder_next_thread, + sysprof_unwinder_get_thread, + sysprof_unwinder_memory_read, + sysprof_unwinder_set_initial_registers, + NULL, /* detach */ + NULL, /* thread_detach */ +}; + +static inline int +sysprof_unwinder_frame_cb (Dwfl_Frame *frame, + void *user_data) +{ + SysprofUnwinder *unwinder = current_unwinder; + Dwarf_Addr pc; + Dwarf_Addr sp; + bool is_activation; + guint8 sp_register_id; + + if (unwinder->addresses_len == unwinder->addresses_capacity) + return DWARF_CB_ABORT; + + if (!dwfl_frame_pc (frame, &pc, &is_activation)) + return DWARF_CB_ABORT; + + if (unwinder->abi == PERF_SAMPLE_REGS_ABI_64) + sp_register_id = 7; + else if (unwinder->abi == PERF_SAMPLE_REGS_ABI_32) + sp_register_id = 4; + else + return DWARF_CB_ABORT; + + if (dwfl_frame_reg (frame, sp_register_id, &sp) < 0) + return DWARF_CB_ABORT; + + unwinder->addresses[unwinder->addresses_len++] = pc; + + return DWARF_CB_OK; +} + +static inline guint +sysprof_unwind (SysprofLiveProcess *self, + Dwfl *dwfl, + Elf *elf, + GPid pid, + GPid tid, + guint64 abi, + const guint64 *registers, + guint n_registers, + const guint8 *stack, + gsize stack_len, + guint64 *addresses, + guint n_addresses) +{ +#if defined(__x86_64__) || defined(__i386__) + SysprofUnwinder unwinder; + + g_assert (dwfl != NULL); + g_assert (elf != NULL); + + /* Ignore anything byt 32/64 defined abi */ + if (!(abi == PERF_SAMPLE_REGS_ABI_32 || abi == PERF_SAMPLE_REGS_ABI_64)) + return 0; + + /* Make sure we have registers/stack to work with */ + if (registers == NULL || stack == NULL || stack_len == 0) + return 0; + + /* 9 registers on 32-bit x86, 17 on 64-bit x86_64 */ + if (!((abi == PERF_SAMPLE_REGS_ABI_32 && n_registers == 9) || + (abi == PERF_SAMPLE_REGS_ABI_64 && n_registers == 17))) + return 0; + + unwinder.process = self; + unwinder.sp = registers[7]; + unwinder.pc = registers[8]; + unwinder.base_address = unwinder.sp; + unwinder.addresses = addresses; + unwinder.addresses_capacity = n_addresses; + unwinder.addresses_len = 0; + unwinder.pid = pid; + unwinder.tid = tid; + unwinder.stack = stack; + unwinder.stack_len = stack_len; + unwinder.abi = abi; + unwinder.registers = registers; + unwinder.n_registers = n_registers; + + current_unwinder = &unwinder; + dwfl_getthread_frames (dwfl, tid, sysprof_unwinder_frame_cb, NULL); + current_unwinder = NULL; + + return unwinder.addresses_len; +#else + return 0; +#endif +} + +G_GNUC_NO_INLINE static int +_pidfd_open (int pid, + unsigned flags) +{ + int pidfd = syscall (SYS_pidfd_open, pid, flags); + + if (pidfd != -1) + { + int old_flags = fcntl (pidfd, F_GETFD); + + if (old_flags != -1) + fcntl (pidfd, F_SETFD, old_flags | FD_CLOEXEC); + } + + return pidfd; +} + +static int +sysprof_live_process_find_elf (Dwfl_Module *module, + void **user_data, + const char *module_name, + Dwarf_Addr base_addr, + char **filename, + Elf **elf) +{ + g_assert (current_unwinder != NULL); + g_assert (current_unwinder->process != NULL); + + *filename = NULL; + *elf = NULL; + + if (module_name[0] == '/') + { + g_autofree char *path = g_strdup_printf ("/proc/%u/root/%s", current_unwinder->pid, module_name); + g_autofd int fd = open (path, O_RDONLY | O_CLOEXEC); + + if (fd != -1) + { + *elf = dwelf_elf_begin (fd); + return g_steal_fd (&fd); + } + } + + return dwfl_linux_proc_find_elf (module, user_data, module_name, base_addr, filename, elf); +} + +static int +sysprof_live_process_find_debuginfo (Dwfl_Module *module, + void **user_data, + const char *module_name, + Dwarf_Addr base_addr, + const char *file_name, + const char *debuglink_file, + GElf_Word debuglink_crc, + char **debuginfo_file_name) +{ + return -1; +} + +SysprofLiveProcess * +sysprof_live_process_new (GPid pid) +{ + SysprofLiveProcess *live_process; + + live_process = g_atomic_rc_box_new0 (SysprofLiveProcess); + live_process->pid = pid; + live_process->fd = _pidfd_open (pid, 0); + live_process->root = g_strdup_printf ("/proc/%u/root/", pid); + live_process->callbacks.find_elf = sysprof_live_process_find_elf; + live_process->callbacks.find_debuginfo = sysprof_live_process_find_debuginfo; + live_process->callbacks.debuginfo_path = g_new0 (char *, 2); + live_process->callbacks.debuginfo_path[0] = g_build_filename (live_process->root, "usr/lib/debug", NULL); + + return live_process; +} + +SysprofLiveProcess * +sysprof_live_process_ref (SysprofLiveProcess *live_process) +{ + g_return_val_if_fail (live_process != NULL, NULL); + + return g_atomic_rc_box_acquire (live_process); +} + +static void +sysprof_live_process_finalize (gpointer data) +{ + SysprofLiveProcess *live_process = data; + + if (live_process->fd != -1) + { + close (live_process->fd); + live_process->fd = -1; + } + + g_clear_pointer (&live_process->elf, elf_end); + g_clear_pointer (&live_process->dwfl, dwfl_end); + g_clear_pointer (&live_process->root, g_free); + g_clear_pointer (&live_process->callbacks.debuginfo_path, g_free); +} + +void +sysprof_live_process_unref (SysprofLiveProcess *live_process) +{ + g_return_if_fail (live_process != NULL); + + g_atomic_rc_box_release_full (live_process, sysprof_live_process_finalize); +} + +gboolean +sysprof_live_process_is_active (SysprofLiveProcess *self) +{ + g_return_val_if_fail (self != NULL, FALSE); + + return self->fd > -1; +} + +static Dwfl * +sysprof_live_process_get_dwfl (SysprofLiveProcess *self) +{ + g_assert (self != NULL); + + if G_UNLIKELY (self->dwfl == NULL) + { + self->dwfl = dwfl_begin (&self->callbacks); + + dwfl_linux_proc_report (self->dwfl, self->pid); + dwfl_report_end (self->dwfl, NULL, NULL); + + if (self->fd > -1) + { + char path[64]; + g_autofd int exe_fd = -1; + + g_snprintf (path, sizeof path, "/proc/%u/exe", self->pid); + exe_fd = open (path, O_RDONLY); + + if (exe_fd > -1) + { + self->elf = elf_begin (exe_fd, ELF_C_READ_MMAP, NULL); + + if (self->elf != NULL) + dwfl_attach_state (self->dwfl, self->elf, self->pid, &thread_callbacks, self); + } + } + else + g_warning ("Attmpting to load exited process\n"); + } + + return self->dwfl; +} + +guint +sysprof_live_process_unwind (SysprofLiveProcess *self, + GPid tid, + guint64 abi, + const guint8 *stack, + gsize stack_len, + const guint64 *registers, + guint n_registers, + guint64 *addresses, + guint n_addresses) +{ +#if defined(__x86_64__) || defined(__i386__) + Dwfl *dwfl; + + g_assert (self != NULL); + g_assert (stack != NULL); + g_assert (registers != NULL); + g_assert (addresses != NULL); + + if (!sysprof_live_process_is_active (self)) + return 0; + + if (!(dwfl = sysprof_live_process_get_dwfl (self))) + return 0; + + if (self->elf == NULL) + return 0; + + g_assert (self->dwfl != NULL); + g_assert (self->elf != NULL); + + return sysprof_unwind (self, + self->dwfl, + self->elf, + self->pid, + tid, + abi, + registers, + n_registers, + stack, + stack_len, + addresses, + n_addresses); +#else + return 0; +#endif +} + +void +sysprof_live_process_add_map (SysprofLiveProcess *self, + guint64 begin, + guint64 end, + guint64 offset, + guint64 inode, + const char *filename) +{ + g_assert (self != NULL); + + /* We'll reparse VMAs on next use */ + g_clear_pointer (&self->dwfl, dwfl_end); + g_clear_pointer (&self->elf, elf_end); +} diff --git a/src/sysprof-live-unwinder/sysprof-live-process.h b/src/sysprof-live-unwinder/sysprof-live-process.h new file mode 100644 index 00000000..e2601a5b --- /dev/null +++ b/src/sysprof-live-unwinder/sysprof-live-process.h @@ -0,0 +1,50 @@ +/* + * sysprof-live-pid.h + * + * Copyright 2024 Christian Hergert + * + * 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 3 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, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +typedef struct _SysprofLiveProcess SysprofLiveProcess; + +SysprofLiveProcess *sysprof_live_process_new (GPid pid); +SysprofLiveProcess *sysprof_live_process_ref (SysprofLiveProcess *self); +void sysprof_live_process_unref (SysprofLiveProcess *self); +gboolean sysprof_live_process_is_active (SysprofLiveProcess *self); +void sysprof_live_process_add_map (SysprofLiveProcess *self, + guint64 begin, + guint64 end, + guint64 offset, + guint64 inode, + const char *filename); +guint sysprof_live_process_unwind (SysprofLiveProcess *self, + GPid tid, + guint64 abi, + const guint8 *stack, + gsize stack_len, + const guint64 *registers, + guint n_registers, + guint64 *addresses, + guint n_addresses); + +G_END_DECLS diff --git a/src/sysprof-live-unwinder/sysprof-live-unwinder.c b/src/sysprof-live-unwinder/sysprof-live-unwinder.c new file mode 100644 index 00000000..c3b954b2 --- /dev/null +++ b/src/sysprof-live-unwinder/sysprof-live-unwinder.c @@ -0,0 +1,427 @@ +/* + * sysprof-live-unwinder.c + * + * Copyright 2024 Christian Hergert + * + * 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 3 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, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include +#include + +#include + +#include + +#include "sysprof-live-process.h" +#include "sysprof-live-unwinder.h" +#include "sysprof-maps-parser-private.h" + +struct _SysprofLiveUnwinder +{ + GObject parent_instance; + SysprofCaptureWriter *writer; + GHashTable *live_pids_by_pid; +}; + +G_DEFINE_FINAL_TYPE (SysprofLiveUnwinder, sysprof_live_unwinder, G_TYPE_OBJECT) + +enum { + CLOSED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +static char * +sysprof_live_unwinder_read_file (SysprofLiveUnwinder *self, + const char *path, + gboolean insert_into_capture) +{ + gint64 when = SYSPROF_CAPTURE_CURRENT_TIME; + char *contents = NULL; + gsize len = 0; + gsize offset = 0; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + g_assert (self->writer != NULL); + + if (!g_file_get_contents (path, &contents, &len, NULL)) + return NULL; + + if (insert_into_capture) + { + while (len > 0) + { + gsize this_write = MIN (len, 4096*4); + + if (!sysprof_capture_writer_add_file (self->writer, + when, + -1, + -1, + path, + this_write == len, + (const guint8 *)&contents[offset], this_write)) + break; + + len -= this_write; + offset += this_write; + } + } + + return contents; +} + +static char * +sysprof_live_unwinder_read_pid_file (SysprofLiveUnwinder *self, + GPid pid, + const char *path_part, + gboolean insert_into_capture) +{ + g_autofree char *path = NULL; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + g_assert (self->writer != NULL); + + path = g_strdup_printf ("/proc/%d/%s", pid, path_part); + + return sysprof_live_unwinder_read_file (self, path, insert_into_capture); +} + +static SysprofLiveProcess * +sysprof_live_unwinder_find_pid (SysprofLiveUnwinder *self, + GPid pid, + gboolean send_comm) +{ + SysprofLiveProcess *live_pid; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + + if (pid < 0) + return NULL; + + if (!(live_pid = g_hash_table_lookup (self->live_pids_by_pid, GINT_TO_POINTER (pid)))) + { + gint64 now = SYSPROF_CAPTURE_CURRENT_TIME; + + live_pid = sysprof_live_process_new (pid); + + g_hash_table_replace (self->live_pids_by_pid, GINT_TO_POINTER (pid), live_pid); + + if (send_comm) + { + g_autofree char *path = g_strdup_printf ("/proc/%d/comm", pid); + g_autofree char *comm = NULL; + gsize len; + + if (g_file_get_contents (path, &comm, &len, NULL)) + { + g_autofree char *tmp = comm; + comm = g_strstrip (g_utf8_make_valid (tmp, len)); + sysprof_capture_writer_add_process (self->writer, now, -1, pid, comm); + } + } + + if (sysprof_live_process_is_active (live_pid)) + { + g_autofree char *mountinfo = sysprof_live_unwinder_read_pid_file (self, pid, "mountinfo", TRUE); + g_autofree char *maps = sysprof_live_unwinder_read_pid_file (self, pid, "maps", FALSE); + + if (maps != NULL) + { + SysprofMapsParser maps_parser; + guint64 begin, end, offset, inode; + char *filename; + + sysprof_maps_parser_init (&maps_parser, maps, -1); + + while (sysprof_maps_parser_next (&maps_parser, &begin, &end, &offset, &inode, &filename)) + { + sysprof_live_process_add_map (live_pid, begin, end, offset, inode, filename); + sysprof_capture_writer_add_map (self->writer, now, -1, pid, + begin, end, offset, + inode, filename); + g_free (filename); + } + } + } + } + + return live_pid; +} + +static void +sysprof_live_unwinder_mine_pids (SysprofLiveUnwinder *self) +{ + g_autoptr(GDir) dir = NULL; + const char *name; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + g_assert (self->writer != NULL); + + if (!(dir = g_dir_open ("/proc", 0, NULL))) + return; + + while ((name = g_dir_read_name (dir))) + { + GPid pid; + + if (!g_ascii_isdigit (*name)) + continue; + + if (!(pid = atoi (name))) + continue; + + sysprof_live_unwinder_find_pid (self, pid, TRUE); + } +} + +static void +sysprof_live_unwinder_finalize (GObject *object) +{ + SysprofLiveUnwinder *self = (SysprofLiveUnwinder *)object; + + g_clear_pointer (&self->writer, sysprof_capture_writer_unref); + g_clear_pointer (&self->live_pids_by_pid, g_hash_table_unref); + + G_OBJECT_CLASS (sysprof_live_unwinder_parent_class)->finalize (object); +} + +static void +sysprof_live_unwinder_class_init (SysprofLiveUnwinderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = sysprof_live_unwinder_finalize; + + signals[CLOSED] = + g_signal_new ("closed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, 0); +} + +static void +sysprof_live_unwinder_init (SysprofLiveUnwinder *self) +{ + self->live_pids_by_pid = g_hash_table_new_full (NULL, + NULL, + NULL, + (GDestroyNotify)sysprof_live_process_unref); +} + +SysprofLiveUnwinder * +sysprof_live_unwinder_new (SysprofCaptureWriter *writer, + int kallsyms_fd) +{ + SysprofLiveUnwinder *self; + g_autofree char *mounts = NULL; + + g_return_val_if_fail (writer != NULL, NULL); + + self = g_object_new (SYSPROF_TYPE_LIVE_UNWINDER, NULL); + self->writer = sysprof_capture_writer_ref (writer); + + if (kallsyms_fd != -1) + { + sysprof_capture_writer_add_file_fd (writer, + SYSPROF_CAPTURE_CURRENT_TIME, + -1, + -1, + "/proc/kallsyms", + kallsyms_fd); + close (kallsyms_fd); + } + + mounts = sysprof_live_unwinder_read_file (self, "/proc/mounts", TRUE); + + sysprof_live_unwinder_mine_pids (self); + + return self; +} + +void +sysprof_live_unwinder_seen_process (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + const char *comm) +{ + G_GNUC_UNUSED SysprofLiveProcess *live_pid; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + + live_pid = sysprof_live_unwinder_find_pid (self, pid, FALSE); + + sysprof_capture_writer_add_process (self->writer, time, cpu, pid, comm); +} + +void +sysprof_live_unwinder_process_exited (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid) +{ + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + + sysprof_capture_writer_add_exit (self->writer, time, cpu, pid); +} + +void +sysprof_live_unwinder_process_forked (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid parent_tid, + GPid tid) +{ + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + + sysprof_capture_writer_add_fork (self->writer, time, cpu, parent_tid, tid); +} + +void +sysprof_live_unwinder_track_mmap (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + SysprofCaptureAddress begin, + SysprofCaptureAddress end, + SysprofCaptureAddress offset, + guint64 inode, + const char *filename, + const char *build_id) +{ + G_GNUC_UNUSED SysprofLiveProcess *live_pid; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + + live_pid = sysprof_live_unwinder_find_pid (self, pid, TRUE); + + if (build_id != NULL) + sysprof_capture_writer_add_map_with_build_id (self->writer, time, cpu, pid, + begin, end, offset, + inode, filename, + build_id); + else + sysprof_capture_writer_add_map (self->writer, time, cpu, pid, + begin, end, offset, + inode, filename); + + sysprof_live_process_add_map (live_pid, begin, end, offset, inode, filename); +} + +void +sysprof_live_unwinder_process_sampled (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + GPid tid, + const SysprofCaptureAddress *addresses, + guint n_addresses) +{ + G_GNUC_UNUSED SysprofLiveProcess *live_pid; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + + live_pid = sysprof_live_unwinder_find_pid (self, pid, TRUE); + + sysprof_capture_writer_add_sample (self->writer, time, cpu, pid, tid, + addresses, n_addresses); +} + +void +sysprof_live_unwinder_process_sampled_with_stack (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + GPid tid, + const SysprofCaptureAddress *addresses, + guint n_addresses, + const guint8 *stack, + guint64 stack_size, + guint64 stack_dyn_size, + guint64 abi, + const guint64 *registers, + guint n_registers) +{ + SysprofLiveProcess *live_pid; + SysprofCaptureAddress unwound[256]; + gboolean found_user = FALSE; + guint pos; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + g_assert (stack != NULL); + g_assert (stack_dyn_size <= stack_size); + + if (stack_dyn_size == 0 || n_addresses >= G_N_ELEMENTS (unwound)) + { + sysprof_live_unwinder_process_sampled (self, time, cpu, pid, tid, addresses, n_addresses); + return; + } + + live_pid = sysprof_live_unwinder_find_pid (self, pid, TRUE); + + /* Copy addresses over (which might be kernel, context-switch, etc until + * we get to the PERF_CONTEXT_USER. We'll decode the stack right into the + * location after that. + */ + for (pos = 0; pos < n_addresses; pos++) + { + unwound[pos] = addresses[pos]; + + if (addresses[pos] == PERF_CONTEXT_USER) + { + found_user = TRUE; + break; + } + } + + /* If we didn't find a user context (but we have a stack size) synthesize + * the PERF_CONTEXT_USER now. + */ + if (!found_user && pos < G_N_ELEMENTS (unwound)) + unwound[pos++] = PERF_CONTEXT_USER; + + /* Now request the live process unwind the user-space stack */ + if (pos < G_N_ELEMENTS (unwound)) + { + guint n_unwound; + + n_unwound = sysprof_live_process_unwind (live_pid, + tid, + abi, + stack, + stack_dyn_size, + registers, + n_registers, + &unwound[pos], + G_N_ELEMENTS (unwound) - pos); + + /* Only take DWARF unwind if it was better */ + if (pos + n_unwound > n_addresses) + { + addresses = unwound; + n_addresses = pos + n_unwound; + } + } + + sysprof_capture_writer_add_sample (self->writer, time, cpu, pid, tid, addresses, n_addresses); +} diff --git a/src/sysprof-live-unwinder/sysprof-live-unwinder.h b/src/sysprof-live-unwinder/sysprof-live-unwinder.h new file mode 100644 index 00000000..e27ed9a4 --- /dev/null +++ b/src/sysprof-live-unwinder/sysprof-live-unwinder.h @@ -0,0 +1,79 @@ +/* + * sysprof-live-unwinder.h + * + * Copyright 2024 Christian Hergert + * + * 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 3 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, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define SYSPROF_TYPE_LIVE_UNWINDER (sysprof_live_unwinder_get_type()) + +G_DECLARE_FINAL_TYPE (SysprofLiveUnwinder, sysprof_live_unwinder, SYSPROF, LIVE_UNWINDER, GObject) + +SysprofLiveUnwinder *sysprof_live_unwinder_new (SysprofCaptureWriter *writer, + int kallsyms_fd); +void sysprof_live_unwinder_seen_process (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + const char *comm); +void sysprof_live_unwinder_process_exited (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid); +void sysprof_live_unwinder_process_forked (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid parent_tid, + GPid tid); +void sysprof_live_unwinder_track_mmap (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + SysprofCaptureAddress begin, + SysprofCaptureAddress end, + SysprofCaptureAddress offset, + guint64 inode, + const char *filename, + const char *build_id); +void sysprof_live_unwinder_process_sampled (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + GPid tid, + const SysprofCaptureAddress *addresses, + guint n_addresses); +void sysprof_live_unwinder_process_sampled_with_stack (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + GPid tid, + const SysprofCaptureAddress *addresses, + guint n_addresses, + const guint8 *stack, + guint64 stack_size, + guint64 stack_dyn_size, + guint64 abi, + const guint64 *registers, + guint n_registers); + +G_END_DECLS diff --git a/src/sysprof-live-unwinder/tests/meson.build b/src/sysprof-live-unwinder/tests/meson.build new file mode 100644 index 00000000..9e1a945a --- /dev/null +++ b/src/sysprof-live-unwinder/tests/meson.build @@ -0,0 +1,35 @@ +sysprof_live_unwinder_test_env = [ + 'G_DEBUG=gc-friendly', + 'GSETTINGS_BACKEND=memory', + 'MALLOC_CHECK_=2', +] + +sysprof_live_unwinder_testsuite_c_args = [ + '-DG_ENABLE_DEBUG', + '-UG_DISABLE_ASSERT', + '-UG_DISABLE_CAST_CHECKS', + '-DBUILDDIR="@0@"'.format(meson.current_build_dir()), +] + +sysprof_live_unwinder_testsuite = { + 'test-live-unwinder' : {'skip': true}, +} + +sysprof_live_unwinder_testsuite_deps = [ + libsysprof_static_dep, +] + +if polkit_agent_dep.found() + sysprof_live_unwinder_testsuite_deps += polkit_agent_dep +endif + +foreach test, params: sysprof_live_unwinder_testsuite + test_exe = executable(test, '@0@.c'.format(test), + c_args: sysprof_live_unwinder_testsuite_c_args, + dependencies: sysprof_live_unwinder_testsuite_deps, + ) + + if not params.get('skip', false) + test(test, test_exe, env: sysprof_live_unwinder_test_env) + endif +endforeach diff --git a/src/sysprof-live-unwinder/tests/test-live-unwinder.c b/src/sysprof-live-unwinder/tests/test-live-unwinder.c new file mode 100644 index 00000000..114cc568 --- /dev/null +++ b/src/sysprof-live-unwinder/tests/test-live-unwinder.c @@ -0,0 +1,391 @@ +/* + * test-live-unwinder.c + * + * Copyright 2024 Christian Hergert + * + * 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 3 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, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include +#include + +#include + +#include +#include + +#include + +#include + +#include "sysprof-perf-event-stream-private.h" + +#if HAVE_POLKIT_AGENT +# define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +# include +# include +#endif + +#include + +#define N_WAKEUP_EVENTS 149 + +/* The following was provided to Sysprof by Serhei Makarov as part + * of the eu-stacktrace prototype work. + */ +#ifdef _ASM_X86_PERF_REGS_H +/* #define SYSPROF_ARCH_PREFERRED_REGS PERF_REG_EXTENDED_MASK -- error on x86_64 due to including segment regs*/ +#define REG(R) (1ULL << PERF_REG_X86_ ## R) +#define DWARF_NEEDED_REGS (/* no FLAGS */ REG(IP) | REG(SP) | REG(AX) | REG(CX) | REG(DX) | REG(BX) | REG(SI) | REG(DI) | REG(SP) | REG(BP) | /* no segment regs */ REG(R8) | REG(R9) | REG(R10) | REG(R11) | REG(R12) | REG(R13) | REG(R14) | REG(R15)) +/* XXX register ordering is defined in linux arch/x86/include/uapi/asm/perf_regs.h; + see code in tools/perf/util/intel-pt.c intel_pt_add_gp_regs() + and note how registers are added in the same order as the perf_regs.h enum */ +#define SYSPROF_ARCH_PREFERRED_REGS DWARF_NEEDED_REGS +/* TODO: add other architectures, imitating the linux tools/perf tree */ +#else +# define SYSPROF_ARCH_PREFERRED_REGS PERF_REG_EXTENDED_MASK +#endif /* _ASM_{arch}_PERF_REGS_H */ + +static gboolean sample_stack; +static char *kallsyms = NULL; +static int sample_stack_size = 8192; + +static void +open_perf_stream_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GDBusConnection *connection = (GDBusConnection *)object; + g_autoptr(DexPromise) promise = user_data; + g_autoptr(GUnixFDList) fd_list = NULL; + g_autoptr(GVariant) ret = NULL; + g_autoptr(GError) error = NULL; + + g_assert (G_IS_DBUS_CONNECTION (connection)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (DEX_IS_PROMISE (promise)); + + if ((ret = g_dbus_connection_call_with_unix_fd_list_finish (connection, &fd_list, result, &error))) + { + int handle; + int fd; + + g_variant_get (ret, "(h)", &handle); + + if (-1 != (fd = g_unix_fd_list_get (fd_list, handle, &error))) + { + dex_promise_resolve_fd (promise, g_steal_fd (&fd)); + return; + } + } + + g_assert (error != NULL); + + dex_promise_reject (promise, g_steal_pointer (&error)); +} + +static int +open_perf_stream (GDBusConnection *bus, + int cpu, + GError **error) +{ + struct perf_event_attr attr = {0}; + gboolean with_mmap2 = TRUE; + gboolean use_software = FALSE; + + g_assert (G_IS_DBUS_CONNECTION (bus)); + g_assert (cpu >= 0); + g_assert (error != NULL); + +try_again: + attr.sample_type = PERF_SAMPLE_IP + | PERF_SAMPLE_TID + | PERF_SAMPLE_IDENTIFIER + | PERF_SAMPLE_CALLCHAIN + | PERF_SAMPLE_TIME; + + if (sample_stack) + { + attr.sample_type |= (PERF_SAMPLE_REGS_USER | PERF_SAMPLE_STACK_USER); + attr.sample_stack_user = sample_stack_size; + attr.sample_regs_user = SYSPROF_ARCH_PREFERRED_REGS; + } + + attr.wakeup_events = N_WAKEUP_EVENTS; + attr.disabled = TRUE; + attr.mmap = TRUE; + attr.mmap2 = with_mmap2; + attr.comm = 1; + attr.task = 1; + attr.exclude_idle = 1; + attr.sample_id_all = 1; + +#ifdef HAVE_PERF_CLOCKID + attr.clockid = sysprof_clock; + attr.use_clockid = 1; +#endif + + attr.size = sizeof attr; + + if (use_software) + { + attr.type = PERF_TYPE_SOFTWARE; + attr.config = PERF_COUNT_SW_CPU_CLOCK; + attr.sample_period = 1000000; + } + else + { + attr.type = PERF_TYPE_HARDWARE; + attr.config = PERF_COUNT_HW_CPU_CYCLES; + attr.sample_period = 1200000; + } + + { + g_autoptr(GVariant) options = _sysprof_perf_event_attr_to_variant (&attr); + g_autoptr(DexPromise) promise = dex_promise_new (); + g_autofd int fd = -1; + + g_dbus_connection_call_with_unix_fd_list (bus, + "org.gnome.Sysprof3", + "/org/gnome/Sysprof3", + "org.gnome.Sysprof3.Service", + "PerfEventOpen", + g_variant_new ("(@a{sv}iiht)", + options, + -1, + cpu, + -1, + 0), + G_VARIANT_TYPE ("(h)"), + G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION, + G_MAXUINT, + NULL, + dex_promise_get_cancellable (promise), + open_perf_stream_cb, + dex_ref (promise)); + + fd = dex_await_fd (dex_ref (promise), error); + + if (*error == NULL) + { + g_printerr ("CPU[%d]: opened perf_event stream as FD %d\n", cpu, fd); + return g_steal_fd (&fd); + } + + fd = -1; + } + + if (with_mmap2) + { + g_clear_error (error); + with_mmap2 = FALSE; + goto try_again; + } + + if (use_software == FALSE) + { + g_clear_error (error); + with_mmap2 = TRUE; + use_software = TRUE; + goto try_again; + } + + g_assert (*error != NULL); + + return -1; +} + +static DexFuture * +main_fiber (gpointer user_data) +{ + g_autoptr(GSubprocessLauncher) launcher = NULL; + g_autoptr(GDBusConnection) bus = NULL; + g_autoptr(GSubprocess) subprocess = NULL; + g_autoptr(GPtrArray) argv = NULL; + g_autoptr(GError) error = NULL; + g_autofd int writer_fd = -1; + int n_cpu = g_get_num_processors (); + int next_target_fd = 3; + + /* Get our bus we will use for authorization */ + if (!(bus = dex_await_object (dex_bus_get (G_BUS_TYPE_SYSTEM), &error))) + return dex_future_new_for_error (g_steal_pointer (&error)); + + /* Setup our launcher we'll use to map FDs into the translator */ + launcher = g_subprocess_launcher_new (0); + + /* Setup our argv which will notify the child about where to + * find the FDs containing perf event streams. + */ + argv = g_ptr_array_new_with_free_func (g_free); + g_ptr_array_add (argv, g_build_filename (BUILDDIR, "..", "sysprof-live-unwinder", NULL)); + + /* Provide kallsyms from the file provided */ + if (kallsyms != NULL) + { + int fd = open (kallsyms, O_RDONLY | O_CLOEXEC); + + if (fd != -1) + { + g_subprocess_launcher_take_fd (launcher, fd, next_target_fd); + g_ptr_array_add (argv, g_strdup_printf ("--kallsyms=%d", next_target_fd++)); + } + } + + if (sample_stack) + g_ptr_array_add (argv, g_strdup_printf ("--stack-size=%u", sample_stack_size)); + + g_printerr ("sysprof-live-unwinder at %s\n", (const char *)argv->pdata[0]); + + /* First try to open a perf_event stream for as many CPUs as we + * can before we get complaints from the kernel. + */ + for (int cpu = 0; cpu < n_cpu; cpu++) + { + g_autoptr(GError) cpu_error = NULL; + g_autofd int perf_fd = open_perf_stream (bus, cpu, &cpu_error); + + if (perf_fd == -1) + { + g_printerr ("CPU[%d]: %s\n", cpu, cpu_error->message); + continue; + } + + if (0 != ioctl (perf_fd, PERF_EVENT_IOC_ENABLE)) + { + int errsv = errno; + g_warning ("Failed to enable perf_fd: %s", g_strerror (errsv)); + } + + g_ptr_array_add (argv, g_strdup_printf ("--perf-fd=%d:%d", next_target_fd, cpu)); + g_subprocess_launcher_take_fd (launcher, g_steal_fd (&perf_fd), next_target_fd); + + next_target_fd++; + } + + /* Now create a FD for our destination capture. */ + if (-1 == (writer_fd = open ("translated.syscap", O_CREAT|O_RDWR|O_CLOEXEC, 0664)) || + ftruncate (writer_fd, 0) != 0) + return dex_future_new_for_errno (errno); + g_ptr_array_add (argv, g_strdup_printf ("--capture-fd=%d", next_target_fd)); + g_subprocess_launcher_take_fd (launcher, g_steal_fd (&writer_fd), next_target_fd); + next_target_fd++; + + /* Null-terminate our argv */ + g_ptr_array_add (argv, NULL); + + /* Spawn our worker process with the perf FDs and writer provided */ + if (!(subprocess = g_subprocess_launcher_spawnv (launcher, + (const char * const *)argv->pdata, + &error))) + return dex_future_new_for_error (g_steal_pointer (&error)); + + /* Now wait for the translation process to complete */ + if (!dex_await_boolean (dex_subprocess_wait_check (subprocess), &error)) + return dex_future_new_for_error (g_steal_pointer (&error)); + + return dex_future_new_true (); +} + +static DexFuture * +finally_cb (DexFuture *future, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + GMainLoop *main_loop = user_data; + + if (!dex_await (dex_ref (future), &error)) + { + g_printerr ("Error: %s\n", error->message); + exit (EXIT_FAILURE); + } + + g_main_loop_quit (main_loop); + + return NULL; +} + +int +main (int argc, + char *argv[]) +{ +#if HAVE_POLKIT_AGENT + PolkitAgentListener *polkit = NULL; + PolkitSubject *subject = NULL; +#endif + g_autoptr(GOptionContext) context = NULL; + g_autoptr(GMainLoop) main_loop = NULL; + g_autoptr(GError) error = NULL; + DexFuture *future; + GOptionEntry entries[] = { + { "sample-stack", 's', 0, G_OPTION_ARG_NONE, &sample_stack, "If the stack should be sampled for user-space unwinding" }, + { "sample-stack-size", 'S', 0, G_OPTION_ARG_INT, &sample_stack_size, "If size of the stack to sample in bytes" }, + { "kallsyms", 'k', 0, G_OPTION_ARG_FILENAME, &kallsyms, "Specify kallsyms for use" }, + { NULL } + }; + + sysprof_clock_init (); + dex_init (); + + main_loop = g_main_loop_new (NULL, FALSE); + context = g_option_context_new ("- test sysprof-live-unwinder"); + g_option_context_add_main_entries (context, entries, NULL); + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("%s\n", error->message); + return EXIT_FAILURE; + } + +#if HAVE_POLKIT_AGENT + /* Start polkit agent so that we can elevate privileges from a TTY */ + if (g_getenv ("DESKTOP_SESSION") == NULL && + (subject = polkit_unix_process_new_for_owner (getpid (), 0, -1))) + { + g_autoptr(GError) pkerror = NULL; + + polkit = polkit_agent_text_listener_new (NULL, NULL); + polkit_agent_listener_register (polkit, + POLKIT_AGENT_REGISTER_FLAGS_NONE, + subject, + NULL, + NULL, + &pkerror); + + if (pkerror != NULL) + { + g_dbus_error_strip_remote_error (pkerror); + g_printerr ("Failed to register polkit agent: %s\n", + pkerror->message); + } + } +#endif + + future = dex_scheduler_spawn (NULL, 0, main_fiber, NULL, NULL); + future = dex_future_finally (future, + finally_cb, + g_main_loop_ref (main_loop), + (GDestroyNotify) g_main_loop_unref); + dex_future_disown (future); + + g_main_loop_run (main_loop); + + g_clear_pointer (&kallsyms, g_free); + + return EXIT_SUCCESS; +} diff --git a/src/sysprofd/ipc-unwinder-impl.c b/src/sysprofd/ipc-unwinder-impl.c new file mode 100644 index 00000000..7f218de6 --- /dev/null +++ b/src/sysprofd/ipc-unwinder-impl.c @@ -0,0 +1,247 @@ +/* + * ipc-unwinder-impl.c + * + * Copyright 2024 Christian Hergert + * + * 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 3 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, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "ipc-unwinder-impl" + +#include "config.h" + +#include + +#include +#include +#include + +#include + +#include + +#include "ipc-unwinder-impl.h" + +struct _IpcUnwinderImpl +{ + IpcUnwinderSkeleton parent_instance; +}; + +static void +child_setup (gpointer data) +{ + prctl (PR_SET_PDEATHSIG, SIGKILL); +} + +static gboolean +ipc_unwinder_impl_handle_unwind (IpcUnwinder *unwinder, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + guint stack_size, + GVariant *arg_perf_fds, + GVariant *arg_event_fd) +{ + g_autoptr(GSubprocessLauncher) launcher = NULL; + g_autoptr(GSubprocess) subprocess = NULL; + g_autoptr(GUnixFDList) out_fd_list = NULL; + g_autoptr(GPtrArray) argv = NULL; + g_autoptr(GError) error = NULL; + g_autofd int our_fd = -1; + g_autofd int their_fd = -1; + g_autofd int event_fd = -1; + GVariantIter iter; + int capture_fd_handle; + int pair[2]; + int next_target_fd = 3; + int perf_fd_handle; + int cpu; + + g_assert (IPC_IS_UNWINDER_IMPL (unwinder)); + g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); + g_assert (!fd_list || G_IS_UNIX_FD_LIST (fd_list)); + + if (stack_size == 0 || stack_size % sysconf (_SC_PAGESIZE) != 0) + { + g_dbus_method_invocation_return_error_literal (g_steal_pointer (&invocation), + G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Stack size must be a multiple of the page size"); + return TRUE; + } + + if (fd_list == NULL) + { + g_dbus_method_invocation_return_error_literal (g_steal_pointer (&invocation), + G_DBUS_ERROR, + G_DBUS_ERROR_FILE_NOT_FOUND, + "Missing perf FDs"); + return TRUE; + } + + launcher = g_subprocess_launcher_new (0); + argv = g_ptr_array_new_with_free_func (g_free); + + g_ptr_array_add (argv, g_strdup (PACKAGE_LIBEXECDIR "/sysprof-live-unwinder")); + g_ptr_array_add (argv, g_strdup_printf ("--stack-size=%u", stack_size)); + + if (-1 == (event_fd = g_unix_fd_list_get (fd_list, g_variant_get_handle (arg_event_fd), &error))) + { + g_dbus_method_invocation_return_gerror (g_steal_pointer (&invocation), error); + return TRUE; + } + + g_ptr_array_add (argv, g_strdup_printf ("--event-fd=%u", next_target_fd)); + g_subprocess_launcher_take_fd (launcher, g_steal_fd (&event_fd), next_target_fd++); + + g_variant_iter_init (&iter, arg_perf_fds); + + while (g_variant_iter_loop (&iter, "(hi)", &perf_fd_handle, &cpu)) + { + g_autofd int perf_fd = g_unix_fd_list_get (fd_list, perf_fd_handle, &error); + + if (perf_fd < 0) + { + g_dbus_method_invocation_return_gerror (g_steal_pointer (&invocation), error); + return TRUE; + } + + g_ptr_array_add (argv, g_strdup_printf ("--perf-fd=%d:%d", next_target_fd, cpu)); + g_subprocess_launcher_take_fd (launcher, + g_steal_fd (&perf_fd), + next_target_fd++); + } + + g_subprocess_launcher_set_child_setup (launcher, child_setup, NULL, NULL); + + if (socketpair (AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair) < 0) + { + int errsv = errno; + g_dbus_method_invocation_return_error_literal (g_steal_pointer (&invocation), + G_IO_ERROR, + g_io_error_from_errno (errsv), + g_strerror (errsv)); + return TRUE; + } + + our_fd = g_steal_fd (&pair[0]); + their_fd = g_steal_fd (&pair[1]); + + out_fd_list = g_unix_fd_list_new (); + capture_fd_handle = g_unix_fd_list_append (out_fd_list, their_fd, &error); + + if (capture_fd_handle < 0) + { + g_dbus_method_invocation_return_gerror (g_steal_pointer (&invocation), error); + return TRUE; + } + + g_ptr_array_add (argv, g_strdup_printf ("--capture-fd=%d", next_target_fd)); + g_subprocess_launcher_take_fd (launcher, g_steal_fd (&our_fd), next_target_fd++); + + g_ptr_array_add (argv, NULL); + + if (!(subprocess = g_subprocess_launcher_spawnv (launcher, (const char * const *)argv->pdata, &error))) + { + g_dbus_method_invocation_return_gerror (g_steal_pointer (&invocation), error); + return TRUE; + } + + ipc_unwinder_complete_unwind (unwinder, + g_steal_pointer (&invocation), + out_fd_list, + g_variant_new_handle (capture_fd_handle)); + + g_subprocess_wait_async (subprocess, NULL, NULL, NULL); + + return TRUE; +} + +static void +unwinder_iface_init (IpcUnwinderIface *iface) +{ + iface->handle_unwind = ipc_unwinder_impl_handle_unwind; +} + +G_DEFINE_FINAL_TYPE_WITH_CODE (IpcUnwinderImpl, ipc_unwinder_impl, IPC_TYPE_UNWINDER_SKELETON, + G_IMPLEMENT_INTERFACE (IPC_TYPE_UNWINDER, unwinder_iface_init)) + +static gboolean +ipc_unwinder_impl_g_authorize_method (GDBusInterfaceSkeleton *skeleton, + GDBusMethodInvocation *invocation) +{ + PolkitAuthorizationResult *res = NULL; + PolkitAuthority *authority = NULL; + PolkitSubject *subject = NULL; + const gchar *peer_name; + gboolean ret = TRUE; + + g_assert (IPC_IS_UNWINDER_IMPL (skeleton)); + g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); + + peer_name = g_dbus_method_invocation_get_sender (invocation); + + if (!(authority = polkit_authority_get_sync (NULL, NULL)) || + !(subject = polkit_system_bus_name_new (peer_name)) || + !(res = polkit_authority_check_authorization_sync (authority, + POLKIT_SUBJECT (subject), + "org.gnome.sysprof3.profile", + NULL, + POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION, + NULL, + NULL)) || + !polkit_authorization_result_get_is_authorized (res)) + { + g_dbus_method_invocation_return_error (g_steal_pointer (&invocation), + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Not authorized to make request"); + ret = FALSE; + } + + g_clear_object (&authority); + g_clear_object (&subject); + g_clear_object (&res); + + return ret; +} + +static void +ipc_unwinder_impl_finalize (GObject *object) +{ + G_OBJECT_CLASS (ipc_unwinder_impl_parent_class)->finalize (object); +} + +static void +ipc_unwinder_impl_class_init (IpcUnwinderImplClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GDBusInterfaceSkeletonClass *skeleton_class = G_DBUS_INTERFACE_SKELETON_CLASS (klass); + + object_class->finalize = ipc_unwinder_impl_finalize; + + skeleton_class->g_authorize_method = ipc_unwinder_impl_g_authorize_method; +} + +static void +ipc_unwinder_impl_init (IpcUnwinderImpl *self) +{ +} + +IpcUnwinder * +ipc_unwinder_impl_new (void) +{ + return g_object_new (IPC_TYPE_UNWINDER_IMPL, NULL); +} diff --git a/src/sysprofd/ipc-unwinder-impl.h b/src/sysprofd/ipc-unwinder-impl.h new file mode 100644 index 00000000..ebe7a2f0 --- /dev/null +++ b/src/sysprofd/ipc-unwinder-impl.h @@ -0,0 +1,34 @@ +/* + * ipc-unwinder-impl.h + * + * Copyright 2024 Christian Hergert + * + * 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 3 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, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "ipc-unwinder.h" + +G_BEGIN_DECLS + +#define IPC_TYPE_UNWINDER_IMPL (ipc_unwinder_impl_get_type()) + +G_DECLARE_FINAL_TYPE (IpcUnwinderImpl, ipc_unwinder_impl, IPC, UNWINDER_IMPL, IpcUnwinderSkeleton) + +IpcUnwinder *ipc_unwinder_impl_new (void); + +G_END_DECLS diff --git a/src/sysprofd/meson.build b/src/sysprofd/meson.build index 34901f44..b7e87ebe 100644 --- a/src/sysprofd/meson.build +++ b/src/sysprofd/meson.build @@ -10,18 +10,24 @@ ipc_service_src = gnome.gdbus_codegen('ipc-service', namespace: 'Ipc', ) +ipc_unwinder_src = gnome.gdbus_codegen('ipc-unwinder', + sources: 'org.gnome.Sysprof3.Unwinder.xml', + interface_prefix: 'org.gnome.Sysprof3.', + namespace: 'Ipc', +) + sysprofd_sources = [ 'sysprofd.c', 'ipc-rapl-profiler.c', 'ipc-service-impl.c', + 'ipc-unwinder-impl.c', 'sysprof-turbostat.c', 'helpers.c', ipc_profiler_src, ipc_service_src, + ipc_unwinder_src, ] -pkglibexecdir = join_paths(get_option('prefix'), get_option('libexecdir')) - sysprofd_deps = [ glib_dep, gio_dep, diff --git a/src/sysprofd/org.gnome.Sysprof3.Unwinder.xml b/src/sysprofd/org.gnome.Sysprof3.Unwinder.xml new file mode 100644 index 00000000..fb2c7848 --- /dev/null +++ b/src/sysprofd/org.gnome.Sysprof3.Unwinder.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/src/sysprofd/sysprofd.c b/src/sysprofd/sysprofd.c index e39d59c4..7a7101e8 100644 --- a/src/sysprofd/sysprofd.c +++ b/src/sysprofd/sysprofd.c @@ -30,9 +30,11 @@ #include "ipc-rapl-profiler.h" #include "ipc-service-impl.h" +#include "ipc-unwinder-impl.h" #define V3_PATH "/org/gnome/Sysprof3" #define RAPL_PATH "/org/gnome/Sysprof3/RAPL" +#define UNWINDER_PATH "/org/gnome/Sysprof3/Unwinder" #define NAME_ACQUIRE_DELAY_SECS 3 #define INACTIVITY_TIMEOUT_SECS 120 @@ -126,6 +128,7 @@ main (gint argc, { g_autoptr(IpcProfiler) rapl = ipc_rapl_profiler_new (); g_autoptr(IpcService) v3_service = ipc_service_impl_new (); + g_autoptr(IpcUnwinder) unwinder = ipc_unwinder_impl_new (); g_signal_connect (v3_service, "activity", G_CALLBACK (activity_cb), NULL); g_signal_connect (rapl, "activity", G_CALLBACK (activity_cb), NULL); @@ -133,7 +136,8 @@ main (gint argc, activity_cb (NULL, NULL); if (g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (v3_service), bus, V3_PATH, &error) && - g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (rapl), bus, RAPL_PATH, &error)) + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (rapl), bus, RAPL_PATH, &error) && + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (unwinder), bus, UNWINDER_PATH, &error)) { for (guint i = 0; i < G_N_ELEMENTS (bus_names); i++) { -- 2.45.2