From 16c1b4a0979520602c796b2d5cdfe990ff000317 Mon Sep 17 00:00:00 2001 From: Stephen Brennan Date: Tue, 15 Aug 2023 23:40:45 -0700 Subject: [PATCH] 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_fd(). The user must still have sudo privileges. Signed-off-by: Stephen Brennan --- drgn/cli.py | 5 +- drgn/internal/sudohelper.py | 126 ++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 drgn/internal/sudohelper.py diff --git a/drgn/cli.py b/drgn/cli.py index 01b277bcd..4e0dad795 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") @@ -263,8 +264,10 @@ def _main() -> None: prog.set_core_dump(args.core) elif args.pid is not None: prog.set_pid(args.pid or os.getpid()) - else: + elif os.getuid() == 0: prog.set_kernel() + else: + prog.set_fd(open_via_sudo("/proc/kcore")) 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..ba887fe56 --- /dev/null +++ b/drgn/internal/sudohelper.py @@ -0,0 +1,126 @@ +# Copyright (c) Stephen Brennan +# SPDX-License-Identifier: LGPL-2.1-or-later + +"""Helper for opening a file as root and transmitting it via unix socket""" +import argparse +import array +import errno +import os +from pathlib import Path +import socket +import subprocess +import sys +import tempfile +from typing import Union + + +def recv_fd(sock: socket.socket) -> int: + """Receive a file descripter on the socket""" + # Borrows heavily from the Python docs for sock.recvmsg() + fds = array.array("i") + msg, ancdata, flags, addr = sock.recvmsg(4096, socket.CMSG_SPACE(fds.itemsize)) + if msg == b"success": + level, typ, data = ancdata[0] + assert level == socket.SOL_SOCKET + assert typ == socket.SCM_RIGHTS + data = data[: fds.itemsize] + fds.frombytes(data) + return fds[0] + raise Exception(msg.decode()) + + +def recv_error(sock: socket.socket) -> None: + """Try to receive an error message from the sudo helper""" + sock.setblocking(False) + try: + msg, ancdata, flags, addr = sock.recvmsg(4096) + except OSError as e: + if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK): + raise Exception("sudo helper never connected to socket") from None + raise + raise Exception(msg.decode()) + + +def send_fd(sock: socket.socket, fd: int) -> None: + """Send a file descripter on the socket""" + # Borrows heavily from the Python docs for sock.sendmsg() + fds = array.array("i", [fd]) + sock.sendmsg( + [b"success"], + [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)], + ) + + +def open_via_sudo( + path: Union[Path, str], + flags: int = os.O_RDONLY, + mode: int = 0o777, +) -> int: + """Implements os.open() using sudo to get permissions""" + # Currently does not support dir_fd argument + path = str(path) + with tempfile.TemporaryDirectory() as td: + sockpath = Path(td) / "sock" + sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + sock.bind(str(sockpath)) + + try: + proc = subprocess.run( + [ + "sudo", + sys.executable, + "-B", + __file__, + str(sockpath), + path, + str(flags), + str(mode), + ], + check=False, + ) + if proc.returncode == 0: + return recv_fd(sock) + else: + recv_error(sock) + assert False # No return from recv_error + finally: + # sudo tends to mess up the terminal, get it into a sane state + subprocess.run(["stty", "sane"]) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "socket", + type=Path, + help="path to unix domain socket", + ) + parser.add_argument( + "file", + type=Path, + help="filename to open", + ) + parser.add_argument( + "flags", + type=int, + help="numeric flags", + ) + parser.add_argument( + "mode", + type=int, + help="numeric mode", + ) + args = parser.parse_args() + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + sock.connect(str(args.socket)) + try: + fd = os.open(args.file, args.flags, args.mode) + send_fd(sock, fd) + except Exception as e: + sock.sendmsg([str(e).encode()]) + sys.exit(1) + + +if __name__ == "__main__": + main()