Skip to content

Commit 316b093

Browse files
committed
tests: linux_kernel: test built-in ORC unwinding
Loading built-in ORC is a difficult functionality to test: it is best tested when there is no debuginfo file. Thus, we add two tests: one simpler test in which the kernel has debuginfo, but a module does not, and we must unwind a stack with functions from the module. The second test is more complex, where we create a program with no debuginfo at all, and provide it just enough data to initialize the module API and unwind with built-in ORC. In both cases, to verify that drgn is actually using ORC, we capture its log messages. Signed-off-by: Stephen Brennan <[email protected]>
1 parent 459b9ce commit 316b093

File tree

2 files changed

+149
-2
lines changed

2 files changed

+149
-2
lines changed

tests/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import contextlib
55
import functools
6+
import logging
67
import os
78
import sys
89
from typing import Any, Mapping, NamedTuple, Optional
@@ -455,3 +456,14 @@ def modifyenv(vars: Mapping[str, Optional[str]]):
455456
del os.environ[key]
456457
else:
457458
os.environ[key] = old_value
459+
460+
461+
@contextlib.contextmanager
462+
def drgn_log_level(level: int):
463+
logger = logging.getLogger("drgn")
464+
old_level = logger.getEffectiveLevel()
465+
logger.setLevel(level)
466+
try:
467+
yield
468+
finally:
469+
logger.setLevel(old_level)

tests/linux_kernel/test_stack_trace.py

Lines changed: 137 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# Copyright (c) Meta Platforms, Inc. and affiliates.
22
# SPDX-License-Identifier: LGPL-2.1-or-later
33

4+
import logging
45
import os
6+
import re
57
import unittest
68

