From 9a93fe9d7651dbe4868583c87a5e306a0515c11f Mon Sep 17 00:00:00 2001
From: Stephen Brennan <stephen@brennan.io>
Date: Tue, 15 Aug 2023 23:38:04 -0700
Subject: [PATCH 1/3] Allow Program.set_core_dump() and
 program_from_core_dump() to accept FD

This allows an open file descriptor to be passed into Drgn and treated
as a core dump.  There are many use cases, but one interesting one is
that the FD could be sent by a helper process running as root, allowing
a non-root Drgn process to debug the running kernel.

Signed-off-by: Stephen Brennan <stephen@brennan.io>
---
 _drgn.pyi                |  8 ++--
 libdrgn/drgn.h.in        | 21 ++++++++++
 libdrgn/program.c        | 83 +++++++++++++++++++++++++++++++++++-----
 libdrgn/program.h        |  7 ++++
 libdrgn/python/drgnpy.h  |  2 +
 libdrgn/python/program.c | 14 +++++--
 libdrgn/python/util.c    | 24 +++++++++++-
 7 files changed, 140 insertions(+), 19 deletions(-)

diff --git a/_drgn.pyi b/_drgn.pyi
index 007501d5f..35af2772b 100644
--- a/_drgn.pyi
+++ b/_drgn.pyi
@@ -451,7 +451,7 @@ class Program:
             return an :class:`Object`.
         """
         ...
-    def set_core_dump(self, path: Path) -> None:
+    def set_core_dump(self, path: Union[Path, int]) -> None:
         """
         Set the program to a core dump.
 
@@ -459,7 +459,7 @@ class Program:
         mapped executable and libraries. It does not load any debugging
         symbols; see :meth:`load_default_debug_info()`.
 
-        :param path: Core dump file path.
+        :param path: Core dump file path or open file descriptor.
         """
         ...
     def set_kernel(self) -> None:
@@ -888,12 +888,12 @@ def filename_matches(haystack: Optional[str], needle: Optional[str]) -> bool:
     """
     ...
 
-def program_from_core_dump(path: Path) -> Program:
+def program_from_core_dump(path: Union[Path, int]) -> Program:
     """
     Create a :class:`Program` from a core dump file. The type of program (e.g.,
     userspace or kernel) is determined automatically.
 
-    :param path: Core dump file path.
+    :param path: Core dump file path or open file descriptor.
     """
     ...
 
