summaryrefslogtreecommitdiff
path: root/lldb/test/API/functionalities/unwind/libunwind_ret_injection/TestLibUnwindRetInjection.py
blob: e03234d1b5077f799118596ea66c2fd7a2813d07 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""
Test that libunwind correctly injects 'ret' instructions to rebalance execution flow
when unwinding C++ exceptions. This is important for Apple Processor Trace analysis.
"""

import lldb
import os
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
from lldbsuite.test import configuration


class LibunwindRetInjectionTestCase(TestBase):
    @skipIf(archs=no_match(["arm64", "arm64e", "aarch64"]))
    @skipUnlessDarwin
    @skipIfOutOfTreeLibunwind
    def test_ret_injection_on_exception_unwind(self):
        """Test that __libunwind_Registers_arm64_jumpto receives correct walkedFrames count and injects the right number of ret instructions."""
        self.build()

        exe = self.getBuildArtifact("a.out")
        target = self.dbg.CreateTarget(exe)
        self.assertTrue(target, VALID_TARGET)

        # Find the just-built libunwind, not the system one.
        # llvm_tools_dir is typically <build>/bin, so lib is a sibling.
        self.assertIsNotNone(
            configuration.llvm_tools_dir,
            "llvm_tools_dir must be set to find in-tree libunwind",
        )

        llvm_lib_dir = os.path.join(
            os.path.dirname(configuration.llvm_tools_dir), "lib"
        )

        # Find the libunwind library (platform-agnostic).
        libunwind_path = None
        for filename in os.listdir(llvm_lib_dir):
            if filename.startswith("libunwind.") or filename.startswith("unwind."):
                libunwind_path = os.path.join(llvm_lib_dir, filename)
                break

        self.assertIsNotNone(
            libunwind_path, f"Could not find libunwind in {llvm_lib_dir}"
        )

        # Set breakpoint in __libunwind_Registers_arm64_jumpto.
        # This is the function that performs the actual jump and ret injection.
        bp = target.BreakpointCreateByName("__libunwind_Registers_arm64_jumpto")
        self.assertTrue(bp.IsValid())
        self.assertGreater(bp.GetNumLocations(), 0)

        # Set up DYLD_INSERT_LIBRARIES to use the just-built libunwind.
        launch_info = lldb.SBLaunchInfo(None)
        env = target.GetEnvironment()
        env.Set("DYLD_INSERT_LIBRARIES", libunwind_path, True)
        launch_info.SetEnvironment(env, False)

        # Launch the process with our custom libunwind.
        error = lldb.SBError()
        process = target.Launch(launch_info, error)
        self.assertSuccess(
            error, f"Failed to launch process with libunwind at {libunwind_path}"
        )
        self.assertTrue(process, PROCESS_IS_VALID)

        # We should hit the breakpoint in __libunwind_Registers_arm64_jumpto
        # during the exception unwinding phase 2.
        threads = lldbutil.get_threads_stopped_at_breakpoint(process, bp)
        self.assertEqual(len(threads), 1, "Should have stopped at breakpoint")

        thread = threads[0]
        frame = thread.GetFrameAtIndex(0)

        # Verify we're in __libunwind_Registers_arm64_jumpto.
        function_name = frame.GetFunctionName()
        self.assertTrue(
            "__libunwind_Registers_arm64_jumpto" in function_name,
            f"Expected to be in __libunwind_Registers_arm64_jumpto, got {function_name}",
        )

        # On ARM64, the walkedFrames parameter should be in register x1 (second parameter).
        # According to the ARM64 calling convention, integer arguments are passed in x0-x7.
        # x0 = Registers_arm64* pointer.
        # x1 = unsigned walkedFrames.
        error = lldb.SBError()
        x1_value = frame.register["x1"].GetValueAsUnsigned(error)
        self.assertSuccess(error, "Failed to read x1 register")

        # According to the code in UnwindCursor.hpp, the walkedFrames value represents:
        # 1. The number of frames walked in unwind_phase2 to reach the landing pad.
        # 2. Plus _EXTRA_LIBUNWIND_FRAMES_WALKED = 5 - 1 = 4 additional libunwind frames.
        #
        # From the comment in the code:
        #   frame #0: __libunwind_Registers_arm64_jumpto
        #   frame #1: Registers_arm64::returnto
        #   frame #2: UnwindCursor::jumpto
        #   frame #3: __unw_resume
        #   frame #4: __unw_resume_with_frames_walked
        #   frame #5: unwind_phase2
        #
        # Since __libunwind_Registers_arm64_jumpto returns to the landing pad,
        # we subtract 1, so _EXTRA_LIBUNWIND_FRAMES_WALKED = 4.
        #
        # For our test program:
        # - unwind_phase2 starts walking (frame 0 counted here).
        # - Walks through: func_d (throw site), func_c, func_b, func_a.
        # - Finds landing pad in main.
        # That's approximately 4-5 frames from the user code.
        # Plus the 4 extra libunwind frames.
        #
        # So we expect x1 to be roughly 8-10.
        expected_min_frames = 8
        expected_max_frames = 13  # Allow some variation for libc++abi frames.

        self.assertGreaterEqual(
            x1_value,
            expected_min_frames,
            f"walkedFrames (x1) should be >= {expected_min_frames}, got {x1_value}. "
            "This is the number of 'ret' instructions that will be executed.",
        )

        self.assertLessEqual(
            x1_value,
            expected_max_frames,
            f"walkedFrames (x1) should be <= {expected_max_frames}, got {x1_value}. "
            "Value seems too high.",
        )

        # Now step through the ret injection loop and count the actual number of 'ret' executions.
        # The loop injects exactly x1_value ret instructions before continuing with register restoration.
        # We step until we hit the first 'ldp' instruction (register restoration starts with 'ldp x2, x3, [x0, #0x010]').
        ret_executed_count = 0
        max_steps = 100  # Safety limit to prevent infinite loops.

        for step_count in range(max_steps):
            # Get current instruction.
            pc = frame.GetPC()
            inst = process.ReadMemory(pc, 4, lldb.SBError())

            # Disassemble current instruction.
            current_inst = target.GetInstructions(lldb.SBAddress(pc, target), inst)[0]
            mnemonic = current_inst.GetMnemonic(target)
            operands = current_inst.GetOperands(target)

            # Check if we've reached the register restoration part (first ldp after the loop).
            if mnemonic == "ldp":
                # We've exited the ret injection loop.
                break

            # Count 'ret' instructions that get executed.
            if mnemonic == "ret":
                self.assertEqual(operands, "x16")
                ret_executed_count += 1

            # Step one instruction.
            thread.StepInstruction(False)  # False = step over.

            # Update frame reference.
            frame = thread.GetFrameAtIndex(0)

        # Verify we didn't hit the safety limit.
        self.assertLess(
            step_count,
            max_steps - 1,
            f"Stepped {max_steps} times without reaching 'ldp' instruction. Something is wrong.",
        )

        # The number of executed 'ret' instructions should match x1_value.
        # According to the implementation, the loop executes exactly x1_value times.
        self.assertEqual(
            ret_executed_count,
            x1_value,
            f"Expected {x1_value} 'ret' instructions to be executed (matching x1 register), "
            f"but counted {ret_executed_count} executed 'ret' instructions.",
        )