79
from _drgn_util.platform import NORMALIZED_MACHINE_NAME
8-
from drgn import Object, Program, reinterpret
9-
from tests import assertReprPrettyEqualsStr, modifyenv
10+
from drgn import MissingDebugInfoError, Object, Program, TypeMember, reinterpret
11+
from drgn.helpers.linux import load_module_kallsyms, load_vmlinux_kallsyms
12+
from tests import assertReprPrettyEqualsStr, drgn_log_level, modifyenv
1013
from tests.linux_kernel import (
1114
LinuxKernelTestCase,
1215
fork_and_stop,
@@ -59,6 +62,60 @@ def test_by_pid_dwarf(self):
5962
def test_by_pid_orc(self):
6063
self._test_by_pid(True)
6164

65+
def _check_logged_orc_message(self, captured_logs, module):
66+
# To be sure that we actually used ORC to unwind through the drgn_test
67+
# stack frames, search for the log output. We don't know which ORC
68+
# version is used, so just ensure that we have a log line that mentions
69+
# loading ORC.
70+
expr = re.compile(
71+
r"DEBUG:drgn:Loaded built-in ORC \(v\d+\) for module " + module
72+
)
73+
for line in captured_logs.output:
74+
if expr.fullmatch(line):
75+
break
76+
else:
77+
self.fail(f"Did not load built-in ORC for {module}")
78+
79+
@unittest.skipUnless(
80+
NORMALIZED_MACHINE_NAME == "x86_64",
81+
f"{NORMALIZED_MACHINE_NAME} does not use ORC",
82+
)
83+
@skip_unless_have_test_kmod
84+
def test_by_pid_builtin_orc(self):
85+
# ORC was introduced in kernel 4.14. Detect the presence of ORC or skip
86+
# the test.
87+
try:
88+
self.prog.symbol("__start_orc_unwind")
89+
except LookupError:
90+
ver = self.prog["UTS_RELEASE"].string_().decode()
91+
self.skipTest(f"ORC is not available for {ver}")
92+
93+
with drgn_log_level(logging.DEBUG):
94+
# Create a program with the core kernel debuginfo loaded,
95+
# but without module debuginfo. Load a symbol finder using
96+
# kallsyms so that the module's stack traces can still have
97+
# usable frame names.
98+
prog = Program()
99+
prog.set_kernel()
100+
try:
101+
prog.load_default_debug_info()
102+
except MissingDebugInfoError:
103+
pass
104+
kallsyms = load_module_kallsyms(prog)
105+
prog.register_symbol_finder("module_kallsyms", kallsyms, enable_index=1)
106+
for thread in prog.threads():
107+
if b"drgn_test_kthread".startswith(thread.object.comm.string_()):
108+
pid = thread.tid
109+
break
110+
else:
111+
self.fail("couldn't find drgn_test_kthread")
112+
# We must set drgn's log level manually, beacuse it won't log messages
113+
# to the logger if it isn't enabled for them.
114+
with self.assertLogs("drgn", logging.DEBUG) as log:
115+
self._test_drgn_test_kthread_trace(prog.stack_trace(pid))
116+
117+
self._check_logged_orc_message(log, "drgn_test")
118+
62119
@skip_unless_have_test_kmod
63120
def test_by_pt_regs(self):
64121
pt_regs = self.prog["drgn_test_kthread_pt_regs"]
@@ -104,6 +161,84 @@ def test_locals(self):
104161
else:
105162
self.fail("Couldn't find drgn_test_kthread_fn3 frame")
106163

164+
@unittest.skipUnless(
165+
NORMALIZED_MACHINE_NAME == "x86_64",
166+
f"{NORMALIZED_MACHINE_NAME} does not use ORC",
167+
)
168+
def test_vmlinux_builtin_orc(self):
169+
# ORC was introduced in kernel 4.14. Detect the presence of ORC or skip
170+
# the test.
171+
try:
172+
self.prog.symbol("__start_orc_unwind")
173+
except LookupError:
174+
ver = self.prog["UTS_RELEASE"].string_().decode()
175+
self.skipTest(f"ORC is not available for {ver}")
176+
177+
with drgn_log_level(logging.DEBUG):
178+
# It is difficult to test stack unwinding in a program without also
179+
# loading types, which necessarily will also make DWARF CFI and ORC
180+
# available in the debug file. The way we get around this is by creating
181+
# a new program with no debuginfo, getting a pt_regs from the program
182+
# that has debuginfo, and then using that to unwind the kernel. We still
183+
# need a symbol finder, and we'll need the Module API to recognize the
184+
# kernel address range correctly.
185+
prog = Program()
186+
prog.set_kernel()
187+
prog.register_symbol_finder(
188+
"vmlinux_kallsyms", load_vmlinux_kallsyms(prog), enable_index=0
189+
)
190+
main, _ = prog.main_module(name="kernel", create=True)
191+
main.address_range = self.prog.main_module().address_range
192+
193+
# Luckily, all drgn cares about for x86_64 pt_regs is that it is a
194+
# structure. Rather than creating a matching struct pt_regs definition,
195+
# we can just create a dummy one of the correct size:
196+
# struct pt_regs { unsigned char[size]; };
197+
# Drgn will happily use that and reinterpret the bytes correctly.
198+
real_pt_regs_type = self.prog.type("struct pt_regs")
199+
fake_pt_regs_type = prog.struct_type(
200+
tag="pt_regs",
201+
size=real_pt_regs_type.size,
202+
members=[
203+
TypeMember(
204+
prog.array_type(
205+
prog.int_type("unsigned char", 1, False),
206+
real_pt_regs_type.size,
207+
),
208+
"data",
209+
),
210+
],
211+
)
212+
213+
with fork_and_stop() as pid:
214+
trace = self.prog.stack_trace(pid)
215+
regs_dict = trace[0].registers()
216+
pt_regs_obj = Object(
217+
self.prog,
218+
real_pt_regs_type,
219+
{
220+
"bp": regs_dict["rbp"],
221+
"sp": regs_dict["rsp"],
222+
"ip": regs_dict["rip"],
223+
"r15": regs_dict["r15"],
224+
},
225+
)
226+
fake_pt_regs_obj = Object.from_bytes_(
227+
prog, fake_pt_regs_type, pt_regs_obj.to_bytes_()
228+
)
229+
# We must set drgn's log level manually, beacuse it won't log messages
230+
# to the logger if it isn't enabled for them.
231+
with self.assertLogs("drgn", logging.DEBUG) as log:
232+
no_debuginfo_trace = prog.stack_trace(fake_pt_regs_obj)
233+
234+
dwarf_pcs = []
235+
for frame in trace:
236+
if not dwarf_pcs or dwarf_pcs[-1] != frame.pc:
237+
dwarf_pcs.append(frame.pc)
238+
orc_pcs = [frame.pc for frame in no_debuginfo_trace]
239+
self.assertEqual(dwarf_pcs, orc_pcs)
240+
self._check_logged_orc_message(log, "kernel")
241+
107242
def test_registers(self):
108243
# Smoke test that we get at least one register and that
109244
# StackFrame.registers() agrees with StackFrame.register().

0 commit comments

Comments
 (0)