diff --git a/.readthedocs.yml b/.readthedocs.yaml similarity index 68% rename from .readthedocs.yml rename to .readthedocs.yaml index fe579b18e..03853730e 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yaml @@ -1,4 +1,8 @@ version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3" sphinx: configuration: docs/conf.py python: 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/contrib/ptdrgn.py b/contrib/ptdrgn.py new file mode 100644 index 000000000..1e31f7483 --- /dev/null +++ b/contrib/ptdrgn.py @@ -0,0 +1,210 @@ +#!/usr/bin/python3 +# Copyright (c) 2023, Oracle and/or its affiliates. +# Copyright (c) Meta Platforms, Inc. and affiliates. +# SPDX-License-Identifier: LGPL-2.1-or-later +""" +Drgn CLI, but using ptpython rather than the standard code.interact() + +NOTE: this is definitely a bit of a hack, using implementation details of Drgn +*and* ptpython. It may break at any time, but it is also quite useful, and this +makes it worth sharing. + +Requires: "pip install ptpython" which brings in pygments and prompt_toolkit +""" +import functools +import importlib +import os +import shutil +import sys +from typing import Any, Callable, Dict, Optional, Set + +from prompt_toolkit.completion import Completion, Completer +from prompt_toolkit.formatted_text import PygmentsTokens +from prompt_toolkit.formatted_text import fragment_list_to_text, to_formatted_text +from ptpython import embed +from ptpython.completer import DictionaryCompleter +from ptpython.repl import run_config +from pygments.lexers.c_cpp import CLexer + +import drgn +import drgn.cli + + +class DummyForRepr: + """ + A dummy class to pass back to _format_result_output() that pretends to have + the given repr() + """ + + def __init__(self, s): + self.s = s + + def __repr__(self): + return self.s + + +class DummyForPtRepr: + """A similar dummy class for the __pt_repr__() method.""" + + def __init__(self, s): + self.s = s + + def __pt_repr__(self): + return self.s + + +def _maybe_c_format(s): + """Given a string, try to use pygments to highlight it it as a C string.""" + try: + tokens = CLexer().get_tokens_unprocessed(s) + formatted = PygmentsTokens([(tokentype, value) for index, tokentype, value in tokens]) + to_format = DummyForPtRepr(formatted) + except Exception as e: + to_format = DummyForRepr(s) + return to_format + + +@functools.lru_cache(maxsize=1) +def _object_fields() -> Set[str]: + return set(dir(drgn.Object)) + + +class ReorderDrgnObjectCompleter(Completer): + """A completer which puts Object member fields above Object defaults""" + + def __init__(self, c: Completer): + self.c = c + + def get_completions(self, document, complete_event): + completions = list(self.c.get_completions(document, complete_event)) + if not completions: + return completions + text = completions[0].text + member_fields = [] + # If the first completion is "absent_", it is *very likely* that we are + # now looking at the completion of on Object. Move the default Object + # attributes to the end of the list so that we get the struct attributes + if text == "absent_": + fields = _object_fields() + for i in reversed(range(len(completions))): + text = completions[i].text + if text not in fields: + member_fields.append(completions[i]) + del completions[i] + return list(reversed(member_fields)) + completions + return completions + + +def configure(repl) -> None: + """ + Muck around with the internals of PythonRepl so that we will special case the + drgn data structures, similar to how drgn messes with sys.displayhook. We can + do C syntax highlighting too, which is really nice. + + This also automatically runs the default config file: + ~/.config/ptpython/config.py + """ + _format_result_output_orig = repl._format_result_output + + def _format_result_output(result: object): + if isinstance(result, drgn.Object): + s = result.format_(columns=shutil.get_terminal_size((0, 0)).columns) + to_format = _maybe_c_format(s) + elif isinstance(result, (drgn.StackFrame, drgn.StackTrace)): + to_format = DummyForRepr(str(result)) + elif isinstance(result, drgn.Type): + to_format = _maybe_c_format(str(result)) + else: + to_format = result + return _format_result_output_orig(to_format) + + repl._format_result_output = _format_result_output + run_config(repl) + repl._completer = ReorderDrgnObjectCompleter(repl._completer) + repl.completer = ReorderDrgnObjectCompleter(repl.completer) + + +def run_interactive( + prog: drgn.Program, + banner_func: Optional[Callable[[str], str]] = None, + globals_func: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None, + quiet: bool = False, +) -> None: + """ + Run drgn's :ref:`interactive-mode` via ptpython + + :param prog: Pre-configured program to run against. Available as a global + named ``prog`` in the CLI. + :param banner_func: Optional function to modify the printed banner. Called + with the default banner, and must return a string to use as the new + banner. The default banner does not include the drgn version, which can + be retrieved via :func:`version_header()`. + :param globals_func: Optional function to modify globals provided to the + session. Called with a dictionary of default globals, and must return a + dictionary to use instead. + :param quiet: Whether to suppress non-fatal warnings. + """ + init_globals: Dict[str, Any] = { + "prog": prog, + "drgn": drgn, + "__name__": "__main__", + "__doc__": None, + } + drgn_globals = [ + "NULL", + "Object", + "cast", + "container_of", + "execscript", + "offsetof", + "reinterpret", + "sizeof", + ] + for attr in drgn_globals: + init_globals[attr] = getattr(drgn, attr) + + banner = f"""\ +For help, type help(drgn). +>>> import drgn +>>> from drgn import {", ".join(drgn_globals)} +>>> from drgn.helpers.common import *""" + + module = importlib.import_module("drgn.helpers.common") + for name in module.__dict__["__all__"]: + init_globals[name] = getattr(module, name) + if prog.flags & drgn.ProgramFlags.IS_LINUX_KERNEL: + banner += "\n>>> from drgn.helpers.linux import *" + module = importlib.import_module("drgn.helpers.linux") + for name in module.__dict__["__all__"]: + init_globals[name] = getattr(module, name) + + if banner_func: + banner = banner_func(banner) + if globals_func: + init_globals = globals_func(init_globals) + + old_path = list(sys.path) + # The ptpython history file format is different from a standard readline + # history file since it must handle multi-line input, and it includes some + # metadata as well. Use a separate history format, even though it would be + # nice to share. + histfile = os.path.expanduser("~/.drgn_history.ptpython") + try: + sys.path.insert(0, "") + + print(banner) + embed( + globals=init_globals, + history_filename=histfile, + title="drgn", + configure=configure, + ) + finally: + sys.path[:] = old_path + + +if __name__ == "__main__": + # Muck around with the internals of drgn: swap out run_interactive() with our + # ptpython version, and then call main as if nothing happened. + drgn.cli.run_interactive = run_interactive + drgn.cli._main() diff --git a/docs/release_highlights.rst b/docs/release_highlights.rst index fa12e68e5..384d6de06 100644 --- a/docs/release_highlights.rst +++ b/docs/release_highlights.rst @@ -6,5 +6,6 @@ from the full `release notes `_. .. toctree:: + release_highlights/0.0.24.rst release_highlights/0.0.23.rst release_highlights/0.0.22.rst diff --git a/docs/release_highlights/0.0.24.rst b/docs/release_highlights/0.0.24.rst new file mode 100644 index 000000000..b57382640 --- /dev/null +++ b/docs/release_highlights/0.0.24.rst @@ -0,0 +1,115 @@ +0.0.24 (Released September 8th, 2023) +===================================== + +These are some of the highlights of drgn 0.0.24. See the `GitHub release +`_ for the full release +notes, including more improvements and bug fixes. + +.. highlight:: pycon + +Linked List Length Helper +------------------------- + +This release added :func:`~drgn.helpers.linux.list.list_count_nodes()`, which +returns the length of a Linux kernel linked list:: + + >>> list_count_nodes(prog["workqueues"].address_of_()) + 29 + +Networking Helpers +------------------ + +This release added a couple of Linux kernel networking helpers requested by +Jakub Kicinski. + +:func:`~drgn.helpers.linux.net.netdev_priv()` returns the private data of a +network device:: + + >>> dev = netdev_get_by_name(prog, "wlp0s20f3") + >>> netdev_priv(dev) + (void *)0xffff9419c9dec9c0 + >>> netdev_priv(dev, "struct ieee80211_sub_if_data") + *(struct ieee80211_sub_if_data *)0xffff9419c9dec9c0 = { + ... + } + +:func:`~drgn.helpers.linux.net.skb_shinfo()` returns the shared info for a +socket buffer. + +C++ Lookups +----------- + +This release added support for a few C++ features. + +Simple Type Specifiers +^^^^^^^^^^^^^^^^^^^^^^ + +Unlike C, C++ allows referring to ``class``, ``struct``, ``union``, and +``enum`` types without their respective keywords. For example: + +.. code-block:: c++ + + class Foo { ... }; + Foo foo; // Equivalent to class Foo foo; + +Previously, drgn always required the keyword, so ``prog.type("class Foo")`` +would succeed but ``prog.type("Foo")`` would fail with a :class:`LookupError`. +This requirement was surprising to C++ developers, so it was removed. For C++ +programs, ``prog.type("Foo")`` will now find a ``class``, ``struct``, +``union``, or ``enum`` type named ``Foo`` (for C programs, the keyword is still +required). + +Nested Classes +^^^^^^^^^^^^^^ + +Again unlike C, C++ allows ``class``, ``struct``, and ``union`` types to be +defined inside of other ``class``, ``struct``, and ``union`` types. For example: + +.. code-block:: c++ + + class Foo { + public: + class Bar { ... }; + ... + }; + Foo::Bar bar; + +drgn can now find such types with ``prog.type("Foo::Bar")``. + +Member Functions +^^^^^^^^^^^^^^^^ + +C++ supports member functions (a.k.a. methods). For example: + +.. code-block:: c++ + + class Foo { + int method() { ... } + }; + +drgn can now find member functions with :meth:`drgn.Program.function()`, +:meth:`drgn.Program.object()`, or :meth:`drgn.Program[] +` (e.g., ``prog.function("Foo::method")`` or +``prog["Foo::method"]``). + +Split DWARF +----------- + +drgn now supports split DWARF object (.dwo) files. This is enabled by the +``-gsplit-dwarf`` option in GCC and Clang or for the Linux kernel with +``CONFIG_DEBUG_INFO_SPLIT=y``. + +Split DWARF package (.dwp) file support is still in progress. + +Performance Improvements +------------------------ + +Thierry Treyer found a bug that made us search through much more debugging +information than necessary when getting a stack trace. Fixing this made stack +traces almost twice as fast. + +The C++ lookup and split DWARF support mentioned above require processing more +information in drgn's debugging information indexing step, which it does on +startup and whenever debugging information is manually loaded. This could've +been a performance regression, but instead, indexing was reworked from the +ground up in a way that's usually *faster* despite the added features. diff --git a/docs/support_matrix.rst b/docs/support_matrix.rst index 29116aacf..960a952e9 100644 --- a/docs/support_matrix.rst +++ b/docs/support_matrix.rst @@ -70,7 +70,7 @@ currently fully supported are: .. Keep this in sync with vmtest/config.py. -- 6.0-6.5 +- 6.0-6.6 - 5.10-5.19 - 5.4 - 4.19 diff --git a/drgn/__init__.py b/drgn/__init__.py index 35ff14fdc..5e9ec7fb4 100644 --- a/drgn/__init__.py +++ b/drgn/__init__.py @@ -142,7 +142,7 @@ if sys.version_info >= (3, 8): - _open_code = io.open_code + _open_code = io.open_code # novermin else: from typing import BinaryIO diff --git a/drgn/cli.py b/drgn/cli.py index 01b277bcd..44839ec33 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") @@ -262,22 +263,22 @@ def _main() -> None: if args.core is not None: prog.set_core_dump(args.core) elif args.pid is not None: - prog.set_pid(args.pid or os.getpid()) + try: + prog.set_pid(args.pid or os.getpid()) + except PermissionError as e: + sys.exit( + f"{e}\nerror: attaching to live process requires ptrace attach permissions" + ) else: - prog.set_kernel() - except PermissionError as e: - print(e, file=sys.stderr) - if args.pid is not None: - print( - "error: attaching to live process requires ptrace attach permissions", - file=sys.stderr, - ) - elif args.core is None: - print( - "error: drgn debugs the live kernel by default, which requires root", - file=sys.stderr, - ) - sys.exit(1) + try: + prog.set_kernel() + except PermissionError as e: + if shutil.which("sudo") is None: + sys.exit( + f"{e}\ndrgn debugs the live kernel by default, which requires root" + ) + else: + prog.set_core_dump(open_via_sudo("/proc/kcore", os.O_RDONLY)) except OSError as e: sys.exit(e) except ValueError as e: diff --git a/drgn/helpers/linux/mm.py b/drgn/helpers/linux/mm.py index e28da56b7..918bdd2bf 100644 --- a/drgn/helpers/linux/mm.py +++ b/drgn/helpers/linux/mm.py @@ -660,20 +660,32 @@ def compound_order(page: Object) -> Object: if not PageHead(page): return Object(prog, "unsigned int", 0) - # Before Linux kernel commit 379708ffde1b ("mm: add the first tail page to - # struct folio") (in v6.1), the compound order is in struct page. Since - # that commit, it is also in struct folio. Since Linux kernel commit - # 1c5509be58f6 ("mm: remove 'First tail page' members from struct page") - # (in v6.3), it is _only_ in struct folio. + # Since Linux kernel commit ebc1baf5c9b4 ("mm: free up a word in the first + # tail page") (in v6.6), the compound order is in the low byte of struct + # folio::_flags_1 (from_folio = 2). Between that and Linux kernel commit + # Linux kernel commit 379708ffde1b ("mm: add the first tail page to struct + # folio") (in v6.1), the compound order is in struct folio::_folio_order + # (from_folio = 1). Before Linux kernel commit 1c5509be58f6 ("mm: remove + # 'First tail page' members from struct page") (in v6.3), the compound + # order is in struct page::compound_order of the first tail page + # (from_folio = 0). try: from_folio = prog.cache["compound_order_from_folio"] except KeyError: + from_folio = 0 try: - from_folio = prog.type("struct folio").has_member("_folio_order") + struct_folio = prog.type("struct folio") except LookupError: - from_folio = False + pass + else: + if struct_folio.has_member("_folio_order"): + from_folio = 1 + elif struct_folio.has_member("_flags_1"): + from_folio = 2 prog.cache["compound_order_from_folio"] = from_folio - if from_folio: + if from_folio == 2: + return cast("unsigned int", cast("struct folio *", page)._flags_1 & 0xFF) + elif from_folio == 1: return cast("unsigned int", cast("struct folio *", page)._folio_order) else: return cast("unsigned int", page[1].compound_order) diff --git a/drgn/helpers/linux/slab.py b/drgn/helpers/linux/slab.py index ab6f0bf84..ce7774579 100644 --- a/drgn/helpers/linux/slab.py +++ b/drgn/helpers/linux/slab.py @@ -309,16 +309,22 @@ def _slub_get_freelist(freelist: Object, freelist_set: Set[int]) -> None: ptr = self._freelist_dereference(ptr + freelist_offset) cpu_freelists: Set[int] = set() - cpu_slab = slab_cache.cpu_slab.read_() - # Since Linux kernel commit bb192ed9aa71 ("mm/slub: Convert most struct - # page to struct slab by spatch") (in v5.17), the current slab for a - # CPU is `struct slab *slab`. Before that, it is `struct page *page`. - cpu_slab_attr = "slab" if hasattr(cpu_slab, "slab") else "page" - for cpu in for_each_online_cpu(self._prog): - this_cpu_slab = per_cpu_ptr(cpu_slab, cpu) - slab = getattr(this_cpu_slab, cpu_slab_attr).read_() - if slab and slab.slab_cache == slab_cache: - _slub_get_freelist(this_cpu_slab.freelist, cpu_freelists) + try: + # cpu_slab doesn't exist for CONFIG_SLUB_TINY. + cpu_slab = slab_cache.cpu_slab.read_() + except AttributeError: + pass + else: + # Since Linux kernel commit bb192ed9aa71 ("mm/slub: Convert most + # struct page to struct slab by spatch") (in v5.17), the current + # slab for a CPU is `struct slab *slab`. Before that, it is `struct + # page *page`. + cpu_slab_attr = "slab" if hasattr(cpu_slab, "slab") else "page" + for cpu in for_each_online_cpu(self._prog): + this_cpu_slab = per_cpu_ptr(cpu_slab, cpu) + slab = getattr(this_cpu_slab, cpu_slab_attr).read_() + if slab and slab.slab_cache == slab_cache: + _slub_get_freelist(this_cpu_slab.freelist, cpu_freelists) self._slub_get_freelist = _slub_get_freelist self._cpu_freelists = cpu_freelists diff --git a/drgn/internal/sudohelper.py b/drgn/internal/sudohelper.py new file mode 100644 index 000000000..0016bca5a --- /dev/null +++ b/drgn/internal/sudohelper.py @@ -0,0 +1,73 @@ +# 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 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", + "-p", + f"[sudo] password for %p to open {path}: ", + 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() diff --git a/libdrgn/configure.ac b/libdrgn/configure.ac index 67cf70080..1c705948d 100644 --- a/libdrgn/configure.ac +++ b/libdrgn/configure.ac @@ -1,7 +1,7 @@ dnl Copyright (c) Meta Platforms, Inc. and affiliates. dnl SPDX-License-Identifier: LGPL-2.1-or-later -AC_INIT([libdrgn], [0.0.23], +AC_INIT([libdrgn], [0.0.24], [https://github.com/osandov/drgn/issues],, [https://github.com/osandov/drgn]) 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/dwarf_info.c b/libdrgn/dwarf_info.c index f0c5b3280..0afbc43ae 100644 --- a/libdrgn/dwarf_info.c +++ b/libdrgn/dwarf_info.c @@ -1792,6 +1792,12 @@ drgn_dwarf_info_update_index(struct drgn_dwarf_index_state *state) if (dbinfo->dwarf.global.saved_err) return drgn_error_copy(dbinfo->dwarf.global.saved_err); + size_t new_cus_size = drgn_dwarf_index_cu_vector_size(cus); + for (int i = 0; i < drgn_num_threads; i++) + new_cus_size += drgn_dwarf_index_cu_vector_size(&state->cus[i]); + if (new_cus_size == drgn_dwarf_index_cu_vector_size(cus)) + return NULL; + // Per-thread array of maps to populate. Thread 0 uses the maps in the // dbinfo directly. These are merged into the dbinfo and freed. _cleanup_free_ union { @@ -1809,9 +1815,6 @@ drgn_dwarf_info_update_index(struct drgn_dwarf_index_state *state) return &drgn_enomem; } - size_t new_cus_size = drgn_dwarf_index_cu_vector_size(cus); - for (int i = 0; i < drgn_num_threads; i++) - new_cus_size += drgn_dwarf_index_cu_vector_size(&state->cus[i]); if (!drgn_dwarf_index_cu_vector_reserve(cus, new_cus_size)) return &drgn_enomem; for (int i = 0; i < drgn_num_threads; i++) 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) { 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; diff --git a/vmtest/config.py b/vmtest/config.py index ca74bf251..38513cfb2 100644 --- a/vmtest/config.py +++ b/vmtest/config.py @@ -13,6 +13,7 @@ # Kernel versions that we run tests on and therefore support. Keep this in sync # with docs/support_matrix.rst. SUPPORTED_KERNEL_VERSIONS = ( + "6.6", "6.5", "6.4", "6.3", @@ -36,7 +37,7 @@ ) KERNEL_ORG_COMPILER_VERSION = "12.2.0" -VMTEST_KERNEL_VERSION = 21 +VMTEST_KERNEL_VERSION = 22 BASE_KCONFIG = """ @@ -168,28 +169,40 @@ class KernelFlavor(NamedTuple): CONFIG_SLUB=y # For slab tests. CONFIG_SLUB_DEBUG=y + CONFIG_RANDOMIZE_BASE=y """, ), KernelFlavor( name="alternative", - description="SLAB allocator", + description="SLAB allocator, no KASLR", config=""" CONFIG_SMP=y CONFIG_SLAB=y + # Linux kernel commit eb07c4f39c3e ("mm/slab: rename + # CONFIG_SLAB to CONFIG_SLAB_DEPRECATED") (in v6.5) renamed the + # option for SLAB. + CONFIG_SLAB_DEPRECATED=y + CONFIG_RANDOMIZE_BASE=n """, ), KernelFlavor( name="tiny", - description="!SMP, !PREEMPT, and SLOB allocator", + description="no SMP, no PREEMPT, no KASLR, and SLUB_TINY or SLOB allocator", config=""" CONFIG_SMP=n CONFIG_SLOB=y # Linux kernel commit 149b6fa228ed ("mm, slob: rename CONFIG_SLOB to # CONFIG_SLOB_DEPRECATED") (in v6.2) renamed the option for SLOB. CONFIG_SLOB_DEPRECATED=y + # Linux kernel commit c9929f0e344a ("mm/slob: remove + # CONFIG_SLOB") (in v6.4) removed SLOB. Use SLUB_TINY instead, + # which was introduced in Linux kernel commit e240e53ae0ab + # ("mm, slub: add CONFIG_SLUB_TINY") (in v6.2). + CONFIG_SLUB_TINY=y # CONFIG_PREEMPT_DYNAMIC is not set CONFIG_PREEMPT_NONE=y # !PREEMPTION && !SMP will also select TINY_RCU. + CONFIG_RANDOMIZE_BASE=n """, ), ) diff --git a/vmtest/kbuild.py b/vmtest/kbuild.py index 1b3279378..dc4a197b6 100644 --- a/vmtest/kbuild.py +++ b/vmtest/kbuild.py @@ -491,15 +491,19 @@ async def package(self, format: str, output_dir: Path) -> Path: env=self._env, ) - # `make modules_install` creates these as symlinks to the absolute - # path of the source directory. Delete them, make build a - # directory, and make source a symlink to build. + # `make modules_install` creates build as a symlink to the absolute + # path of the build directory. Delete it and make it an empty + # directory for us to populate. modules_build_dir = modules_dir / "build" - modules_source_dir = modules_dir / "source" modules_build_dir.unlink() modules_build_dir.mkdir() - modules_source_dir.unlink() - modules_source_dir.symlink_to("build") + # Before Linux kernel commit d8131c2965d5 ("kbuild: remove + # $(MODLIB)/source symlink") (in v6.6), source is a symlink to the + # absolute path of the source directory. It's not needed. + try: + (modules_dir / "source").unlink() + except FileNotFoundError: + pass logger.info("copying vmlinux") vmlinux = modules_build_dir / "vmlinux" diff --git a/vmtest/rootfsbuild.py b/vmtest/rootfsbuild.py index 8c260f19e..d149cf414 100644 --- a/vmtest/rootfsbuild.py +++ b/vmtest/rootfsbuild.py @@ -12,7 +12,7 @@ if sys.version_info < (3, 8): from typing_extensions import Literal else: - from typing import Literal + from typing import Literal # novermin from vmtest.config import ARCHITECTURES, HOST_ARCHITECTURE, Architecture