From a777ea79e70b1574aa40d338fef58df697045ba5 Mon Sep 17 00:00:00 2001 From: Omar Sandoval Date: Mon, 16 Sep 2024 11:04:19 -0700 Subject: [PATCH 1/9] drgndoc: allow formatting __init__() signature together with class In all of our current code, we want to document a class separately from its "constructor"; see commit c801e5e9b1b1 ("drgndoc: format __init__() signature separately from class"). But for some trivial classes, like one that will be added in an upcoming change, it makes sense to collapse them. So, for classes with no docstring and one __init__() signature with a docstring, document the class with the __init__() signature and docstring. Signed-off-by: Omar Sandoval --- docs/exts/drgndoc/format.py | 60 +++++++++++++++++++++++-------------- docs/exts/drgndoc/parse.py | 5 +++- 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/docs/exts/drgndoc/format.py b/docs/exts/drgndoc/format.py index c9a2e7eac..33829a8b5 100644 --- a/docs/exts/drgndoc/format.py +++ b/docs/exts/drgndoc/format.py @@ -393,10 +393,44 @@ def _format_class( ) -> List[str]: node = resolved.node + init_signatures: List[FunctionSignature] = [] + try: + init = resolved.attr("__init__") + except KeyError: + pass + else: + if isinstance(init.node, Function): + init_signatures = [ + signature + for signature in init.node.signatures + if signature.docstring is not None + ] + + init_context_class = resolved.name + if context_class: + init_context_class = context_class + "." + init_context_class + lines = [] + if rst and len(init_signatures) == 1 and node.docstring is None: + class_signature, class_docstring_lines = self._format_function_signature( + init_signatures[0], + init.modules, + init.classes, + context_module, + init_context_class, + rst, + False, + ) + del init_signatures[0] + else: + class_signature = "" + class_docstring_lines = ( + node.docstring.splitlines() if node.docstring else [] + ) + if rst: - lines.append(f".. py:class:: {name}") + lines.append(f".. py:class:: {name}{class_signature}") if node.bases: visitor = _FormatVisitor( @@ -417,32 +451,14 @@ def _format_class( lines.append("") lines.append((" " if rst else "") + "Bases: " + ", ".join(bases)) - if node.docstring: - docstring_lines = node.docstring.splitlines() + if class_docstring_lines: if lines: lines.append("") if rst: - for line in docstring_lines: + for line in class_docstring_lines: lines.append(" " + line) else: - lines.extend(docstring_lines) - - init_signatures: Sequence[FunctionSignature] = () - try: - init = resolved.attr("__init__") - except KeyError: - pass - else: - if isinstance(init.node, Function): - init_signatures = [ - signature - for signature in init.node.signatures - if signature.docstring is not None - ] - - init_context_class = resolved.name - if context_class: - init_context_class = context_class + "." + init_context_class + lines.extend(class_docstring_lines) for i, signature_node in enumerate(init_signatures): if lines: diff --git a/docs/exts/drgndoc/parse.py b/docs/exts/drgndoc/parse.py index 5e8a39f0f..f02c82cdc 100644 --- a/docs/exts/drgndoc/parse.py +++ b/docs/exts/drgndoc/parse.py @@ -111,7 +111,10 @@ def __init__( self.attrs = attrs def has_docstring(self) -> bool: - return self.docstring is not None + if self.docstring is not None: + return True + init = self.attrs.get("__init__") + return isinstance(init, Function) and init.has_docstring() class FunctionSignature: From 781b3d91e03afa897a067afb3e48c566476dd01b Mon Sep 17 00:00:00 2001 From: Omar Sandoval Date: Thu, 29 Aug 2024 22:05:51 -0700 Subject: [PATCH 2/9] Move machine name and syscall numbers to new _drgn_util.platform module An upcoming helper will need these. We also need them from vmtest when _drgn may not be built yet, though, so they can't go in drgn.internal. Create a new package, _drgn_util, and move them there. Signed-off-by: Omar Sandoval --- .packit.yaml | 4 +- .pre-commit-config.yaml | 2 +- _drgn_util/__init__.py | 12 +++++ _drgn_util/platform.py | 55 ++++++++++++++++++++++ setup.py | 2 +- tests/linux_kernel/__init__.py | 2 +- tests/linux_kernel/bpf.py | 2 +- tests/linux_kernel/helpers/test_boot.py | 2 +- tests/linux_kernel/helpers/test_bpf.py | 2 +- tests/linux_kernel/helpers/test_kconfig.py | 2 +- tests/linux_kernel/helpers/test_mm.py | 2 +- tests/linux_kernel/test_stack_trace.py | 2 +- tests/linux_kernel/vmcore/test_vmcore.py | 2 +- util.py | 52 -------------------- vmtest/config.py | 4 +- vmtest/download.py | 3 +- vmtest/enter_kdump.py | 2 +- 17 files changed, 84 insertions(+), 68 deletions(-) create mode 100644 _drgn_util/__init__.py create mode 100644 _drgn_util/platform.py diff --git a/.packit.yaml b/.packit.yaml index 12b5e27c2..bbad1d878 100644 --- a/.packit.yaml +++ b/.packit.yaml @@ -10,8 +10,8 @@ upstream_package_name: drgn downstream_package_name: python-drgn actions: get-current-version: "python3 setup.py --version" - # Fetch the specfile from Rawhide, drop any patches and disable rpmautospec - post-upstream-clone: "bash -c \"curl -s https://src.fedoraproject.org/rpms/python-drgn/raw/main/f/python-drgn.spec | sed -e '/^Patch[0-9]/d' -e '/^%autochangelog$/d' > python-drgn.spec\"" + # Fetch the specfile from Rawhide, drop any patches, disable rpmautospec, and add the _drgn_util package + post-upstream-clone: "bash -c \"curl -s https://src.fedoraproject.org/rpms/python-drgn/raw/main/f/python-drgn.spec | sed -e '/^Patch[0-9]/d' -e '/^%autochangelog$/d' -e 's!^%{python3_sitearch}/%{pypi_name}$!%{python3_sitearch}/%{pypi_name}\\\\n%{python3_sitearch}/_%{pypi_name}_util!' > python-drgn.spec\"" srpm_build_deps: - bash diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a2ff4a6d..a55b895d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: hooks: - id: mypy args: [--show-error-codes, --strict, --no-warn-return-any, --no-warn-unused-ignores] - files: ^(drgn/.*\.py|_drgn.pyi|tools/.*\.py)$ + files: ^(drgn/.*\.py|_drgn.pyi|_drgn_util/.*\.py|tools/.*\.py)$ - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: diff --git a/_drgn_util/__init__.py b/_drgn_util/__init__.py new file mode 100644 index 000000000..dce9f389c --- /dev/null +++ b/_drgn_util/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# SPDX-License-Identifier: LGPL-2.1-or-later + +""" +Internal utilities for drgn + +This package contains utilities shared between the drgn package and supporting +build/test code. You should not use them. + +This package must not depend on the drgn package itself since it is used before +the _drgn extension module is built. +""" diff --git a/_drgn_util/platform.py b/_drgn_util/platform.py new file mode 100644 index 000000000..e067f5acd --- /dev/null +++ b/_drgn_util/platform.py @@ -0,0 +1,55 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# SPDX-License-Identifier: LGPL-2.1-or-later + +import platform +import re + +NORMALIZED_MACHINE_NAME = platform.machine() +if NORMALIZED_MACHINE_NAME.startswith("aarch64") or NORMALIZED_MACHINE_NAME == "arm64": + NORMALIZED_MACHINE_NAME = "aarch64" +elif NORMALIZED_MACHINE_NAME.startswith("arm") or NORMALIZED_MACHINE_NAME == "sa110": + NORMALIZED_MACHINE_NAME = "arm" +elif re.fullmatch(r"i.86", NORMALIZED_MACHINE_NAME): + NORMALIZED_MACHINE_NAME = "i386" +elif NORMALIZED_MACHINE_NAME.startswith("ppc64"): + NORMALIZED_MACHINE_NAME = "ppc64" +elif NORMALIZED_MACHINE_NAME.startswith("ppc"): + NORMALIZED_MACHINE_NAME = "ppc" +elif NORMALIZED_MACHINE_NAME == "riscv": + NORMALIZED_MACHINE_NAME = "riscv32" +elif re.match(r"sh[0-9]", NORMALIZED_MACHINE_NAME): + NORMALIZED_MACHINE_NAME = "sh" +elif NORMALIZED_MACHINE_NAME == "sun4u": + NORMALIZED_MACHINE_NAME = "sparc64" + +SYS = { + "aarch64": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, + "alpha": {"bpf": 515, "perf_event_open": 493}, + "arc": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, + "arm": {"bpf": 386, "kexec_file_load": 401, "perf_event_open": 364}, + "csky": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, + "hexagon": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, + "i386": {"bpf": 357, "perf_event_open": 336}, + "loongarch": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, + "loongarch64": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, + "m68k": {"bpf": 354, "perf_event_open": 332}, + "microblaze": {"bpf": 387, "perf_event_open": 366}, + # TODO: mips is missing here because I don't know how to distinguish + # between the o32 and n32 ABIs. + "mips64": {"bpf": 315, "perf_event_open": 292}, + "nios2": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, + "openrisc": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, + "parisc": {"bpf": 341, "kexec_file_load": 355, "perf_event_open": 318}, + "parisc64": {"bpf": 341, "kexec_file_load": 355, "perf_event_open": 318}, + "ppc": {"bpf": 361, "perf_event_open": 319}, + "ppc64": {"bpf": 361, "perf_event_open": 319}, + "riscv32": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, + "riscv64": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, + "s390": {"bpf": 351, "kexec_file_load": 381, "perf_event_open": 331}, + "s390x": {"bpf": 351, "kexec_file_load": 381, "perf_event_open": 331}, + "sh": {"bpf": 375, "perf_event_open": 336}, + "sparc": {"bpf": 349, "perf_event_open": 327}, + "sparc64": {"bpf": 349, "perf_event_open": 327}, + "x86_64": {"bpf": 321, "kexec_file_load": 320, "perf_event_open": 298}, + "xtensa": {"bpf": 340, "perf_event_open": 327}, +}.get(NORMALIZED_MACHINE_NAME, {}) diff --git a/setup.py b/setup.py index f897a6b99..6535bde97 100755 --- a/setup.py +++ b/setup.py @@ -452,7 +452,7 @@ def get_version(): setup( name="drgn", version=get_version(), - packages=find_packages(include=["drgn", "drgn.*"]), + packages=find_packages(include=["drgn", "drgn.*", "_drgn_util", "_drgn_util.*"]), package_data={"drgn": ["../_drgn.pyi", "py.typed"]}, # This is here so that setuptools knows that we have an extension; it's # actually built using autotools/make. diff --git a/tests/linux_kernel/__init__.py b/tests/linux_kernel/__init__.py index 6e8c839da..9114f597e 100644 --- a/tests/linux_kernel/__init__.py +++ b/tests/linux_kernel/__init__.py @@ -19,9 +19,9 @@ from typing import NamedTuple import unittest +from _drgn_util.platform import NORMALIZED_MACHINE_NAME, SYS import drgn from tests import TestCase -from util import NORMALIZED_MACHINE_NAME, SYS class LinuxKernelTestCase(TestCase): diff --git a/tests/linux_kernel/bpf.py b/tests/linux_kernel/bpf.py index 5d8570739..ae288ed68 100644 --- a/tests/linux_kernel/bpf.py +++ b/tests/linux_kernel/bpf.py @@ -5,8 +5,8 @@ import ctypes from typing import NamedTuple +from _drgn_util.platform import SYS from tests.linux_kernel import _check_ctypes_syscall, _syscall -from util import SYS # enum bpf_cmd BPF_MAP_CREATE = 0 diff --git a/tests/linux_kernel/helpers/test_boot.py b/tests/linux_kernel/helpers/test_boot.py index 580f54efb..00393dc53 100644 --- a/tests/linux_kernel/helpers/test_boot.py +++ b/tests/linux_kernel/helpers/test_boot.py @@ -4,9 +4,9 @@ import re import unittest +from _drgn_util.platform import NORMALIZED_MACHINE_NAME from drgn.helpers.linux.boot import pgtable_l5_enabled from tests.linux_kernel import LinuxKernelTestCase -from util import NORMALIZED_MACHINE_NAME class TestBoot(LinuxKernelTestCase): diff --git a/tests/linux_kernel/helpers/test_bpf.py b/tests/linux_kernel/helpers/test_bpf.py index 146812e4c..45c4e3517 100644 --- a/tests/linux_kernel/helpers/test_bpf.py +++ b/tests/linux_kernel/helpers/test_bpf.py @@ -6,6 +6,7 @@ import sys import unittest +from _drgn_util.platform import NORMALIZED_MACHINE_NAME from drgn.helpers.linux.bpf import ( bpf_btf_for_each, bpf_link_for_each, @@ -34,7 +35,6 @@ bpf_prog_load, ) from tests.linux_kernel.helpers.test_cgroup import tmp_cgroups -from util import NORMALIZED_MACHINE_NAME class TestBpf(LinuxKernelTestCase): diff --git a/tests/linux_kernel/helpers/test_kconfig.py b/tests/linux_kernel/helpers/test_kconfig.py index ab01d98d8..0ced33763 100644 --- a/tests/linux_kernel/helpers/test_kconfig.py +++ b/tests/linux_kernel/helpers/test_kconfig.py @@ -5,9 +5,9 @@ import re import unittest +from _drgn_util.platform import NORMALIZED_MACHINE_NAME from drgn.helpers.linux.kconfig import get_kconfig from tests.linux_kernel import LinuxKernelTestCase -from util import NORMALIZED_MACHINE_NAME class TestKconfig(LinuxKernelTestCase): diff --git a/tests/linux_kernel/helpers/test_mm.py b/tests/linux_kernel/helpers/test_mm.py index 68033e196..ef708cd29 100644 --- a/tests/linux_kernel/helpers/test_mm.py +++ b/tests/linux_kernel/helpers/test_mm.py @@ -10,6 +10,7 @@ import tempfile import unittest +from _drgn_util.platform import NORMALIZED_MACHINE_NAME from drgn import NULL, FaultError from drgn.helpers.linux.mm import ( PFN_PHYS, @@ -59,7 +60,6 @@ skip_unless_have_full_mm_support, skip_unless_have_test_kmod, ) -from util import NORMALIZED_MACHINE_NAME class TestMm(LinuxKernelTestCase): diff --git a/tests/linux_kernel/test_stack_trace.py b/tests/linux_kernel/test_stack_trace.py index d156b7e74..8617d57e6 100644 --- a/tests/linux_kernel/test_stack_trace.py +++ b/tests/linux_kernel/test_stack_trace.py @@ -4,6 +4,7 @@ import os import unittest +from _drgn_util.platform import NORMALIZED_MACHINE_NAME from drgn import Object, Program, reinterpret from tests import assertReprPrettyEqualsStr, modifyenv from tests.linux_kernel import ( @@ -12,7 +13,6 @@ skip_unless_have_stack_tracing, skip_unless_have_test_kmod, ) -from util import NORMALIZED_MACHINE_NAME @skip_unless_have_stack_tracing diff --git a/tests/linux_kernel/vmcore/test_vmcore.py b/tests/linux_kernel/vmcore/test_vmcore.py index 4e55b1aae..7ad7f75eb 100644 --- a/tests/linux_kernel/vmcore/test_vmcore.py +++ b/tests/linux_kernel/vmcore/test_vmcore.py @@ -1,10 +1,10 @@ # Copyright (c) Meta Platforms, Inc. and affiliates. # SPDX-License-Identifier: LGPL-2.1-or-later +from _drgn_util.platform import NORMALIZED_MACHINE_NAME from drgn import ProgramFlags from drgn.helpers.linux.pid import find_task from tests.linux_kernel.vmcore import LinuxVMCoreTestCase -from util import NORMALIZED_MACHINE_NAME class TestVMCore(LinuxVMCoreTestCase): diff --git a/util.py b/util.py index 1ae0471b3..b0e207f02 100644 --- a/util.py +++ b/util.py @@ -4,7 +4,6 @@ from functools import total_ordering import os from pathlib import Path -import platform import re from typing import Union @@ -114,54 +113,3 @@ def __lt__(self, other: object) -> bool: def __str__(self) -> str: return self._release - - -NORMALIZED_MACHINE_NAME = platform.machine() -if NORMALIZED_MACHINE_NAME.startswith("aarch64") or NORMALIZED_MACHINE_NAME == "arm64": - NORMALIZED_MACHINE_NAME = "aarch64" -elif NORMALIZED_MACHINE_NAME.startswith("arm") or NORMALIZED_MACHINE_NAME == "sa110": - NORMALIZED_MACHINE_NAME = "arm" -elif re.fullmatch(r"i.86", NORMALIZED_MACHINE_NAME): - NORMALIZED_MACHINE_NAME = "i386" -elif NORMALIZED_MACHINE_NAME.startswith("ppc64"): - NORMALIZED_MACHINE_NAME = "ppc64" -elif NORMALIZED_MACHINE_NAME.startswith("ppc"): - NORMALIZED_MACHINE_NAME = "ppc" -elif NORMALIZED_MACHINE_NAME == "riscv": - NORMALIZED_MACHINE_NAME = "riscv32" -elif re.match(r"sh[0-9]", NORMALIZED_MACHINE_NAME): - NORMALIZED_MACHINE_NAME = "sh" -elif NORMALIZED_MACHINE_NAME == "sun4u": - NORMALIZED_MACHINE_NAME = "sparc64" - -SYS = { - "aarch64": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "alpha": {"bpf": 515, "perf_event_open": 493}, - "arc": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "arm": {"bpf": 386, "kexec_file_load": 401, "perf_event_open": 364}, - "csky": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "hexagon": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "i386": {"bpf": 357, "perf_event_open": 336}, - "loongarch": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "loongarch64": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "m68k": {"bpf": 354, "perf_event_open": 332}, - "microblaze": {"bpf": 387, "perf_event_open": 366}, - # TODO: mips is missing here because I don't know how to distinguish - # between the o32 and n32 ABIs. - "mips64": {"bpf": 315, "perf_event_open": 292}, - "nios2": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "openrisc": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "parisc": {"bpf": 341, "kexec_file_load": 355, "perf_event_open": 318}, - "parisc64": {"bpf": 341, "kexec_file_load": 355, "perf_event_open": 318}, - "ppc": {"bpf": 361, "perf_event_open": 319}, - "ppc64": {"bpf": 361, "perf_event_open": 319}, - "riscv32": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "riscv64": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "s390": {"bpf": 351, "kexec_file_load": 381, "perf_event_open": 331}, - "s390x": {"bpf": 351, "kexec_file_load": 381, "perf_event_open": 331}, - "sh": {"bpf": 375, "perf_event_open": 336}, - "sparc": {"bpf": 349, "perf_event_open": 327}, - "sparc64": {"bpf": 349, "perf_event_open": 327}, - "x86_64": {"bpf": 321, "kexec_file_load": 320, "perf_event_open": 298}, - "xtensa": {"bpf": 340, "perf_event_open": 327}, -}.get(NORMALIZED_MACHINE_NAME, {}) diff --git a/vmtest/config.py b/vmtest/config.py index 9c99f4545..8d4cb914a 100644 --- a/vmtest/config.py +++ b/vmtest/config.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Dict, Mapping, NamedTuple, Sequence -from util import NORMALIZED_MACHINE_NAME +from _drgn_util.platform import NORMALIZED_MACHINE_NAME # Kernel versions that we run tests on and therefore support. Keep this in sync # with docs/support_matrix.rst. @@ -232,7 +232,7 @@ class KernelFlavor(NamedTuple): class Architecture(NamedTuple): # Architecture name. This matches the names used by - # util.NORMALIZED_MACHINE_NAME and qemu-system-$arch_name. + # _drgn_util.platform.NORMALIZED_MACHINE_NAME and qemu-system-$arch_name. name: str # Value of ARCH variable to build the Linux kernel. kernel_arch: str diff --git a/vmtest/download.py b/vmtest/download.py index b0bd0b853..11327afe0 100644 --- a/vmtest/download.py +++ b/vmtest/download.py @@ -29,7 +29,8 @@ ) import urllib.request -from util import NORMALIZED_MACHINE_NAME, KernelVersion +from _drgn_util.platform import NORMALIZED_MACHINE_NAME +from util import KernelVersion from vmtest.config import ( ARCHITECTURES, HOST_ARCHITECTURE, diff --git a/vmtest/enter_kdump.py b/vmtest/enter_kdump.py index 2552dc712..674006af2 100644 --- a/vmtest/enter_kdump.py +++ b/vmtest/enter_kdump.py @@ -6,7 +6,7 @@ import re import subprocess -from util import NORMALIZED_MACHINE_NAME, SYS +from _drgn_util.platform import NORMALIZED_MACHINE_NAME, SYS KEXEC_FILE_ON_CRASH = 2 KEXEC_FILE_NO_INITRAMFS = 4 From e22d452283a4950f65e930f3323ef414f7e5e47c Mon Sep 17 00:00:00 2001 From: Omar Sandoval Date: Thu, 29 Aug 2024 16:35:35 -0700 Subject: [PATCH 3/9] Move tests/elf.py to _drgn_util/elf.py An upcoming helper will need some ELF constant definitions. Signed-off-by: Omar Sandoval --- {tests => _drgn_util}/elf.py | 2 +- scripts/{gen_tests_elf_py.py => gen_elf_py.py} | 4 ++-- tests/dwarfwriter.py | 2 +- tests/elfwriter.py | 2 +- tests/test_program.py | 2 +- tests/test_symbol.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) rename {tests => _drgn_util}/elf.py (99%) rename scripts/{gen_tests_elf_py.py => gen_elf_py.py} (92%) diff --git a/tests/elf.py b/_drgn_util/elf.py similarity index 99% rename from tests/elf.py rename to _drgn_util/elf.py index 24b82af78..244f6b9bc 100644 --- a/tests/elf.py +++ b/_drgn_util/elf.py @@ -1,6 +1,6 @@ # Copyright (c) Meta Platforms, Inc. and affiliates. # SPDX-License-Identifier: LGPL-2.1-or-later -# Generated by scripts/gen_tests_elf_py.py. +# Generated by scripts/gen_elf_py.py. import enum diff --git a/scripts/gen_tests_elf_py.py b/scripts/gen_elf_py.py similarity index 92% rename from scripts/gen_tests_elf_py.py rename to scripts/gen_elf_py.py index 68cd11192..6c46fbf37 100755 --- a/scripts/gen_tests_elf_py.py +++ b/scripts/gen_elf_py.py @@ -9,7 +9,7 @@ def main() -> None: - argparse.ArgumentParser(description="Generate tests/elf.py from elf.h").parse_args() + argparse.ArgumentParser(description="Generate elf.py from elf.h").parse_args() contents = subprocess.check_output( ["gcc", "-dD", "-E", "-"], @@ -50,7 +50,7 @@ def main() -> None: """\ # Copyright (c) Meta Platforms, Inc. and affiliates. # SPDX-License-Identifier: LGPL-2.1-or-later -# Generated by scripts/gen_tests_elf_py.py. +# Generated by scripts/gen_elf_py.py. import enum """ diff --git a/tests/dwarfwriter.py b/tests/dwarfwriter.py index 619a53a72..70ad23e69 100644 --- a/tests/dwarfwriter.py +++ b/tests/dwarfwriter.py @@ -6,9 +6,9 @@ from typing import Any, NamedTuple, Optional, Sequence, Union import zlib +from _drgn_util.elf import ET, SHT from tests.assembler import _append_sleb128, _append_uleb128 from tests.dwarf import DW_AT, DW_FORM, DW_LNCT, DW_TAG, DW_UT -from tests.elf import ET, SHT from tests.elfwriter import ElfSection, create_elf_file diff --git a/tests/elfwriter.py b/tests/elfwriter.py index c8f80dc4a..54b1a11e3 100644 --- a/tests/elfwriter.py +++ b/tests/elfwriter.py @@ -5,7 +5,7 @@ from typing import List, NamedTuple, Optional, Sequence import zlib -from tests.elf import ET, PT, SHF, SHN, SHT, STB, STT, STV +from _drgn_util.elf import ET, PT, SHF, SHN, SHT, STB, STT, STV class ElfSection: diff --git a/tests/test_program.py b/tests/test_program.py index 39d3dc1e3..b3b22483b 100644 --- a/tests/test_program.py +++ b/tests/test_program.py @@ -7,6 +7,7 @@ import tempfile import unittest.mock +from _drgn_util.elf import ET, PT from drgn import ( Architecture, FaultError, @@ -35,7 +36,6 @@ TestCase, mock_program, ) -from tests.elf import ET, PT from tests.elfwriter import ElfSection, create_elf_file diff --git a/tests/test_symbol.py b/tests/test_symbol.py index 7e3b81e5e..ee84c7e29 100644 --- a/tests/test_symbol.py +++ b/tests/test_symbol.py @@ -2,10 +2,10 @@ # SPDX-License-Identifier: LGPL-2.1-or-later import tempfile +from _drgn_util.elf import ET, PT, SHT, STB, STT from drgn import Program, Symbol, SymbolBinding, SymbolKind from tests import TestCase from tests.dwarfwriter import dwarf_sections -from tests.elf import ET, PT, SHT, STB, STT from tests.elfwriter import ElfSection, ElfSymbol, create_elf_file From c752b8aed8dd6e1b1651853978899588df5f8f41 Mon Sep 17 00:00:00 2001 From: Omar Sandoval Date: Thu, 29 Aug 2024 22:30:23 -0700 Subject: [PATCH 4/9] _drgn_util.platform: add finit_module to SYS This will be used by an upcoming helper. Signed-off-by: Omar Sandoval --- _drgn_util/platform.py | 151 +++++++++++++++++++++++++++++++++-------- 1 file changed, 122 insertions(+), 29 deletions(-) diff --git a/_drgn_util/platform.py b/_drgn_util/platform.py index e067f5acd..ca775042d 100644 --- a/_drgn_util/platform.py +++ b/_drgn_util/platform.py @@ -22,34 +22,127 @@ elif NORMALIZED_MACHINE_NAME == "sun4u": NORMALIZED_MACHINE_NAME = "sparc64" + SYS = { - "aarch64": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "alpha": {"bpf": 515, "perf_event_open": 493}, - "arc": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "arm": {"bpf": 386, "kexec_file_load": 401, "perf_event_open": 364}, - "csky": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "hexagon": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "i386": {"bpf": 357, "perf_event_open": 336}, - "loongarch": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "loongarch64": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "m68k": {"bpf": 354, "perf_event_open": 332}, - "microblaze": {"bpf": 387, "perf_event_open": 366}, - # TODO: mips is missing here because I don't know how to distinguish - # between the o32 and n32 ABIs. - "mips64": {"bpf": 315, "perf_event_open": 292}, - "nios2": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "openrisc": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "parisc": {"bpf": 341, "kexec_file_load": 355, "perf_event_open": 318}, - "parisc64": {"bpf": 341, "kexec_file_load": 355, "perf_event_open": 318}, - "ppc": {"bpf": 361, "perf_event_open": 319}, - "ppc64": {"bpf": 361, "perf_event_open": 319}, - "riscv32": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "riscv64": {"bpf": 280, "kexec_file_load": 294, "perf_event_open": 241}, - "s390": {"bpf": 351, "kexec_file_load": 381, "perf_event_open": 331}, - "s390x": {"bpf": 351, "kexec_file_load": 381, "perf_event_open": 331}, - "sh": {"bpf": 375, "perf_event_open": 336}, - "sparc": {"bpf": 349, "perf_event_open": 327}, - "sparc64": {"bpf": 349, "perf_event_open": 327}, - "x86_64": {"bpf": 321, "kexec_file_load": 320, "perf_event_open": 298}, - "xtensa": {"bpf": 340, "perf_event_open": 327}, + "alpha": { + "bpf": 515, + "finit_module": 507, + "perf_event_open": 493, + }, + "arc": { + "bpf": 280, + "finit_module": 273, + "kexec_file_load": 294, + "perf_event_open": 241, + }, + "arm": { + "bpf": 386, + "finit_module": 379, + "kexec_file_load": 401, + "perf_event_open": 364, + }, + "csky": { + "bpf": 280, + "finit_module": 273, + "kexec_file_load": 294, + "perf_event_open": 241, + }, + "i386": { + "bpf": 357, + "finit_module": 350, + "perf_event_open": 336, + }, + "m68k": { + "bpf": 354, + "finit_module": 348, + "perf_event_open": 332, + }, + "microblaze": { + "bpf": 387, + "finit_module": 380, + "perf_event_open": 366, + }, + "mips64": { + "bpf": 315, + "finit_module": 307, + "perf_event_open": 292, + }, + "nios2": { + "bpf": 280, + "finit_module": 273, + "kexec_file_load": 294, + "perf_event_open": 241, + }, + "parisc": { + "bpf": 341, + "finit_module": 333, + "kexec_file_load": 355, + "perf_event_open": 318, + }, + "parisc64": { + "bpf": 341, + "finit_module": 333, + "kexec_file_load": 355, + "perf_event_open": 318, + }, + "ppc": { + "bpf": 361, + "finit_module": 353, + "perf_event_open": 319, + }, + "ppc64": { + "bpf": 361, + "finit_module": 353, + "perf_event_open": 319, + }, + "riscv32": { + "bpf": 280, + "finit_module": 273, + "kexec_file_load": 294, + "perf_event_open": 241, + }, + "riscv64": { + "bpf": 280, + "finit_module": 273, + "kexec_file_load": 294, + "perf_event_open": 241, + }, + "s390": { + "bpf": 351, + "finit_module": 344, + "kexec_file_load": 381, + "perf_event_open": 331, + }, + "s390x": { + "bpf": 351, + "finit_module": 344, + "kexec_file_load": 381, + "perf_event_open": 331, + }, + "sh": { + "bpf": 375, + "finit_module": 368, + "perf_event_open": 336, + }, + "sparc": { + "bpf": 349, + "finit_module": 342, + "perf_event_open": 327, + }, + "sparc64": { + "bpf": 349, + "finit_module": 342, + "perf_event_open": 327, + }, + "x86_64": { + "bpf": 321, + "finit_module": 313, + "kexec_file_load": 320, + "perf_event_open": 298, + }, + "xtensa": { + "bpf": 340, + "finit_module": 332, + "perf_event_open": 327, + }, }.get(NORMALIZED_MACHINE_NAME, {}) From 1652183026a89c6e848375581e26a8e463f59158 Mon Sep 17 00:00:00 2001 From: Omar Sandoval Date: Tue, 17 Sep 2024 16:58:09 -0700 Subject: [PATCH 5/9] Add experimental helpers for calling kernel functions and writing to memory These are highly-requested features that I didn't implement for awhile because I thought they would be too difficult or hacky. The approach that I finally came up with is slightly deranged but solid: we manually generate a kernel module ELF file and machine code to do a function call. An alternative approach that I tried was generating C source code for a kernel module and building it, but that relies on having kernel-devel and a compatible toolchain installed, and it's slow. (Kudos to Matthew Wilcox for suggesting the kernel module approach last year: https://lwn.net/Articles/953256/.) For now, this is under a new drgn.helpers.experimental package, in the drgn.helpers.experimental.kmodify module, with no stability guarantees, but we still have tests for it. Signed-off-by: Omar Sandoval --- drgn/helpers/experimental/__init__.py | 11 + drgn/helpers/experimental/kmodify.py | 1376 ++++++++++++++++++++ tests/linux_kernel/helpers/test_kmodify.py | 325 +++++ tests/linux_kernel/kmod/drgn_test.c | 155 +++ 4 files changed, 1867 insertions(+) create mode 100644 drgn/helpers/experimental/__init__.py create mode 100644 drgn/helpers/experimental/kmodify.py create mode 100644 tests/linux_kernel/helpers/test_kmodify.py diff --git a/drgn/helpers/experimental/__init__.py b/drgn/helpers/experimental/__init__.py new file mode 100644 index 000000000..a547de2f2 --- /dev/null +++ b/drgn/helpers/experimental/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# SPDX-License-Identifier: LGPL-2.1-or-later + +""" +Experimental +------------ + +The ``drgn.helpers.experimental`` package contains experimental helpers with no +stability guarantees. They may change, move to another package, or be removed. +They are not automatically imported by the CLI. +""" diff --git a/drgn/helpers/experimental/kmodify.py b/drgn/helpers/experimental/kmodify.py new file mode 100644 index 000000000..325195b64 --- /dev/null +++ b/drgn/helpers/experimental/kmodify.py @@ -0,0 +1,1376 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# SPDX-License-Identifier: LGPL-2.1-or-later + +""" +Kmodify +------- + +The ``drgn.helpers.experimental.kmodify`` module provides experimental helpers +for modifying the state of the running kernel. This works by loading a +temporary kernel module, so the kernel must support loadable kernel modules +(``CONFIG_MODULES=y``) and allow loading unsigned modules +(``CONFIG_MODULE_SIG_FORCE=n``). It is currently only implemented for x86-64. + +.. warning:: + These helpers are powerful but **extremely** dangerous. Use them with care. +""" + +import ctypes +import errno +import operator +import os +import random +import re +import string +import struct +import sys +from typing import ( + TYPE_CHECKING, + Any, + List, + Mapping, + NamedTuple, + Optional, + Sequence, + Tuple, + Union, +) + +if TYPE_CHECKING: + from _typeshed import SupportsWrite + + if sys.version_info < (3, 11): + from typing_extensions import assert_never + else: + from typing import assert_never # novermin + +from _drgn_util.elf import ET, SHF, SHN, SHT, STB, STT, STV +from _drgn_util.platform import SYS +from drgn import ( + Architecture, + FaultError, + IntegerLike, + Object, + ObjectAbsentError, + PlatformFlags, + PrimitiveType, + Program, + ProgramFlags, + Type, + TypeKind, + alignof, + cast, + implicit_convert, + offsetof, + sizeof, +) +from drgn.helpers.common.prog import takes_program_or_default + +__all__ = ( + "call_function", + "pass_pointer", + "write_memory", + "write_object", +) + + +_c = ctypes.CDLL(None, use_errno=True) +_syscall = _c.syscall +_syscall.restype = ctypes.c_long + + +# os.memfd_create() was added in Python 3.8. +if hasattr(os, "memfd_create"): + _memfd_create = os.memfd_create # novermin +else: + __memfd_create = _c.memfd_create + __memfd_create.restype = ctypes.c_int + __memfd_create.argtypes = [ctypes.c_char_p, ctypes.c_uint] + + def _memfd_create( + name: str, + flags: int = 1, # MFD_CLOEXEC + ) -> int: + fd = __memfd_create(os.fsencode(name), flags) + if fd < 0: + errnum = ctypes.get_errno() + raise OSError(errnum, os.strerror(errnum)) + return fd + + +class _ElfSection: + def __init__( + self, + *, + name: str, + type: SHT, + flags: SHF = SHF(0), + data: bytes, + addr: int = 0, + link: int = 0, + info: int = 0, + addralign: int = 1, + entsize: int = 0, + ) -> None: + self.name = name + self.type = type + self.flags = flags + self.data = data + self.addr = addr + self.link = link + self.info = info + self.addralign = addralign + self.entsize = entsize + + +class _ElfSymbol(NamedTuple): + name: str + value: int + size: int + section: Union[str, SHN] + type: STT + binding: STB + visibility: STV = STV.DEFAULT + + +class _ElfRelocation(NamedTuple): + offset: int + type: int + symbol_name: str + section_symbol: bool + addend: int = 0 + + +def _write_elf( + file: "SupportsWrite[bytes]", + *, + machine: int, + is_little_endian: bool, + is_64_bit: bool, + rela: bool, + sections: Sequence[_ElfSection], + symbols: Sequence[_ElfSymbol], + relocations: Mapping[str, Sequence[_ElfRelocation]], +) -> None: + endian = "<" if is_little_endian else ">" + if is_64_bit: + ehdr_struct = struct.Struct(endian + "16BHHIQQQIHHHHHH") + shdr_struct = struct.Struct(endian + "IIQQQQIIQQ") + rela_struct = struct.Struct(endian + "QQq") + + def r_info(sym: int, type: int) -> int: + return (sym << 32) | type + + sym_struct = struct.Struct(endian + "IBBHQQ") + + def sym_fields(sym: _ElfSymbol) -> Tuple[int, int, int, int, int]: + return ( + (sym.binding << 4) + (sym.type & 0xF), + sym.visibility, + section_name_to_index[sym.section] + if isinstance(sym.section, str) + else sym.section, + sym.value, + sym.size, + ) + + else: + ehdr_struct = struct.Struct(endian + "16BHHIIIIIHHHHHH") + shdr_struct = struct.Struct(endian + "10I") + rela_struct = struct.Struct(endian + "IIi") + + def r_info(sym: int, type: int) -> int: + return (sym << 8) | type + + sym_struct = struct.Struct(endian + "IIIBBH") + + def sym_fields(sym: _ElfSymbol) -> Tuple[int, int, int, int, int]: + return ( + sym.value, + sym.size, + (sym.binding << 4) + (sym.type & 0xF), + sym.visibility, + section_name_to_index[sym.section] + if isinstance(sym.section, str) + else sym.section, + ) + + section_symbols = [ + _ElfSymbol( + name="", + value=0, + size=0, + type=STT.SECTION, + binding=STB.LOCAL, + section=section.name, + ) + for section in sections + if section.type == SHT.PROGBITS + ] + section_name_to_symbol_index = { + sym.section: i for i, sym in enumerate(section_symbols, 1) + } + symbol_name_to_index = { + sym.name: i for i, sym in enumerate(symbols, 1 + len(section_symbols)) + } + section_symbols.extend(symbols) + symbols = section_symbols + del section_symbols + + def relocation_symbol_index(reloc: _ElfRelocation) -> int: + if reloc.section_symbol: + return section_name_to_symbol_index[reloc.symbol_name] + else: + return symbol_name_to_index[reloc.symbol_name] + + if rela: + reloc_prefix = ".rela" + reloc_sht = SHT.RELA + reloc_size = rela_struct.size + + def relocation_data(relocations: Sequence[_ElfRelocation]) -> bytes: + data = bytearray(len(relocations) * rela_struct.size) + for i, relocation in enumerate(relocations): + rela_struct.pack_into( + data, + i * rela_struct.size, + relocation.offset, + r_info( + relocation_symbol_index(relocation), + relocation.type, + ), + relocation.addend, + ) + return data + + else: + raise NotImplementedError("SHT_REL relocations") + + symtab_section_index = 1 + len(sections) + len(relocations) + + sections = list(sections) + i = 0 + while i < len(sections): + section = sections[i] + try: + section_relocations = relocations[section.name] + except KeyError: + i += 1 + continue + sections.insert( + i + 1, + _ElfSection( + name=reloc_prefix + section.name, + type=reloc_sht, + flags=SHF.INFO_LINK, + data=relocation_data(section_relocations), + link=symtab_section_index, + info=i + 1, + addralign=8 if is_64_bit else 4, + entsize=reloc_size, + ), + ) + i += 2 + + section_name_to_index = {section.name: i for i, section in enumerate(sections, 1)} + + if len(sections) < symtab_section_index - 1: + raise ValueError( + f"relocations for unknown section {', '.join(relocations.keys() - section_name_to_index)}" + ) + + symtab_data = bytearray((len(symbols) + 1) * sym_struct.size) + strtab_data = bytearray(1) + + sym_local_end = 1 + for i, sym in enumerate(symbols, 1): + if sym.name: + st_name = len(strtab_data) + strtab_data.extend(sym.name.encode()) + strtab_data.append(0) + else: + st_name = 0 + + sym_struct.pack_into( + symtab_data, i * sym_struct.size, st_name, *sym_fields(sym) + ) + if sym.binding == STB.LOCAL: + if sym_local_end != i: + raise ValueError("local symbol after non-local symbol") + sym_local_end = i + 1 + + sections.append( + _ElfSection( + name=".symtab", + type=SHT.SYMTAB, + data=symtab_data, + link=len(sections) + 2, + info=sym_local_end, + entsize=sym_struct.size, + ) + ) + sections.append(_ElfSection(name=".strtab", type=SHT.STRTAB, data=strtab_data)) + + shstrtab_data = bytearray(1) + sh_name = [] + for section in sections: + sh_name.append(len(shstrtab_data)) + shstrtab_data.extend(section.name.encode()) + shstrtab_data.append(0) + sh_name.append(len(shstrtab_data)) + shstrtab_data.extend(b".shstrtab\0") + sections.append(_ElfSection(name=".shstrtab", type=SHT.STRTAB, data=shstrtab_data)) + + shnum = len(sections) + 1 # + 1 for the SHT_NULL section + headers_size = ehdr_struct.size + shdr_struct.size * shnum + file.write( + ehdr_struct.pack( + 0x7F, # ELFMAG0 + ord("E"), # ELFMAG1 + ord("L"), # ELFMAG2 + ord("F"), # ELFMAG3 + 2 if is_64_bit else 1, # EI_CLASS = ELFCLASS64 or ELFCLASS32 + 1 if is_little_endian else 2, # EI_DATA = ELFDATA2LSB or ELFDATA2MSB + 1, # EI_VERSION = EV_CURRENT + 0, # EI_OSABI = ELFOSABI_NONE + 0, # EI_ABIVERSION + 0, + 0, + 0, + 0, + 0, + 0, + 0, # EI_PAD + ET.REL, # e_type + machine, + 1, # e_version = EV_CURRENT + 0, # e_entry + 0, # e_phoff + ehdr_struct.size, # e_shoff + 0, # e_flags + ehdr_struct.size, # e_ehsize + 0, # e_phentsize + 0, # e_phnum + shdr_struct.size, # e_shentsize + shnum, # e_shnum + shnum - 1, # e_shstrndx + ) + ) + + # SHT_NULL section. + file.write(bytes(shdr_struct.size)) + + section_data_offset = headers_size + for i, section in enumerate(sections): + section_data_offset += -section_data_offset % section.addralign + file.write( + shdr_struct.pack( + sh_name[i], # sh_name + section.type, # sh_type + section.flags, # sh_flags + section.addr, # sh_addr + section_data_offset, # sh_offset + len(section.data), # sh_size + section.link, # sh_link + section.info, # sh_info + section.addralign, # sh_addralign + section.entsize, # sh_entsize + ) + ) + section_data_offset += len(section.data) + + section_data_offset = headers_size + for section in sections: + padding = -section_data_offset % section.addralign + if padding: + file.write(bytes(padding)) + section_data_offset += padding + file.write(section.data) + section_data_offset += len(section.data) + + +# Abstract syntax tree-ish representation of code to inject. +class _Integer: + def __init__(self, size: int, value: IntegerLike) -> None: + self.size = size + self.value = operator.index(value) & ((1 << (size * 8)) - 1) + + +class _Symbol(NamedTuple): + name: str + offset: int = 0 + section: bool = False + + +class _Call(NamedTuple): + func: _Symbol + args: Sequence[Union[_Integer, _Symbol]] + + +class _StoreReturnValue(NamedTuple): + size: int + dst: _Symbol + + +class _Return(NamedTuple): + value: _Integer + + +class _ReturnIfLastReturnValueNonZero(NamedTuple): + value: _Integer + + +_FunctionBodyNode = Union[ + _Call, _StoreReturnValue, _Return, _ReturnIfLastReturnValueNonZero +] + + +class _Function(NamedTuple): + body: Sequence[_FunctionBodyNode] + + +class _CodeGen_x86_64: + _R_X86_64_PC32 = 2 + _R_X86_64_PLT32 = 4 + _R_X86_64_32S = 11 + + _rax = 0 + _r11 = 11 + + _argument_registers = ( + 7, # rdi + 6, # rsi, + 2, # rdx + 1, # rcx + 8, # r8 + 9, # r9 + ) + + def __init__(self) -> None: + self.code = bytearray() + self.relocations: List[_ElfRelocation] = [] + self._epilogue_jumps: List[int] = [] + + def enter_frame(self, size: int) -> None: + if size < 0: + raise ValueError("invalid stack frame size") + + self.code.extend( + # endbr64 + # This is only needed if CONFIG_X86_KERNEL_IBT=y, but it's much + # simpler to do it unconditionally, and it's a no-op if not needed. + b"\xF3\x0F\x1E\xFA" + # Set up the frame pointer. + # push %rbp + b"\x55" + # mov %rsp, %rbp + b"\x48\x89\xE5" + ) + + # The System V ABI requires that rsp % 16 == 0 on function entry. We + # need to make sure that rsp % 16 == 8 in the function body so that the + # return address pushed by the call will make rsp % 16 == 0. push %rbp + # makes rsp % 16 == 8. So, we need to align the requested size up to 16 + # bytes. + size = (size + 15) & ~15 + if size > 0: + # sub $size, %rsp + if size < 128: + self.code.extend(b"\x48\x83\xEC") + self.code.append(size) + else: + self.code.extend(b"\x48\x81\xEC") + self.code.extend(size.to_bytes(4, "little", signed=True)) + + def leave_frame(self) -> None: + # Fix up all of the jumps to the epilogue. + for offset in self._epilogue_jumps: + self.code[offset - 4 : offset] = (len(self.code) - offset).to_bytes( + 4, "little", signed=True + ) + + self.code.extend( + # leave + b"\xC9" + # ret + b"\xC3" + ) + + def _mov_imm(self, value: int, reg: int) -> None: + assert value >= 0 and value <= 0xFFFFFFFFFFFFFFFF + assert reg < 16 + if value <= 0xFFFFFFFF: + if reg >= 8: + self.code.append(0x41) # REX.B + reg -= 8 + self.code.append(0xB8 + reg) + self.code.extend(value.to_bytes(4, "little")) + else: + rex = 0x48 # REX.W + if reg >= 8: + rex |= 1 # REX.B + reg -= 8 + self.code.append(rex) + if value >= 0xFFFFFFFF80000000: + self.code.append(0xC7) + self.code.append(0xC0 + reg) + self.code.extend((value & 0xFFFFFFFF).to_bytes(4, "little")) + else: + self.code.append(0xB8 + reg) + self.code.extend(value.to_bytes(8, "little")) + + def _mov_symbol(self, sym: _Symbol, reg: int) -> None: + rex = 0x48 # REX.W + if reg >= 8: + rex |= 1 # REX.B + reg -= 8 + self.code.append(rex) + self.code.append(0xC7) + self.code.append(0xC0 + reg) + self.relocations.append( + _ElfRelocation( + offset=len(self.code), + type=self._R_X86_64_32S, + symbol_name=sym.name, + section_symbol=sym.section, + addend=sym.offset, + ) + ) + self.code.extend(bytes(4)) + + def _store_rax_on_stack(self, offset: int) -> None: + # mov %rax, offset(%rsp) + if offset == 0: + self.code.extend(b"\x48\x89\x04\x24") + elif -128 <= offset < 128: + self.code.extend(b"\x48\x89\x44\x24") + self.code.append(offset & 0xFF) + else: + self.code.extend(b"\x48\x89\x84\x24") + self.code.extend(offset.to_bytes(4, "little", signed=True)) + + def _store_imm_on_stack(self, value: int, offset: int) -> None: + if (0 <= value <= 0x7FFFFFFF) or ( + 0xFFFFFFFF80000000 <= value <= 0xFFFFFFFFFFFFFFFF + ): + # mov $value, offset(%rsp) + if offset == 0: + self.code.extend(b"\x48\xC7\x04\x24") + elif -128 <= offset < 128: + self.code.extend(b"\x48\xC7\x44\x24") + self.code.append(offset & 0xFF) + else: + self.code.extend(b"\x48\xC7\x84\x24") + self.code.extend(offset.to_bytes(4, "little", signed=True)) + self.code.extend((value & 0xFFFFFFFF).to_bytes(4, "little")) + else: + self._mov_imm(value, self._rax) + self._store_rax_on_stack(offset) + + def _store_symbol_on_stack(self, sym: _Symbol, offset: int) -> None: + self._mov_symbol(sym, self._rax) + self._store_rax_on_stack(offset) + + def call(self, func: _Symbol, args: Sequence[Union[_Integer, _Symbol]]) -> None: + for i, arg in enumerate(args): + if i < len(self._argument_registers): + reg = self._argument_registers[i] + if isinstance(arg, _Integer): + self._mov_imm(arg.value, reg) + else: + self._mov_symbol(arg, reg) + else: + stack_offset = 8 * (i - len(self._argument_registers)) + if isinstance(arg, _Integer): + self._store_imm_on_stack(arg.value, stack_offset) + else: + self._store_symbol_on_stack(arg, stack_offset) + + # call near + self.code.append(0xE8) + self.relocations.append( + _ElfRelocation( + offset=len(self.code), + type=self._R_X86_64_PLT32, + symbol_name=func.name, + section_symbol=func.section, + addend=-4, + ) + ) + self.code.extend(bytes(4)) + + def store_return_value(self, size: int, dst: _Symbol) -> None: + if size == 1: + # movb %al, ... + self.code.extend(b"\x88\x05") + elif size == 2: + # movw %ax, ... + self.code.extend(b"\x66\x89\x05") + elif size == 4: + # movl %eax, ... + self.code.extend(b"\x89\x05") + elif size == 8: + # movq %rax, ... + self.code.extend(b"\x48\x89\x05") + else: + raise NotImplementedError("{size}-byte return values not implemented") + self.relocations.append( + _ElfRelocation( + offset=len(self.code), + type=self._R_X86_64_PC32, + symbol_name=dst.name, + section_symbol=dst.section, + addend=dst.offset - 4, + ) + ) + # ... 0x0(%rip) + self.code.extend(bytes(4)) + + def return_(self, value: _Integer, last: bool) -> None: + if value.size > 8: + raise NotImplementedError( + "return values larger than 8 bytes not implemented" + ) + self._mov_imm(value.value, self._rax) + # Jump to the function epilogue. If this return is the last operation, + # we can fall through instead of jumping. + if not last: + # jmp + self.code.extend(b"\xE9\x00\x00\x00\x00") + # The destination needs to be fixed up later. + self._epilogue_jumps.append(len(self.code)) + + def return_if_last_return_value_nonzero(self, value: _Integer) -> None: + if value.size > 8: + raise NotImplementedError( + "return values larger than 8 bytes not implemented" + ) + # mov %rax, %rdx + self.code.extend(b"\x48\x89\xC2") + self._mov_imm(value.value, self._rax) + # Jump to the function epilogue if the last return value was non-zero. + self.code.extend( + # test %rdx, %rdx + b"\x48\x85\xD2" + # jnz + b"\x0F\x85\x00\x00\x00\x00" + ) + # The destination needs to be fixed up later. + self._epilogue_jumps.append(len(self.code)) + + +class _Arch_X86_64: + ELF_MACHINE = 62 # EM_X86_64 + RELA = True + ABSOLUTE_ADDRESS_RELOCATION_TYPE = 1 # R_X86_64_64 + + @staticmethod + def code_gen(func: _Function) -> Tuple[bytes, Sequence[_ElfRelocation]]: + needed_stack_size = 0 + for node in func.body: + if not isinstance(node, _Call): + continue + stack_size = len(_CodeGen_x86_64._argument_registers) * -8 + for arg in node.args: + if isinstance(arg, _Integer): + if arg.size > 8: + raise NotImplementedError( + "passing integers larger than 8 bytes not implemented" + ) + stack_size += 8 + elif isinstance(arg, _Symbol): + stack_size += 8 + else: + assert_never(arg) + if stack_size > needed_stack_size: + needed_stack_size = stack_size + + code_gen = _CodeGen_x86_64() + + code_gen.enter_frame(needed_stack_size) + + for i, node in enumerate(func.body): + if isinstance(node, _Call): + code_gen.call(node.func, node.args) + elif isinstance(node, _StoreReturnValue): + code_gen.store_return_value(node.size, node.dst) + elif isinstance(node, _Return): + code_gen.return_(node.value, last=i == len(func.body) - 1) + elif isinstance(node, _ReturnIfLastReturnValueNonZero): + code_gen.return_if_last_return_value_nonzero(node.value) + else: + assert_never(node) + + code_gen.leave_frame() + + return code_gen.code, code_gen.relocations + + +class _Kmodify: + def __init__(self, prog: Program) -> None: + if prog.flags & ( + ProgramFlags.IS_LINUX_KERNEL | ProgramFlags.IS_LIVE | ProgramFlags.IS_LOCAL + ) != ( + ProgramFlags.IS_LINUX_KERNEL | ProgramFlags.IS_LIVE | ProgramFlags.IS_LOCAL + ): + raise ValueError("kmodify is only available for the running kernel") + platform = prog.platform + if platform is None: + raise ValueError("program platform is not known") + self.prog = prog + self.is_little_endian = bool(platform.flags & PlatformFlags.IS_LITTLE_ENDIAN) + self.is_64_bit = bool(platform.flags & PlatformFlags.IS_64_BIT) + + if platform.arch == Architecture.X86_64: + # When we add support for another architecture, we're going to need + # an _Arch Protocol. + self.arch = _Arch_X86_64 + else: + raise NotImplementedError( + f"kmodify not implemented for {platform.arch.name} architecture" + ) + + _KMOD_NAME_CHARS = string.digits + string.ascii_letters + + def insert( + self, + *, + name: str, + code: bytes, + code_relocations: Sequence[_ElfRelocation], + data: bytes, + data_alignment: int, + symbols: Sequence[_ElfSymbol], + ) -> int: + struct_module = self.prog.type("struct module") + + module_name = "".join( + [ + "drgn_kmodify_", + # Randomize to avoid name collisions. + *random.choices(self._KMOD_NAME_CHARS, k=12), + "_", + name, + ] + ).encode("ascii")[: sizeof(struct_module.member("name").type) - 1] + + sections = [ + _ElfSection( + name=".init.text", + type=SHT.PROGBITS, + flags=SHF.ALLOC | SHF.EXECINSTR, + data=code, + # This should be good enough for any supported architecture. + addralign=16, + ), + _ElfSection( + name=".data", + type=SHT.PROGBITS, + flags=SHF.WRITE | SHF.ALLOC, + data=data, + addralign=data_alignment, + ), + _ElfSection( + name=".gnu.linkonce.this_module", + type=SHT.PROGBITS, + flags=SHF.WRITE | SHF.ALLOC, + data=Object( + self.prog, struct_module, {"name": module_name} + ).to_bytes_(), + addralign=alignof(struct_module), + ), + _ElfSection( + name=".modinfo", + type=SHT.PROGBITS, + flags=SHF.ALLOC, + data=b"".join( + [ + b"%b=%b\0" % (key, value) + for key, value in ( + (b"license", b"GPL"), + (b"depends", b""), + # A retpoline kernel complains when loading a + # non-retpoline module. We never make indirect + # calls, so we can claim to be a retpoline module. + # (Note that it's harmless to set this for + # non-retpoline kernels.) + (b"retpoline", b"Y"), + (b"name", module_name), + (b"vermagic", self.prog["vermagic"].string_()), + ) + ] + ), + ), + ] + + symbols = [ + *symbols, + _ElfSymbol( + name="init_module", + value=0, + size=len(code), + type=STT.FUNC, + binding=STB.GLOBAL, + section=".init.text", + ), + ] + + relocations = { + ".init.text": code_relocations, + ".gnu.linkonce.this_module": [ + _ElfRelocation( + offset=offsetof(struct_module, "init"), + type=self.arch.ABSOLUTE_ADDRESS_RELOCATION_TYPE, + symbol_name="init_module", + section_symbol=False, + ) + ], + } + + with open(_memfd_create(module_name.decode() + ".ko"), "wb") as f: + _write_elf( + f, + machine=self.arch.ELF_MACHINE, + is_little_endian=self.is_little_endian, + is_64_bit=self.is_64_bit, + rela=self.arch.RELA, + sections=sections, + symbols=symbols, + relocations=relocations, + ) + f.flush() + + if _syscall( + ctypes.c_long(SYS["finit_module"]), + ctypes.c_int(f.fileno()), + ctypes.c_char_p(b""), + ctypes.c_int(0), + ): + return -ctypes.get_errno() + else: + return 0 + + +@takes_program_or_default +def write_memory(prog: Program, address: IntegerLike, value: bytes) -> None: + """ + Write a byte string to kernel memory. + + >>> os.uname().sysname + 'Linux' + >>> write_memory(prog["init_uts_ns"].name.sysname.address_, b"Lol\\0") + >>> os.uname().sysname + 'Lol' + + .. warning:: + This attempts to detect writes to bad addresses and raise a + :class:`~drgn.FaultError`, but this is best-effort and may still crash + the kernel. Writing bad data can of course also cause a crash when the + data is used. Additionally, this is not atomic, so the data may be + accessed while it is partially written. + + :param address: Address to write to. + :param value: Byte string to write. + :raises FaultError: if the address cannot be written to + """ + copy_to_kernel_nofault_address = None + for copy_to_kernel_nofault, copy_from_kernel_nofault in ( + # Names used since Linux kernel commit fe557319aa06 ("maccess: rename + # probe_kernel_{read,write} to copy_{from,to}_kernel_nofault") (in + # v5.8-rc2). + ("copy_to_kernel_nofault", "copy_from_kernel_nofault"), + # Names used before Linux kernel commit 48c49c0e5f31 ("maccess: remove + # various unused weak aliases") (in v5.8-rc1). + ("__probe_kernel_write", "probe_kernel_read"), + # Names briefly used between those two commits. + ("probe_kernel_write", "probe_kernel_read"), + ): + try: + copy_to_kernel_nofault_address = prog[copy_to_kernel_nofault].address_ + break + except KeyError: + pass + if copy_to_kernel_nofault_address is None: + raise LookupError("copy_to_kernel_nofault not found") + + kmodify = _Kmodify(prog) + address = operator.index(address) + sizeof_int = sizeof(prog.type("int")) + sizeof_void_p = sizeof(prog.type("void *")) + sizeof_size_t = sizeof(prog.type("size_t")) + code, code_relocations = kmodify.arch.code_gen( + _Function( + [ + # copy_to_kernel_nofault() can still fault in some cases; see + # https://lore.kernel.org/all/f0e171cbae576758d9387cee374dd65088e75b07.1725223574.git.osandov@fb.com/ + # copy_from_kernel_nofault() catches some of those cases. + _Call( + _Symbol(copy_from_kernel_nofault), + [ + _Symbol(".data", section=True, offset=len(value)), + _Integer(sizeof_void_p, address), + _Integer(sizeof_size_t, 1), + ], + ), + _ReturnIfLastReturnValueNonZero( + _Integer(sizeof_int, -errno.EFAULT), + ), + _Call( + _Symbol(copy_to_kernel_nofault), + [ + _Integer(sizeof_void_p, address), + _Symbol(".data", section=True), + _Integer(sizeof_size_t, len(value)), + ], + ), + _ReturnIfLastReturnValueNonZero( + _Integer(sizeof_int, -errno.EFAULT), + ), + _Return(_Integer(sizeof_int, -errno.EINPROGRESS)), + ] + ) + ) + ret = kmodify.insert( + name=f"write_{len(value)}", + code=code, + code_relocations=code_relocations, + data=value + b"\0", + # Align generously so that the copy can use larger units and small + # copies can be slightly less racy. + data_alignment=16, + symbols=[ + # copy_to_kernel_nofault() is not exported. + _ElfSymbol( + name=copy_to_kernel_nofault, + value=copy_to_kernel_nofault_address, + size=0, + type=STT.FUNC, + binding=STB.LOCAL, + section=SHN.ABS, + ), + _ElfSymbol( + name=copy_from_kernel_nofault, + value=0, + size=0, + type=STT.NOTYPE, + binding=STB.GLOBAL, + section=SHN.UNDEF, + ), + ], + ) + if ret != -errno.EINPROGRESS: + if ret == -errno.EFAULT: + raise FaultError("could not write to memory", address) + elif ret: + raise OSError(-ret, os.strerror(-ret)) + else: + raise ValueError("module init did not run") + + +def _underlying_type(type: Type) -> Type: + while type.kind == TypeKind.TYPEDEF: + type = type.type + return type + + +def write_object( + object: Object, value: Any, *, dereference: Optional[bool] = None +) -> None: + """ + Write to an object in kernel memory. + + >>> os.system("uptime -p") + up 12 minutes + >>> write_object(prog["init_time_ns"].offsets.boottime.tv_sec, 1000000000) + >>> os.system("uptime -p") + up 3 decades, 1 year, 37 weeks, 1 hour, 59 minutes + + .. warning:: + The warnings about :func:`write_memory()` also apply to + ``write_object()``. + + :param object: Object to write to. + :param value: Value to write. This may be an :class:`~drgn.Object` or a + Python value. Either way, it will be converted to the type of *object*. + :param dereference: If *object* is a pointer, whether to dereference it. If + ``True``, then write to the object pointed to by *object* + (``*ptr = value``). If ``False``, then write to the pointer itself + (``ptr = value``). This is a common source of confusion, so it is + required if *object* is a pointer. + :raises ValueError: is *object* is not a reference object (i.e., its + address is not known) + :raises TypeError: if *object* is a pointer and *dereference* is not given + :raises TypeError: if *object* is not a pointer and *dereference* is + ``True`` + """ + type = object.type_ + if _underlying_type(type).kind == TypeKind.POINTER: + if dereference is None: + raise TypeError( + "to write to pointed-to object (*ptr = value), use dereference=True; " + "to write to pointer itself (ptr = value), use dereference=False" + ) + elif dereference: + object = object[0] + elif dereference: + raise TypeError("object is not a pointer") + + address = object.address_ + if address is None: + raise ValueError("cannot write to value object") + if isinstance(value, Object): + value = implicit_convert(type, value) + else: + value = Object(object.prog_, type, value) + write_memory(object.prog_, address, value.to_bytes_()) + + +def _default_argument_promotions(obj: Object) -> Object: + type = _underlying_type(obj.type_) + if type.kind == TypeKind.INT: + return +obj + elif type.primitive == PrimitiveType.C_FLOAT: + return cast("double", obj) + else: + return obj + + +@takes_program_or_default +def call_function(prog: Program, func: Union[str, Object], *args: Any) -> Object: + """ + Call a function in the kernel. + + Arguments can be either :class:`~drgn.Object`\\ s or Python values. The + function return value is returned as an :class:`~drgn.Object`: + + >>> # GFP_KERNEL isn't in the kernel debug info + >>> # We have to use this trick to get it. + >>> for flag in prog["gfpflag_names"]: + ... if flag.name.string_() == b"GFP_KERNEL": + ... GFP_KERNEL = flag.mask + ... break + ... + >>> # kmalloc() is actually a macro. + >>> # We have to call the underlying function. + >>> p = call_function("__kmalloc_noprof", 13, GFP_KERNEL) + >>> p + (void *)0xffff991701ef43c0 + >>> identify_address(p) + 'slab object: kmalloc-16+0x0' + >>> call_function("kfree", p) + (void) + >>> identify_address(p) + 'free slab object: kmalloc-16+0x0' + + Variadic functions are also supported: + + >>> call_function("_printk", "Hello, world! %d\\n", Object(prog, "int", 1234)) + (int)18 + >>> os.system("dmesg | tail -1") + [ 1138.223004] Hello, world! 1234 + + Constructed values can be passed by pointer using :class:`pass_pointer()`: + + >>> sb = prog["init_fs"].root.mnt.mnt_sb + >>> sb.s_shrink.scan_objects + (unsigned long (*)(struct shrinker *, struct shrink_control *))super_cache_scan+0x0 = 0xffffffffbda4c487 + >>> sc = pass_pointer(Object(prog, "struct shrink_control", + ... {"gfp_mask": GFP_KERNEL, "nr_to_scan": 100, "nr_scanned": 100})) + >>> call_function(sb.s_shrink.scan_objects, sb.s_shrink, sc) + (unsigned long)31 + + If the function modifies the passed value, the :class:`pass_pointer` object + is updated: + + >>> sc.object + (struct shrink_control){ + .gfp_mask = (gfp_t)3264, + .nid = (int)0, + .nr_to_scan = (unsigned long)1, + .nr_scanned = (unsigned long)100, + .memcg = (struct mem_cgroup *)0x0, + } + + .. note:: + It is not possible to call some functions, including inlined functions + and function-like macros. If the unavailable function is a wrapper + around another function, sometimes the wrapped function can be called + instead. + + .. warning:: + Calling a function incorrectly may cause the kernel to crash or + misbehave in various ways. + + The function is called from process context. Note that the function may + have context, locking, or reference counting requirements. + + :param func: Function to call. May be a function name, function object, or + function pointer object. + :param args: Function arguments. :class:`int`, :class:`float`, and + :class:`bool` arguments are converted as "literals" with + ``Object(prog, value=...)``. :class:`str` and :class:`bytes` arguments + are automatically converted to a ``char`` array object. + :class:`pass_pointer` arguments are copied to the kernel, passed by + pointer, and copied back. + :return: Function return value. + :raises TypeError: if the passed arguments have incorrect types for the + function + :raises ObjectAbsentError: if function cannot be called because it is + inlined + :raises LookupError: if a function with the given name is not found + (possibly because it is actually a function-like macro) + """ + if not isinstance(func, Object): + func = prog.function(func) + + kmodify = _Kmodify(prog) + + func_type = _underlying_type(func.type_) + try: + if func_type.kind == TypeKind.FUNCTION: + func_pointer = func.address_of_() + elif func_type.kind == TypeKind.POINTER: + func_type = _underlying_type(func_type.type) + if func_type.kind != TypeKind.FUNCTION: + raise TypeError("func must be function or function pointer") + func_pointer = func.read_() + else: + raise TypeError("func must be function or function pointer") + except ObjectAbsentError: + raise ObjectAbsentError("function is absent, likely inlined") from None + + return_type = _underlying_type(func_type.type) + if return_type.kind not in { + TypeKind.VOID, + TypeKind.INT, + TypeKind.BOOL, + TypeKind.ENUM, + TypeKind.POINTER, + }: + raise NotImplementedError(f"{return_type} return values not implemented") + + if len(args) < len(func_type.parameters): + raise TypeError(f"not enough arguments for {func_pointer}; got {len(args)}") + if not func_type.is_variadic and len(args) > len(func_type.parameters): + raise TypeError(f"too many arguments for {func_pointer}; got {len(args)}") + + call_args: List[Union[_Integer, _Symbol]] = [] + out_pointers = [] + data = bytearray() + data_alignment = 1 + + def align_data(alignment: int) -> None: + nonlocal data_alignment + if alignment > data_alignment: + data_alignment = alignment + data.extend(bytes(-len(data) % alignment)) + + for i, arg in enumerate(args): + if i < len(func_type.parameters): + parameter_type = _underlying_type(func_type.parameters[i].type) + + if ( + ( + isinstance(arg, (str, bytes, bytearray)) + or ( + isinstance(arg, pass_pointer) + and isinstance(arg.object, (str, bytes, bytearray)) + ) + ) + and i < len(func_type.parameters) + and parameter_type.kind == TypeKind.POINTER + and _underlying_type(parameter_type.type).primitive + in ( + PrimitiveType.C_CHAR, + PrimitiveType.C_SIGNED_CHAR, + PrimitiveType.C_UNSIGNED_CHAR, + ) + ): + # Convert strings to null-terminated character arrays. + if not isinstance(arg, pass_pointer): + arg = pass_pointer(arg) + if isinstance(arg.object, str): + arg.object = arg.object.encode() + arg.object = Object( + prog, + prog.array_type( + parameter_type.type, + len(arg.object) + 1, + language=func_type.language, + ), + arg.object, + ) + elif ( + isinstance(arg, Object) + and _underlying_type(arg.type_).kind == TypeKind.ARRAY + ): + # Convert arrays to pointers. + if arg.address_ is None: + arg = pass_pointer(arg) + else: + arg = arg + 0 + + if isinstance(arg, pass_pointer): + if not isinstance(arg.object, Object): + arg.object = Object(prog, value=arg.object) + type = arg.object.type_ + underlying_type = _underlying_type(type) + if underlying_type.kind == TypeKind.ARRAY: + type = underlying_type.type + if i < len(func_type.parameters): + # We don't need the result, just type checking. + implicit_convert( + func_type.parameters[i].type, + Object(prog, prog.pointer_type(type), 0), + ) + value = arg.object.to_bytes_() + + align_data(alignof(arg.object.type_)) + out_pointers.append((arg, len(data))) + call_args.append(_Symbol(".data", section=True, offset=len(data))) + data.extend(value) + else: + if isinstance(arg, Object): + if i < len(func_type.parameters): + arg = implicit_convert(func_type.parameters[i].type, arg) + else: + arg = _default_argument_promotions(arg) + else: + arg = Object(prog, value=arg) + + type = _underlying_type(arg.type_) + if type.kind not in { + TypeKind.INT, + TypeKind.BOOL, + TypeKind.ENUM, + TypeKind.POINTER, + }: + if type.kind in { + TypeKind.FLOAT, + TypeKind.STRUCT, + TypeKind.UNION, + TypeKind.CLASS, + }: + raise NotImplementedError( + f"passing {type} by value not implemented" + ) + else: + raise ValueError(f"cannot pass {type} by value") + + call_args.append(_Integer(sizeof(type), arg.value_())) + + function_body: List[_FunctionBodyNode] = [_Call(_Symbol("func"), call_args)] + symbols = [ + _ElfSymbol( + name="func", + value=func_pointer.value_(), + size=0, + type=STT.FUNC, + binding=STB.LOCAL, + section=SHN.ABS, + ) + ] + + if return_type.kind != TypeKind.VOID: + align_data(alignof(return_type)) + return_offset = len(data) + return_size = sizeof(return_type) + data.extend(bytes(return_size)) + function_body.append( + _StoreReturnValue( + return_size, + _Symbol(".data", section=True, offset=return_offset), + ) + ) + + # copy_to_user() is the more obvious choice, but it's an inline function. + # Renamed in Linux kernel commit c0ee37e85e0e ("maccess: rename + # probe_user_{read,write} to copy_{from,to}_user_nofault") (in v5.8-rc2). + if "copy_to_user_nofault" in prog: + copy_to_user_nofault = "copy_to_user_nofault" + else: + copy_to_user_nofault = "probe_user_write" + + sizeof_int = sizeof(prog.type("int")) + if data: + out_buf = ctypes.create_string_buffer(len(data)) + function_body.append( + _Call( + _Symbol(copy_to_user_nofault), + [ + _Integer(sizeof(prog.type("void *")), ctypes.addressof(out_buf)), + _Symbol(".data", section=True), + _Integer(sizeof(prog.type("size_t")), len(data)), + ], + ) + ) + function_body.append( + _ReturnIfLastReturnValueNonZero(_Integer(sizeof_int, -errno.EFAULT)) + ) + symbols.append( + _ElfSymbol( + name=copy_to_user_nofault, + value=0, + size=0, + type=STT.NOTYPE, + binding=STB.GLOBAL, + section=SHN.UNDEF, + ) + ) + + function_body.append(_Return(_Integer(sizeof_int, -errno.EINPROGRESS))) + + code, code_relocations = kmodify.arch.code_gen(_Function(function_body)) + + kmod_name = "call" + try: + symbol_name_match = re.match(r"[0-9a-zA-Z_]+", prog.symbol(func_pointer).name) + if symbol_name_match: + kmod_name = "call_" + symbol_name_match.group() + except LookupError: + pass + + ret = kmodify.insert( + name=kmod_name, + code=code, + code_relocations=code_relocations, + data=data, + data_alignment=data_alignment, + symbols=symbols, + ) + if ret != -errno.EINPROGRESS: + if ret: + raise OSError(-ret, os.strerror(-ret)) + else: + raise ValueError("module init did not run") + + for out_pointer, offset in out_pointers: + out_pointer.object = Object.from_bytes_( + prog, out_pointer.object.type_, out_buf, bit_offset=offset * 8 + ) + + if return_type.kind == TypeKind.VOID: + return Object(prog, func_type.type) + else: + return Object.from_bytes_( + prog, func_type.type, out_buf, bit_offset=return_offset * 8 + ) + + +class pass_pointer: + object: Any + """ + Wrapped object. Updated to an :class:`~drgn.Object` containing the final + value after the function call. + """ + + def __init__(self, object: Any) -> None: + """ + Wrapper used to pass values to :func:`call_function()` by pointer. + + :param object: :class:`~drgn.Object` or Python value to wrap. + """ + self.object = object + + def __repr__(self) -> str: + return f"pass_pointer({self.object!r})" diff --git a/tests/linux_kernel/helpers/test_kmodify.py b/tests/linux_kernel/helpers/test_kmodify.py new file mode 100644 index 000000000..6c0c183fe --- /dev/null +++ b/tests/linux_kernel/helpers/test_kmodify.py @@ -0,0 +1,325 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# SPDX-License-Identifier: LGPL-2.1-or-later + +import os +import random +import unittest + +from _drgn_util.platform import NORMALIZED_MACHINE_NAME +from drgn import FaultError, Object +from drgn.helpers.experimental.kmodify import ( + call_function, + pass_pointer, + write_memory, + write_object, +) +from tests.linux_kernel import LinuxKernelTestCase, skip_unless_have_test_kmod + +skip_unless_have_kmodify = unittest.skipUnless( + NORMALIZED_MACHINE_NAME == "x86_64", + f"kmodify is not implemented for {NORMALIZED_MACHINE_NAME}", +) + + +@skip_unless_have_test_kmod +@skip_unless_have_kmodify +class TestCallFunction(LinuxKernelTestCase): + def assert_called(self, name, args, expected_return_value=None): + if expected_return_value is None: + expected_return_value = Object(self.prog, "void") + before = self.prog[f"drgn_kmodify_test_{name}_called"].read_() + return_value = call_function(self.prog["drgn_kmodify_test_" + name], *args) + self.assertEqual( + self.prog[f"drgn_kmodify_test_{name}_called"].read_(), + before + 1, + ) + self.assertIdentical(return_value, expected_return_value) + + def assert_returns(self, name, expected_return_value): + self.assert_called(name, (), expected_return_value) + + def test_void_return(self): + self.assert_returns("void_return", Object(self.prog, "void")) + + def test_integer_returns(self): + for name, return_value in ( + ("signed_char", Object(self.prog, "signed char", -66)), + ("unsigned_char", Object(self.prog, "unsigned char", 200)), + ("short", Object(self.prog, "short", -666)), + ("unsigned_short", Object(self.prog, "unsigned short", 7777)), + ("int", Object(self.prog, "int", -12345)), + ("unsigned_int", Object(self.prog, "unsigned int", 54321)), + ("long", Object(self.prog, "long", -2468013579)), + ("unsigned_long", Object(self.prog, "unsigned long", 4000000000)), + ("long_long", Object(self.prog, "long long", -9080706050403020100)), + ( + "unsigned_long_long", + Object(self.prog, "unsigned long long", 12345678909876543210), + ), + ): + with self.subTest(name=name): + self.assert_returns(name + "_return", return_value) + + def test_integer_args(self): + self.assert_called( + "signed_args", + ( + Object(self.prog, "signed char", -66), + Object(self.prog, "short", -666), + Object(self.prog, "int", -12345), + Object(self.prog, "long", -2468013579), + Object(self.prog, "long long", -9080706050403020100), + ), + ) + self.assert_called( + "unsigned_args", + ( + Object(self.prog, "unsigned char", 200), + Object(self.prog, "unsigned short", 7777), + Object(self.prog, "unsigned int", 54321), + Object(self.prog, "unsigned long", 4000000000), + Object(self.prog, "unsigned long long", 12345678909876543210), + ), + ) + + def test_integer_literal_args(self): + self.assert_called( + "signed_args", + ( + -66, + -666, + -12345, + -2468013579, + -9080706050403020100, + ), + ) + self.assert_called( + "unsigned_args", + ( + 200, + 7777, + 54321, + 4000000000, + 12345678909876543210, + ), + ) + + def test_many_args(self): + self.assert_called( + "many_args", + ( + 48, + -66, + -666, + -12345, + -2468013579, + -9080706050403020100, + 200, + 7777, + 54321, + 4000000000, + 12345678909876543210, + ), + ) + + def test_enum_returns(self): + self.assert_returns( + "enum_return", Object(self.prog, "enum drgn_kmodify_enum", 2) + ) + + def test_enum_args(self): + args = ( + self.prog["DRGN_KMODIFY_ONE"], + pass_pointer(Object(self.prog, "enum drgn_kmodify_enum", 2)), + ) + self.assert_called("enum_args", args) + self.assertIdentical( + args[1].object, Object(self.prog, "enum drgn_kmodify_enum", 3) + ) + + def test_pointer_returns(self): + self.assert_returns( + "pointer_return", self.prog["drgn_kmodify_test_ptr"].read_() + ) + + def test_pointer_args(self): + self.assert_called( + "pointer_args", (self.prog["drgn_kmodify_test_ptr"].read_(),) + ) + + def test_string_args(self): + for msg, f in ( + ("str", lambda t, s: s), + ("bytes", lambda t, s: s.encode()), + ( + "array", + lambda t, s: Object( + self.prog, + self.prog.array_type(self.prog.type(t), len(s) + 1), + s.encode(), + ), + ), + ("str pointer", lambda t, s: pass_pointer(s)), + ("bytes pointer", lambda t, s: pass_pointer(s.encode())), + ( + "array pointer", + lambda t, s: pass_pointer( + Object( + self.prog, + self.prog.array_type(self.prog.type(t), len(s) + 1), + s.encode(), + ) + ), + ), + ( + "pointer", + lambda t, s: self.prog[f"drgn_kmodify_test_{t.replace(' ', '_')}_str"], + ), + ): + with self.subTest(msg): + self.assert_called( + "string_args", + ( + f("char", "Hello"), + f("signed char", ", "), + f("unsigned char", "world"), + f("const char", "!"), + ), + ) + + def test_integer_out_params(self): + args = [ + pass_pointer(Object(self.prog, "signed char", -66)), + pass_pointer(Object(self.prog, "short", -666)), + pass_pointer(-12345), + pass_pointer(Object(self.prog, "long", -2468013579)), + pass_pointer(Object(self.prog, "long long", -9080706050403020100)), + ] + self.assert_called("integer_out_params", args) + self.assertIdentical( + [ptr.object for ptr in args], + [ + Object(self.prog, "signed char", 33), + Object(self.prog, "short", 333), + Object(self.prog, "int", 23456), + Object(self.prog, "long", 2222222222), + Object(self.prog, "long long", 9090909090909090909), + ], + ) + + def test_array_out_params(self): + arg = pass_pointer(Object(self.prog, "long [3]", [1, 2, 3])) + self.assert_called("array_out_params", (arg,)) + self.assertIdentical(arg.object, Object(self.prog, "long [3]", [2, 3, 5])) + + def test_array_out_params_extra(self): + arg = pass_pointer(Object(self.prog, "long [4]", [1, 2, 3, 100])) + self.assert_called("array_out_params", (arg,)) + self.assertIdentical(arg.object, Object(self.prog, "long [4]", [2, 3, 5, 100])) + + def test_array_out_params_inferred(self): + self.assert_called( + "array_out_params", (Object(self.prog, "long [3]", [1, 2, 3]),) + ) + + def test_many_out_params(self): + args = [ + pass_pointer(Object(self.prog, "char", 48)), + pass_pointer(Object(self.prog, "signed char", -66)), + pass_pointer(Object(self.prog, "short", -666)), + pass_pointer(Object(self.prog, "int", -12345)), + pass_pointer(Object(self.prog, "long", -2468013579)), + pass_pointer(Object(self.prog, "long long", -9080706050403020100)), + pass_pointer(Object(self.prog, "unsigned char", 200)), + pass_pointer(Object(self.prog, "unsigned short", 7777)), + pass_pointer(Object(self.prog, "unsigned int", 54321)), + pass_pointer(Object(self.prog, "unsigned long", 4000000000)), + pass_pointer(Object(self.prog, "unsigned long long", 12345678909876543210)), + ] + self.assert_called("many_out_params", args) + self.assertIdentical( + [arg.object for arg in args], + [ + Object(self.prog, "char", 16), + Object(self.prog, "signed char", -22), + Object(self.prog, "short", -222), + Object(self.prog, "int", -4115), + Object(self.prog, "long", -822671193), + Object(self.prog, "long long", -3026902016801006700), + Object(self.prog, "unsigned char", 66), + Object(self.prog, "unsigned short", 2592), + Object(self.prog, "unsigned int", 18107), + Object(self.prog, "unsigned long", 1333333333), + Object(self.prog, "unsigned long long", 4115226303292181070), + ], + ) + + +@skip_unless_have_test_kmod +@skip_unless_have_kmodify +class TestWriteMemory(LinuxKernelTestCase): + def test_write_memory(self): + buf = os.urandom(16) + write_memory(self.prog, self.prog["drgn_kmodify_test_memory"].address_, buf) + self.assertEqual( + self.prog.read(self.prog["drgn_kmodify_test_memory"].address_, len(buf)), + buf, + ) + + def test_fault(self): + self.assertRaises(FaultError, write_memory, self.prog, 0, b"asdf") + + +@skip_unless_have_test_kmod +@skip_unless_have_kmodify +class TestWriteObject(LinuxKernelTestCase): + def test_python_value(self): + value = random.randrange(2**31) + write_object(self.prog["drgn_kmodify_test_int"], value) + self.assertEqual(self.prog["drgn_kmodify_test_int"].value_(), value) + + def test_object_value(self): + value = random.randrange(2**31) + write_object( + self.prog["drgn_kmodify_test_int"], Object(self.prog, "long", value) + ) + self.assertEqual(self.prog["drgn_kmodify_test_int"].value_(), value) + + def test_pointer_dereference(self): + value = random.randrange(2**31) + write_object( + self.prog["drgn_kmodify_test_int"].address_of_(), value, dereference=True + ) + self.assertEqual(self.prog["drgn_kmodify_test_int"].value_(), value) + + def test_pointer_no_dereference(self): + write_object( + self.prog["drgn_kmodify_test_int_ptr"], + self.prog["drgn_kmodify_test_int"].address_of_(), + dereference=False, + ) + self.assertEqual( + self.prog["drgn_kmodify_test_int_ptr"], + self.prog["drgn_kmodify_test_int"].address_of_(), + ) + write_object(self.prog["drgn_kmodify_test_int_ptr"], 0, dereference=False) + self.assertEqual(self.prog["drgn_kmodify_test_int_ptr"].value_(), 0) + + def test_pointer_ambiguous(self): + self.assertRaisesRegex( + TypeError, + "use dereference", + write_object, + self.prog["drgn_kmodify_test_int_ptr"], + 0, + ) + + def test_not_pointer(self): + self.assertRaisesRegex( + TypeError, + "not a pointer", + write_object, + self.prog["drgn_kmodify_test_int"], + 0, + dereference=True, + ) diff --git a/tests/linux_kernel/kmod/drgn_test.c b/tests/linux_kernel/kmod/drgn_test.c index 2b277cead..07dd285c5 100644 --- a/tests/linux_kernel/kmod/drgn_test.c +++ b/tests/linux_kernel/kmod/drgn_test.c @@ -1182,6 +1182,161 @@ int drgn_test_function(int x) return x + 1; } +// kmodify + +enum drgn_kmodify_enum { + DRGN_KMODIFY_ONE = 1, + DRGN_KMODIFY_TWO, + DRGN_KMODIFY_THREE, +}; + +struct drgn_kmodify_test_struct { + void *v; + int *i; +}; +struct drgn_kmodify_test_struct *drgn_kmodify_test_ptr = + &(struct drgn_kmodify_test_struct){}; + +char drgn_kmodify_test_memory[16]; + +int drgn_kmodify_test_int; +int *drgn_kmodify_test_int_ptr; + +#define DEFINE_KMODIFY_TEST_RETURN(name, return_type, return_value) \ +int drgn_kmodify_test_##name##_called = 0; \ +return_type drgn_kmodify_test_##name(void); \ +return_type drgn_kmodify_test_##name(void) \ +{ \ + drgn_kmodify_test_##name##_called++; \ + return (return_value); \ +} + +#define DEFINE_KMODIFY_TEST_ARGS(name, parameters, condition) \ +int drgn_kmodify_test_##name##_called = 0; \ +void drgn_kmodify_test_##name parameters; \ +void drgn_kmodify_test_##name parameters \ +{ \ + if (condition) \ + drgn_kmodify_test_##name##_called++; \ +} + +DEFINE_KMODIFY_TEST_ARGS(void_return, (void), 1) +DEFINE_KMODIFY_TEST_RETURN(signed_char_return, signed char, -66) +DEFINE_KMODIFY_TEST_RETURN(unsigned_char_return, unsigned char, 200) +DEFINE_KMODIFY_TEST_RETURN(short_return, short, -666) +DEFINE_KMODIFY_TEST_RETURN(unsigned_short_return, unsigned short, 7777) +DEFINE_KMODIFY_TEST_RETURN(int_return, int, -12345) +DEFINE_KMODIFY_TEST_RETURN(unsigned_int_return, unsigned int, 54321U) +DEFINE_KMODIFY_TEST_RETURN(long_return, long, -2468013579L) +DEFINE_KMODIFY_TEST_RETURN(unsigned_long_return, unsigned long, 4000000000UL) +DEFINE_KMODIFY_TEST_RETURN(long_long_return, long long, -9080706050403020100LL) +DEFINE_KMODIFY_TEST_RETURN(unsigned_long_long_return, unsigned long long, + 12345678909876543210ULL) +DEFINE_KMODIFY_TEST_RETURN(pointer_return, struct drgn_kmodify_test_struct *, + drgn_kmodify_test_ptr) +DEFINE_KMODIFY_TEST_RETURN(enum_return, enum drgn_kmodify_enum, + DRGN_KMODIFY_TWO) + +DEFINE_KMODIFY_TEST_ARGS( + signed_args, + (signed char c, short s, int i, long l, long long ll), + (c == -66 && s == -666 && i == -12345 && l == -2468013579L && ll == -9080706050403020100LL) +) +DEFINE_KMODIFY_TEST_ARGS( + unsigned_args, + (unsigned char c, unsigned short s, unsigned int i, unsigned long l, unsigned long long ll), + (c == 200 && s == 7777 && i == 54321 && l == 4000000000UL && ll == 12345678909876543210ULL) +) + +DEFINE_KMODIFY_TEST_ARGS( + many_args, + (char c, + signed char sc, short ss, int si, long sl, long long sll, + unsigned char uc, unsigned short us, unsigned int ui, unsigned long ul, unsigned long long ull), + (c == 48 + && sc == -66 && ss == -666 && si == -12345 && sl == -2468013579L && sll == -9080706050403020100LL + && uc == 200 && us == 7777 && ui == 54321 && ul == 4000000000UL && ull == 12345678909876543210ULL) +) + +DEFINE_KMODIFY_TEST_ARGS( + enum_args, + (enum drgn_kmodify_enum a1, enum drgn_kmodify_enum *a2), + ({ + int match = a1 == DRGN_KMODIFY_ONE && *a2 == DRGN_KMODIFY_TWO; + *a2 = DRGN_KMODIFY_THREE; + match; + }) +) + +DEFINE_KMODIFY_TEST_ARGS( + pointer_args, + (struct drgn_kmodify_test_struct *ptr), + (ptr == drgn_kmodify_test_ptr) +) + +char *drgn_kmodify_test_char_str = "Hello"; +signed char *drgn_kmodify_test_signed_char_str = ", "; +unsigned char *drgn_kmodify_test_unsigned_char_str = "world"; +const char *drgn_kmodify_test_const_char_str = "!"; +DEFINE_KMODIFY_TEST_ARGS( + string_args, + (char *c, signed char *sc, unsigned char *uc, const char *cc), + (strcmp(c, drgn_kmodify_test_char_str) == 0 && + strcmp(sc, drgn_kmodify_test_signed_char_str) == 0 && + strcmp(uc, drgn_kmodify_test_unsigned_char_str) == 0 && + strcmp(cc, drgn_kmodify_test_const_char_str) == 0) +) + +DEFINE_KMODIFY_TEST_ARGS( + integer_out_params, + (signed char *c, short *s, int *i, long *l, long long *ll), + ({ + int match = *c == -66 && *s == -666 && *i == -12345 && *l == -2468013579L && *ll == -9080706050403020100LL; + *c = 33; + *s = 333; + *i = 23456; + *l = 2222222222L; + *ll = 9090909090909090909LL; + match; + }) +) + +DEFINE_KMODIFY_TEST_ARGS( + array_out_params, + (long arr[3]), + ({ + int match = arr[0] == 1 && arr[1] == 2 && arr[2] == 3; + arr[0] = 2; + arr[1] = 3; + arr[2] = 5; + match; + }) +) + +DEFINE_KMODIFY_TEST_ARGS( + many_out_params, + (char *c, + signed char *sc, short *ss, int *si, long *sl, long long *sll, + unsigned char *uc, unsigned short *us, unsigned int *ui, unsigned long *ul, unsigned long long *ull), + ({ + int match = (*c == 48 + && *sc == -66 && *ss == -666 && *si == -12345 && *sl == -2468013579L && *sll == -9080706050403020100LL + && *uc == 200 && *us == 7777 && *ui == 54321 && *ul == 4000000000UL && *ull == 12345678909876543210ULL); + *c /= 3; + *sc /= 3; + *ss /= 3; + *si /= 3; + *sl /= 3; + *sll /= 3; + *uc /= 3; + *us /= 3; + *ui /= 3; + *ul /= 3; + *ull /= 3; + match; + }) +) + static void drgn_test_exit(void) { drgn_test_slab_exit(); From ee328188c1e07515ab05b4b812c9fe43622ec65b Mon Sep 17 00:00:00 2001 From: Omar Sandoval Date: Wed, 18 Sep 2024 01:54:19 -0700 Subject: [PATCH 6/9] drgn.helpers.experimental.kmodify: fix typos in docs Signed-off-by: Omar Sandoval --- drgn/helpers/experimental/kmodify.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/drgn/helpers/experimental/kmodify.py b/drgn/helpers/experimental/kmodify.py index 325195b64..95015dc39 100644 --- a/drgn/helpers/experimental/kmodify.py +++ b/drgn/helpers/experimental/kmodify.py @@ -997,7 +997,7 @@ def write_object( (``*ptr = value``). If ``False``, then write to the pointer itself (``ptr = value``). This is a common source of confusion, so it is required if *object* is a pointer. - :raises ValueError: is *object* is not a reference object (i.e., its + :raises ValueError: if *object* is not a reference object (i.e., its address is not known) :raises TypeError: if *object* is a pointer and *dereference* is not given :raises TypeError: if *object* is not a pointer and *dereference* is @@ -1109,13 +1109,12 @@ def call_function(prog: Program, func: Union[str, Object], *args: Any) -> Object :param args: Function arguments. :class:`int`, :class:`float`, and :class:`bool` arguments are converted as "literals" with ``Object(prog, value=...)``. :class:`str` and :class:`bytes` arguments - are automatically converted to a ``char`` array object. - :class:`pass_pointer` arguments are copied to the kernel, passed by - pointer, and copied back. + are converted to ``char`` array objects. :class:`pass_pointer` + arguments are copied to the kernel, passed by pointer, and copied back. :return: Function return value. :raises TypeError: if the passed arguments have incorrect types for the function - :raises ObjectAbsentError: if function cannot be called because it is + :raises ObjectAbsentError: if the function cannot be called because it is inlined :raises LookupError: if a function with the given name is not found (possibly because it is actually a function-like macro) From e2a41bc87634467b9868985bc4065809b6d2e71e Mon Sep 17 00:00:00 2001 From: Omar Sandoval Date: Mon, 16 Sep 2024 14:40:33 -0700 Subject: [PATCH 7/9] contrib/dm_crypt_key.py: handle kernels before v6.7 Signed-off-by: Omar Sandoval --- contrib/dm_crypt_key.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/contrib/dm_crypt_key.py b/contrib/dm_crypt_key.py index 7269021e9..d4147eeae 100755 --- a/contrib/dm_crypt_key.py +++ b/contrib/dm_crypt_key.py @@ -87,8 +87,15 @@ def main(): ) child_tfm = cryptd_ctx.child xts_ctx = aes_xts_ctx(cryptd_ctx.child) - crypt_aes_ctx = xts_ctx.crypt_ctx - tweak_aes_ctx = xts_ctx.tweak_ctx + try: + crypt_aes_ctx = xts_ctx.crypt_ctx + tweak_aes_ctx = xts_ctx.tweak_ctx + except AttributeError: + # Before Linux kernel commit d148736ff17d ("crypto: x86/aesni - + # Correct the data type in struct aesni_xts_ctx") (in v6.7), the + # AES contexts were arrays that we need to cast. + crypt_aes_ctx = cast("struct crypto_aes_ctx *", xts_ctx.raw_crypt_ctx) + tweak_aes_ctx = cast("struct crypto_aes_ctx *", xts_ctx.raw_tweak_ctx) elif is_function(exit, "xts_exit_tfm"): xts_ctx = cast("struct xts_tfm_ctx *", crypto_skcipher_ctx(tfm)) lskcipher_tfm = cast( From 8488aad836fd1b6e1d8c8576f56b70161549fcee Mon Sep 17 00:00:00 2001 From: Stephen Brennan Date: Wed, 14 Aug 2024 10:58:01 -0700 Subject: [PATCH 8/9] helpers.linux.fs: support d_path() without vfsmount There are some diagnostic use cases where all one has is a dentry and no vfsmount. This is technically ambiguous, because a superblock may be mounted in several places due to bind mounts, filesystem namespaces, etc. The dentry's full path would depend on the specific mount point. But when we're doing debugging, we frequently just want any representative filesystem path for the dentry. It turns out that the kernel always puts new mountpoints at the end of the superblock's list of mounts, so the first one is likely to be the most relevant anyway. Thus, arbitrarily choosing this first mountpoint is a good way to get a representative path. Update d_path() to accept a single dentry as well. Signed-off-by: Stephen Brennan --- drgn/helpers/linux/fs.py | 52 +++++++++++++++++++++++---- tests/linux_kernel/helpers/test_fs.py | 14 ++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/drgn/helpers/linux/fs.py b/drgn/helpers/linux/fs.py index bd429729a..7424db96e 100644 --- a/drgn/helpers/linux/fs.py +++ b/drgn/helpers/linux/fs.py @@ -167,16 +167,54 @@ def d_path(vfsmnt: Object, dentry: Object) -> bytes: ... +@overload +def d_path(dentry: Object) -> bytes: + """ + Return the full path of a dentry. + + Since a mount is not provided, this arbitrarily selects a mount to determine + the path. + + :param dentry: ``struct dentry *`` + """ + ... + + def d_path( # type: ignore # Need positional-only arguments. - path_or_vfsmnt: Object, dentry: Optional[Object] = None + arg1: Object, arg2: Optional[Object] = None ) -> bytes: - if dentry is None: - vfsmnt = path_or_vfsmnt.mnt - dentry = path_or_vfsmnt.dentry.read_() + if arg2 is None: + try: + mnt = container_of(arg1.mnt, "struct mount", "mnt") + dentry = arg1.dentry.read_() + except AttributeError: + # Select an arbitrary mount from this dentry's super block. We + # choose the first non-internal mount. Internal mounts exist for + # kernel filesystems (e.g. debugfs) and they are mounted at "/". + # Paths from these mounts aren't usable in userspace and they're + # confusing. If there's no other option, we will use the first + # internal mount we encountered. + # + # The MNT_INTERNAL flag is defined as a macro in the kernel source. + # Introduced in 2.6.34 and has not been modified since. + MNT_INTERNAL = 0x4000 + internal_mnt = None + dentry = arg1 + for mnt in list_for_each_entry( + "struct mount", dentry.d_sb.s_mounts.address_of_(), "mnt_instance" + ): + if mnt.mnt.mnt_flags & MNT_INTERNAL: + internal_mnt = internal_mnt or mnt + continue + break + else: + if internal_mnt is not None: + mnt = internal_mnt + else: + raise ValueError("Could not find a mount for this dentry") else: - vfsmnt = path_or_vfsmnt - dentry = dentry.read_() - mnt = container_of(vfsmnt, "struct mount", "mnt") + mnt = container_of(arg1, "struct mount", "mnt") + dentry = arg2.read_() d_op = dentry.d_op.read_() if d_op and d_op.d_dname: diff --git a/tests/linux_kernel/helpers/test_fs.py b/tests/linux_kernel/helpers/test_fs.py index 3e54a0861..206447b51 100644 --- a/tests/linux_kernel/helpers/test_fs.py +++ b/tests/linux_kernel/helpers/test_fs.py @@ -42,6 +42,20 @@ def test_d_path(self): task = find_task(self.prog, os.getpid()) self.assertEqual(d_path(task.fs.pwd.address_of_()), os.fsencode(os.getcwd())) + def test_d_path_dentry_only(self): + # This test could fail if we are running inside a container or if we are + # in a bind mount. + task = find_task(self.prog, os.getpid()) + self.assertEqual(d_path(task.fs.pwd.dentry), os.fsencode(os.getcwd())) + + def test_d_path_no_internal_mount(self): + if not os.path.isdir("/sys/kernel/tracing"): + self.skipTest("The /sys/kernel/tracing directory is not mounted") + path = path_lookup(self.prog, "/sys/kernel/tracing/trace_pipe") + # The first mount for this super block is usually MNT_INTERNAL, but we + # don't want that one. Ensure we skip it. + self.assertEqual(d_path(path.dentry), b"/sys/kernel/tracing/trace_pipe") + def test_dentry_path(self): pwd = os.fsencode(os.getcwd()) task = find_task(self.prog, os.getpid()) From ad9b2415aca36bb4f569d60c9a82fe47c7bce000 Mon Sep 17 00:00:00 2001 From: Omar Sandoval Date: Wed, 18 Sep 2024 10:30:04 -0700 Subject: [PATCH 9/9] pre-commit: update hooks and apply fixes The only changes are that Black 24 updated the style for stubs and if-else expressions. Signed-off-by: Omar Sandoval --- .pre-commit-config.yaml | 10 +-- _drgn.pyi | 83 ++++++++++++++++++++ docs/exts/drgndoc/format.py | 8 +- docs/exts/drgndoc/parse.py | 6 +- drgn/helpers/common/prog.py | 26 +++--- drgn/helpers/experimental/kmodify.py | 16 ++-- tests/linux_kernel/helpers/test_mapletree.py | 64 +++++++++------ tools/fsrefs.py | 9 +-- 8 files changed, 162 insertions(+), 60 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a55b895d6..a8fd32a43 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,26 +1,26 @@ exclude: ^contrib/ repos: - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort name: isort (python) - repo: https://github.com/psf/black - rev: 23.11.0 + rev: 24.8.0 hooks: - id: black - repo: https://github.com/pycqa/flake8 - rev: 6.1.0 + rev: 7.1.1 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.7.1 + rev: v1.11.2 hooks: - id: mypy args: [--show-error-codes, --strict, --no-warn-return-any, --no-warn-unused-ignores] files: ^(drgn/.*\.py|_drgn.pyi|_drgn_util/.*\.py|tools/.*\.py)$ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace exclude_types: [diff] diff --git a/_drgn.pyi b/_drgn.pyi index 97d8cea5e..736fa7160 100644 --- a/_drgn.pyi +++ b/_drgn.pyi @@ -134,6 +134,7 @@ class Program: :param name: Object name. """ ... + def __contains__(self, name: str) -> bool: """ Implement ``name in self``. Return whether an object (variable, @@ -142,6 +143,7 @@ class Program: :param name: Object name. """ ... + def variable(self, name: str, filename: Optional[str] = None) -> Object: """ Get the variable with the given name. @@ -159,6 +161,7 @@ class Program: the given file """ ... + def constant(self, name: str, filename: Optional[str] = None) -> Object: """ Get the constant (e.g., enumeration constant) with the given name. @@ -180,6 +183,7 @@ class Program: the given file """ ... + def function(self, name: str, filename: Optional[str] = None) -> Object: """ Get the function with the given name. @@ -197,6 +201,7 @@ class Program: the given file """ ... + def object( self, name: str, @@ -218,6 +223,7 @@ class Program: the given file """ ... + def symbol(self, __address_or_name: Union[IntegerLike, str]) -> Symbol: """ Get a symbol containing the given address, or a symbol with the given @@ -238,6 +244,7 @@ class Program: the given name """ ... + def symbols( self, __address_or_name: Union[None, IntegerLike, str] = None, @@ -255,6 +262,7 @@ class Program: :param address_or_name: Address or name to search for. """ ... + def stack_trace( self, # Object is already IntegerLike, but this explicitly documents that it @@ -284,6 +292,7 @@ class Program: ``struct task_struct *`` object. """ ... + def stack_trace_from_pcs(self, pcs: Sequence[IntegerLike]) -> StackTrace: """ Get a stack trace with the supplied list of program counters. @@ -291,6 +300,7 @@ class Program: :param pcs: List of program counters. """ ... + @overload def type(self, name: str, filename: Optional[str] = None) -> Type: """ @@ -306,6 +316,7 @@ class Program: the given file """ ... + @overload def type(self, __type: Type) -> Type: """ @@ -327,9 +338,11 @@ class Program: :return: The exact same type. """ ... + def threads(self) -> Iterator[Thread]: """Get an iterator over all of the threads in the program.""" ... + def thread(self, tid: IntegerLike) -> Thread: """ Get the thread with the given thread ID. @@ -338,6 +351,7 @@ class Program: :raises LookupError: if no thread has the given thread ID """ ... + def main_thread(self) -> Thread: """ Get the main thread of the program. @@ -347,6 +361,7 @@ class Program: :raises ValueError: if the program is the Linux kernel """ ... + def crashed_thread(self) -> Thread: """ Get the thread that caused the program to crash. @@ -360,6 +375,7 @@ class Program: :raises ValueError: if the program is live (i.e., not a core dump) """ ... + def read( self, address: IntegerLike, size: IntegerLike, physical: bool = False ) -> bytes: @@ -382,18 +398,23 @@ class Program: :raises ValueError: if *size* is negative """ ... + def read_u8(self, address: IntegerLike, physical: bool = False) -> int: """ """ ... + def read_u16(self, address: IntegerLike, physical: bool = False) -> int: """ """ ... + def read_u32(self, address: IntegerLike, physical: bool = False) -> int: """ """ ... + def read_u64(self, address: IntegerLike, physical: bool = False) -> int: """ """ ... + def read_word(self, address: IntegerLike, physical: bool = False) -> int: """ Read an unsigned integer from the program's memory in the program's @@ -414,6 +435,7 @@ class Program: :raises FaultError: if the address is invalid; see :meth:`read()` """ ... + def add_memory_segment( self, address: IntegerLike, @@ -439,6 +461,7 @@ class Program: another :ref:`buffer ` type. """ ... + def register_type_finder( self, name: str, @@ -463,9 +486,11 @@ class Program: :raises ValueError: if there is already a finder with the given name """ ... + def registered_type_finders(self) -> Set[str]: """Return the names of all registered type finders.""" ... + def set_enabled_type_finders(self, names: Sequence[str]) -> None: """ Set the list of enabled type finders. @@ -479,9 +504,11 @@ class Program: given more than once """ ... + def enabled_type_finders(self) -> List[str]: """Return the names of enabled type finders, in order.""" ... + def register_object_finder( self, name: str, @@ -506,9 +533,11 @@ class Program: :raises ValueError: if there is already a finder with the given name """ ... + def registered_object_finders(self) -> Set[str]: """Return the names of all registered object finders.""" ... + def set_enabled_object_finders(self, names: Sequence[str]) -> None: """ Set the list of enabled object finders. @@ -522,9 +551,11 @@ class Program: given more than once """ ... + def enabled_object_finders(self) -> List[str]: """Return the names of enabled object finders, in order.""" ... + def register_symbol_finder( self, name: str, @@ -561,9 +592,11 @@ class Program: :raises ValueError: if there is already a finder with the given name """ ... + def registered_symbol_finders(self) -> Set[str]: """Return the names of all registered symbol finders.""" ... + def set_enabled_symbol_finders(self, names: Sequence[str]) -> None: """ Set the list of enabled symbol finders. @@ -580,9 +613,11 @@ class Program: given more than once """ ... + def enabled_symbol_finders(self) -> List[str]: """Return the names of enabled symbol finders, in order.""" ... + def add_type_finder( self, fn: Callable[[TypeKind, str, Optional[str]], Optional[Type]] ) -> None: @@ -603,6 +638,7 @@ class Program: 4. The finder is always enabled before any existing finders. """ ... + def add_object_finder( self, fn: Callable[[Program, str, FindObjectFlags, Optional[str]], Optional[Object]], @@ -620,6 +656,7 @@ class Program: 2. The finder is always enabled before any existing finders. """ ... + def set_core_dump(self, path: Union[Path, int]) -> None: """ Set the program to a core dump. @@ -631,6 +668,7 @@ class Program: :param path: Core dump file path or open file descriptor. """ ... + def set_kernel(self) -> None: """ Set the program to the running operating system kernel. @@ -640,6 +678,7 @@ class Program: :meth:`load_default_debug_info()`. """ ... + def set_pid(self, pid: int) -> None: """ Set the program to a running process. @@ -651,6 +690,7 @@ class Program: :param pid: Process ID. """ ... + def load_debug_info( self, paths: Optional[Iterable[Path]] = None, @@ -684,6 +724,7 @@ class Program: are still loaded """ ... + def load_default_debug_info(self) -> None: """ Load debugging information which can automatically be determined from @@ -727,6 +768,7 @@ class Program: :param lang: :attr:`Type.language` """ ... + def int_type( self, name: str, @@ -749,6 +791,7 @@ class Program: :param lang: :attr:`Type.language` """ ... + def bool_type( self, name: str, @@ -769,6 +812,7 @@ class Program: :param lang: :attr:`Type.language` """ ... + def float_type( self, name: str, @@ -789,6 +833,7 @@ class Program: :param lang: :attr:`Type.language` """ ... + @overload def struct_type( self, @@ -811,6 +856,7 @@ class Program: :param lang: :attr:`Type.language` """ ... + @overload def struct_type( self, @@ -824,6 +870,7 @@ class Program: ) -> Type: """Create a new incomplete structure type.""" ... + @overload def union_type( self, @@ -840,6 +887,7 @@ class Program: this is the same as as :meth:`struct_type()`. """ ... + @overload def union_type( self, @@ -853,6 +901,7 @@ class Program: ) -> Type: """Create a new incomplete union type.""" ... + @overload def class_type( self, @@ -869,6 +918,7 @@ class Program: this is the same as as :meth:`struct_type()`. """ ... + @overload def class_type( self, @@ -882,6 +932,7 @@ class Program: ) -> Type: """Create a new incomplete class type.""" ... + @overload def enum_type( self, @@ -902,6 +953,7 @@ class Program: :param lang: :attr:`Type.language` """ ... + @overload def enum_type( self, @@ -914,6 +966,7 @@ class Program: ) -> Type: """Create a new incomplete enumerated type.""" ... + def typedef_type( self, name: str, @@ -931,6 +984,7 @@ class Program: :param lang: :attr:`Type.language` """ ... + def pointer_type( self, type: Type, @@ -952,6 +1006,7 @@ class Program: :param lang: :attr:`Type.language` """ ... + def array_type( self, type: Type, @@ -969,6 +1024,7 @@ class Program: :param lang: :attr:`Type.language` """ ... + def function_type( self, type: Type, @@ -1318,6 +1374,7 @@ class Object: The default is ``None``, which means the object is not a bit field. """ ... + @overload def __init__(self, prog: Program, *, value: Union[int, float, bool]) -> None: """ @@ -1330,6 +1387,7 @@ class Object: :param value: Value of the literal. """ ... + @overload def __init__( self, @@ -1348,6 +1406,7 @@ class Object: the object. """ ... + @overload def __init__( self, @@ -1406,6 +1465,7 @@ class Object: :param name: Attribute name. """ ... + def __getitem__(self, idx: IntegerLike) -> Object: """ Implement ``self[idx]``. Get the array element at the given index. @@ -1433,6 +1493,7 @@ class Object: :raises TypeError: if this object is not a pointer or array """ ... + def __len__(self) -> int: """ Implement ``len(self)``. Get the number of elements in this object. @@ -1445,6 +1506,7 @@ class Object: :raises TypeError: if this object is not an array with complete type """ ... + def value_(self) -> Any: """ Get the value of this object as a Python object. @@ -1461,6 +1523,7 @@ class Object: ``void``) """ ... + def string_(self) -> bytes: """ Read a null-terminated string pointed to by this object. @@ -1477,6 +1540,7 @@ class Object: :raises TypeError: if this object is not a pointer or array """ ... + def member_(self, name: str) -> Object: """ Get a member of this object. @@ -1496,6 +1560,7 @@ class Object: given name """ ... + def address_of_(self) -> Object: """ Get a pointer to this object. @@ -1510,6 +1575,7 @@ class Object: :raises ValueError: if this object is a value """ ... + def read_(self) -> Object: """ Read this object (which may be a reference or a value) and return it as @@ -1527,9 +1593,11 @@ class Object: ``void``) """ ... + def to_bytes_(self) -> bytes: """Return the binary representation of this object's value.""" ... + @classmethod def from_bytes_( cls, @@ -1555,6 +1623,7 @@ class Object: The default is ``None``, which means the object is not a bit field. """ ... + def format_( self, *, @@ -1630,6 +1699,7 @@ class Object: value (i.e., for C, zero-initialized). Defaults to ``False``. """ ... + def __iter__(self) -> Iterator[Object]: ... def __bool__(self) -> bool: ... def __lt__(self, other: Any) -> bool: ... @@ -1992,6 +2062,7 @@ class StackFrame: :param name: Object name. """ ... + def __contains__(self, name: str) -> bool: """ Implement ``name in self``. Return whether an object with the given @@ -2000,6 +2071,7 @@ class StackFrame: :param name: Object name. """ ... + def locals(self) -> List[str]: """ Get a list of the names of all local objects (local variables, function @@ -2010,6 +2082,7 @@ class StackFrame: :meth:`[] <.__getitem__>` operator to check. """ ... + def source(self) -> Tuple[str, int, int]: """ Get the source code location of this frame. @@ -2018,6 +2091,7 @@ class StackFrame: :raises LookupError: if the source code location is not available """ ... + def symbol(self) -> Symbol: """ Get the function symbol at this stack frame. @@ -2029,6 +2103,7 @@ class StackFrame: prog.symbol(frame.pc - (0 if frame.interrupted else 1)) """ ... + def register(self, reg: str) -> int: """ Get the value of the given register at this stack frame. @@ -2038,12 +2113,14 @@ class StackFrame: :raises LookupError: if the register value is not known """ ... + def registers(self) -> Dict[str, int]: """ Get the values of all available registers at this stack frame as a dictionary with the register names as keys. """ ... + def _repr_pretty_(self, p: Any, cycle: bool) -> None: ... class Type: @@ -2169,6 +2246,7 @@ class Type: def type_name(self) -> str: """Get a descriptive full name of this type.""" ... + def is_complete(self) -> bool: """ Get whether this type is complete (i.e., the type definition is known). @@ -2178,6 +2256,7 @@ class Type: is always ``True``. """ ... + def qualified(self, qualifiers: Qualifiers) -> Type: """ Get a copy of this type with different qualifiers. @@ -2187,9 +2266,11 @@ class Type: :param qualifiers: New type qualifiers. """ ... + def unqualified(self) -> Type: """Get a copy of this type with no qualifiers.""" ... + def member(self, name: str) -> TypeMember: """ Look up a member in this type by name. @@ -2206,6 +2287,7 @@ class Type: name """ ... + def has_member(self, name: str) -> bool: """ Return whether this type has a member with the given name. @@ -2217,6 +2299,7 @@ class Type: :raises TypeError: if this type is not a structure, union, or class type """ + def _repr_pretty_(self, p: Any, cycle: bool) -> None: ... class TypeMember: diff --git a/docs/exts/drgndoc/format.py b/docs/exts/drgndoc/format.py index 33829a8b5..9d77e14ea 100644 --- a/docs/exts/drgndoc/format.py +++ b/docs/exts/drgndoc/format.py @@ -349,9 +349,11 @@ def _is_posonly(arg: ast.arg) -> bool: visit_arg( arg, default, - name=arg.arg[2:] - if num_pep_570_posonlyargs <= i < num_posonlyargs - else arg.arg, + name=( + arg.arg[2:] + if num_pep_570_posonlyargs <= i < num_posonlyargs + else arg.arg + ), ) if i == num_posonlyargs - 1: signature.append(", /") diff --git a/docs/exts/drgndoc/parse.py b/docs/exts/drgndoc/parse.py index f02c82cdc..5d9a63d88 100644 --- a/docs/exts/drgndoc/parse.py +++ b/docs/exts/drgndoc/parse.py @@ -27,12 +27,10 @@ class _PreTransformer(ast.NodeTransformer): # Replace string forward references with the parsed expression. @overload - def _visit_annotation(self, node: ast.expr) -> ast.expr: - ... + def _visit_annotation(self, node: ast.expr) -> ast.expr: ... @overload - def _visit_annotation(self, node: None) -> None: - ... + def _visit_annotation(self, node: None) -> None: ... def _visit_annotation(self, node: Optional[ast.expr]) -> Optional[ast.expr]: if isinstance(node, ast.Constant) and isinstance(node.value, str): diff --git a/drgn/helpers/common/prog.py b/drgn/helpers/common/prog.py index 5f3ce15d2..54349534d 100644 --- a/drgn/helpers/common/prog.py +++ b/drgn/helpers/common/prog.py @@ -35,30 +35,32 @@ R_co = TypeVar("R_co", covariant=True) class TakesProgram(Protocol[P, R_co]): - def __call__(self, prog: Program, *args: P.args, **kwargs: P.kwargs) -> R_co: - ... + def __call__( + self, prog: Program, *args: P.args, **kwargs: P.kwargs + ) -> R_co: ... class TakesProgramOrDefault(Protocol[P, R_co]): @overload - def __call__(self, prog: Program, *args: P.args, **kwargs: P.kwargs) -> R_co: - ... + def __call__( + self, prog: Program, *args: P.args, **kwargs: P.kwargs + ) -> R_co: ... @overload - def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R_co: - ... + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R_co: ... class TakesObjectOrProgramOrDefault(Protocol[P, R_co]): @overload - def __call__(self, prog: Program, *args: P.args, **kwargs: P.kwargs) -> R_co: - ... + def __call__( + self, prog: Program, *args: P.args, **kwargs: P.kwargs + ) -> R_co: ... @overload - def __call__(self, __obj: Object, *args: P.args, **kwargs: P.kwargs) -> R_co: - ... + def __call__( + self, __obj: Object, *args: P.args, **kwargs: P.kwargs + ) -> R_co: ... @overload - def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R_co: - ... + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R_co: ... def takes_program_or_default(f: "TakesProgram[P, R]") -> "TakesProgramOrDefault[P, R]": diff --git a/drgn/helpers/experimental/kmodify.py b/drgn/helpers/experimental/kmodify.py index 95015dc39..c25d94c97 100644 --- a/drgn/helpers/experimental/kmodify.py +++ b/drgn/helpers/experimental/kmodify.py @@ -167,9 +167,11 @@ def sym_fields(sym: _ElfSymbol) -> Tuple[int, int, int, int, int]: return ( (sym.binding << 4) + (sym.type & 0xF), sym.visibility, - section_name_to_index[sym.section] - if isinstance(sym.section, str) - else sym.section, + ( + section_name_to_index[sym.section] + if isinstance(sym.section, str) + else sym.section + ), sym.value, sym.size, ) @@ -190,9 +192,11 @@ def sym_fields(sym: _ElfSymbol) -> Tuple[int, int, int, int, int]: sym.size, (sym.binding << 4) + (sym.type & 0xF), sym.visibility, - section_name_to_index[sym.section] - if isinstance(sym.section, str) - else sym.section, + ( + section_name_to_index[sym.section] + if isinstance(sym.section, str) + else sym.section + ), ) section_symbols = [ diff --git a/tests/linux_kernel/helpers/test_mapletree.py b/tests/linux_kernel/helpers/test_mapletree.py index 97474790c..3911b8ab3 100644 --- a/tests/linux_kernel/helpers/test_mapletree.py +++ b/tests/linux_kernel/helpers/test_mapletree.py @@ -274,9 +274,11 @@ def test_mtree_load_three_levels_dense_1(self): ulong_max = (1 << (sizeof(self.prog.type("unsigned long")) * 8)) - 1 for mt, arange in self.maple_trees("three_levels_dense_1"): node_slots = self.prog[ - "drgn_test_maple_arange64_slots" - if arange - else "drgn_test_maple_range64_slots" + ( + "drgn_test_maple_arange64_slots" + if arange + else "drgn_test_maple_range64_slots" + ) ].value_() n = 2 * (node_slots - 1) * (maple_range64_slots - 1) + ( maple_range64_slots - 1 @@ -297,9 +299,11 @@ def test_mt_for_each_three_levels_dense_1(self): maple_range64_slots = self.prog["drgn_test_maple_range64_slots"].value_() for mt, arange in self.maple_trees("three_levels_dense_1"): node_slots = self.prog[ - "drgn_test_maple_arange64_slots" - if arange - else "drgn_test_maple_range64_slots" + ( + "drgn_test_maple_arange64_slots" + if arange + else "drgn_test_maple_range64_slots" + ) ].value_() n = 2 * (node_slots - 1) * (maple_range64_slots - 1) + ( maple_range64_slots - 1 @@ -314,9 +318,11 @@ def test_mtree_load_three_levels_dense_2(self): ulong_max = (1 << (sizeof(self.prog.type("unsigned long")) * 8)) - 1 for mt, arange in self.maple_trees("three_levels_dense_2"): node_slots = self.prog[ - "drgn_test_maple_arange64_slots" - if arange - else "drgn_test_maple_range64_slots" + ( + "drgn_test_maple_arange64_slots" + if arange + else "drgn_test_maple_range64_slots" + ) ].value_() n = 2 * node_slots * maple_range64_slots for i in range(n): @@ -335,9 +341,11 @@ def test_mt_for_each_three_levels_dense_2(self): maple_range64_slots = self.prog["drgn_test_maple_range64_slots"].value_() for mt, arange in self.maple_trees("three_levels_dense_2"): node_slots = self.prog[ - "drgn_test_maple_arange64_slots" - if arange - else "drgn_test_maple_range64_slots" + ( + "drgn_test_maple_arange64_slots" + if arange + else "drgn_test_maple_range64_slots" + ) ].value_() n = 2 * node_slots * maple_range64_slots self.assertIdentical( @@ -350,9 +358,11 @@ def test_mtree_load_three_levels_ranges_1(self): ulong_max = (1 << (sizeof(self.prog.type("unsigned long")) * 8)) - 1 for mt, arange in self.maple_trees("three_levels_ranges_1"): node_slots = self.prog[ - "drgn_test_maple_arange64_slots" - if arange - else "drgn_test_maple_range64_slots" + ( + "drgn_test_maple_arange64_slots" + if arange + else "drgn_test_maple_range64_slots" + ) ].value_() n = 2 * (node_slots - 1) * (maple_range64_slots - 1) + ( maple_range64_slots - 1 @@ -386,9 +396,11 @@ def test_mt_for_each_three_levels_ranges_1(self): ulong_max = (1 << (sizeof(self.prog.type("unsigned long")) * 8)) - 1 for mt, arange in self.maple_trees("three_levels_ranges_1"): node_slots = self.prog[ - "drgn_test_maple_arange64_slots" - if arange - else "drgn_test_maple_range64_slots" + ( + "drgn_test_maple_arange64_slots" + if arange + else "drgn_test_maple_range64_slots" + ) ].value_() n = 2 * (node_slots - 1) * (maple_range64_slots - 1) + ( maple_range64_slots - 1 @@ -407,9 +419,11 @@ def test_mtree_load_three_levels_ranges_2(self): ulong_max = (1 << (sizeof(self.prog.type("unsigned long")) * 8)) - 1 for mt, arange in self.maple_trees("three_levels_ranges_2"): node_slots = self.prog[ - "drgn_test_maple_arange64_slots" - if arange - else "drgn_test_maple_range64_slots" + ( + "drgn_test_maple_arange64_slots" + if arange + else "drgn_test_maple_range64_slots" + ) ].value_() n = 2 * node_slots * maple_range64_slots for i in range(n): @@ -441,9 +455,11 @@ def test_mt_for_each_three_levels_ranges_2(self): ulong_max = (1 << (sizeof(self.prog.type("unsigned long")) * 8)) - 1 for mt, arange in self.maple_trees("three_levels_ranges_2"): node_slots = self.prog[ - "drgn_test_maple_arange64_slots" - if arange - else "drgn_test_maple_range64_slots" + ( + "drgn_test_maple_arange64_slots" + if arange + else "drgn_test_maple_range64_slots" + ) ].value_() n = 2 * node_slots * maple_range64_slots self.assertIdentical( diff --git a/tools/fsrefs.py b/tools/fsrefs.py index 8249e1600..f710511b2 100755 --- a/tools/fsrefs.py +++ b/tools/fsrefs.py @@ -62,14 +62,11 @@ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> bool: if typing.TYPE_CHECKING: class Visitor(typing.Protocol): # novermin - def visit_file(self, file: Object) -> Optional[str]: - ... + def visit_file(self, file: Object) -> Optional[str]: ... - def visit_inode(self, inode: Object) -> Optional[str]: - ... + def visit_inode(self, inode: Object) -> Optional[str]: ... - def visit_path(self, path: Object) -> Optional[str]: - ... + def visit_path(self, path: Object) -> Optional[str]: ... class InodeVisitor: