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/_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/_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/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/_drgn_util/platform.py b/_drgn_util/platform.py new file mode 100644 index 000000000..ca775042d --- /dev/null +++ b/_drgn_util/platform.py @@ -0,0 +1,148 @@ +# 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 = { + "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, {}) 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( diff --git a/docs/exts/drgndoc/format.py b/docs/exts/drgndoc/format.py index c9a2e7eac..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(", /") @@ -393,10 +395,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 +453,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..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): @@ -111,7 +109,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: 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/__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..c25d94c97 --- /dev/null +++ b/drgn/helpers/experimental/kmodify.py @@ -0,0 +1,1379 @@ +# 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: 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 + ``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 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 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) + """ + 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/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/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/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/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/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_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()) 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_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/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/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/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(); 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/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 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: 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