From 739697e6b4e13b2912f0e2bf2a6f71043f45ac38 Mon Sep 17 00:00:00 2001 From: Ryan Wilson Date: Sat, 21 Sep 2024 06:34:04 -0700 Subject: [PATCH] Add name to drgn.Thread Signed-off-by: Ryan Wilson --- _drgn.pyi | 13 ++++ libdrgn/cleanup.h | 9 +++ libdrgn/drgn.h | 9 +++ libdrgn/program.c | 108 +++++++++++++++++++++++++++++ libdrgn/program.h | 1 + libdrgn/python/thread.c | 13 ++++ tests/linux_kernel/test_threads.py | 7 ++ tests/test_thread.py | 15 ++++ 8 files changed, 175 insertions(+) diff --git a/_drgn.pyi b/_drgn.pyi index f1c8c61a2..a8452bb7e 100644 --- a/_drgn.pyi +++ b/_drgn.pyi @@ -1110,6 +1110,19 @@ class Thread: tid: Final[int] """Thread ID (as defined by :manpage:`gettid(2)`).""" + name: Optional[str] + """ + Thread name, or ``None`` if unknown. + + See `PR_SET_NAME + `_ and + `/proc/pid/comm + `_. + + .. note:: + Linux userspace core dumps only save the name of the main thread, so + :attr:`name` will be ``None`` for other threads. + """ object: Final[Object] """ If the program is the Linux kernel, the ``struct task_struct *`` object for diff --git a/libdrgn/cleanup.h b/libdrgn/cleanup.h index d47b0a921..9ca90b4ab 100644 --- a/libdrgn/cleanup.h +++ b/libdrgn/cleanup.h @@ -12,6 +12,7 @@ #include #include +#include #define _cleanup_(x) __attribute__((__cleanup__(x))) @@ -30,6 +31,14 @@ static inline void fclosep(FILE **fp) fclose(*fp); } +/** Call @c close() when the variable goes out of scope. */ +#define _cleanup_close_ _cleanup_(closep) +static inline void closep(int *fd) +{ + if (*fd >= 0) + close(*fd); +} + /** * Get the value of a pointer variable and reset it to @c NULL. * diff --git a/libdrgn/drgn.h b/libdrgn/drgn.h index 8fdee29ca..80ef87d9f 100644 --- a/libdrgn/drgn.h +++ b/libdrgn/drgn.h @@ -3429,6 +3429,15 @@ struct drgn_error *drgn_thread_object(struct drgn_thread *thread, struct drgn_error *drgn_thread_stack_trace(struct drgn_thread *thread, struct drgn_stack_trace **ret); +/** + * Get name for the thread represented by @p thread. + * + * @param[out] ret Returned thread name, or @c NULL if not found. On success, it + * should be freed with free(). On error, it is not modified. + * @return @c NULL on success, non-@c NULL on error. + */ +struct drgn_error *drgn_thread_name(struct drgn_thread *thread, char **ret); + /** @} */ #endif /* DRGN_H */ diff --git a/libdrgn/program.c b/libdrgn/program.c index 8638c4c73..38139fa21 100644 --- a/libdrgn/program.c +++ b/libdrgn/program.c @@ -868,6 +868,35 @@ static struct drgn_error *get_prpsinfo_pid(struct drgn_program *prog, return NULL; } +static struct drgn_error *get_prpsinfo_fname(struct drgn_program *prog, + const char *data, size_t size, + const char **ret) +{ + bool is_64_bit; + struct drgn_error *err = drgn_program_is_64_bit(prog, &is_64_bit); + if (err) + return err; + size_t offset = is_64_bit ? 40 : 28; + // pr_fname is defined as 16 byte buffer in elf_prpsinfo + // https://github.com/torvalds/linux/blob/075dbe9f6e3c21596c5245826a4ee1f1c1676eb8/include/linux/elfcore.h#L73 +#define PR_FNAME_LEN 16 + if (size < offset + PR_FNAME_LEN) { + return drgn_error_create(DRGN_ERROR_OTHER, + "NT_PRPSINFO is truncated"); + } + // No need to make a copy: the data returned by elf_getdata_rawchunk() + // is valid for the lifetime of the Elf handle, and prog->core is valid for + // the lifetime of prog. + const char *tmp = data + offset; + size_t len = strnlen(tmp, PR_FNAME_LEN); + if (len == PR_FNAME_LEN) +#undef PR_FNAME_LEN + return drgn_error_create(DRGN_ERROR_OTHER, + "pr_fname is not null terminated"); + *ret = tmp; + return NULL; +} + struct drgn_error *drgn_thread_dup_internal(const struct drgn_thread *thread, struct drgn_thread *ret) { @@ -949,6 +978,7 @@ drgn_program_cache_core_dump_notes(struct drgn_program *prog) uint32_t first_prstatus_tid; bool found_prpsinfo = false; uint32_t prpsinfo_pid; + const char *prpsinfo_fname = NULL; if (prog->core_dump_notes_cached) return NULL; @@ -1011,6 +1041,12 @@ drgn_program_cache_core_dump_notes(struct drgn_program *prog) &prpsinfo_pid); if (err) goto err; + err = get_prpsinfo_fname(prog, + (char *)data->d_buf + desc_offset, + nhdr.n_descsz, + &prpsinfo_fname); + if (err) + goto err; found_prpsinfo = true; } else if (nhdr.n_type == NT_PRSTATUS) { uint32_t tid; @@ -1042,6 +1078,7 @@ drgn_program_cache_core_dump_notes(struct drgn_program *prog) &prpsinfo_pid); /* If the PID isn't found, then this is NULL. */ prog->main_thread = it.entry; + prog->core_dump_fname_cached = prpsinfo_fname; } if (found_prstatus) { /* @@ -1489,6 +1526,77 @@ drgn_thread_object(struct drgn_thread *thread, const struct drgn_object **ret) return NULL; } +static struct drgn_error * +drgn_thread_name_linux_kernel(struct drgn_thread *thread, char **ret) +{ + struct drgn_error *err; + DRGN_OBJECT(comm, drgn_object_program(&thread->object)); + err = drgn_object_member_dereference(&comm, &thread->object, "comm"); + if (!err) + err = drgn_object_read_c_string(&comm, ret); + return err; +} + +static struct drgn_error * +drgn_thread_name_userspace_live(struct drgn_thread *thread, char **ret) +{ +#define FORMAT "/proc/%" PRIu32 "/comm" + char path[sizeof(FORMAT) + - sizeof("%" PRIu32) + + max_decimal_length(uint32_t) + + 1]; + snprintf(path, sizeof(path), FORMAT, thread->tid); +#undef FORMAT + _cleanup_close_ int fd = open(path, O_RDONLY); + if (fd < 0) + return drgn_error_create_os("open", errno, path); + // While userspace threads use 16 byte buffer, kernel threads use a 64 byte buffer + // https://github.com/torvalds/linux/blob/075dbe9f6e3c21596c5245826a4ee1f1c1676eb8/fs/proc/array.c#L101 + char buf[64]; + ssize_t bytes_read = read_all(fd, buf, sizeof(buf)); + if (bytes_read < 0) + return drgn_error_create_os("read", errno, path); + + if (bytes_read > 0 && buf[bytes_read - 1] == '\n') + bytes_read--; + char *tmp = strndup(buf, bytes_read); + if (!tmp) + return &drgn_enomem; + *ret = tmp; + return NULL; +} + +static struct drgn_error * +drgn_thread_name_userspace_core(struct drgn_thread *thread, char **ret) +{ + struct drgn_error *err = drgn_program_cache_core_dump_notes(thread->prog); + if (err) + return err; + // Core dumps only contain the main thread name so check if this is the main thread. + // Otherwise, set ret to NULL which will return None in Python. + bool is_main_thread = thread->prog->main_thread && thread->prog->main_thread->tid == thread->tid; + if (is_main_thread && thread->prog->core_dump_fname_cached) { + char *tmp = strdup(thread->prog->core_dump_fname_cached); + if (!tmp) + return &drgn_enomem; + *ret = tmp; + } else { + *ret = NULL; + } + return NULL; +} + +LIBDRGN_PUBLIC struct drgn_error * +drgn_thread_name(struct drgn_thread *thread, char **ret) +{ + if (thread->prog->flags & DRGN_PROGRAM_IS_LINUX_KERNEL) + return drgn_thread_name_linux_kernel(thread, ret); + else if (thread->prog->flags & DRGN_PROGRAM_IS_LIVE) + return drgn_thread_name_userspace_live(thread, ret); + else + return drgn_thread_name_userspace_core(thread, ret); +} + struct drgn_error *drgn_program_find_prstatus(struct drgn_program *prog, uint32_t tid, struct nstring *ret) { diff --git a/libdrgn/program.h b/libdrgn/program.h index 4cafa2a3f..2adad78ff 100644 --- a/libdrgn/program.h +++ b/libdrgn/program.h @@ -140,6 +140,7 @@ struct drgn_program { uint64_t aarch64_insn_pac_mask; bool core_dump_notes_cached; bool prefer_orc_unwinder; + const char *core_dump_fname_cached; /* * Linux kernel-specific. diff --git a/libdrgn/python/thread.c b/libdrgn/python/thread.c index 88bdca86b..6de0f0013 100644 --- a/libdrgn/python/thread.c +++ b/libdrgn/python/thread.c @@ -56,6 +56,18 @@ static DrgnObject *Thread_get_object(Thread *self) return_ptr(ret); } +static PyObject *Thread_get_name(Thread *self) +{ + _cleanup_free_ char *ret = NULL; + struct drgn_error *err = drgn_thread_name(&self->thread, &ret); + if (err) + return set_drgn_error(err); + if (!ret) { + Py_RETURN_NONE; + } + return PyUnicode_DecodeFSDefault(ret); +} + static PyObject *Thread_stack_trace(Thread *self) { struct drgn_error *err; @@ -72,6 +84,7 @@ static PyObject *Thread_stack_trace(Thread *self) static PyGetSetDef Thread_getset[] = { {"tid", (getter)Thread_get_tid, NULL, drgn_Thread_tid_DOC}, {"object", (getter)Thread_get_object, NULL, drgn_Thread_object_DOC}, + {"name", (getter)Thread_get_name, NULL, drgn_Thread_name_DOC}, {}, }; diff --git a/tests/linux_kernel/test_threads.py b/tests/linux_kernel/test_threads.py index b49214805..f363403c7 100644 --- a/tests/linux_kernel/test_threads.py +++ b/tests/linux_kernel/test_threads.py @@ -50,3 +50,10 @@ def test_crashed_thread(self): "crashed thread is only defined for core dumps", self.prog.crashed_thread, ) + + def test_thread_name(self): + pid = os.getpid() + with open(f"/proc/{pid}/comm", "r") as f: + comm = f.read().strip() + thread = self.prog.thread(pid) + self.assertEqual(comm, thread.name) diff --git a/tests/test_thread.py b/tests/test_thread.py index 313c93646..a74ba433f 100644 --- a/tests/test_thread.py +++ b/tests/test_thread.py @@ -34,6 +34,13 @@ def test_crashed_thread(self): self.prog.crashed_thread, ) + def test_thread_name(self): + with open(f"/proc/{os.getpid()}/comm", "r") as f: + comm = f.read().strip() + self.assertEqual(self.prog.main_thread().name, comm) + for thread in self.prog.threads(): + self.assertIsNotNone(thread.name) + class TestCoreDump(TestCase): TIDS = ( @@ -55,6 +62,8 @@ class TestCoreDump(TestCase): MAIN_TID = 2265413 CRASHED_TID = 2265419 + MAIN_THREAD_NAME = "segfault_random" + @classmethod def setUpClass(cls): super().setUpClass() @@ -79,3 +88,9 @@ def test_main_thread(self): def test_crashed_thread(self): self.assertEqual(self.prog.crashed_thread().tid, self.CRASHED_TID) + + def test_thread_name(self): + self.assertEqual(self.prog.main_thread().name, self.MAIN_THREAD_NAME) + for tid in self.TIDS: + if tid != self.MAIN_TID: + self.assertIsNone(self.prog.thread(tid).name)