diff --git a/libdrgn/drgn.h.in b/libdrgn/drgn.h.in
index 2d0863e5f..2a3f82154 100644
--- a/libdrgn/drgn.h.in
+++ b/libdrgn/drgn.h.in
@@ -670,6 +670,14 @@ drgn_program_add_object_finder(struct drgn_program *prog,
 struct drgn_error *drgn_program_set_core_dump(struct drgn_program *prog,
 					      const char *path);
 
+/**
+ * Set a @ref drgn_program to a core dump from a file descriptor.
+ *
+ * @param[in] path Core dump file descriptor.
+ * @return @c NULL on success, non-@c NULL on error.
+ */
+struct drgn_error *drgn_program_set_core_dump_fd(struct drgn_program *prog, int fd);
+
 /**
  * Set a @ref drgn_program to the running operating system kernel.
  *
@@ -712,6 +720,19 @@ struct drgn_error *drgn_program_load_debug_info(struct drgn_program *prog,
 struct drgn_error *drgn_program_from_core_dump(const char *path,
 					       struct drgn_program **ret);
 
+/**
+ * Create a @ref drgn_program from a core dump file descriptor.
+ *
+ * Same as @ref drgn_program_from_core_dump but with an already-opened file
+ * descriptor.
+ *
+ * @param[in] fd Core dump file path descriptor.
+ * @param[out] ret Returned program.
+ * @return @c NULL on success, non-@c NULL on error.
+ */
+struct drgn_error *drgn_program_from_core_dump_fd(int fd,
+						  struct drgn_program **ret);
+
 /**
  * Create a @ref drgn_program from the running operating system kernel.
  *
diff --git a/libdrgn/program.c b/libdrgn/program.c
index 52430348b..b24ad38e5 100644
--- a/libdrgn/program.c
+++ b/libdrgn/program.c
@@ -226,8 +226,8 @@ static struct drgn_error *has_kdump_signature(const char *path, int fd,
 	return NULL;
 }
 
-LIBDRGN_PUBLIC struct drgn_error *
-drgn_program_set_core_dump(struct drgn_program *prog, const char *path)
+struct drgn_error *
+drgn_program_set_core_dump_fd_internal(struct drgn_program *prog, int fd, const char *path)
 {
 	struct drgn_error *err;
 	GElf_Ehdr ehdr_mem, *ehdr;
@@ -241,14 +241,7 @@ drgn_program_set_core_dump(struct drgn_program *prog, const char *path)
 	size_t vmcoreinfo_size = 0;
 	bool have_nt_taskstruct = false, is_proc_kcore;
 
-	err = drgn_program_check_initialized(prog);
-	if (err)
-		return err;
-
-	prog->core_fd = open(path, O_RDONLY);
-	if (prog->core_fd == -1)
-		return drgn_error_create_os("open", errno, path);
-
+	prog->core_fd = fd;
 	err = has_kdump_signature(path, prog->core_fd, &is_kdump);
 	if (err)
 		goto out_fd;
@@ -595,6 +588,39 @@ drgn_program_set_core_dump(struct drgn_program *prog, const char *path)
 	return err;
 }
 
+LIBDRGN_PUBLIC struct drgn_error *
+drgn_program_set_core_dump_fd(struct drgn_program *prog, int fd)
+{
+	struct drgn_error *err;
+
+	err = drgn_program_check_initialized(prog);
+	if (err)
+		return err;
+
+	#define FORMAT "/proc/self/fd/%d"
+	char path[sizeof(FORMAT) - sizeof("%d") + max_decimal_length(int) + 1];
+	snprintf(path, sizeof(path), FORMAT, fd);
+	#undef FORMAT
+
+	return drgn_program_set_core_dump_fd_internal(prog, fd, path);
+}
+
+LIBDRGN_PUBLIC struct drgn_error *
+drgn_program_set_core_dump(struct drgn_program *prog, const char *path)
+{
+	struct drgn_error *err;
+
+	err = drgn_program_check_initialized(prog);
+	if (err)
+		return err;
+
+	int fd = open(path, O_RDONLY);
+	if (fd == -1)
+		return drgn_error_create_os("open", errno, path);
+
+	return drgn_program_set_core_dump_fd_internal(prog, fd, path);
+}
+
 LIBDRGN_PUBLIC struct drgn_error *
 drgn_program_set_kernel(struct drgn_program *prog)
 {
@@ -1497,6 +1523,21 @@ struct drgn_error *drgn_program_init_core_dump(struct drgn_program *prog,
 	return err;
 }
 
+struct drgn_error *drgn_program_init_core_dump_fd(struct drgn_program *prog, int fd)
+{
+	struct drgn_error *err;
+
+	err = drgn_program_set_core_dump_fd(prog, fd);
+	if (err)
+		return err;
+	err = drgn_program_load_debug_info(prog, NULL, 0, true, true);
+	if (err && err->code == DRGN_ERROR_MISSING_DEBUG_INFO) {
+		drgn_error_destroy(err);
+		err = NULL;
+	}
+	return err;
+}
+
 struct drgn_error *drgn_program_init_kernel(struct drgn_program *prog)
 {
 	struct drgn_error *err;
@@ -1549,6 +1590,28 @@ drgn_program_from_core_dump(const char *path, struct drgn_program **ret)
 	return NULL;
 }
 
+LIBDRGN_PUBLIC struct drgn_error *
+drgn_program_from_core_dump_fd(int fd, struct drgn_program **ret)
+{
+	struct drgn_error *err;
+	struct drgn_program *prog;
+
+	prog = malloc(sizeof(*prog));
+	if (!prog)
+		return &drgn_enomem;
+
+	drgn_program_init(prog, NULL);
+	err = drgn_program_init_core_dump_fd(prog, fd);
+	if (err) {
+		drgn_program_deinit(prog);
+		free(prog);
+		return err;
+	}
+
+	*ret = prog;
+	return NULL;
+}
+
 LIBDRGN_PUBLIC struct drgn_error *
 drgn_program_from_kernel(struct drgn_program **ret)
 {
diff --git a/libdrgn/program.h b/libdrgn/program.h
index 7b870dc5b..337ce578d 100644
--- a/libdrgn/program.h
+++ b/libdrgn/program.h
@@ -226,6 +226,13 @@ void drgn_program_set_platform(struct drgn_program *prog,
 struct drgn_error *drgn_program_init_core_dump(struct drgn_program *prog,
 					       const char *path);
 
+/**
+ * Implement @ref drgn_program_from_core_dump_fd() on an initialized @ref
+ * drgn_program.
+ */
+struct drgn_error *drgn_program_init_core_dump_fd(struct drgn_program *prog,
+						  int fd);
+
 /**
  * Implement @ref drgn_program_from_kernel() on an initialized @ref
  * drgn_program.
diff --git a/libdrgn/python/drgnpy.h b/libdrgn/python/drgnpy.h
index 6f5fc6d57..9491e5b03 100644
--- a/libdrgn/python/drgnpy.h
+++ b/libdrgn/python/drgnpy.h
@@ -330,7 +330,9 @@ struct index_arg {
 int index_converter(PyObject *o, void *p);
 
 struct path_arg {
+	bool allow_fd;
 	bool allow_none;
+	int fd;
 	char *path;
 	Py_ssize_t length;
 	PyObject *object;
diff --git a/libdrgn/python/program.c b/libdrgn/python/program.c
index 693e93cd7..f15b44b63 100644
--- a/libdrgn/python/program.c
+++ b/libdrgn/python/program.c
@@ -515,13 +515,16 @@ static PyObject *Program_set_core_dump(Program *self, PyObject *args,
 {
 	static char *keywords[] = {"path", NULL};
 	struct drgn_error *err;
-	struct path_arg path = {};
+	struct path_arg path = { .allow_fd = true };
 
 	if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&:set_core_dump",
 					 keywords, path_converter, &path))
 		return NULL;
 
-	err = drgn_program_set_core_dump(&self->prog, path.path);
+	if (path.fd >= 0)
+		err = drgn_program_set_core_dump_fd(&self->prog, path.fd);
+	else
+		err = drgn_program_set_core_dump(&self->prog, path.path);
 	path_cleanup(&path);
 	if (err)
 		return set_drgn_error(err);
@@ -1247,7 +1250,7 @@ Program *program_from_core_dump(PyObject *self, PyObject *args, PyObject *kwds)
 {
 	static char *keywords[] = {"path", NULL};
 	struct drgn_error *err;
-	struct path_arg path = {};
+	struct path_arg path = { .allow_fd = true };
 	if (!PyArg_ParseTupleAndKeywords(args, kwds,
 					 "O&:program_from_core_dump", keywords,
 					 path_converter, &path))
@@ -1260,7 +1263,10 @@ Program *program_from_core_dump(PyObject *self, PyObject *args, PyObject *kwds)
 		return NULL;
 	}
 
-	err = drgn_program_init_core_dump(&prog->prog, path.path);
+	if (path.fd >= 0)
+		err = drgn_program_init_core_dump_fd(&prog->prog, path.fd);
+	else
+		err = drgn_program_init_core_dump(&prog->prog, path.path);
 	path_cleanup(&path);
 	if (err)
 		return set_drgn_error(err);
diff --git a/libdrgn/python/util.c b/libdrgn/python/util.c
index c01e86554..16200f456 100644
--- a/libdrgn/python/util.c
+++ b/libdrgn/python/util.c
@@ -86,7 +86,29 @@ int path_converter(PyObject *o, void *p)
 	}
 
 	struct path_arg *path = p;
-	if (path->allow_none && o == Py_None) {
+	path->fd = -1;
+	path->path = NULL;
+	path->length = 0;
+	path->bytes = NULL;
+	if (path->allow_fd && PyIndex_Check(o)) {
+		_cleanup_pydecref_ PyObject *fd_obj = PyNumber_Index(o);
+		if (!fd_obj)
+			return 0;
+		int overflow;
+		long fd = PyLong_AsLongAndOverflow(fd_obj, &overflow);
+		if (fd == -1 && PyErr_Occurred())
+			return 0;
+		if (overflow > 0 || fd > INT_MAX) {
+			PyErr_SetString(PyExc_OverflowError,
+					"fd is greater than maximum");
+			return 0;
+		}
+		if (fd < 0) {
+			PyErr_SetString(PyExc_ValueError, "fd is negative");
+			return 0;
+		}
+		path->fd = fd;
+	} else if (path->allow_none && o == Py_None) {
 		path->path = NULL;
 		path->length = 0;
 		path->bytes = NULL;

From f67d05f2966a140398f3e3936aeda383c37fea41 Mon Sep 17 00:00:00 2001
From: Stephen Brennan <stephen@brennan.io>
Date: Wed, 16 Aug 2023 23:13:59 -0700
Subject: [PATCH 2/3] libdrgn: linux_kernel: fallback section iterator

The files within /sys/module/*/sections seem to normally be 400
permissions, only accessible by root. Normally for live use, this is not
a problem because we are running as root. However, if we're running as
non-root, then we may get EACCES on these files.

To handle this, fall back to using the non-live approach if we get an
EACCES. Even if we do get the error, we can continue to use the
/sys/module/*/notes files to maintain a partial speedup.

Signed-off-by: Stephen Brennan <stephen@brennan.io>
---
 libdrgn/linux_kernel.c | 70 ++++++++++++++++++++++++++----------------
 1 file changed, 44 insertions(+), 26 deletions(-)

diff --git a/libdrgn/linux_kernel.c b/libdrgn/linux_kernel.c
index 1b3080e1b..7c53c271c 100644
--- a/libdrgn/linux_kernel.c
+++ b/libdrgn/linux_kernel.c
@@ -412,6 +412,7 @@ struct kernel_module_iterator {
 	/* Address of `struct list_head modules`. */
 	uint64_t head;
 	bool use_sys_module;
+	bool use_sys_module_sections;
 };
 
 static void kernel_module_iterator_deinit(struct kernel_module_iterator *it)
