├── drgn_tools ├── __init__.py ├── bitops.py ├── itertools.py ├── config.py ├── cmdline.py ├── taint.py ├── printk.py ├── mounts.py ├── device.py ├── logging.py ├── buddyinfo.py ├── virtutil.py ├── runq.py ├── partition.py ├── lsmod.py ├── lockup.py ├── deadlock.py ├── sys.py ├── iscsi.py └── cli.py ├── testing ├── __init__.py ├── heavyvm │ ├── __init__.py │ ├── ks │ │ ├── ol9-u6-uek7-x86_64-ks.cfg │ │ ├── ol9-u6-uek8-x86_64-ks.cfg │ │ ├── ol8-u10-uek6-x86_64-ks.cfg │ │ ├── ol8-u10-uek7-x86_64-ks.cfg │ │ ├── ol7-u9-uek4-x86_64-ks.cfg │ │ ├── ol7-u9-uek5-x86_64-ks.cfg │ │ └── ol7-u9-uek6-x86_64-ks.cfg │ └── images.py ├── litevm │ ├── __init__.py │ └── mod │ │ ├── ol7uek5x86_64 │ │ ├── 9p.ko │ │ └── 0001-9p-adapt-to-out-of-tree-build.patch │ │ └── README ├── vmcore │ ├── __init__.py │ ├── manage.py │ └── test.py ├── requirements-litevm.txt ├── requirements-vmcore.txt ├── requirements-heavyvm.txt ├── QEMU.md ├── util.py └── rpm.py ├── doc ├── changelog.md ├── drgn-tools.png ├── corelens.rst ├── installing.rst ├── index.rst ├── Makefile ├── conf.py └── code-quality.rst ├── pyproject.toml ├── requirements-dev.txt ├── setup.cfg ├── .header.txt ├── .gitignore ├── tests ├── test_md.py ├── test_sys.py ├── test_scsi.py ├── test_lockup.py ├── test_iscsi.py ├── test_multipath.py ├── test_blockinfo.py ├── test_slabinfo.py ├── test_dm.py ├── test_fsnotify.py ├── test_targetcli.py ├── test_buddyinfo.py ├── test_cmdline.py ├── test_rds.py ├── test_lsmod.py ├── test_virtutil.py ├── test_ext4_dirlock.py ├── test_sysctl.py ├── test_runq.py ├── test_kvm.py ├── test_memstate.py ├── test_nfs_tools.py ├── test_file.py ├── test_vectorinfo.py ├── test_irq.py ├── test_dentry.py ├── test_virtio.py ├── test_smp.py ├── test_module.py ├── test_nvme.py ├── test_mounts.py ├── test_kernfs_memcg.py ├── test_bt.py ├── test_partition.py ├── test_block.py ├── test_meminfo.py ├── test_numastat.py ├── test_task.py ├── test_cpuinfo.py ├── test_lock.py ├── test_workqueue.py ├── test_debuginfo.py ├── test_mm.py └── test_list_lru.py ├── man ├── Makefile └── corelens.1.scd ├── tox.ini ├── .pre-commit-config.yaml ├── Makefile ├── CONTRIBUTING.md ├── SECURITY.md ├── LICENSE.txt ├── .gitlab-ci.yml ├── .github └── workflows │ └── litevm.yml ├── setup.py ├── mkdist.py ├── drgn_tools.ini ├── README.md └── extras └── corelens.py /drgn_tools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/heavyvm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/litevm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testing/vmcore/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/changelog.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 79 3 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | drgn>=0.0.25,<0.0.30 2 | pytest<7.1 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_files = LICENSE.txt,THIRD_PARTY_LICENSES.txt 3 | -------------------------------------------------------------------------------- /doc/drgn-tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oracle-samples/drgn-tools/HEAD/doc/drgn-tools.png -------------------------------------------------------------------------------- /doc/corelens.rst: -------------------------------------------------------------------------------- 1 | CORELENS(1) 2 | =========== 3 | 4 | .. raw:: html 5 | :file: ../man/corelens.1.html 6 | -------------------------------------------------------------------------------- /testing/requirements-litevm.txt: -------------------------------------------------------------------------------- 1 | # Requirements for running the litevm tests. 2 | # Used in Github CI. 3 | pytest 4 | -------------------------------------------------------------------------------- /testing/requirements-vmcore.txt: -------------------------------------------------------------------------------- 1 | # Requirements for running the vmcore tests. 2 | # Used in Gitlab CI. 3 | pytest 4 | -------------------------------------------------------------------------------- /testing/requirements-heavyvm.txt: -------------------------------------------------------------------------------- 1 | # Requirements for running the heavyvm tests. 2 | # Used in Gitlab CI. 3 | paramiko 4 | pytest 5 | -------------------------------------------------------------------------------- /testing/litevm/mod/ol7uek5x86_64/9p.ko: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oracle-samples/drgn-tools/HEAD/testing/litevm/mod/ol7uek5x86_64/9p.ko -------------------------------------------------------------------------------- /doc/installing.rst: -------------------------------------------------------------------------------- 1 | Installing drgn-tools 2 | ===================== 3 | 4 | Installing drgn-tools is simple: 5 | 6 | .. code-block:: 7 | 8 | pip install drgn-tools 9 | -------------------------------------------------------------------------------- /.header.txt: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | *.egg-info 4 | build 5 | dist 6 | .tox 7 | .coverage 8 | config.mk 9 | venv 10 | drgn_tools/_version.py 11 | deploy_dev.sh 12 | drgntools-fio.dat 13 | testdata 14 | test.log 15 | vmcore.xml 16 | -------------------------------------------------------------------------------- /tests/test_md.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import md 4 | 5 | 6 | def test_show_md(prog): 7 | md.show_md(prog) 8 | -------------------------------------------------------------------------------- /tests/test_sys.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import sys 4 | 5 | 6 | def test_sysinfo(prog): 7 | sys.print_sysinfo(prog) 8 | -------------------------------------------------------------------------------- /tests/test_scsi.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import scsi 4 | 5 | 6 | def test_scsi(prog): 7 | scsi.print_scsi_hosts(prog) 8 | -------------------------------------------------------------------------------- /testing/litevm/mod/README: -------------------------------------------------------------------------------- 1 | 9P for UEK4 and UEK5 2 | ==================== 3 | 4 | 9P filesystem is not enabled or built for UEK4 and UEK5. Thankfully, it can be 5 | built out-of-tree very simply for UEK5. This directory contains the necessary 6 | modification and compiled 9p.ko for UEK5. 7 | -------------------------------------------------------------------------------- /tests/test_lockup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import lockup 4 | 5 | 6 | def test_lockup(prog): 7 | lockup.scan_lockup(prog) 8 | -------------------------------------------------------------------------------- /tests/test_iscsi.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import iscsi 4 | 5 | 6 | def test_iscsi(prog): 7 | iscsi.print_iscsi_sessions(prog) 8 | -------------------------------------------------------------------------------- /tests/test_multipath.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import multipath 4 | 5 | 6 | def test_show_mp(prog): 7 | multipath.show_mp(prog) 8 | -------------------------------------------------------------------------------- /tests/test_blockinfo.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import block 4 | 5 | 6 | def test_blockinfo(prog): 7 | block.print_block_devs_info(prog) 8 | -------------------------------------------------------------------------------- /tests/test_slabinfo.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import slabinfo 4 | 5 | 6 | def test_blockinfo(prog): 7 | slabinfo.print_slab_info(prog) 8 | -------------------------------------------------------------------------------- /tests/test_dm.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import dm 4 | 5 | 6 | def test_show_dm(prog): 7 | dm.show_dm(prog) 8 | dm.show_dm_table(prog) 9 | -------------------------------------------------------------------------------- /tests/test_fsnotify.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools.fsnotify import fsnotify_show 4 | 5 | 6 | def test_fsnotify(prog): 7 | fsnotify_show(prog) 8 | -------------------------------------------------------------------------------- /tests/test_targetcli.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import targetcli 4 | 5 | 6 | def test_targetcli(prog): 7 | targetcli.dump_targetcli(prog) 8 | -------------------------------------------------------------------------------- /tests/test_buddyinfo.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import buddyinfo 4 | 5 | 6 | def test_meminfo(prog): 7 | buddyinfo.show_all_zones_buddyinfo(prog) 8 | -------------------------------------------------------------------------------- /tests/test_cmdline.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import cmdline 4 | 5 | 6 | def test_cmdline(prog): 7 | cmdline.get_cmdline(prog) 8 | cmdline.show_cmdline(prog) 9 | -------------------------------------------------------------------------------- /tests/test_rds.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import rds 4 | 5 | 6 | def test_run_rds(prog): 7 | rds.report(prog) 8 | rds.rds_ib_conn_ring_info(prog, 0xDEADBEEF) 9 | -------------------------------------------------------------------------------- /tests/test_lsmod.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import drgn_tools.lsmod as lsmod 4 | 5 | 6 | def test_lsmod(prog): 7 | lsmod.print_module_summary(prog) 8 | lsmod.print_module_parameters(prog) 9 | -------------------------------------------------------------------------------- /tests/test_virtutil.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import virtutil 4 | 5 | 6 | def test_virtutil(prog): 7 | virtutil.show_cpuhp_state(prog) 8 | virtutil.show_platform(prog) 9 | -------------------------------------------------------------------------------- /tests/test_ext4_dirlock.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import ext4_dirlock 4 | 5 | 6 | def test_ext4_dirlock_scan(prog): 7 | ext4_dirlock.ext4_dirlock_scan(prog) 8 | ext4_dirlock.ext4_dirlock_scan(prog, True) 9 | -------------------------------------------------------------------------------- /tests/test_sysctl.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import drgn_tools.sysctl as sysctl 4 | 5 | 6 | def test_get_sysctl_table(prog): 7 | # smoke test 8 | sysctl_table = sysctl.get_sysctl_table(prog) 9 | assert len(sysctl_table) > 10 10 | -------------------------------------------------------------------------------- /tests/test_runq.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn import ProgramFlags 4 | 5 | import drgn_tools.runq as runq 6 | 7 | 8 | def test_run_queue(prog): 9 | if ProgramFlags.IS_LIVE & prog.flags: 10 | return 11 | runq.run_queue(prog) 12 | -------------------------------------------------------------------------------- /tests/test_kvm.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import kvm 4 | 5 | 6 | def test_kvmutil(prog): 7 | kvm.print_vm_list(prog) 8 | kvm.print_vcpu_list(prog) 9 | kvm.print_memslot_info(prog) 10 | kvm.print_ioeventfd_info(prog) 11 | kvm.print_kvmstat_info(prog) 12 | -------------------------------------------------------------------------------- /tests/test_memstate.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import pytest 4 | 5 | from drgn_tools import memstate 6 | 7 | 8 | def test_memstate(prog, kver): 9 | if kver.uek_version is not None and kver.uek_version <= 4: 10 | pytest.skip("Unsupported kernel version") 11 | memstate.run_memstate(prog) 12 | -------------------------------------------------------------------------------- /man/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | 4 | # Note: manual pages are written in the scd format, but rather than depend on 5 | # scdoc for the RPM build, we simply include the resulting roff files in git. 6 | 7 | all: corelens.1 corelens.1.html 8 | 9 | %: %.scd 10 | scdoc < $< > $@ 11 | 12 | %.html: % 13 | pandoc -f man -t html $< > $@ 14 | -------------------------------------------------------------------------------- /tests/test_nfs_tools.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import pytest 4 | 5 | import drgn_tools.nfs_tools 6 | 7 | 8 | def test_nfs(prog): 9 | try: 10 | prog.module("nfs") 11 | except LookupError: 12 | pytest.skip("NFS module not loaded") 13 | 14 | # This is just a smoke test 15 | drgn_tools.nfs_tools.nfsshow(prog) 16 | -------------------------------------------------------------------------------- /tests/test_file.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import drgn_tools.file as file 4 | 5 | 6 | def test_filecache_dump(prog): 7 | # smoke test 8 | file.filecache_dump(prog, 10, 10) 9 | 10 | 11 | def test_for_each_file_system_page_in_pagecache(prog): 12 | # smoke test 13 | fst = prog["file_systems"].next 14 | file.for_each_file_system_page_in_pagecache(fst) 15 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | drgn-tools 2 | ========== 3 | 4 | drgn-tools is the Oracle Linux Sustaining Team's collection of drgn helpers and 5 | scripts. Please use the contents below to learn about the library, and see the 6 | Git repository `here`__. 7 | 8 | __ https://github.com/oracle-samples/drgn-tools 9 | 10 | Contents 11 | -------- 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | installing.rst 17 | code-quality.rst 18 | testing.rst 19 | api.rst 20 | corelens.rst 21 | 22 | .. toctree:: 23 | :maxdepth: 1 24 | 25 | changelog.md 26 | -------------------------------------------------------------------------------- /tests/test_vectorinfo.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import pytest 4 | 5 | from drgn_tools import vectorinfo 6 | 7 | 8 | def test_vectorinfo(prog, kver): 9 | if kver.arch != "x86_64": 10 | pytest.skip("Only x86_64 is supported") 11 | if kver.uek_version is not None and kver.uek_version < 6: 12 | pytest.skip("UEK6 or later is required") 13 | vectorinfo.print_vector_matrix(prog) 14 | vectorinfo.print_vectors(prog, True) 15 | -------------------------------------------------------------------------------- /drgn_tools/bitops.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from typing import Iterable 4 | 5 | 6 | def for_each_bit_set(val: int, depth: int = 64) -> Iterable[int]: 7 | """ 8 | List offset of each set bit in one word 9 | 10 | :param val: value of the world 11 | :param depth: maximum bit to be checked 12 | :returns: each set bit as one iterator 13 | """ 14 | for index in range(depth): 15 | if val & 0x1: 16 | yield index 17 | val >>= 1 18 | -------------------------------------------------------------------------------- /tests/test_irq.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import irq 4 | 5 | 6 | def test_print_all_irqs(prog): 7 | irq.print_all_irqs(prog) 8 | 9 | 10 | def test_print_irqs_affinities(prog): 11 | irq.print_irqs_affinities(prog) 12 | 13 | 14 | def test_show_each_cpu_irq_stats(prog): 15 | irq.show_each_cpu_irq_stats(prog) 16 | 17 | 18 | def test_show_irq_stats(prog): 19 | irq.show_irq_stats(prog) 20 | 21 | 22 | def test_show_cpu_irq_stats(prog): 23 | irq.show_cpu_irq_stats(prog, 0) 24 | -------------------------------------------------------------------------------- /tests/test_dentry.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import drgn_tools.dentry as dentry 4 | from drgn_tools.itertools import take 5 | 6 | LIMIT = 5 7 | 8 | 9 | def test_for_each_dentry_in_hashtable(prog): 10 | it = dentry.for_each_dentry_in_hashtable(prog) 11 | for d in take(LIMIT, it): 12 | assert d.type_.type_name() == "struct dentry *" 13 | 14 | 15 | def test_list_dentries_in_hashtable(prog): 16 | dentry.list_dentries_in_hashtable(prog, LIMIT) 17 | 18 | 19 | def test_ls(prog): 20 | dentry.ls(prog, "/", None, 0) 21 | -------------------------------------------------------------------------------- /tests/test_virtio.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import virtio 4 | 5 | 6 | def test_virtio_show(prog): 7 | print("===== DUMP ALL VIRTIO DEVICES ONLY =====") 8 | # Print all virtio device 9 | virtio.virtio_show(prog) 10 | 11 | print("===== DUMP ALL VIRTIO DEVICES AND VRING =====") 12 | # Print all virtio device also all vrings 13 | virtio.virtio_show(prog, show_vq=True) 14 | 15 | print("===== DUMP virtio0 AND VRING =====") 16 | # Print virtio0 device also vrings 17 | virtio.virtio_show(prog, show_vq=True, vd_name="virtio0") 18 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tests/test_smp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import pytest 4 | from drgn.helpers.linux.cpumask import for_each_possible_cpu 5 | 6 | from drgn_tools import smp 7 | 8 | 9 | def test_is_cur_csd_pending(prog): 10 | for cpu in for_each_possible_cpu(prog): 11 | print(smp.is_cur_csd_pending(prog, cpu)) 12 | 13 | 14 | def test_is_call_single_queue_empty(prog): 15 | for cpu in for_each_possible_cpu(prog): 16 | print(smp.is_call_single_queue_empty(prog, cpu)) 17 | 18 | 19 | @pytest.mark.skip_live # flaky on live systems 20 | def test_dump_smp_ipi_state(prog): 21 | smp.dump_smp_ipi_state(prog) 22 | -------------------------------------------------------------------------------- /tests/test_module.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import pytest 4 | 5 | from drgn_tools.module import module_exports 6 | 7 | 8 | @pytest.fixture 9 | def common_mod(prog): 10 | COMMON_MODS = [ 11 | "nf_nat", 12 | "ib_core", 13 | "9pnet", 14 | "libcrc32c", 15 | ] 16 | for name in COMMON_MODS: 17 | try: 18 | return prog.module(name).object 19 | except LookupError: 20 | pass 21 | pytest.fail("No common kernel module found in program") 22 | 23 | 24 | def test_module_exports(prog, common_mod): 25 | # smoke test 26 | assert module_exports(common_mod) 27 | -------------------------------------------------------------------------------- /tests/test_nvme.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn_tools import nvme 4 | 5 | 6 | def test_nvme_show(prog): 7 | print("===== Dump NVMe namespace info =====") 8 | nvme.show_ns_info(prog) 9 | 10 | print("===== Dump NVMe controller info =====") 11 | nvme.show_ctrl_info(prog) 12 | 13 | print("===== Dump NVMe firmware info =====") 14 | nvme.show_firmware_info(prog) 15 | 16 | print("===== Dump NVMe queue info =====") 17 | nvme.show_queue_info(prog) 18 | 19 | print("===== Dump NVMe queue map =====") 20 | nvme.show_queue_map(prog) 21 | 22 | print("===== Dump NVMe MSI mask =====") 23 | nvme.show_msi_mask(prog) 24 | -------------------------------------------------------------------------------- /drgn_tools/itertools.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from typing import Any 4 | from typing import Generator 5 | from typing import Iterable 6 | from typing import TypeVar 7 | 8 | T = TypeVar("T") 9 | 10 | 11 | def count(it: Iterable[Any]) -> int: 12 | """Count the contents of any iterator (consumes it)""" 13 | return sum(1 for _ in it) 14 | 15 | 16 | def take(n: int, it: Iterable[T]) -> Generator[T, None, None]: 17 | """ 18 | Yield at most the first ``n`` items from ``it`` 19 | 20 | :param n: maximum number of elements to yield 21 | :param it: iterator to yield from 22 | """ 23 | for i, e in enumerate(it): 24 | if i == n: 25 | break 26 | yield e 27 | -------------------------------------------------------------------------------- /drgn_tools/config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | """ 4 | Configuration support 5 | 6 | Ideally drgn-tools shouldn't require much in the way of configuration, but some 7 | things like debuginfo fetching are better if they can be configured. 8 | """ 9 | import configparser 10 | from functools import lru_cache 11 | from pathlib import Path 12 | 13 | __all__ = ("get_config",) 14 | 15 | 16 | CONFIG_PATHS = [ 17 | Path("/etc/drgn_tools.ini"), 18 | Path.home() / ".config/drgn_tools.ini", 19 | ] 20 | 21 | 22 | @lru_cache(maxsize=1) 23 | def get_config() -> configparser.ConfigParser: 24 | """ 25 | Return drgn-tools configuration information 26 | """ 27 | config = configparser.ConfigParser() 28 | config.read(CONFIG_PATHS) 29 | return config 30 | -------------------------------------------------------------------------------- /tests/test_mounts.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from operator import itemgetter 4 | 5 | import pytest 6 | 7 | from drgn_tools import mounts 8 | 9 | 10 | def test_print_mounts(prog): 11 | mounts.mountinfo(prog) 12 | 13 | 14 | def get_proc_mounts(): 15 | fields_0_2_1 = itemgetter(0, 2, 1) 16 | proc_mount_table = list() 17 | with open("/proc/mounts", "r") as f: 18 | for line in f.readlines(): 19 | field_0, field_2, field_1 = fields_0_2_1(line.split()) 20 | proc_mount_table.append([field_0, field_2, field_1]) 21 | return proc_mount_table 22 | 23 | 24 | @pytest.mark.skip_vmcore("*") 25 | def test_show_mounts(prog): 26 | prog_table = mounts.get_mountinfo(prog) 27 | proc_table = get_proc_mounts() 28 | 29 | for row in proc_table: 30 | assert row in prog_table 31 | -------------------------------------------------------------------------------- /drgn_tools/cmdline.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | """ 4 | Helpers for command line 5 | """ 6 | import argparse 7 | 8 | from drgn import Program 9 | 10 | from drgn_tools.corelens import CorelensModule 11 | 12 | 13 | def get_cmdline(prog: Program) -> str: 14 | """ 15 | Returns the kernel command line 16 | """ 17 | str_cmdline = prog["saved_command_line"] 18 | return str_cmdline.string_().decode("utf-8") 19 | 20 | 21 | def show_cmdline(prog: Program) -> None: 22 | """ 23 | Prints the kernel command line 24 | """ 25 | str_cmdline = get_cmdline(prog) 26 | print(str_cmdline) 27 | 28 | 29 | class CmdLine(CorelensModule): 30 | """Display the kernel command line""" 31 | 32 | name = "cmdline" 33 | 34 | def run(self, prog: Program, args: argparse.Namespace) -> None: 35 | show_cmdline(prog) 36 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | 4 | [tox] 5 | envlist = py36,py37,py38,py39,py310,py311 6 | skip_missing_interpreters = true 7 | 8 | [testenv] 9 | # Allows us to use the system drgn, if it is present 10 | sitepackages = true 11 | deps = 12 | -rrequirements-dev.txt 13 | commands = 14 | python -m pytest --junitxml=test.xml -o junit_logging=all {posargs} 15 | passenv = DRGNTOOLS_*, GITLAB_CI, GITHUB_ACTIONS 16 | 17 | [testenv:docs] 18 | deps = 19 | -rrequirements-dev.txt 20 | sphinx 21 | sphinx-autodoc-typehints 22 | sphinx-reference-rename 23 | myst-parser 24 | commands = sphinx-build -d "{toxworkdir}/docs_doctree" doc "{toxworkdir}/docs_out" --color -W -bhtml {posargs} 25 | python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))' 26 | 27 | [testenv:runner] 28 | deps = 29 | -r testing/requirements.txt 30 | commands = {posargs} 31 | passenv = VMCORE_*, GITLAB_CI, GITHUB_ACTIONS 32 | 33 | [flake8] 34 | max-line-length = 80 35 | -------------------------------------------------------------------------------- /testing/heavyvm/ks/ol9-u6-uek7-x86_64-ks.cfg: -------------------------------------------------------------------------------- 1 | # Generated by Anaconda 34.25.0.29 2 | # Generated by pykickstart v3.32 3 | #version=OL9 4 | # Use text-based install 5 | text 6 | 7 | # After finishing, poweroff 8 | poweroff 9 | 10 | repo --name="AppStream" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL9/appstream/x86_64/ 11 | repo --name="UEK7" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL9/UEKR7/x86_64/ 12 | 13 | %addon com_redhat_kdump --enable --reserve-mb='auto' 14 | 15 | %end 16 | 17 | # Keyboard layouts 18 | keyboard --xlayouts='us' 19 | # System language 20 | lang en_US.UTF-8 21 | 22 | # Network information 23 | network --bootproto=dhcp --device=ens3 --ipv6=auto --activate 24 | 25 | # Use network installation 26 | url --url="http://yum.oracle.com/repo/OracleLinux/OL9/baseos/latest/x86_64/" 27 | 28 | %packages 29 | @^minimal-environment 30 | tar 31 | 32 | %end 33 | 34 | # Run the Setup Agent on first boot 35 | firstboot --enable 36 | 37 | # Generated using Blivet version 3.4.0 38 | ignoredisk --only-use=sda 39 | autopart 40 | # Partition clearing information 41 | clearpart --none --initlabel 42 | 43 | # System timezone 44 | timezone America/Los_Angeles --utc 45 | 46 | # Root password 47 | rootpw password 48 | -------------------------------------------------------------------------------- /testing/heavyvm/ks/ol9-u6-uek8-x86_64-ks.cfg: -------------------------------------------------------------------------------- 1 | # Generated by Anaconda 34.25.0.29 2 | # Generated by pykickstart v3.32 3 | #version=OL9 4 | # Use text-based install 5 | text 6 | 7 | # After finishing, poweroff 8 | poweroff 9 | 10 | repo --name="AppStream" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL9/appstream/x86_64/ 11 | repo --name="UEK8" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL9/UEKR8/x86_64/ 12 | 13 | %addon com_redhat_kdump --enable --reserve-mb='auto' 14 | 15 | %end 16 | 17 | # Keyboard layouts 18 | keyboard --xlayouts='us' 19 | # System language 20 | lang en_US.UTF-8 21 | 22 | # Network information 23 | network --bootproto=dhcp --device=ens3 --ipv6=auto --activate 24 | 25 | # Use network installation 26 | url --url="http://yum.oracle.com/repo/OracleLinux/OL9/baseos/latest/x86_64/" 27 | 28 | %packages 29 | @^minimal-environment 30 | tar 31 | 32 | %end 33 | 34 | # Run the Setup Agent on first boot 35 | firstboot --enable 36 | 37 | # Generated using Blivet version 3.4.0 38 | ignoredisk --only-use=sda 39 | autopart 40 | # Partition clearing information 41 | clearpart --none --initlabel 42 | 43 | # System timezone 44 | timezone America/Los_Angeles --utc 45 | 46 | # Root password 47 | rootpw password 48 | -------------------------------------------------------------------------------- /tests/test_kernfs_memcg.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import drgn 4 | 5 | from drgn_tools import kernfs_memcg as kernfs_memcg 6 | 7 | 8 | def test_dump_page_cache_pages_pinning_cgroups(prog: drgn.Program) -> None: 9 | kernfs_memcg.dump_page_cache_pages_pinning_cgroups(prog, 10, 1000000) 10 | 11 | 12 | def test_dump_memcgroup_hierarchy(prog: drgn.Program) -> None: 13 | kernfs_memcg.dump_memcgroup_hierarchy(prog) 14 | 15 | 16 | def test_kernfs_node_of_memcgroup(prog: drgn.Program) -> None: 17 | count = 0 18 | for kn in kernfs_memcg.for_each_kernfs_node(prog): 19 | if kernfs_memcg.kernfs_node_of_memcgroup(kn): 20 | count = count + 1 21 | if count >= 5: 22 | print("Found 5 memcgroup, kernfs_node objects.") 23 | break 24 | 25 | 26 | def test_get_num_active_mem_cgroups(prog: drgn.Program) -> None: 27 | count = kernfs_memcg.get_num_active_mem_cgroups(prog) 28 | print(f"number of active memcgroups: {count}\n") 29 | 30 | 31 | def test_get_num_dying_mem_cgroups(prog: drgn.Program) -> None: 32 | count = kernfs_memcg.get_num_dying_mem_cgroups(prog) 33 | print(f"number of dying memcgroups: {count}\n") 34 | -------------------------------------------------------------------------------- /drgn_tools/taint.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | """ 4 | Contains definitions for kernel taint values 5 | """ 6 | from enum import IntEnum 7 | 8 | from drgn.helpers.common.format import decode_flags 9 | 10 | 11 | class Taint(IntEnum): 12 | """ 13 | Kernel and module taint flags 14 | 15 | These flags are not recorded in any enum type, only preprocessor 16 | definitions, since they need to be used in assembly listings in the kernel. 17 | Record them here. They can be found at ``include/linux/panic.h`` or for 18 | older kernels, ``include/linux/kernel.h``. 19 | """ 20 | 21 | PROPRIETARY_MODULE = 0 22 | FORCED_MODULE = 1 23 | CPU_OUT_OF_SPEC = 2 24 | FORCED_RMMOD = 3 25 | MACHINE_CHECK = 4 26 | BAD_PAGE = 5 27 | USER = 6 28 | DIE = 7 29 | OVERRIDDEN_ACPI_TABLE = 8 30 | WARN = 9 31 | CRAP = 10 32 | FIRMWARE_WORKAROUND = 11 33 | OOT_MODULE = 12 34 | UNSIGNED_MODULE = 13 35 | SOFTLOCKUP = 14 36 | LIVEPATCH = 15 37 | AUX = 16 38 | RANDSTRUCT = 17 39 | FLAGS_COUNT = 18 40 | 41 | @classmethod 42 | def decode(cls, value: int) -> str: 43 | fields = [(v.name, v) for v in cls] 44 | return decode_flags(value, fields, bit_numbers=True) 45 | -------------------------------------------------------------------------------- /testing/litevm/mod/ol7uek5x86_64/0001-9p-adapt-to-out-of-tree-build.patch: -------------------------------------------------------------------------------- 1 | From adeaab63e32ae085f298f8dfb04a654f1ee742a6 Mon Sep 17 00:00:00 2001 2 | From: Stephen Brennan 3 | Date: Thu, 3 Aug 2023 09:49:52 -0700 4 | Subject: [PATCH] 9p: adapt to out-of-tree build 5 | 6 | The UEK5 kernel configuration does not include CONFIG_9P_FS. Update the 7 | Makefile so that this directory can build out-of-tree. Simply boot a 8 | UEK5 kernel, with kernel-uek-devel installed, and run "make" in this 9 | directory to create an out-of-tree 9p.ko. 10 | 11 | Signed-off-by: Stephen Brennan 12 | --- 13 | fs/9p/Makefile | 11 ++++++++--- 14 | 1 file changed, 8 insertions(+), 3 deletions(-) 15 | 16 | diff --git a/fs/9p/Makefile b/fs/9p/Makefile 17 | index e7800a5c7395..73881b20dd0a 100644 18 | --- a/fs/9p/Makefile 19 | +++ b/fs/9p/Makefile 20 | @@ -1,5 +1,7 @@ 21 | # SPDX-License-Identifier: GPL-2.0 22 | -obj-$(CONFIG_9P_FS) := 9p.o 23 | +TARGET ?= $(shell uname -r) 24 | + 25 | +obj-m := 9p.o 26 | 27 | 9p-objs := \ 28 | vfs_super.o \ 29 | @@ -13,5 +15,8 @@ obj-$(CONFIG_9P_FS) := 9p.o 30 | fid.o \ 31 | xattr.o 32 | 33 | -9p-$(CONFIG_9P_FSCACHE) += cache.o 34 | -9p-$(CONFIG_9P_FS_POSIX_ACL) += acl.o 35 | +all: 36 | + make -C /lib/modules/$(TARGET)/build M=$(PWD) modules 37 | + 38 | +clean: 39 | + make -C /lib/modules/$(TARGET)/build M=$(PWD) clean 40 | -- 41 | 2.39.2 42 | 43 | -------------------------------------------------------------------------------- /testing/heavyvm/ks/ol8-u10-uek6-x86_64-ks.cfg: -------------------------------------------------------------------------------- 1 | #version=OL8 2 | # Use text-based install 3 | text 4 | 5 | # After finishing, poweroff 6 | poweroff 7 | 8 | repo --name="AppStream" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL8/appstream/x86_64/ 9 | repo --name="UEK6" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL8/UEKR6/x86_64/ 10 | 11 | %packages 12 | @^minimal-environment 13 | kexec-tools 14 | tar 15 | 16 | %end 17 | 18 | # Keyboard layouts 19 | keyboard --xlayouts='us' 20 | # System language 21 | lang en_US.UTF-8 22 | 23 | # Network information 24 | network --bootproto=dhcp --device=ens3 --ipv6=auto --activate 25 | network --hostname=localhost.localdomain 26 | 27 | # Use network installation 28 | url --url="http://yum.oracle.com/repo/OracleLinux/OL8/baseos/latest/x86_64/" 29 | 30 | # Run the Setup Agent on first boot 31 | firstboot --enable 32 | 33 | ignoredisk --only-use=sda 34 | autopart 35 | # Partition clearing information 36 | clearpart --none --initlabel 37 | 38 | # System timezone 39 | timezone America/Los_Angeles --isUtc 40 | 41 | # Root password 42 | rootpw password 43 | 44 | %addon com_redhat_kdump --enable --reserve-mb='auto' 45 | 46 | %end 47 | 48 | %anaconda 49 | pwpolicy root --minlen=6 --minquality=1 --notstrict --nochanges --notempty 50 | pwpolicy user --minlen=6 --minquality=1 --notstrict --nochanges --emptyok 51 | pwpolicy luks --minlen=6 --minquality=1 --notstrict --nochanges --notempty 52 | %end 53 | -------------------------------------------------------------------------------- /testing/heavyvm/ks/ol8-u10-uek7-x86_64-ks.cfg: -------------------------------------------------------------------------------- 1 | #version=OL8 2 | # Use text-based install 3 | text 4 | 5 | # After finishing, poweroff 6 | poweroff 7 | 8 | repo --name="AppStream" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL8/appstream/x86_64/ 9 | repo --name="UEK7" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL8/UEKR7/x86_64/ 10 | 11 | %packages 12 | @^minimal-environment 13 | kexec-tools 14 | tar 15 | 16 | %end 17 | 18 | # Keyboard layouts 19 | keyboard --xlayouts='us' 20 | # System language 21 | lang en_US.UTF-8 22 | 23 | # Network information 24 | network --bootproto=dhcp --device=ens3 --ipv6=auto --activate 25 | network --hostname=localhost.localdomain 26 | 27 | # Use network installation 28 | url --url="http://yum.oracle.com/repo/OracleLinux/OL8/baseos/latest/x86_64/" 29 | 30 | # Run the Setup Agent on first boot 31 | firstboot --enable 32 | 33 | ignoredisk --only-use=sda 34 | autopart 35 | # Partition clearing information 36 | clearpart --none --initlabel 37 | 38 | # System timezone 39 | timezone America/Los_Angeles --isUtc 40 | 41 | # Root password 42 | rootpw password 43 | 44 | %addon com_redhat_kdump --enable --reserve-mb='auto' 45 | 46 | %end 47 | 48 | %anaconda 49 | pwpolicy root --minlen=6 --minquality=1 --notstrict --nochanges --notempty 50 | pwpolicy user --minlen=6 --minquality=1 --notstrict --nochanges --emptyok 51 | pwpolicy luks --minlen=6 --minquality=1 --notstrict --nochanges --notempty 52 | %end 53 | -------------------------------------------------------------------------------- /tests/test_bt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import drgn 4 | from drgn.helpers.linux import cpu_curr 5 | 6 | from drgn_tools import bt 7 | 8 | 9 | def test_bt_smoke(prog): 10 | if prog.flags & drgn.ProgramFlags.IS_LIVE: 11 | thread = prog.thread(1) 12 | else: 13 | try: 14 | thread = prog.crashed_thread() 15 | except Exception: 16 | # On x86_64 uek4, the sysrq does not actually trigger a panic, it 17 | # triggers a NULL pointer dereference, which triggers an "oops", and 18 | # that directly calls into the kexec code without ever calling 19 | # panic(). Thus, panic_cpu == -1, and prog.crashing_cpu() page 20 | # faults because it tries to index the wrong per-cpu variables. 21 | # To handle this, use the x86_64-specific "crashing_cpu" variable. 22 | # Note that on some drgn versions we get "FaultError", others we get 23 | # "Exception". So we just catch Exception here. 24 | pid = cpu_curr(prog, prog["crashing_cpu"]).pid.value_() 25 | thread = prog.thread(pid) 26 | 27 | print("===== STACK TRACE [show_vars=False] =====") 28 | bt.bt(thread, show_vars=False) 29 | print("===== STACK TRACE [show_vars=True] =====") 30 | bt.bt(thread, show_vars=True) 31 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | exclude_types: [diff] 9 | - id: end-of-file-fixer 10 | exclude_types: [diff] 11 | - id: check-yaml 12 | - id: check-added-large-files 13 | exclude: .ko$ 14 | - id: mixed-line-ending 15 | args: ['--fix=lf'] 16 | - repo: https://github.com/psf/black 17 | rev: "23.7.0" 18 | hooks: 19 | - id: black 20 | - repo: https://github.com/pycqa/flake8 21 | rev: "6.1.0" 22 | hooks: 23 | - id: flake8 24 | args: ["--ignore=E203,W503,E501"] 25 | - repo: https://github.com/pre-commit/mirrors-mypy 26 | rev: "v1.4.1" 27 | hooks: 28 | - id: mypy 29 | additional_dependencies: 30 | - types-setuptools 31 | - types-paramiko 32 | - repo: https://github.com/asottile/reorder_python_imports 33 | rev: v3.10.0 34 | hooks: 35 | - id: reorder-python-imports 36 | - repo: https://github.com/netromdk/vermin 37 | rev: v1.6.0 38 | hooks: 39 | - id: vermin 40 | args: ['-t=3.6-', '--violations', '--backport', 'dataclasses', '--eval-annotations'] 41 | - repo: https://github.com/leoll2/copyright_notice_precommit 42 | rev: 0.1.1 43 | hooks: 44 | - id: copyright-notice 45 | exclude: ((man|doc|testing/heavyvm/ks)/.*|.*requirements.*\.txt) 46 | args: [--notice=.header.txt] 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | 4 | VERSION=$(shell grep 'RELEASE_VERSION =' setup.py | sed s/\"//g | awk '{print($$3)}') 5 | 6 | PYTHON ?= python3 7 | 8 | # This allows you to add custom configuration: 9 | # TARGET: set the target for "make rsync" 10 | # It also allows creating custom targets, e.g. for development 11 | -include config.mk 12 | # The TARGET variable could be either a hostname or a hostname:path. Previously 13 | # the makefile expected just a hostname, and it always placed the resulting 14 | # directory at $TARGET:drgn_tools/. But it's nice to have flexibility to change 15 | # the target directory, so we only add that default path if there's not already 16 | # a path specified. 17 | ifneq (,$(TARGET)) 18 | ifeq (,$(findstring :,$(TARGET))) 19 | override TARGET := $(TARGET):drgn_tools/ 20 | else 21 | endif 22 | endif 23 | 24 | .PHONY: litevm-test 25 | litevm-test: 26 | $(PYTHON) -m testing.litevm.vm 27 | 28 | 29 | .PHONY: vmcore-test 30 | vmcore-test: 31 | $(PYTHON) -m testing.vmcore test 32 | 33 | 34 | .PHONY: test 35 | test: litevm-test vmcore-test 36 | 37 | .PHONY: docs 38 | docs: 39 | @$(PYTHON) -m tox -e docs 40 | 41 | drgn_tools/_version.py: 42 | $(PYTHON) setup.py -V 43 | 44 | .PHONY: rsync 45 | rsync: drgn_tools/_version.py 46 | @if [ -z "$(TARGET)" ]; then echo "error: TARGET unspecified. Either set it in config.mk, or use\nmake TARGET=hostname rsync"; exit 1; fi 47 | rsync -avz --exclude "__pycache__" --exclude ".git" --exclude ".mypy_cache" ./drgn_tools $(TARGET) 48 | -------------------------------------------------------------------------------- /tests/test_partition.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from pathlib import Path 4 | 5 | import pytest 6 | from drgn.helpers.linux import for_each_partition 7 | 8 | from drgn_tools import partition 9 | 10 | 11 | def test_partitioninfo(prog): 12 | partition.print_partition_info(prog) 13 | 14 | 15 | @pytest.mark.skip_vmcore("*") 16 | def test_block_helpers(prog): 17 | partitions_sysfs = set() 18 | partitions_drgn = set() 19 | 20 | def sysfs_int(p: Path, s: str, default: int = 0) -> int: 21 | n = p / s 22 | if not n.exists(): 23 | return default 24 | return int(n.open().read().strip()) 25 | 26 | path = Path("/sys/class/block") 27 | for block_dev_dir in path.iterdir(): 28 | size = sysfs_int(block_dev_dir, "size") 29 | ro = bool(sysfs_int(block_dev_dir, "ro")) 30 | start = sysfs_int(block_dev_dir, "start") 31 | dev = (block_dev_dir / "dev").open().read().strip() 32 | maj, min = map(int, dev.split(":")) 33 | name = block_dev_dir.name 34 | partitions_sysfs.add((name, size, ro, start, maj, min)) 35 | 36 | for part in for_each_partition(prog): 37 | info = partition.get_partition_info(part) 38 | partitions_drgn.add( 39 | ( 40 | info.name, 41 | info.nr_sects, 42 | info.ro, 43 | info.start_sect, 44 | info.major, 45 | info.minor, 46 | ) 47 | ) 48 | 49 | assert partitions_sysfs == partitions_drgn 50 | -------------------------------------------------------------------------------- /testing/QEMU.md: -------------------------------------------------------------------------------- 1 | # Qemu Build on OL8 2 | 3 | Unfortunately QEMU on OL8 seems to be missing several useful tools for emulation 4 | and sharing data: 5 | 6 | - NVME emulation 7 | - Block devices mapped to host 8 | 9 | To make it easier to use QEMU features to test drgn-tools, we build the latest 10 | qemu and install it to the system. Here's the steps I used: 11 | 12 | ```bash 13 | mkdir ~/work && cd ~/work 14 | 15 | sudo yum-config-manager --enable ol8_codeready_builder 16 | sudo yum-config-manager --enable ol8_developer_EPEL 17 | 18 | sudo yum remove dtrace # conflicted with a systemtap dependency 19 | 20 | # https://wiki.qemu.org/Hosts/Linux#Fedora_Linux_.2F_Debian_GNU_Linux_.2F_Ubuntu_Linux_.2F_Linux_Mint_distributions 21 | sudo yum install git glib2-devel libfdt-devel pixman-devel zlib-devel bzip2 ninja-build python3 22 | sudo yum install libaio-devel libcap-ng-devel libiscsi-devel capstone-devel \ 23 | gtk3-devel vte291-devel ncurses-devel \ 24 | libseccomp-devel nettle-devel libattr-devel libjpeg-devel \ 25 | brlapi-devel libgcrypt-devel lzo-devel snappy-devel \ 26 | librdmacm-devel libibverbs-devel cyrus-sasl-devel libpng-devel \ 27 | libuuid-devel pulseaudio-libs-devel curl-devel libssh-devel \ 28 | systemtap-sdt-devel libusbx-devel 29 | # Removed libsdl2-devel from the above^ 30 | 31 | # For usermode networking 32 | sudo yum install libslirp-devel 33 | 34 | wget https://download.qemu.org/qemu-7.2.0.tar.xz 35 | tar xvJf qemu-7.2.0.tar.xz 36 | mkdir build 37 | cd build 38 | 39 | ../qemu-7.2.0/configure --target-list=x86_64-softmmu 40 | make -j20 41 | sudo make install 42 | ``` 43 | -------------------------------------------------------------------------------- /testing/heavyvm/ks/ol7-u9-uek4-x86_64-ks.cfg: -------------------------------------------------------------------------------- 1 | #version=DEVEL 2 | # System authorization information 3 | auth --enableshadow --passalgo=sha512 4 | # Use text-based install 5 | text 6 | # After finishing, poweroff 7 | poweroff 8 | # Run the Setup Agent on first boot 9 | firstboot --enable 10 | ignoredisk --only-use=sda 11 | # Keyboard layouts 12 | keyboard --vckeymap=us --xlayouts='us' 13 | # System language 14 | lang en_US.UTF-8 15 | 16 | # Network information 17 | network --bootproto=dhcp --device=ens3 --ipv6=auto --activate 18 | network --hostname=localhost.localdomain 19 | 20 | repo --name="OptionalLatest" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL7/optional/latest/x86_64/ 21 | repo --name="UEK4" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL7/UEKR4/x86_64/ 22 | # Use network installation 23 | url --url="http://yum.oracle.com/repo/OracleLinux/OL7/latest/x86_64/" 24 | # Root password 25 | rootpw password 26 | # System services 27 | services --disabled="chronyd" 28 | # System timezone 29 | timezone America/Los_Angeles --isUtc --nontp 30 | # System bootloader configuration 31 | bootloader --append=" crashkernel=auto" --location=mbr --boot-drive=sda 32 | autopart --type=lvm 33 | # Partition clearing information 34 | clearpart --all --initlabel --drives=sda 35 | 36 | %packages 37 | @^minimal 38 | @core 39 | kexec-tools 40 | tar 41 | 42 | %end 43 | 44 | %addon com_redhat_kdump --enable --reserve-mb='auto' 45 | 46 | %end 47 | 48 | %anaconda 49 | pwpolicy root --minlen=6 --minquality=1 --notstrict --nochanges --notempty 50 | pwpolicy user --minlen=6 --minquality=1 --notstrict --nochanges --emptyok 51 | pwpolicy luks --minlen=6 --minquality=1 --notstrict --nochanges --notempty 52 | %end 53 | -------------------------------------------------------------------------------- /testing/heavyvm/ks/ol7-u9-uek5-x86_64-ks.cfg: -------------------------------------------------------------------------------- 1 | #version=DEVEL 2 | # System authorization information 3 | auth --enableshadow --passalgo=sha512 4 | # Use text-based install 5 | text 6 | # After finishing, poweroff 7 | poweroff 8 | # Run the Setup Agent on first boot 9 | firstboot --enable 10 | ignoredisk --only-use=sda 11 | # Keyboard layouts 12 | keyboard --vckeymap=us --xlayouts='us' 13 | # System language 14 | lang en_US.UTF-8 15 | 16 | # Network information 17 | network --bootproto=dhcp --device=ens3 --ipv6=auto --activate 18 | network --hostname=localhost.localdomain 19 | 20 | repo --name="OptionalLatest" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL7/optional/latest/x86_64/ 21 | repo --name="UEK5" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL7/UEKR5/x86_64/ 22 | # Use network installation 23 | url --url="http://yum.oracle.com/repo/OracleLinux/OL7/latest/x86_64/" 24 | # Root password 25 | rootpw password 26 | # System services 27 | services --disabled="chronyd" 28 | # System timezone 29 | timezone America/Los_Angeles --isUtc --nontp 30 | # System bootloader configuration 31 | bootloader --append=" crashkernel=auto" --location=mbr --boot-drive=sda 32 | autopart --type=lvm 33 | # Partition clearing information 34 | clearpart --all --initlabel --drives=sda 35 | 36 | %packages 37 | @^minimal 38 | @core 39 | kexec-tools 40 | tar 41 | 42 | %end 43 | 44 | %addon com_redhat_kdump --enable --reserve-mb='auto' 45 | 46 | %end 47 | 48 | %anaconda 49 | pwpolicy root --minlen=6 --minquality=1 --notstrict --nochanges --notempty 50 | pwpolicy user --minlen=6 --minquality=1 --notstrict --nochanges --emptyok 51 | pwpolicy luks --minlen=6 --minquality=1 --notstrict --nochanges --notempty 52 | %end 53 | -------------------------------------------------------------------------------- /testing/heavyvm/ks/ol7-u9-uek6-x86_64-ks.cfg: -------------------------------------------------------------------------------- 1 | #version=DEVEL 2 | # System authorization information 3 | auth --enableshadow --passalgo=sha512 4 | # Use text-based install 5 | text 6 | # After finishing, poweroff 7 | poweroff 8 | # Run the Setup Agent on first boot 9 | firstboot --enable 10 | ignoredisk --only-use=sda 11 | # Keyboard layouts 12 | keyboard --vckeymap=us --xlayouts='us' 13 | # System language 14 | lang en_US.UTF-8 15 | 16 | # Network information 17 | network --bootproto=dhcp --device=ens3 --ipv6=auto --activate 18 | network --hostname=localhost.localdomain 19 | 20 | repo --name="OptionalLatest" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL7/optional/latest/x86_64/ 21 | repo --name="UEK6" --baseurl=http://yum.oracle.com/repo/OracleLinux/OL7/UEKR6/x86_64/ 22 | # Use network installation 23 | url --url="http://yum.oracle.com/repo/OracleLinux/OL7/latest/x86_64/" 24 | # Root password 25 | rootpw password 26 | # System services 27 | services --disabled="chronyd" 28 | # System timezone 29 | timezone America/Los_Angeles --isUtc --nontp 30 | # System bootloader configuration 31 | bootloader --append=" crashkernel=auto" --location=mbr --boot-drive=sda 32 | autopart --type=lvm 33 | # Partition clearing information 34 | clearpart --all --initlabel --drives=sda 35 | 36 | %packages 37 | @^minimal 38 | @core 39 | kexec-tools 40 | tar 41 | 42 | %end 43 | 44 | %addon com_redhat_kdump --enable --reserve-mb='auto' 45 | 46 | %end 47 | 48 | %anaconda 49 | pwpolicy root --minlen=6 --minquality=1 --notstrict --nochanges --notempty 50 | pwpolicy user --minlen=6 --minquality=1 --notstrict --nochanges --emptyok 51 | pwpolicy luks --minlen=6 --minquality=1 --notstrict --nochanges --notempty 52 | %end 53 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this repository 2 | 3 | We welcome your contributions! There are multiple ways to contribute. 4 | 5 | ## Opening issues 6 | 7 | For bugs or enhancement requests, please file a GitHub issue unless it's 8 | security related. When filing a bug remember that the better written the bug is, 9 | the more likely it is to be fixed. If you think you've found a security 10 | vulnerability, do not raise a GitHub issue and follow the instructions in our 11 | [security policy](./SECURITY.md). 12 | 13 | ## Contributing code 14 | 15 | We welcome your code contributions. Before submitting code via a pull request, 16 | you will need to have signed the [Oracle Contributor Agreement][OCA] (OCA) and 17 | your commits need to include the following line using the name and e-mail 18 | address you used to sign the OCA: 19 | 20 | ```text 21 | Signed-off-by: Your Name 22 | ``` 23 | 24 | This can be automatically added to pull requests by committing with `--sign-off` 25 | or `-s`, e.g. 26 | 27 | ```text 28 | git commit --signoff 29 | ``` 30 | 31 | Only pull requests from committers that can be verified as having signed the OCA 32 | can be accepted. 33 | 34 | ## Pull request process 35 | 36 | Please see the documentation 37 | [here](https://oracle-samples.github.io/drgn-tools/) for contribution 38 | guidelines, including code quality expectations and pull request steps. 39 | 40 | ## Code of conduct 41 | 42 | Follow the [Golden Rule](https://en.wikipedia.org/wiki/Golden_Rule). If you'd 43 | like more specific guidelines, see the [Contributor Covenant Code of Conduct][COC]. 44 | 45 | [OCA]: https://oca.opensource.oracle.com 46 | [COC]: https://www.contributor-covenant.org/version/1/4/code-of-conduct/ 47 | -------------------------------------------------------------------------------- /drgn_tools/printk.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | """ 4 | Additional helpers for printk utilities 5 | """ 6 | import argparse 7 | import os 8 | import subprocess 9 | import sys 10 | from typing import Optional 11 | 12 | from drgn import Program 13 | from drgn.helpers.linux.printk import get_dmesg 14 | 15 | from drgn_tools.corelens import CorelensModule 16 | 17 | 18 | FALLBACK_PAGER = "less" 19 | 20 | 21 | def dmesg(prog: Program, pager: Optional[str] = None) -> None: 22 | """ 23 | Display the kernel log in a pager 24 | 25 | The pager is selected in the following manner. First, if the pager argument 26 | is provided, that is used. Second, if the ``PAGER`` environment variable is 27 | defined, that is used. Finally, the fallback value of ``less`` is used. 28 | 29 | :param prog: Program to retrieve log for 30 | :param pager: Override pager selection 31 | """ 32 | if pager is None: 33 | real_pager = os.getenv("PAGER", FALLBACK_PAGER) 34 | else: 35 | real_pager = pager 36 | log = get_dmesg(prog) 37 | subprocess.run([real_pager], check=True, input=log) 38 | 39 | 40 | class DmesgModule(CorelensModule): 41 | """Display the kernel log""" 42 | 43 | name = "dmesg" 44 | 45 | def run(self, prog: Program, args: argparse.Namespace) -> None: 46 | # Avoid the overhead of decoding and then re-encoding the bytes: just 47 | # write the bytes directly to stdout. Also, avoid any encoding errors. 48 | # There's no guaranteed encoding for the kernel log anyway. 49 | sys.stdout.buffer.write(get_dmesg(prog)) 50 | print() 51 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting security vulnerabilities 2 | 3 | Oracle values the independent security research community and believes that 4 | responsible disclosure of security vulnerabilities helps us ensure the security 5 | and privacy of all our users. 6 | 7 | Please do NOT raise a GitHub Issue to report a security vulnerability. If you 8 | believe you have found a security vulnerability, please submit a report to 9 | [secalert_us@oracle.com][1] preferably with a proof of concept. Please review 10 | some additional information on [how to report security vulnerabilities to Oracle][2]. 11 | We encourage people who contact Oracle Security to use email encryption using 12 | [our encryption key][3]. 13 | 14 | We ask that you do not use other channels or contact the project maintainers 15 | directly. 16 | 17 | Non-vulnerability related security issues including ideas for new or improved 18 | security features are welcome on GitHub Issues. 19 | 20 | ## Security updates, alerts and bulletins 21 | 22 | Security updates will be released on a regular cadence. Many of our projects 23 | will typically release security fixes in conjunction with the 24 | Oracle Critical Patch Update program. Additional 25 | information, including past advisories, is available on our [security alerts][4] 26 | page. 27 | 28 | ## Security-related information 29 | 30 | We will provide security related information such as a threat model, considerations 31 | for secure use, or any known security issues in our documentation. Please note 32 | that labs and sample code are intended to demonstrate a concept and may not be 33 | sufficiently hardened for production use. 34 | 35 | [1]: mailto:secalert_us@oracle.com 36 | [2]: https://www.oracle.com/corporate/security-practices/assurance/vulnerability/reporting.html 37 | [3]: https://www.oracle.com/security-alerts/encryptionkey.html 38 | [4]: https://www.oracle.com/security-alerts/ 39 | -------------------------------------------------------------------------------- /tests/test_block.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import os 4 | import shutil 5 | import subprocess 6 | import time 7 | from pathlib import Path 8 | 9 | import pytest 10 | from drgn import FaultError 11 | 12 | from drgn_tools import block 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def fio(prog_type): 17 | if not shutil.which("fio"): 18 | pytest.skip("fio is not available") 19 | if "DRGNTOOLS_BLOCK_TEST_DIR" in os.environ: 20 | path = Path(os.environ["DRGNTOOLS_BLOCK_TEST_DIR"]) 21 | else: 22 | path = Path.cwd() 23 | path = path / "drgntools-fio.dat" 24 | proc = subprocess.Popen( 25 | [ 26 | "fio", 27 | f"--filename={path}", 28 | "--direct=1", 29 | "--rw=randrw", 30 | "--bs=4k", 31 | "--ioengine=libaio", 32 | "--iodepth=128", 33 | "--size=100m", 34 | "--runtime=120", 35 | "--numjobs=4", 36 | "--time_based", 37 | "--group_reporting", 38 | "--name=iotest", 39 | ] 40 | ) 41 | try: 42 | # say what you will about sleep-sync, it does the job normally 43 | time.sleep(5) 44 | yield 45 | finally: 46 | if proc.poll() is not None: 47 | pytest.fail("The fio process died before all tests completed!") 48 | proc.terminate() 49 | proc.wait() 50 | 51 | 52 | @pytest.mark.skip_vmcore("*") 53 | def test_dump_inflight_io(prog, fio): 54 | # Retry this test a few times since it is flaky on a live system 55 | for _ in range(3): 56 | try: 57 | block.dump_inflight_io(prog) 58 | break 59 | except FaultError: 60 | pass 61 | else: 62 | pytest.fail("Inflight I/O failed 3 times") 63 | -------------------------------------------------------------------------------- /drgn_tools/mounts.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import argparse 4 | from typing import List 5 | 6 | import drgn.helpers.linux.fs 7 | 8 | from drgn_tools.corelens import CorelensModule 9 | from drgn_tools.table import print_table 10 | 11 | 12 | def get_mountinfo(prog: drgn.Program) -> List[List[str]]: 13 | """ 14 | Get all the mount points from the vmcore 15 | 16 | :param prog: drgn program 17 | :returns: List of mount points 18 | """ 19 | 20 | mount_table = list() 21 | 22 | mounts = prog["init_task"].nsproxy.mnt_ns 23 | for mnt in drgn.helpers.linux.fs.for_each_mount(mounts): 24 | devname = mnt.mnt_devname 25 | sb = mnt.mnt.mnt_sb 26 | fstype = sb.s_type.name.string_() 27 | if sb.s_subtype: 28 | fstype += b"." + sb.s_subtype.string_() 29 | mntpt = drgn.helpers.linux.fs.d_path( 30 | mnt.mnt_parent.mnt.address_of_(), mnt.mnt_mountpoint 31 | ) 32 | 33 | mount_stats = [ 34 | devname.string_().decode("utf-8"), 35 | fstype.decode("utf-8"), 36 | mntpt.decode("utf-8"), 37 | ] 38 | mount_table.append(mount_stats) 39 | return mount_table 40 | 41 | 42 | def mountinfo(prog: drgn.Program) -> None: 43 | """ 44 | Print all the mount points from the vmcore 45 | 46 | :param prog: drgn program 47 | :returns: None 48 | """ 49 | mnt_tbl = get_mountinfo(prog) 50 | mnt_tbl.insert(0, ["-------", "------", "-------"]) 51 | mnt_tbl.insert(0, ["DEVNAME", "TYPE", "DIRNAME"]) 52 | print_table(mnt_tbl) 53 | 54 | 55 | class Mounts(CorelensModule): 56 | """Print info about all mount points""" 57 | 58 | name = "mounts" 59 | 60 | def run(self, prog: drgn.Program, args: argparse.Namespace) -> None: 61 | mountinfo(prog) 62 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023, Oracle and/or its affiliates. 2 | 3 | The Universal Permissive License (UPL), Version 1.0 4 | 5 | Subject to the condition set forth below, permission is hereby granted to any 6 | person obtaining a copy of this software, associated documentation and/or data 7 | (collectively the "Software"), free of charge and under any and all copyright 8 | rights in the Software, and any and all patent rights owned or freely 9 | licensable by each licensor hereunder covering either (i) the unmodified 10 | Software as contributed to or provided by such licensor, or (ii) the Larger 11 | Works (as defined below), to deal in both 12 | 13 | (a) the Software, and 14 | (b) any piece of software and/or hardware listed in the 15 | lrgrwrks.txt file if one is included with the Software (each a "Larger 16 | Work" to which the Software is contributed by such licensors), 17 | 18 | without restriction, including without limitation the rights to copy, create 19 | derivative works of, display, perform, and distribute the Software and make, 20 | use, sell, offer for sale, import, export, have made, and have sold the 21 | Software and the Larger Work(s), and to sublicense the foregoing rights on 22 | either these or other terms. 23 | 24 | This license is subject to the following condition: 25 | The above copyright notice and either this complete permission notice or at 26 | a minimum a reference to the UPL must be included in all copies or 27 | substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | SOFTWARE. 36 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | vmtest: 2 | # Virtual machine tests run in parallel and take a fair bit of memory. Use 3 | # resource_group to limit the concurrency of these tests so only one runs at a 4 | # time. 5 | resource_group: VM 6 | script: 7 | - python -m venv venv --system-site-packages 8 | - venv/bin/pip install -r testing/requirements-heavyvm.txt 9 | - git archive HEAD -o archive.tar.gz 10 | - mkdir -p tmp/overlays tmp/info 11 | # Whenever changes to the VM images are made, the "generation number" should 12 | # be incremented. We keep around the current and the prior generation, so 13 | # that older branches will continue to test against the older generation of 14 | # VMs, rather than failing. Note that even the stable branch should update 15 | # to the latest generation of VMs promptly, and developers should rebase 16 | # their branches as soon as possible. This system just allows for a little 17 | # bit of slack in the process. 18 | - venv/bin/python -m testing.heavyvm.runner --image-dir /var/drgn-tools/images-gen01 --vm-info-dir tmp/info --overlay-dir tmp/overlays --tarball archive.tar.gz 19 | artifacts: 20 | when: always 21 | paths: 22 | - heavyvm.xml 23 | reports: 24 | junit: heavyvm.xml 25 | 26 | vmcore DWARF: 27 | script: 28 | - python -m venv venv --system-site-packages 29 | - venv/bin/pip install -r testing/requirements-vmcore.txt 30 | - venv/bin/python -m testing.vmcore.test -j 4 --core-directory /var/drgn-tools/vmcores 31 | artifacts: 32 | when: always 33 | paths: 34 | - vmcore.xml 35 | reports: 36 | junit: vmcore.xml 37 | 38 | vmcore CTF: 39 | script: 40 | - python -m venv venv --system-site-packages 41 | - venv/bin/pip install -r testing/requirements-vmcore.txt 42 | - venv/bin/python -m testing.vmcore.test -j 4 --ctf --core-directory /var/drgn-tools/vmcores 43 | artifacts: 44 | when: always 45 | paths: 46 | - vmcore.xml 47 | reports: 48 | junit: vmcore.xml 49 | -------------------------------------------------------------------------------- /tests/test_meminfo.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import drgn 4 | from drgn import ProgramFlags 5 | 6 | from drgn_tools import meminfo 7 | 8 | 9 | def test_meminfo(prog): 10 | meminfo.show_all_meminfo(prog) 11 | 12 | if not (ProgramFlags.IS_LIVE & prog.flags): 13 | return 14 | 15 | page_shift = prog.constant("PAGE_SHIFT").value_() 16 | 17 | # Get mm statistics from the live vmcore. 18 | corelens_mm_stats = meminfo.get_all_meminfo(prog) 19 | 20 | # Parse mm statistics from /proc/meminfo. 21 | proc_mm_stats = {} 22 | f = open("/proc/meminfo", "r") 23 | lines = f.readlines() 24 | for line in lines: 25 | try: 26 | key, value = line.split(":") 27 | key, value = key.strip(), value.strip() 28 | if "kB" in value: 29 | value = int(value[:-2].strip()) 30 | proc_mm_stats[key] = value 31 | except Exception: 32 | continue 33 | 34 | if prog.platform.arch == drgn.Architecture.X86_64: 35 | test_exact_match_mm_stats = [ 36 | "MemTotal", 37 | "SwapTotal", 38 | "CommitLimit", 39 | "VmallocTotal", 40 | "CmaTotal", 41 | ] 42 | elif prog.platform.arch == drgn.Architecture.AARCH64: 43 | test_exact_match_mm_stats = [ 44 | "MemTotal", 45 | "SwapTotal", 46 | "CommitLimit", 47 | "CmaTotal", 48 | ] 49 | else: 50 | raise Exception("Target vmcore's architecture is not supported.") 51 | 52 | # These meminfo statistics in ``corelens_mm_stats`` are in kB 53 | mm_stats_in_kb = ["KernelStack", "VmallocTotal", "HardwareCorrupted"] 54 | 55 | for item in test_exact_match_mm_stats: 56 | if item not in proc_mm_stats: 57 | assert item not in corelens_mm_stats 58 | else: 59 | assert item in corelens_mm_stats 60 | 61 | if item in mm_stats_in_kb: 62 | val_kb = corelens_mm_stats[item] 63 | else: 64 | val_kb = corelens_mm_stats[item] << (page_shift - 10) 65 | assert val_kb == proc_mm_stats[item] 66 | -------------------------------------------------------------------------------- /drgn_tools/device.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | """ 4 | Helpers for working with the Linux kernel device model 5 | """ 6 | from drgn import Object 7 | from drgn.helpers.linux.list import list_for_each_entry 8 | 9 | 10 | def bus_to_subsys(bus_type: Object) -> Object: 11 | """ 12 | Return the ``struct subsys_private *`` corresponding to a 13 | ``struct bus_type *``. See the kernel function of the same name. 14 | """ 15 | if hasattr(bus_type, "p"): 16 | return bus_type.p 17 | 18 | # Since v6.3, commit d2bf38c088e0d ("driver core: remove private pointer 19 | # from struct bus_type"), the private pointer is gone. We now need to lookup 20 | # subsys_private by iterating over the bus list and finding the one related 21 | # to this. 22 | bus_kset = bus_type.prog_["bus_kset"] 23 | for subsys in list_for_each_entry( 24 | "struct subsys_private", 25 | bus_kset.list.address_of_(), 26 | "subsys.kobj.entry", 27 | ): 28 | if subsys.bus == bus_type: 29 | return subsys 30 | bus_name = bus_type.name.string_().decode() 31 | raise ValueError(f"Could not find subsys_private for bus_type {bus_name}") 32 | 33 | 34 | def class_to_subsys(class_: Object) -> Object: 35 | """ 36 | Return the ``struct subsys_private *`` corresponding to a 37 | ``struct class *``. See the kernel function of the same name. 38 | """ 39 | if hasattr(class_, "p"): 40 | return class_.p 41 | 42 | # Since v6.4, commit 2df418cf4b720 ("driver core: class: remove subsystem 43 | # private pointer from struct class"), the private pointer is gone. We now 44 | # need to lookup subsys_private by iterating over the class list and finding 45 | # the one related to this. 46 | class_kset = class_.prog_["class_kset"] 47 | for subsys in list_for_each_entry( 48 | "struct subsys_private", 49 | class_kset.list.address_of_(), 50 | "subsys.kobj.entry", 51 | ): 52 | if subsys.member_("class") == class_: 53 | return subsys 54 | class_name = class_.name.string_().decode() 55 | raise ValueError(f"Could not find subsys_private for class {class_name}") 56 | -------------------------------------------------------------------------------- /drgn_tools/logging.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | """ 4 | Helpers for logging with some added context info. 5 | """ 6 | import contextlib 7 | import logging 8 | import typing as t 9 | 10 | 11 | class PrependedLoggerAdapter(logging.LoggerAdapter): 12 | __context: t.List[t.Tuple[str, t.Any]] 13 | 14 | def __init__(self, *args, **kwargs): 15 | super().__init__(*args, *kwargs) 16 | self.__context = [] 17 | 18 | @contextlib.contextmanager 19 | def add_context(self, **kwargs: t.Any) -> t.Iterator[None]: 20 | self.__context.extend(kwargs.items()) 21 | try: 22 | yield 23 | finally: 24 | del self.__context[-len(kwargs) :] 25 | 26 | def process( 27 | self, message: str, kwargs: t.MutableMapping[str, t.Any] 28 | ) -> t.Tuple[str, t.MutableMapping[str, str]]: 29 | if not self.__context: 30 | return message, kwargs 31 | pfx = " ".join(f"[{k}={str(v)}]" for k, v in self.__context) 32 | return ( 33 | f"{pfx} {message}", 34 | kwargs, 35 | ) 36 | 37 | 38 | def get_logger(name: str) -> PrependedLoggerAdapter: 39 | return PrependedLoggerAdapter(logging.getLogger(name), {}) 40 | 41 | 42 | class FilterMissingDebugSymbolsMessages(logging.Filter): 43 | def filter(self, rec: logging.LogRecord): 44 | # Drgn C log messages are logged with "%s" and the entirety of the 45 | # message as a C string by C code. We'd like to check the contents of 46 | # the message string, but we don't want to cause _every_ log message to 47 | # get formatted, even if we aren't enabled for them. Use short-circuit 48 | # evaluation to ensure that the log message looks like a drgn C log 49 | # message prior to calling getMessage(), which should then be quite 50 | # cheap. 51 | # 52 | # Return True if the message is to be emitted. Thus, return False when 53 | # we want to filter the message, i.e. if the message matches the 54 | # "missing debugging symbols for" text. 55 | if rec.msg != "%s": 56 | return True 57 | msg = rec.getMessage() 58 | return not ( 59 | msg.startswith("missing debugging symbols for") 60 | or msg.startswith("... missing ") 61 | ) 62 | -------------------------------------------------------------------------------- /tests/test_numastat.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn import ProgramFlags 4 | 5 | from drgn_tools import meminfo 6 | from drgn_tools import numastat 7 | 8 | 9 | def test_numastat(prog): 10 | numastat.show_all_nodes_meminfo(prog) 11 | 12 | 13 | def test_numastat_all_nodes_meminfo(prog): 14 | if not (ProgramFlags.IS_LIVE & prog.flags): 15 | return 16 | 17 | page_shift = prog.constant("PAGE_SHIFT").value_() 18 | numastat_per_node_mm_stats = [] 19 | active_nodes = meminfo.get_active_numa_nodes(prog) 20 | num_active_nodes = len(active_nodes) 21 | 22 | for node in active_nodes: 23 | node_mm_stats = numastat.get_per_node_meminfo(prog, node) 24 | numastat_per_node_mm_stats.append(node_mm_stats) 25 | 26 | sys_fs_per_node_mm_stats = [] 27 | # Parse mm statistics from /sys/devices/system/node/node*/meminfo. 28 | for node_id in range(num_active_nodes): 29 | sys_fs_mm_stats = {} 30 | 31 | f = open(f"/sys/devices/system/node/node{node_id}/meminfo", "r") 32 | lines = f.readlines() 33 | for line in lines: 34 | try: 35 | key, value = line.split(":") 36 | key, value = key.strip(), value.strip() 37 | if "Node" in key: 38 | key = key.split(" ")[-1].strip() 39 | if "kB" in value: 40 | value = int(value[:-2].strip()) 41 | else: 42 | value = int(value) 43 | sys_fs_mm_stats[key] = value 44 | except Exception: 45 | continue 46 | 47 | sys_fs_per_node_mm_stats.append(sys_fs_mm_stats) 48 | 49 | for node_id in range(num_active_nodes): 50 | sys_fs_mm_stats = sys_fs_per_node_mm_stats[node_id] 51 | numastat_mm_stats = numastat_per_node_mm_stats[node_id] 52 | 53 | for name, value in sys_fs_mm_stats.items(): 54 | assert name in numastat_mm_stats 55 | 56 | # The result in sys fs is in kB. 57 | if name == "MemTotal": 58 | numastat_value_kb = numastat_mm_stats[name] << ( 59 | page_shift - 10 60 | ) 61 | assert value == numastat_value_kb 62 | 63 | # The result in sys fs is in number of pages. 64 | if name == "HugePages_Total": 65 | assert value == numastat_mm_stats[name] 66 | -------------------------------------------------------------------------------- /tests/test_task.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import pytest 4 | from drgn.helpers.linux.pid import find_task 5 | from drgn.helpers.linux.pid import for_each_task 6 | 7 | from drgn_tools import task 8 | from drgn_tools.workqueue import for_each_worker 9 | 10 | 11 | def test_show_taskinfo(prog): 12 | print("===== Task information in their last arrival order =====") 13 | task.show_tasks_last_runtime(for_each_task(prog)) 14 | print("===== Display task information =====") 15 | task.show_taskinfo(prog, for_each_task(prog)) 16 | 17 | 18 | def test_user_kernel_threads(prog): 19 | init = find_task(prog, 1) 20 | assert task.is_user(init) 21 | assert task.is_group_leader(init) 22 | 23 | for worker in for_each_worker(prog): 24 | if worker.task: 25 | kworker = worker.task 26 | break 27 | else: 28 | pytest.fail("no kworker available to test kthread helper") 29 | 30 | assert task.is_kthread(kworker) 31 | assert not task.is_user(kworker) 32 | assert task.is_group_leader(kworker) 33 | 34 | 35 | def test_count_interruptible_tasks(prog): 36 | task.count_tasks_in_state(prog, "TASK_INTERRUPTIBLE") 37 | 38 | 39 | def test_count_uninterruptible_tasks(prog): 40 | task.count_tasks_in_state(prog, "TASK_UNINTERRUPTIBLE") 41 | 42 | 43 | def test_count_stopped_tasks(prog): 44 | task.count_tasks_in_state(prog, "TASK_STOPPED") 45 | 46 | 47 | def test_count_traced_tasks(prog): 48 | task.count_tasks_in_state(prog, "TASK_TRACED") 49 | 50 | 51 | def test_count_exit_dead_tasks(prog): 52 | task.count_tasks_in_state(prog, "EXIT_DEAD") 53 | 54 | 55 | def test_count_exit_zombie_tasks(prog): 56 | task.count_tasks_in_state(prog, "EXIT_ZOMBIE") 57 | 58 | 59 | def test_count_parked_tasks(prog): 60 | task.count_tasks_in_state(prog, "TASK_PARKED") 61 | 62 | 63 | def test_count_dead_tasks(prog): 64 | task.count_tasks_in_state(prog, "TASK_DEAD") 65 | 66 | 67 | def test_count_wakekill_tasks(prog): 68 | task.count_tasks_in_state(prog, "TASK_WAKEKILL") 69 | 70 | 71 | def test_count_waking_tasks(prog): 72 | task.count_tasks_in_state(prog, "TASK_WAKING") 73 | 74 | 75 | def test_count_noload_tasks(prog): 76 | task.count_tasks_in_state(prog, "TASK_NOLOAD") 77 | 78 | 79 | def test_count_new_tasks(prog): 80 | task.count_tasks_in_state(prog, "TASK_NEW") 81 | 82 | 83 | def test_count_killable_tasks(prog): 84 | task.count_tasks_in_state(prog, "TASK_KILLABLE") 85 | 86 | 87 | def test_count_idle_tasks(prog): 88 | task.count_tasks_in_state(prog, "TASK_IDLE") 89 | -------------------------------------------------------------------------------- /drgn_tools/buddyinfo.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | """ 4 | Helpers for dumping details about the per-zone buddy page allocator 5 | """ 6 | import argparse 7 | from typing import Any 8 | from typing import List 9 | 10 | from drgn import Object 11 | from drgn import Program 12 | 13 | from drgn_tools.corelens import CorelensModule 14 | from drgn_tools.meminfo import for_each_node_zone 15 | from drgn_tools.meminfo import get_active_numa_nodes 16 | from drgn_tools.table import print_table 17 | 18 | 19 | __all__ = ("show_all_zones_buddyinfo", "get_per_zone_buddyinfo") 20 | 21 | 22 | def get_per_zone_buddyinfo(zone: Object): 23 | """ 24 | Pages are managed in memory blocks: each memory zone has an array 25 | ``zone->free_area`` that tracks blocks of all orders. This function parses 26 | and returns a list that records numbers of free blocks. 27 | 28 | :param zone: ``struct zone *`` of the target zone 29 | :returns: A list that records numbers of memory blocks of all orders 30 | """ 31 | free_area = zone.free_area.read_() 32 | return [x.nr_free.value_() for x in free_area] 33 | 34 | 35 | def show_all_zones_buddyinfo(prog: Program): 36 | """Dump numbers of free memory blocks in each zone's buddy allocator.""" 37 | 38 | buddyinfo_table: List[List[Any]] = [] 39 | 40 | active_nodes = get_active_numa_nodes(prog) 41 | for node_id in range(len(active_nodes)): 42 | node_name = f"Node {node_id}" 43 | for zone in for_each_node_zone(prog, active_nodes[node_id]): 44 | zone_name = zone.name.string_().decode("utf-8") 45 | zone_free_blocks = get_per_zone_buddyinfo(zone) 46 | 47 | # For the first iteration, add the table's header 48 | if len(buddyinfo_table) == 0: 49 | max_order = len(zone_free_blocks) 50 | buddyinfo_table.append( 51 | ["Node ID", "Zone", "Order 0"] 52 | + [f"{x: >7}" for x in range(1, max_order)] 53 | ) 54 | 55 | buddyinfo_table.append( 56 | [node_name, zone_name] + [f"{x: >7}" for x in zone_free_blocks] 57 | ) 58 | 59 | # Output 60 | print("Per-zone buddy allocator's information:") 61 | print_table(buddyinfo_table) 62 | 63 | 64 | class BuddyInfoModule(CorelensModule): 65 | """This module shows details about the per-zone buddy page allocator.""" 66 | 67 | name = "buddyinfo" 68 | 69 | def run(self, prog: Program, args: argparse.Namespace) -> None: 70 | # Dump buddyinfo-like statistics for all memory zones. 71 | show_all_zones_buddyinfo(prog) 72 | -------------------------------------------------------------------------------- /testing/heavyvm/images.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | """ 4 | Describes the "heavy" images used for running tests 5 | """ 6 | import dataclasses 7 | from typing import List 8 | 9 | 10 | @dataclasses.dataclass 11 | class ImageInfo: 12 | ol: int 13 | ol_update: int 14 | uek: int 15 | arch: str 16 | iso_url: str 17 | rpms: List[str] 18 | 19 | @property 20 | def iso_name(self) -> str: 21 | return f"ol{self.ol}-u{self.ol_update}-{self.arch}-boot-uek.iso" 22 | 23 | @property 24 | def name(self) -> str: 25 | return f"ol{self.ol}-u{self.ol_update}-uek{self.uek}-{self.arch}" 26 | 27 | @property 28 | def image_name(self) -> str: 29 | return f"{self.name}.qcow" 30 | 31 | @property 32 | def ks_name(self) -> str: 33 | return f"{self.name}-ks.cfg" 34 | 35 | 36 | CONFIGURATIONS = [ 37 | # OL9: UEK 8 38 | ImageInfo( 39 | 9, 40 | 6, 41 | 8, 42 | "x86_64", 43 | "https://yum.oracle.com/ISOS/OracleLinux/OL9/u6/x86_64/OracleLinux-R9-U6-x86_64-boot-uek.iso", # noqa 44 | ["drgn-0.0.32-1.0.1.el9.x86_64.rpm"], 45 | ), 46 | # OL9: UEK 7 47 | ImageInfo( 48 | 9, 49 | 6, 50 | 7, 51 | "x86_64", 52 | "https://yum.oracle.com/ISOS/OracleLinux/OL9/u6/x86_64/OracleLinux-R9-U6-x86_64-boot-uek.iso", # noqa 53 | ["drgn-0.0.32-1.0.1.el9.x86_64.rpm"], 54 | ), 55 | # OL8: UEK 6-7 56 | ImageInfo( 57 | 8, 58 | 10, 59 | 7, 60 | "x86_64", 61 | "https://yum.oracle.com/ISOS/OracleLinux/OL8/u10/x86_64/OracleLinux-R8-U10-x86_64-boot-uek.iso", # noqa 62 | ["drgn-0.0.32-1.0.1.el8.x86_64.rpm"], 63 | ), 64 | ImageInfo( 65 | 8, 66 | 10, 67 | 6, 68 | "x86_64", 69 | "https://yum.oracle.com/ISOS/OracleLinux/OL8/u10/x86_64/OracleLinux-R8-U10-x86_64-boot-uek.iso", # noqa 70 | ["drgn-0.0.32-1.0.1.el8.x86_64.rpm"], 71 | ), 72 | # OL7: UEK 4-6 73 | ImageInfo( 74 | 7, 75 | 9, 76 | 6, 77 | "x86_64", 78 | "https://yum.oracle.com/ISOS/OracleLinux/OL7/u9/x86_64/x86_64-boot-uek.iso", # noqa 79 | ["drgn-0.0.32-1.0.1.el7.x86_64.rpm"], 80 | ), 81 | ImageInfo( 82 | 7, 83 | 9, 84 | 5, 85 | "x86_64", 86 | "https://yum.oracle.com/ISOS/OracleLinux/OL7/u9/x86_64/x86_64-boot-uek.iso", # noqa 87 | ["drgn-0.0.32-1.0.1.el7.x86_64.rpm"], 88 | ), 89 | ImageInfo( 90 | 7, 91 | 9, 92 | 4, 93 | "x86_64", 94 | "https://yum.oracle.com/ISOS/OracleLinux/OL7/u9/x86_64/x86_64-boot-uek.iso", # noqa 95 | ["drgn-0.0.32-1.0.1.el7.x86_64.rpm"], 96 | ), 97 | ] 98 | NAME_TO_CONFIGURATION = {i.name: i for i in CONFIGURATIONS} 99 | -------------------------------------------------------------------------------- /testing/util.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import os 4 | import time 5 | import xml.etree.ElementTree as ET 6 | from contextlib import contextmanager 7 | from pathlib import Path 8 | from typing import Generator 9 | from typing import Optional 10 | 11 | BASE_DIR = (Path(__file__).parent.parent / "testdata").absolute() 12 | """ 13 | Default directory where all testing data object should go. Should 14 | be overridden on the CLI where necessary. 15 | """ 16 | 17 | 18 | def gitlab_section_start( 19 | name: str, text: str, collapsed: bool = False 20 | ) -> None: 21 | """ 22 | Begin a section for gitlab CI output. 23 | """ 24 | if collapsed: 25 | name += "[collapsed=true]" 26 | print( 27 | f"\x1b[0Ksection_start:{int(time.time())}:{name}\r\x1b[0K{text}", 28 | flush=True, 29 | ) 30 | 31 | 32 | def gitlab_section_end(name: str) -> None: 33 | """ 34 | Close the section for gitlab CI output. 35 | """ 36 | print(f"\x1b[0Ksection_end:{int(time.time())}:{name}\r\x1b[0K", flush=True) 37 | 38 | 39 | def github_section_start( 40 | name: str, text: str, collapsed: bool = False 41 | ) -> None: 42 | print(f"::group::{text}", flush=True) 43 | 44 | 45 | def github_section_end(name: str) -> None: 46 | print("::endgroup::", flush=True) 47 | 48 | 49 | if os.environ.get("GITHUB_ACTIONS"): 50 | ci_section_start = github_section_start 51 | ci_section_end = github_section_end 52 | elif os.environ.get("GITLAB_CI"): 53 | ci_section_start = gitlab_section_start 54 | ci_section_end = gitlab_section_end 55 | else: 56 | 57 | def ci_section_start( 58 | name: str, text: str, collapsed: bool = False 59 | ) -> None: 60 | pass 61 | 62 | def ci_section_end(name: str) -> None: 63 | pass 64 | 65 | 66 | @contextmanager 67 | def ci_section( 68 | name: str, text: str, collapsed: bool = False 69 | ) -> Generator[None, None, None]: 70 | ci_section_start(name, text, collapsed=collapsed) 71 | try: 72 | yield 73 | finally: 74 | ci_section_end(name) 75 | 76 | 77 | def combine_junit_xml( 78 | main: Optional[ET.ElementTree], 79 | new: ET.ElementTree, 80 | ) -> ET.ElementTree: 81 | """ 82 | Combine the JUnit XML files created by pytest. While we could use the 83 | "junitxml" PyPI package for this, all we really need to know about JUnit XML 84 | is that there is a root element named "testsuites", child elements of tag 85 | type "testsuite", and then children of type "testcase". 86 | 87 | So, to combine two files, just add its test cases into the root element. 88 | """ 89 | if main is None: 90 | return new 91 | 92 | assert main.getroot().tag == "testsuites" 93 | assert new.getroot().tag == "testsuites" 94 | 95 | main.getroot().extend(new.getroot()) 96 | return main 97 | -------------------------------------------------------------------------------- /.github/workflows/litevm.yml: -------------------------------------------------------------------------------- 1 | name: litevm test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | - labeled 13 | 14 | jobs: 15 | commit-hooks: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-python@v4 20 | name: Set up Python 21 | with: 22 | python-version: '3.12' 23 | - name: Install pre-commit 24 | run: pip install pre-commit 25 | - name: Run pre-commit hooks 26 | run: pre-commit run --all-files --show-diff-on-failure 27 | test: 28 | runs-on: ubuntu-22.04 29 | strategy: 30 | matrix: 31 | # Our minimum supported version of Python is 3.6, used by Oracle Linux 7 32 | # & 8. Ideally we would run tests on that, along with Python 3.9 for 33 | # Oracle Linux 9, and maybe Python 3.12 for an upcoming Oracle Linux 10. 34 | # However, practicality rules here. The binutils provided in Ubuntu 35 | # 20.04 is not recent enough for our libctf usage, but 20.04 is the only 36 | # remaining Ubuntu image with Python 3.6 available in Github actions. 37 | # So we have to eliminate Python 3.6 from our test matrix. Other CI 38 | # tests do run on Python 3.6, and there is the "vermin" pre-commit hook 39 | # which detects code incompatible with 3.6. 40 | python-minor: ["9", "12"] 41 | fail-fast: false 42 | steps: 43 | - uses: actions/checkout@v4 44 | - name: Set up Python 45 | uses: actions/setup-python@v4 46 | with: 47 | python-version: 3.${{ matrix.python-minor }} 48 | - name: Install dependencies 49 | run: | 50 | sudo apt-get update 51 | sudo apt-get install qemu-kvm zstd gzip bzip2 cpio busybox-static fio \ 52 | autoconf automake check gcc git liblzma-dev \ 53 | libelf-dev libdw-dev libtool make pkgconf zlib1g-dev \ 54 | binutils-dev rpm2cpio 55 | - name: Setup test environment 56 | # Pinned virtualenv and tox are for the last versions which support 57 | # detecting Python 3.6 and running tests on it. 58 | run: | 59 | python -m venv venv 60 | venv/bin/pip install -r testing/requirements-litevm.txt 61 | venv/bin/pip install setuptools 62 | - name: Build and install drgn with CTF support 63 | run: | 64 | cd .. 65 | git clone https://github.com/brenns10/drgn -b ctf_0.0.33 66 | cd drgn 67 | ../drgn-tools/venv/bin/pip install . 68 | - name: Run tests 69 | env: 70 | DRGN_TOOLS_ALLOW_MISSING_LATEST: ${{ contains(github.event.pull_request.labels.*.name, 'allow-missing-latest') && '1' || '0' }} 71 | run: | 72 | venv/bin/python -m testing.litevm.vm --delete-after-test --with-ctf 73 | -------------------------------------------------------------------------------- /tests/test_cpuinfo.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | from drgn import Architecture 4 | from drgn import ProgramFlags 5 | 6 | from drgn_tools import cpuinfo 7 | 8 | 9 | def test_cpuinfo(prog): 10 | cpuinfo.print_cpu_info(prog) 11 | 12 | if not (ProgramFlags.IS_LIVE & prog.flags): 13 | return 14 | 15 | if ( 16 | prog.platform.arch == Architecture.X86_64 17 | or prog.platform.arch == Architecture.AARCH64 18 | ): 19 | file = open("/proc/cpuinfo", "r") 20 | lines = file.readlines() 21 | file.close() 22 | cpu_data_from_proc = dict() 23 | for line in lines: 24 | try: 25 | title, value = line.split(":") 26 | title, value = title.strip(), value.strip() 27 | cpu_data_from_proc[title] = value 28 | except Exception: 29 | continue 30 | 31 | if prog.platform.arch == Architecture.X86_64: 32 | cpu_data_from_corelens = cpuinfo.x86_get_cpu_info(prog) 33 | 34 | assert ( 35 | cpu_data_from_corelens["CPU VENDOR"] 36 | == cpu_data_from_proc["vendor_id"] 37 | ) 38 | assert ( 39 | cpu_data_from_corelens["MODEL NAME"] 40 | == cpu_data_from_proc["model name"] 41 | ) 42 | assert ( 43 | str(cpu_data_from_corelens["CPU FAMILY"]) 44 | == cpu_data_from_proc["cpu family"] 45 | ) 46 | if "microcode" in cpu_data_from_proc: 47 | assert ( 48 | str(cpu_data_from_corelens["MICROCODE"]) 49 | == cpu_data_from_proc["microcode"] 50 | ) 51 | assert cpu_data_from_corelens["CSTATES"] == prog["max_cstate"] 52 | assert ( 53 | cpu_data_from_corelens["CPU FLAGS"] == cpu_data_from_proc["flags"] 54 | ) 55 | assert ( 56 | cpu_data_from_corelens["BUG FLAGS"] == cpu_data_from_proc["bugs"] 57 | ) 58 | 59 | if prog.platform.arch == Architecture.AARCH64: 60 | cpu_data_from_corelens = cpuinfo.aarch64_get_cpu_info(prog) 61 | assert ( 62 | cpu_data_from_corelens["Features"] 63 | == cpu_data_from_proc["Features"] 64 | ) 65 | assert ( 66 | cpu_data_from_corelens["CPU Implementer"] 67 | == cpu_data_from_proc["CPU implementer"] 68 | ) 69 | assert ( 70 | str(cpu_data_from_corelens["CPU Architecture"]) 71 | == cpu_data_from_proc["CPU architecture"] 72 | ) 73 | assert ( 74 | cpu_data_from_corelens["CPU Variant"] 75 | == cpu_data_from_proc["CPU variant"] 76 | ) 77 | assert ( 78 | cpu_data_from_corelens["CPU Part"] 79 | == cpu_data_from_proc["CPU part"] 80 | ) 81 | assert ( 82 | str(cpu_data_from_corelens["CPU Revision"]) 83 | == cpu_data_from_proc["CPU revision"] 84 | ) 85 | -------------------------------------------------------------------------------- /tests/test_lock.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import pytest 4 | from drgn.helpers.linux import for_each_task 5 | 6 | from drgn_tools import lock 7 | from drgn_tools import locking 8 | from drgn_tools.bt import func_name 9 | 10 | 11 | # the rwsem code does not support UEK4, no reason to add support 12 | @pytest.mark.skip_vmcore("*uek4*") 13 | def test_locks(prog): 14 | lock.scan_lock(prog, stack=True) 15 | 16 | 17 | @pytest.mark.skip_live 18 | @pytest.mark.vmcore("*lockmod*") 19 | def test_with_lockmod(prog, debuginfo_type): 20 | lockmod_threads = [] 21 | for task in for_each_task(prog): 22 | if task.comm.string_().startswith(b"lockmod"): 23 | lockmod_threads.append(task) 24 | 25 | if not lockmod_threads: 26 | pytest.skip("no lockmod kernel module found") 27 | 28 | for task in lockmod_threads: 29 | print(f"PID {task.pid.value_()} COMM {task.comm.string_().decode()}") 30 | comm = task.comm.string_() 31 | if b"owner" in comm: 32 | # this owns the locks 33 | continue 34 | 35 | if b"mutex" in comm: 36 | kind = "mutex" 37 | var = "lock" 38 | func_substr = "mutex_lock" 39 | elif b"rwsem" in comm: 40 | kind = "rw_semaphore" 41 | var = "sem" 42 | func_substr = "rwsem" 43 | else: 44 | kind = "semaphore" 45 | var = "sem" 46 | func_substr = "down" 47 | 48 | # There can be multiple frames which may contain the lock, we will need 49 | # to try all of them. 50 | trace = prog.stack_trace(task) 51 | frames = [] 52 | for frame in trace: 53 | fn = func_name(prog, frame) 54 | if fn and func_substr in fn: 55 | frames.append(frame) 56 | if not frames: 57 | pytest.fail("could not find relevant stack frame in lockmod") 58 | 59 | # Test 1: if DWARF debuginfo is present, then this will try to use the 60 | # variable name to access the lock. Otherwise, for CTF we will fall back 61 | # to using the stack offsets. 62 | for frame in frames: 63 | value = locking.get_lock_from_frame(prog, task, frame, kind, var) 64 | if value is not None: 65 | break 66 | else: 67 | pytest.fail(f"Could not find lock using {debuginfo_type}") 68 | 69 | if debuginfo_type == "ctf": 70 | # The second test is redundant, skip it. 71 | continue 72 | 73 | # Test 2: if DWARF debuginfo is present, we can actually give a fake 74 | # variable name! This will force the code to fall back to the stack 75 | # offsets, which should still work. This essentially simulates the 76 | # possibility of a DWARF unwind where we get an absent object. 77 | for frame in frames: 78 | value = locking.get_lock_from_frame( 79 | prog, task, frame, kind, "invalid variable name" 80 | ) 81 | if value is not None: 82 | break 83 | else: 84 | pytest.fail("Could not find lock using fallback method") 85 | -------------------------------------------------------------------------------- /drgn_tools/virtutil.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | """ 4 | Utilities for virtualization 5 | """ 6 | import argparse 7 | 8 | from drgn import Object 9 | from drgn import Program 10 | from drgn.helpers.linux.cpumask import for_each_possible_cpu 11 | from drgn.helpers.linux.percpu import per_cpu 12 | 13 | from drgn_tools.corelens import CorelensModule 14 | 15 | 16 | def get_platform_arch(prog: Program) -> str: 17 | """ 18 | Returns platform architecture 19 | """ 20 | try: 21 | dump_stack_arch_desc = prog["dump_stack_arch_desc_str"] 22 | except KeyError: 23 | # For UEK6 kernels with CTF debuginfo, the "dump_stack_arch_desc_str" 24 | # variable doesn't have type information, despite appearing in the 25 | # symbol table. This is a CTF generation bug. Regardless, the variable 26 | # is declared as char[128], so let's go ahead and use the available 27 | # symbol and hard-code the type. 28 | sym = prog.symbol("dump_stack_arch_desc_str") 29 | dump_stack_arch_desc = Object(prog, "char[128]", address=sym.address) 30 | str_dump_stack_arch_desc = dump_stack_arch_desc.string_().decode("utf-8") 31 | return str_dump_stack_arch_desc 32 | 33 | 34 | def get_platform_hypervisor(prog: Program) -> str: 35 | """ 36 | Returns hypervisor type 37 | Note: This is x86 specific 38 | """ 39 | try: 40 | return prog["x86_hyper_type"].format_(type_name=False) 41 | except KeyError: 42 | return "UNKNOWN Hypervisor (Platform not supported)" 43 | 44 | 45 | def get_cpuhp_state(prog: Program, cpu: int) -> str: 46 | """ 47 | Return CPU state for a given CPU 48 | """ 49 | try: 50 | cpuhp_state = per_cpu(prog["cpuhp_state"], cpu).state 51 | except KeyError: 52 | # Variable cpuhp_state is introduced in cff7d378d3fd ("cpu/hotplug: 53 | # Convert to a state machine for the control processor"), so it is not 54 | # present in UEK4. It is expected for this to fail there. 55 | return "UNKNOWN (missing 'cpuhp_state' variable)" 56 | return cpuhp_state.format_(type_name=False) 57 | 58 | 59 | def show_cpuhp_state(prog: Program) -> None: 60 | """ 61 | Display cpu state for all possible CPUs 62 | """ 63 | for cpu in for_each_possible_cpu(prog): 64 | state = get_cpuhp_state(prog, cpu) 65 | print(f"CPU [{cpu:3d}]: {state}") 66 | 67 | 68 | def get_platform(prog: Program) -> str: 69 | """ 70 | Return platform type 71 | """ 72 | str_platform = ( 73 | get_platform_arch(prog) + " " + get_platform_hypervisor(prog) 74 | ) 75 | return str_platform 76 | 77 | 78 | def show_platform(prog: Program) -> None: 79 | """ 80 | Prints platfrom type 81 | """ 82 | platform = get_platform(prog) 83 | print(platform) 84 | 85 | 86 | class VirtUtil(CorelensModule): 87 | """ 88 | This module contains helper regarding virtualization. 89 | Current functionality are : 90 | cpu hotplug state 91 | platform type, which includes architecture and hypervisor type 92 | """ 93 | 94 | name = "virt" 95 | 96 | def run(self, prog: Program, args: argparse.Namespace) -> None: 97 | show_platform(prog) 98 | return 99 | -------------------------------------------------------------------------------- /drgn_tools/runq.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import argparse 4 | 5 | from drgn import container_of 6 | from drgn import Object 7 | from drgn import Program 8 | from drgn.helpers.common import escape_ascii_string 9 | from drgn.helpers.linux.cpumask import for_each_online_cpu 10 | from drgn.helpers.linux.list import list_for_each_entry 11 | from drgn.helpers.linux.percpu import per_cpu 12 | 13 | from drgn_tools.corelens import CorelensModule 14 | from drgn_tools.task import task_lastrun2now 15 | from drgn_tools.util import timestamp_str 16 | 17 | # List runqueus per cpu 18 | 19 | 20 | def _print_rt_runq(runqueue: Object) -> None: 21 | count = 0 22 | prio_array = hex(runqueue.rt.active.address_ - 16) 23 | print(" RT PRIO_ARRAY:", prio_array) 24 | rt_prio_array = runqueue.rt.active.queue 25 | for que in rt_prio_array: 26 | for t in list_for_each_entry( 27 | "struct sched_rt_entity", que.address_of_(), "run_list" 28 | ): 29 | tsk = container_of(t, "struct task_struct", "rt") 30 | if tsk == runqueue.curr: 31 | continue 32 | count += 1 33 | print( 34 | " " * 4, 35 | '[{:3d}] PID: {:<6d} TASK: {} COMMAND: "{}"'.format( 36 | tsk.prio.value_(), 37 | tsk.pid.value_(), 38 | hex(tsk), 39 | escape_ascii_string(tsk.comm.string_()), 40 | ), 41 | ) 42 | if count == 0: 43 | print(" [no tasks queued]") 44 | 45 | 46 | def _print_cfs_runq(runqueue: Object) -> None: 47 | cfs_root = hex(runqueue.cfs.tasks_timeline.address_of_().value_()) 48 | print(" CFS RB_ROOT:", cfs_root) 49 | count = 0 50 | runq = runqueue.address_of_() 51 | for t in list_for_each_entry( 52 | "struct task_struct", runq.cfs_tasks.address_of_(), "se.group_node" 53 | ): 54 | if t == runqueue.curr: 55 | continue 56 | count += 1 57 | print( 58 | " " * 4, 59 | '[{:3d}] PID: {:<6d} TASK: {} COMMAND: "{}"'.format( 60 | t.prio.value_(), 61 | t.pid.value_(), 62 | hex(t), 63 | escape_ascii_string(t.comm.string_()), 64 | ), 65 | ) 66 | if count == 0: 67 | print(" [no tasks queued]") 68 | 69 | 70 | def run_queue(prog: Program) -> None: 71 | """ 72 | Print tasks which are in the RT and CFS runqueues on each CPU. 73 | processes running more than x seconds. 74 | 75 | :param prog: drgn program 76 | """ 77 | 78 | # _cpu = drgn.helpers.linux.cpumask.for_each_online_cpu(prog) 79 | for cpus in for_each_online_cpu(prog): 80 | runqueue = per_cpu(prog["runqueues"], cpus) 81 | curr_task_addr = runqueue.curr.value_() 82 | curr_task = runqueue.curr[0] 83 | comm = escape_ascii_string(curr_task.comm.string_()) 84 | pid = curr_task.pid.value_() 85 | run_time = task_lastrun2now(curr_task) 86 | prio = curr_task.prio.value_() 87 | print(f"CPU {cpus} RUNQUEUE: {runqueue.address_of_().value_():x}") 88 | print( 89 | f" CURRENT: PID: {pid:<6d} TASK: {curr_task_addr:x} PRIO: {prio}" 90 | f' COMMAND: "{comm}"' 91 | f" RUNTIME: {timestamp_str(run_time)}", 92 | ) 93 | # RT PRIO_ARRAY 94 | _print_rt_runq(runqueue) 95 | # CFS RB_ROOT 96 | _print_cfs_runq(runqueue) 97 | print() 98 | 99 | 100 | class RunQueue(CorelensModule): 101 | """ 102 | List process that are in RT and CFS queue. 103 | """ 104 | 105 | name = "runq" 106 | 107 | def run(self, prog: Program, args: argparse.Namespace) -> None: 108 | run_queue(prog) 109 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import os 4 | import re 5 | import shutil 6 | import subprocess 7 | 8 | from setuptools import setup 9 | 10 | long_description = "drgn helper script repository" 11 | 12 | RELEASE_VERSION = "2.2.0" 13 | PACKAGES = ["drgn_tools"] 14 | 15 | 16 | def get_version(): 17 | try: 18 | with open("drgn_tools/_version.py", "r") as f: 19 | version_py = f.read() 20 | except FileNotFoundError: 21 | version_py = None 22 | 23 | public_version = RELEASE_VERSION 24 | local_version = "+unknown" 25 | 26 | # If this is a git repository, use a git-describe(1)-esque local version. 27 | # Otherwise, get the local version saved in the sdist. 28 | if os.path.exists(".git") and shutil.which("git"): 29 | try: 30 | dirty = bool( 31 | subprocess.check_output( 32 | ["git", "status", "-uno", "--porcelain"], 33 | # Use the environment variable instead of --no-optional-locks 34 | # to support Git < 2.14. 35 | env={**os.environ, "GIT_OPTIONAL_LOCKS": "0"}, 36 | ) 37 | ) 38 | except subprocess.CalledProcessError: 39 | dirty = False 40 | 41 | try: 42 | count = int( 43 | subprocess.check_output( 44 | ["git", "rev-list", "--count", f"v{public_version}.."], 45 | stderr=subprocess.DEVNULL, 46 | universal_newlines=True, 47 | ) 48 | ) 49 | except subprocess.CalledProcessError: 50 | print(f"warning: v{public_version} tag not found") 51 | else: 52 | if count == 0: 53 | local_version = "+dirty" if dirty else "" 54 | else: 55 | commit = subprocess.check_output( 56 | ["git", "rev-parse", "--short", "HEAD"], 57 | universal_newlines=True, 58 | ).strip() 59 | local_version = f"+{count}.g{commit}" 60 | if dirty: 61 | local_version += ".dirty" 62 | elif version_py is not None: 63 | match = re.search( 64 | rf'^__version__ = "{re.escape(public_version)}([^"]*)"$', 65 | version_py, 66 | re.M, 67 | ) 68 | if match: 69 | local_version = match.group(1) 70 | else: 71 | print("warning: drgn_tools/_version.py is invalid") 72 | else: 73 | print("warning: drgn_tools/_version.py not found") 74 | 75 | version = public_version + local_version 76 | 77 | new_version_py = f'__version__ = "{version}"\n' 78 | if new_version_py != version_py: 79 | with open("drgn_tools/_version.py", "w") as f: 80 | f.write(new_version_py) 81 | 82 | return version 83 | 84 | 85 | setup( 86 | name="drgn-tools", 87 | version=get_version(), 88 | description="drgn helper script repository", 89 | long_description=long_description, 90 | install_requires=[ 91 | "drgn>=0.0.32,<0.0.34", 92 | ], 93 | url="https://github.com/oracle-samples/drgn-tools", 94 | author="Oracle Linux Sustaining Engineering Team", 95 | author_email="stephen.s.brennan@oracle.com", 96 | license="UPL", 97 | packages=PACKAGES, 98 | classifiers=[ 99 | "Programming Language :: Python :: 3", 100 | "License :: OSI Approved :: Universal Permissive License (UPL)", 101 | ], 102 | keywords="kernel UEK debug", 103 | entry_points={ 104 | "console_scripts": [ 105 | "DRGN=drgn_tools.cli:main", 106 | "corelens=drgn_tools.corelens:main", 107 | ], 108 | "drgn.plugins": ["oracle=drgn_tools.debuginfo"], 109 | }, 110 | ) 111 | -------------------------------------------------------------------------------- /mkdist.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | """ 4 | Create a zipapp distribution of drgn-tools which can provided to customers. 5 | """ 6 | import argparse 7 | import shutil 8 | import subprocess 9 | import sys 10 | import tempfile 11 | import zipapp 12 | from pathlib import Path 13 | 14 | 15 | def main(): 16 | entry_points = { 17 | "corelens": "drgn_tools.corelens:main", 18 | "cli": "drgn_tools.cli:main", 19 | } 20 | parser = argparse.ArgumentParser( 21 | description="create drgn-tools distributions" 22 | ) 23 | parser.add_argument( 24 | "--interpreter", 25 | default="/usr/bin/python3", 26 | help="Set the interpreter (if different from target system python)", 27 | ) 28 | parser.add_argument( 29 | "--output", 30 | "-o", 31 | default=None, 32 | help="Set the output file", 33 | ) 34 | parser.add_argument( 35 | "--entry-point", 36 | default="corelens", 37 | help=f"Select an entry point ({','.join(entry_points.keys())} " 38 | "or a function name)", 39 | ) 40 | parser.add_argument( 41 | "--quiet", 42 | "-q", 43 | action="store_true", 44 | help="just do it without any prompts or info", 45 | ) 46 | args = parser.parse_args() 47 | 48 | print( 49 | """\ 50 | Please note: the contents of the drgn_tools/ directory will be used to create 51 | this distribution AS-IS! If you have any contents in that directory which should 52 | not be distributed to a customer, please Ctrl-C now and clean them up. You may 53 | want to use: 54 | 55 | git clean -ndx drgn_tools/ 56 | 57 | To see if you have any untracked files. You can use: 58 | 59 | git clean -fdx drgn_tools/ 60 | 61 | To delete everything listed by the prior command. Finally, you can use: 62 | 63 | git status drgn_tools/ 64 | 65 | To verify which files have uncommitted changes. It's totally fine to include 66 | extra files & uncommitted changes, but it's important to be sure you only 67 | include what you intended. 68 | 69 | Please hit enter to acknowledge and continue, or Ctrl-C to abort.\ 70 | """ 71 | ) 72 | input() 73 | base_dir = Path(__file__).parent 74 | if args.entry_point in entry_points: 75 | output_file = args.output or f"{args.entry_point}.pyz" 76 | entry_point = entry_points[args.entry_point] 77 | else: 78 | output_file = args.output or "drgn_tools.pyz" 79 | entry_point = args.entry_point 80 | 81 | # Be sure that we re-generate the "_version.py" file for accuracy 82 | subprocess.run( 83 | [sys.executable, base_dir / "setup.py", "--version"], 84 | check=True, 85 | stdout=subprocess.DEVNULL, 86 | cwd=base_dir, 87 | ) 88 | 89 | # Only the contents of "drgn_tools" should be included. All other files that 90 | # are part of the project should be excluded. 91 | with tempfile.TemporaryDirectory() as td: 92 | tmp = Path(td) 93 | shutil.copytree(base_dir / "drgn_tools", tmp / "drgn_tools") 94 | zipapp.create_archive( 95 | td, output_file, interpreter=args.interpreter, main=entry_point 96 | ) 97 | 98 | print( 99 | f"""\ 100 | Created a distribution: {output_file} 101 | 102 | It can be directly copied to a target system, and it can be directly executed. 103 | The target system MUST have a /usr/bin/python3 and have drgn installed. The 104 | target system DOES NOT need to have drgn-tools installed -- but if it does, that 105 | is fine. Nothing on the target system will be modified. 106 | 107 | You can use "unzip -l {output_file}" to check the contents of the zip file to 108 | ensure only what you intended to include is present.\ 109 | """ 110 | ) 111 | 112 | 113 | if __name__ == "__main__": 114 | main() 115 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | # 4 | # This file only contains a selection of the most common options. For a full 5 | # list see the documentation: 6 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 7 | # -- Path setup -------------------------------------------------------------- 8 | import os 9 | import re 10 | 11 | # -- Project information ----------------------------------------------------- 12 | 13 | project = "drgn-tools" 14 | copyright = "2023, Oracle and/or its affiliates" 15 | author = "Oracle and/or its affiliates" 16 | 17 | # The full version, including alpha/beta/rc tags 18 | with open(os.path.join(os.path.dirname(__file__), "..", "setup.py")) as f: 19 | match = re.search(r"VERSION = \"(.+)\"", f.read()) 20 | assert match, "VERSION variable not found in setup.py" 21 | release = "v" + match.group(1) 22 | 23 | 24 | # -- General configuration --------------------------------------------------- 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be 27 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 28 | # ones. 29 | extensions = [ 30 | "sphinx.ext.autodoc", 31 | "sphinx_autodoc_typehints", 32 | "sphinx_reference_rename", 33 | "sphinx.ext.intersphinx", 34 | "myst_parser", 35 | ] 36 | autodoc_typehints = "description" 37 | 38 | nitpicky = True 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ["_templates"] 42 | 43 | # List of patterns, relative to source directory, that match files and 44 | # directories to ignore when looking for source files. 45 | # This pattern also affects html_static_path and html_extra_path. 46 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 47 | 48 | nitpick_ignore = [("py:class", "drgn_tools.itertools.T")] 49 | 50 | # -- Options for HTML output ------------------------------------------------- 51 | 52 | # The theme to use for HTML and HTML Help pages. See the documentation for 53 | # a list of builtin themes. 54 | # 55 | html_theme = "alabaster" 56 | 57 | # Free, no attribution required, quick logo. 58 | # https://logodust.com 59 | html_logo = "drgn-tools.png" 60 | html_favicon = "drgn-tools.png" 61 | 62 | # Add any paths that contain custom static files (such as style sheets) here, 63 | # relative to this directory. They are copied after the builtin static files, 64 | # so a file named "default.css" will overwrite the builtin "default.css". 65 | html_static_path = [""] 66 | 67 | # -- Options for Intersphinx linking (linking to external docs) 68 | intersphinx_mapping = { 69 | "python": ("https://docs.python.org/3", None), 70 | "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), 71 | "matplotlib": ("https://matplotlib.org", None), 72 | "drgn": ("https://drgn.readthedocs.io/en/latest/", None), 73 | } 74 | 75 | sphinx_reference_rename_mapping = { 76 | "_drgn.IntegerLike": "drgn.IntegerLike", 77 | "_drgn.Object": "drgn.Object", 78 | "_drgn.Program": "drgn.Program", 79 | "_drgn.Thread": "drgn.Thread", 80 | "_drgn.Type": "drgn.Type", 81 | "_drgn.StackFrame": "drgn.StackFrame", 82 | "_drgn.StackTrace": "drgn.StackTrace", 83 | } 84 | 85 | # Allow markdown files 86 | source_suffix = { 87 | ".rst": "restructuredtext", 88 | ".md": "markdown", 89 | } 90 | 91 | 92 | # Logic below skips the subclasses of CorelensModule. They're not interesting 93 | # documentation for developers. 94 | def should_skip_class(app, what, name, obj, skip, options): 95 | if what == "module": 96 | from drgn_tools.corelens import CorelensModule 97 | 98 | try: 99 | if issubclass(obj, CorelensModule) and obj is not CorelensModule: 100 | return True 101 | except Exception: 102 | pass 103 | 104 | return None 105 | 106 | 107 | def setup(app): 108 | app.connect("autodoc-skip-member", should_skip_class) 109 | -------------------------------------------------------------------------------- /drgn_tools/partition.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | """ 4 | Helper to print partition information 5 | """ 6 | import argparse 7 | from typing import NamedTuple 8 | 9 | from drgn import Object 10 | from drgn import Program 11 | from drgn.helpers.linux.block import for_each_partition 12 | from drgn.helpers.linux.block import part_devt 13 | from drgn.helpers.linux.block import part_name 14 | from drgn.helpers.linux.device import MAJOR 15 | from drgn.helpers.linux.device import MINOR 16 | 17 | from drgn_tools.block import blkdev_ro 18 | from drgn_tools.block import blkdev_size 19 | from drgn_tools.corelens import CorelensModule 20 | from drgn_tools.table import Table 21 | 22 | 23 | class PartInfo(NamedTuple): 24 | """ 25 | Partition info, from either ``struct block_device`` or ``struct hd_struct`` 26 | """ 27 | 28 | major: int 29 | minor: int 30 | name: str 31 | start_sect: int 32 | nr_sects: int 33 | ro: bool 34 | obj: Object 35 | 36 | 37 | def get_partinfo_from_blkdev_struct(part: Object) -> PartInfo: 38 | """ 39 | Collect partition information from ``struct block_device`` 40 | Returns a list with partition information for the given partition. 41 | """ 42 | devt = part.bd_dev.value_() 43 | name = part_name(part).decode() 44 | start_sect = int(part.bd_start_sect) 45 | nr_sects = int(blkdev_size(part) / 512) 46 | return PartInfo( 47 | MAJOR(devt), 48 | MINOR(devt), 49 | name, 50 | start_sect, 51 | nr_sects, 52 | # blkdev_ro will never return -1 in case of a struct block_device, so we 53 | # can convert to bool here. 54 | blkdev_ro(part) == 1, 55 | part, 56 | ) 57 | 58 | 59 | def get_partinfo_from_hd_struct(part: Object) -> PartInfo: 60 | """ 61 | Collects partition information from ``struct hd_struct`` 62 | Returns a list with partition information for the given partition. 63 | """ 64 | devt = part_devt(part) 65 | name = part_name(part).decode() 66 | start_sect = int(part.start_sect) 67 | nr_sects = part.nr_sects.value_() 68 | return PartInfo( 69 | MAJOR(devt), 70 | MINOR(devt), 71 | name, 72 | start_sect, 73 | nr_sects, 74 | bool(part.policy), 75 | part, 76 | ) 77 | 78 | 79 | def get_partition_info(part: Object) -> PartInfo: 80 | """ 81 | Returns partition info from ``struct hd_struct`` or ``struct block_device`` 82 | depending on the kernel version. 83 | """ 84 | if "block_device" in part.type_.type_name(): 85 | return get_partinfo_from_blkdev_struct(part) 86 | else: 87 | return get_partinfo_from_hd_struct(part) 88 | 89 | 90 | def print_partition_info(prog: Program) -> None: 91 | """ 92 | Prints partition information 93 | """ 94 | table = Table( 95 | [ 96 | "MAJOR", 97 | "MINOR", 98 | "NAME", 99 | "START:>", 100 | "SECTORS:>", 101 | "READ-ONLY", 102 | "OBJECT:016x", # will be replaced, see below 103 | ] 104 | ) 105 | part_is_blkdev = -1 106 | for part in for_each_partition(prog): 107 | if part_is_blkdev == -1: 108 | if "block_device" in part.type_.type_name(): 109 | part_is_blkdev = 1 110 | table.header[-1] = "BLOCK DEVICE" 111 | else: 112 | part_is_blkdev = 0 113 | table.header[-1] = "HD STRUCT" 114 | info = get_partition_info(part) 115 | table.row(*info._replace(obj=info.obj.value_())) 116 | 117 | table.write() 118 | 119 | 120 | class PartitionInfo(CorelensModule): 121 | """ 122 | Corelens Module for partition information 123 | """ 124 | 125 | name = "partitioninfo" 126 | 127 | def run(self, prog: Program, args: argparse.Namespace) -> None: 128 | print_partition_info(prog) 129 | -------------------------------------------------------------------------------- /drgn_tools/lsmod.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import argparse 4 | from typing import Iterable 5 | 6 | from _drgn import FaultError 7 | from drgn import Object 8 | from drgn import Program 9 | from drgn.helpers.common.format import escape_ascii_string 10 | from drgn.helpers.linux.list import list_for_each_entry 11 | from drgn.helpers.linux.module import for_each_module 12 | from drgn.helpers.linux.module import module_address_regions 13 | 14 | from drgn_tools.corelens import CorelensModule 15 | from drgn_tools.module import module_params 16 | from drgn_tools.table import print_table 17 | 18 | 19 | def for_each_module_use(source_list_addr: Object) -> Iterable[Object]: 20 | """ 21 | Provide the list of ``struct module_use`` as an iterable object 22 | 23 | :param source_list_addr: ``struct module_use.source_list`` Object. 24 | :returns: A list of ``struct module.source_list`` as an iterable object 25 | """ 26 | return list_for_each_entry( 27 | "struct module_use", source_list_addr, "source_list" 28 | ) 29 | 30 | 31 | def print_module_parameters(prog: Program) -> None: 32 | """Prints each loaded module and its parameter values""" 33 | for mod in for_each_module(prog): 34 | print("\n") 35 | name = escape_ascii_string(mod.name.string_()) 36 | print("MODULE NAME:".ljust(15), name) 37 | print("PARAM COUNT:", str(mod.num_kp.value_())) 38 | print("ADDRESS :", hex(mod.num_kp.address_of_())) 39 | if not mod.num_kp: 40 | continue 41 | 42 | table_value = [] 43 | table_value.append(["PARAMETER", "ADDRESS", "TYPE", "VALUE"]) 44 | for name, info in module_params(mod).items(): 45 | try: 46 | if info.value is None: 47 | formatted = "" 48 | elif info.type_name == "charp" and not info.value: 49 | formatted = "(null)" 50 | elif info.type_name in ("charp", "string"): 51 | formatted = escape_ascii_string( 52 | info.value.string_(), 53 | escape_double_quote=True, 54 | escape_backslash=True, 55 | ) 56 | formatted = f'"{formatted}"' 57 | elif info.type_name == "bool": 58 | formatted = "Y" if info.value else "N" 59 | else: 60 | formatted = info.value.format_(type_name=False) 61 | except FaultError: 62 | # As mentioned in decode_param() docstring, a FaultError can 63 | # occur when a module parameter variable is marked __initdata. 64 | formatted = "(page fault)" 65 | table_value.append( 66 | [ 67 | name, 68 | hex(info.kernel_param.address_of_()), 69 | info.type_name, 70 | formatted, 71 | ] 72 | ) 73 | print_table(table_value) 74 | 75 | 76 | def print_module_summary(prog: Program) -> None: 77 | """Print a list of module details and dependencies""" 78 | # List all loaded modules 79 | table_value = [] 80 | table_value.append(["MODULE", "NAME", "SIZE", "REF", "DEPENDENT MODULES"]) 81 | for mod in for_each_module(prog): 82 | dep_mod = [] 83 | for depuse in for_each_module_use(mod.source_list.address_of_()): 84 | dep_mod.append(depuse.source.name.string_().decode("utf-8")) 85 | mem_usage = sum(r[1] for r in module_address_regions(mod)) 86 | name = escape_ascii_string(mod.name.string_()) 87 | table_value.append( 88 | [ 89 | hex(mod.value_()), 90 | name, 91 | str(mem_usage), 92 | str(int(mod.refcnt.counter)), 93 | ",".join(dep_mod), 94 | ] 95 | ) 96 | print_table(table_value) 97 | 98 | 99 | class ListModules(CorelensModule): 100 | """ 101 | List loaded modules, dependencies and their parameter value. 102 | """ 103 | 104 | name = "lsmod" 105 | 106 | def run(self, prog: Program, args: argparse.Namespace) -> None: 107 | print_module_summary(prog) 108 | print_module_parameters(prog) 109 | -------------------------------------------------------------------------------- /tests/test_workqueue.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import drgn 4 | import pytest 5 | from drgn.helpers.linux.cpumask import for_each_online_cpu 6 | from drgn.helpers.linux.percpu import per_cpu 7 | from drgn.helpers.linux.percpu import per_cpu_ptr 8 | from drgn.helpers.linux.pid import for_each_task 9 | 10 | from drgn_tools import workqueue as wq 11 | 12 | # import pytest 13 | 14 | 15 | def system_workqueue_names(prog: drgn.Program): 16 | # Pick some global workqueues that will always exist 17 | return { 18 | prog["system_wq"].name.string_(), 19 | prog["system_highpri_wq"].name.string_(), 20 | prog["system_long_wq"].name.string_(), 21 | prog["system_unbound_wq"].name.string_(), 22 | prog["system_freezable_wq"].name.string_(), 23 | } 24 | 25 | 26 | def test_for_each_workqueue(prog: drgn.Program) -> None: 27 | # The found workqueue names should be a superset of the test names. 28 | wq_names = {workq.name.string_() for workq in wq.for_each_workqueue(prog)} 29 | assert wq_names >= system_workqueue_names(prog) 30 | 31 | 32 | def test_for_each_pool(prog: drgn.Program) -> None: 33 | cpu0_normal_pool = per_cpu(prog["cpu_worker_pools"], 0)[0].address_of_() 34 | cpu0_highprio_pool = per_cpu(prog["cpu_worker_pools"], 0)[1].address_of_() 35 | all_pools = [pool for pool in wq.for_each_pool(prog)] 36 | assert cpu0_normal_pool in all_pools 37 | assert cpu0_highprio_pool in all_pools 38 | 39 | 40 | def test_for_each_worker(prog: drgn.Program) -> None: 41 | kworker_tasks = [ 42 | task.value_() 43 | for task in for_each_task(prog) 44 | if task.comm.string_().decode().startswith("kworker") 45 | ] 46 | kworker_obtained = [ 47 | worker.task.value_() for worker in wq.for_each_worker(prog) 48 | ] 49 | assert kworker_tasks.sort() == kworker_obtained.sort() 50 | 51 | 52 | def test_for_each_pool_worker(prog: drgn.Program) -> None: 53 | test_pool = per_cpu(prog["cpu_worker_pools"], 0)[0].address_ 54 | kworkers = [ 55 | workers.value_() 56 | for workers in wq.for_each_worker(prog) 57 | if workers.pool.value_() == test_pool 58 | ] 59 | pool_kworkers = [ 60 | workers.value_() 61 | for workers in wq.for_each_pool_worker( 62 | per_cpu(prog["cpu_worker_pools"], 0)[0].address_of_() 63 | ) 64 | ] 65 | assert kworkers.sort() == pool_kworkers.sort() 66 | 67 | 68 | def test_for_each_cpu_worker_pool(prog: drgn.Program) -> None: 69 | cpu0_worker_pools = [ 70 | per_cpu(prog["cpu_worker_pools"], 0)[i].address_ for i in [0, 1] 71 | ] 72 | worker_pools = [ 73 | worker_pool.value_() 74 | for worker_pool in wq.for_each_cpu_worker_pool(prog, 0) 75 | ] 76 | assert worker_pools == cpu0_worker_pools 77 | 78 | 79 | def test_for_each_pwq(prog: drgn.Program) -> None: 80 | workq = prog["system_wq"] 81 | pwqs = [pwq.value_() for pwq in wq.for_each_pwq(workq)] 82 | cpu_pwqs_attr = "cpu_pwqs" if hasattr(workq, "cpu_pwqs") else "cpu_pwq" 83 | cpu_pwqs_list = [ 84 | per_cpu_ptr(getattr(workq, cpu_pwqs_attr), cpu).value_() 85 | for cpu in for_each_online_cpu(prog) 86 | ] 87 | assert pwqs.sort() == cpu_pwqs_list.sort() 88 | 89 | 90 | @pytest.mark.skip_live 91 | def test_for_each_pending_work_on_cpu(prog: drgn.Program) -> None: 92 | for work in wq.for_each_pending_work_on_cpu(prog, 0): 93 | pass 94 | 95 | 96 | @pytest.mark.skip_live 97 | def test_for_each_pending_work_in_pool(prog: drgn.Program) -> None: 98 | pool = per_cpu(prog["cpu_worker_pools"], 0)[0].address_of_() 99 | for work in wq.for_each_pending_work_in_pool(pool): 100 | pass 101 | 102 | 103 | @pytest.mark.skip_live 104 | def test_for_each_pending_work_of_pwq(prog: drgn.Program) -> None: 105 | cpu_pwqs_0 = wq.workqueue_get_pwq(prog["system_wq"], 0) 106 | for work in wq.for_each_pending_work_of_pwq(cpu_pwqs_0): 107 | pass 108 | 109 | 110 | @pytest.mark.skip_live 111 | def test_show_all_workqueues(prog: drgn.Program) -> None: 112 | wq.show_all_workqueues(prog) 113 | 114 | 115 | @pytest.mark.skip_live 116 | def test_show_unexpired_delayed_works( 117 | prog: drgn.Program, 118 | ) -> None: 119 | wq.show_unexpired_delayed_works(prog) 120 | -------------------------------------------------------------------------------- /drgn_tools/lockup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | import argparse 4 | import typing 5 | 6 | import drgn 7 | from drgn import Program 8 | from drgn.helpers.common import escape_ascii_string 9 | from drgn.helpers.linux.cpumask import for_each_online_cpu 10 | from drgn.helpers.linux.percpu import per_cpu 11 | 12 | from drgn_tools.bt import bt 13 | from drgn_tools.bt import bt_has_any 14 | from drgn_tools.corelens import CorelensModule 15 | from drgn_tools.table import print_table 16 | from drgn_tools.task import task_lastrun2now 17 | from drgn_tools.util import timestamp_str 18 | 19 | 20 | def scan_lockup( 21 | prog: Program, min_run_time_seconds: int = 1, skip_swapper: bool = True 22 | ) -> None: 23 | """ 24 | Scan potential lockups on cpus and tasks waiting for RCU. 25 | 26 | :param prog: drgn program 27 | :param min_run_time_seconds: int 28 | :param skip_swapper: bool 29 | """ 30 | nr_processes = 0 31 | for cpus in for_each_online_cpu(prog): 32 | runqueue = per_cpu(prog["runqueues"], cpus) 33 | curr_task_addr = runqueue.curr.value_() 34 | curr_task = runqueue.curr[0] 35 | comm = escape_ascii_string(curr_task.comm.string_()) 36 | pid = curr_task.pid.value_() 37 | run_time = task_lastrun2now(curr_task) 38 | prio = curr_task.prio.value_() 39 | if run_time < min_run_time_seconds * 1e9: 40 | continue 41 | if skip_swapper and comm == f"swapper/{cpus}": 42 | continue 43 | print(f"CPU {cpus} RUNQUEUE: {runqueue.address_of_().value_():x}") 44 | print( 45 | f" PID: {pid:<6d} TASK: {curr_task_addr:x} PRIO: {prio}" 46 | f' COMMAND: "{comm}"', 47 | f" LOCKUP TIME: {timestamp_str(run_time)}", 48 | ) 49 | print("\nCalltrace:") 50 | bt(task_or_prog=curr_task.address_of_()) 51 | print() 52 | nr_processes += 1 53 | 54 | print( 55 | f"We found {nr_processes} processes running more than {min_run_time_seconds} seconds" 56 | ) 57 | 58 | dump_tasks_waiting_rcu_gp(prog, min_run_time_seconds) 59 | 60 | 61 | def tasks_waiting_rcu_gp( 62 | prog: Program, 63 | ) -> typing.List[typing.Tuple[drgn.Object, drgn.StackFrame]]: 64 | """ 65 | Detects tasks waiting RCU grace period 66 | 67 | :param prog: drgn program 68 | """ 69 | rcu_gp_fn = ["percpu_ref_switch_to_atomic_sync"] 70 | return bt_has_any(prog, rcu_gp_fn) 71 | 72 | 73 | def dump_tasks_waiting_rcu_gp( 74 | prog: Program, min_run_time_seconds: int 75 | ) -> None: 76 | """ 77 | Prints tasks waiting on rcu grace period with details 78 | 79 | :param prog: drgn program 80 | :param min_run_time_seconds: int 81 | """ 82 | tasks_waiting = tasks_waiting_rcu_gp(prog) 83 | output = [["TASK", "NAME", "PID", "PENDING_TIME"]] 84 | tasks_pids = set() # remove duplicates 85 | if tasks_waiting: 86 | for t, _ in tasks_waiting: 87 | pending_time = timestamp_str(task_lastrun2now(t)) 88 | pid = t.pid.value_() 89 | if ( 90 | pid not in tasks_pids 91 | and task_lastrun2now(t) > min_run_time_seconds * 1e9 92 | ): 93 | output.append( 94 | [ 95 | hex(t.value_()), 96 | escape_ascii_string(t.comm.string_()), 97 | pid, 98 | pending_time, 99 | ] 100 | ) 101 | tasks_pids.add(pid) 102 | print() 103 | print( 104 | f"We found below tasks waiting for rcu grace period over {min_run_time_seconds} seconds:" 105 | ) 106 | print_table(output) 107 | 108 | 109 | class LockUp(CorelensModule): 110 | """Print tasks which have been on-cpu for too long (possible RCU blockers) and tasks waiting RCU grace period if any""" 111 | 112 | name = "lockup" 113 | 114 | def add_args(self, parser: argparse.ArgumentParser) -> None: 115 | parser.add_argument( 116 | "--time", 117 | "-t", 118 | type=float, 119 | default=2, 120 | help="list all the processes that have been running more than