@@ -435,6 +436,7 @@ kernel_module_iterator_init(struct kernel_module_iterator *it,
 	it->build_id_buf = NULL;
 	it->build_id_buf_capacity = 0;
 	it->use_sys_module = use_sys_module;
+	it->use_sys_module_sections = use_sys_module;
 	err = drgn_program_find_type(prog, "struct module", NULL,
 				     &it->module_type);
 	if (err)
@@ -825,14 +827,37 @@ struct kernel_module_section_iterator {
 };
 
 static struct drgn_error *
-kernel_module_section_iterator_init(struct kernel_module_section_iterator *it,
-				    struct kernel_module_iterator *kmod_it)
+kernel_module_section_iterator_init_no_sys_module(struct kernel_module_section_iterator *it,
+						  struct kernel_module_iterator *kmod_it)
 {
 	struct drgn_error *err;
 
+	it->sections_dir = NULL;
+	it->i = 0;
+	it->name = NULL;
+	/* it->nsections = mod->sect_attrs->nsections */
+	err = drgn_object_member(&kmod_it->tmp1, &kmod_it->mod, "sect_attrs");
+	if (err)
+		return err;
+	err = drgn_object_member_dereference(&kmod_it->tmp2, &kmod_it->tmp1,
+					     "nsections");
+	if (err)
+		return err;
+	err = drgn_object_read_unsigned(&kmod_it->tmp2, &it->nsections);
+	if (err)
+		return err;
+	/* kmod_it->tmp1 = mod->sect_attrs->attrs */
+	return drgn_object_member_dereference(&kmod_it->tmp1, &kmod_it->tmp1,
+					      "attrs");
+}
+
+static struct drgn_error *
+kernel_module_section_iterator_init(struct kernel_module_section_iterator *it,
+				    struct kernel_module_iterator *kmod_it)
+{
 	it->kmod_it = kmod_it;
 	it->yielded_percpu = false;
-	if (kmod_it->use_sys_module) {
+	if (kmod_it->use_sys_module_sections) {
 		char *path;
 		if (asprintf(&path, "/sys/module/%s/sections",
 			     kmod_it->name) == -1)
@@ -846,26 +871,7 @@ kernel_module_section_iterator_init(struct kernel_module_section_iterator *it,
 		}
 		return NULL;
 	} else {
-		it->sections_dir = NULL;
-		it->i = 0;
-		it->name = NULL;
-		/* it->nsections = mod->sect_attrs->nsections */
-		err = drgn_object_member(&kmod_it->tmp1, &kmod_it->mod,
-					 "sect_attrs");
-		if (err)
-			return err;
-		err = drgn_object_member_dereference(&kmod_it->tmp2,
-						     &kmod_it->tmp1,
-						     "nsections");
-		if (err)
-			return err;
-		err = drgn_object_read_unsigned(&kmod_it->tmp2,
-						&it->nsections);
-		if (err)
-			return err;
-		/* kmod_it->tmp1 = mod->sect_attrs->attrs */
-		return drgn_object_member_dereference(&kmod_it->tmp1,
-						      &kmod_it->tmp1, "attrs");
+		return kernel_module_section_iterator_init_no_sys_module(it, kmod_it);
 	}
 }
 
@@ -971,8 +977,18 @@ kernel_module_section_iterator_next(struct kernel_module_section_iterator *it,
 	}
 
 	if (it->sections_dir) {
-		return kernel_module_section_iterator_next_live(it, name_ret,
-								address_ret);
+		err = kernel_module_section_iterator_next_live(it, name_ret,
+							       address_ret);
+		if (err && err->code == DRGN_ERROR_OS && err->errnum == EACCES) {
+			closedir(it->sections_dir);
+			drgn_error_destroy(err);
+			it->kmod_it->use_sys_module_sections = false;
+			err = kernel_module_section_iterator_init_no_sys_module(it, it->kmod_it);
+			if (err)
+				return err;
+		} else {
+			return err;
+		}
 	}
 
 	if (it->i >= it->nsections)
@@ -1571,7 +1587,9 @@ report_kernel_modules(struct drgn_debug_info_load_state *load,
 	 * If we're debugging the running kernel, we can use
 	 * /sys/module/$module/notes and /sys/module/$module/sections instead of
 	 * getting the equivalent information from the core dump. This fast path
-	 * can be disabled via an environment variable for testing.
+	 * can be disabled via an environment variable for testing. It may also
+	 * be disabled if we encounter permission issues using
+	 * /sys/module/$module/sections.
 	 */
 	bool use_sys_module = false;
 	if (prog->flags & DRGN_PROGRAM_IS_LIVE) {

From ac752388c86fe250a2ce41fe500a3c6ddff7d594 Mon Sep 17 00:00:00 2001
From: Stephen Brennan <stephen@brennan.io>
Date: Tue, 15 Aug 2023 23:40:45 -0700
Subject: [PATCH 3/3] cli: Open /proc/kcore via sudo when not root

Non-root users can now run Drgn against the running kernel. Drgn will
attempt to use sudo to open /proc/kcore and transmit the opened file
descriptor back to the user process. The file descriptor is then passed
to Program.set_core_dump(). The user must still have sudo privileges.

Signed-off-by: Stephen Brennan <stephen@brennan.io>
---
 drgn/cli.py                 |  6 +++-
 drgn/internal/sudohelper.py | 71 +++++++++++++++++++++++++++++++++++++
 2 files changed, 76 insertions(+), 1 deletion(-)
 create mode 100644 drgn/internal/sudohelper.py

diff --git a/drgn/cli.py b/drgn/cli.py
index 01b277bcd..05a0322f0 100644
--- a/drgn/cli.py
+++ b/drgn/cli.py
@@ -20,6 +20,7 @@
 
 import drgn
 from drgn.internal.rlcompleter import Completer
+from drgn.internal.sudohelper import open_via_sudo
 
 __all__ = ("run_interactive", "version_header")
 
@@ -264,7 +265,10 @@ def _main() -> None:
         elif args.pid is not None:
             prog.set_pid(args.pid or os.getpid())
         else:
-            prog.set_kernel()
+            try:
+                prog.set_kernel()
+            except PermissionError:
+                prog.set_core_dump(open_via_sudo("/proc/kcore", os.O_RDONLY))
     except PermissionError as e:
         print(e, file=sys.stderr)
         if args.pid is not None:
diff --git a/drgn/internal/sudohelper.py b/drgn/internal/sudohelper.py
new file mode 100644
index 000000000..b0f6ec27b
--- /dev/null
+++ b/drgn/internal/sudohelper.py
@@ -0,0 +1,71 @@
+# Copyright (c) Stephen Brennan <stephen@brennan.io>
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+"""Helper for opening a file as root and transmitting it via unix socket"""
+import array
+import os
+from pathlib import Path
+import pickle
+import socket
+import subprocess
+import sys
+import tempfile
+from typing import Union
+
+
+def open_via_sudo(
+    path: Union[Path, str],
+    flags: int,
+    mode: int = 0o777,
+) -> int:
+    """Implements os.open() using sudo to get permissions"""
+    # Currently does not support dir_fd argument
+    with tempfile.TemporaryDirectory() as td:
+        sockpath = Path(td) / "sock"
+        with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as sock:
+            sock.bind(str(sockpath))
+            subprocess.check_call(
+                [
+                    "sudo",
+                    sys.executable,
+                    "-B",
+                    __file__,
+                    sockpath,
+                    path,
+                    str(flags),
+                    str(mode),
+                ],
+            )
+            fds = array.array("i")
+            msg, ancdata, flags, addr = sock.recvmsg(
+                4096, socket.CMSG_SPACE(fds.itemsize)
+            )
+            for level, typ, data in ancdata:
+                if level == socket.SOL_SOCKET and typ == socket.SCM_RIGHTS:
+                    data = data[: fds.itemsize]
+                    fds.frombytes(data)
+                    return fds[0]
+            raise pickle.loads(msg)
+
+
+def main() -> None:
+    sockpath = sys.argv[1]
+    filename = sys.argv[2]
+    flags = int(sys.argv[3])
+    mode = int(sys.argv[4])
+
+    sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
+    sock.connect(sockpath)
+    try:
+        fd = os.open(filename, flags, mode)
+        fds = array.array("i", [fd])
+        sock.sendmsg(
+            [b"success"],
+            [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)],
+        )
+    except Exception as e:
+        sock.sendmsg([pickle.dumps(e)])
+
+
+if __name__ == "__main__":
+    main()