[Lldb-commits] [lldb] [lldb/crashlog] Fix inlined frames in crashlog scripted process (PR #191132)

Med Ismail Bennani via lldb-commits lldb-commits at lists.llvm.org
Fri Apr 10 15:48:03 PDT 2026


https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/191132

>From 5aef0cfc31180be3f43269b2bb932e41b6f93bdf Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Fri, 10 Apr 2026 15:47:49 -0700
Subject: [PATCH] [lldb/crashlog] Fix inlined frames in crashlog scripted
 process

When loading a crashlog via the scripted process, inlined frames are
lost. This happens because `ScriptedThread::LoadArtificialStackFrames`
creates all frames as concrete frames via `SetFrameAtIndex`, completely
bypassing the inline frame synthesis that `StackFrameList::FetchFramesUpTo`
normally performs using `GetParentOfInlinedScope`. Since two crashlog
frames share the same PC when one is inlined into the other,
`CalculateSymbolContext` resolves both to the innermost inlined scope,
which causes the containing function to be dropped from the backtrace.

This patch fixes the issue in two parts:
- On the Python side, `resolve_stackframes` now skips frames whose PC
 matches the next frame's PC. These are inlined frames that LLDB will
 synthesize from debug info when it processes the concrete frames we
 provide. Indices are renumbered accordingly, and `len(frames) == 0`
 is used for first-frame detection.
- On the C++ side, `LoadArtificialStackFrames` now calls
 `StackFrameList::SynthesizeInlineFrames` after creating each concrete
 frame. This new method extracts the inline synthesis loop previously
 duplicated between `FetchFramesUpTo` and `LoadArtificialStackFrames`:
 it calls `GetParentOfInlinedScope` in a loop and creates a
 `StackFrame` for each inlined parent scope.

rdar://154981041

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
---
 lldb/examples/python/crashlog.py              | 47 ++++++++++-----
 .../python/crashlog_scripted_process.py       | 20 ++++++-
 lldb/include/lldb/Target/StackFrameList.h     |  6 ++
 .../Process/scripted/ScriptedThread.cpp       | 19 ++++--
 lldb/source/Target/StackFrameList.cpp         | 60 +++++++++++--------
 .../Python/Crashlog/Inputs/a.out.inline.crash | 49 +++++++++++++++
 .../Python/Crashlog/Inputs/inline_test.c      |  8 +++
 .../Python/Crashlog/inline_crashlog.test      | 19 ++++++
 8 files changed, 178 insertions(+), 50 deletions(-)
 create mode 100644 lldb/test/Shell/ScriptInterpreter/Python/Crashlog/Inputs/a.out.inline.crash
 create mode 100644 lldb/test/Shell/ScriptInterpreter/Python/Crashlog/Inputs/inline_test.c
 create mode 100644 lldb/test/Shell/ScriptInterpreter/Python/Crashlog/inline_crashlog.test

diff --git a/lldb/examples/python/crashlog.py b/lldb/examples/python/crashlog.py
index ea978bb1e1b16..a1be2edd70aa5 100755
--- a/lldb/examples/python/crashlog.py
+++ b/lldb/examples/python/crashlog.py
@@ -266,10 +266,11 @@ def __str__(self):
     class Frame:
         """Class that represents a stack frame in a thread in a darwin crash log"""
 
-        def __init__(self, index, pc, description):
+        def __init__(self, index, pc, description, inlined=False):
             self.pc = pc
             self.description = description
             self.index = index
+            self.inlined = inlined
 
         def __str__(self):
             if self.description:
@@ -729,6 +730,7 @@ def parse_frames(self, thread, json_frames):
             if ident not in self.crashlog.idents:
                 self.crashlog.idents.append(ident)
 
+            inlined = "inline" in json_frame and bool(json_frame["inline"])
             frame_offset = int(json_frame["imageOffset"])
             image_addr = self.get_used_image(image_id)["base"]
             pc = image_addr + frame_offset
@@ -745,7 +747,7 @@ def parse_frames(self, thread, json_frames):
                     "address": frame_offset - location,
                 }
 
-            thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset))
+            thread.frames.append(self.crashlog.Frame(idx, pc, frame_offset, inlined))
 
             # on arm64 systems, if it jump through a null function pointer,
             # we end up at address 0 and the crash reporter unwinder
@@ -802,18 +804,19 @@ def parse_asi_backtrace(self, thread, bt):
                 frame_symbol
             ) = frame_offset = frame_file = frame_line = frame_column = None
 
-            if len(frame_match.groups()) == 3:
+            if len(frame_match.groups()) == 4:
                 # Get the image UUID from the frame image name.
-                (frame_id, frame_img_name, frame_addr) = frame_match.groups()
-            elif len(frame_match.groups()) == 5:
+                frame_id, frame_img_name, frame_addr, _ = frame_match.groups()
+            elif len(frame_match.groups()) == 6:
                 (
                     frame_id,
                     frame_img_name,
                     frame_addr,
                     frame_symbol,
                     frame_offset,
+                    _,
                 ) = frame_match.groups()
-            elif len(frame_match.groups()) == 7:
+            elif len(frame_match.groups()) == 8:
                 (
                     frame_id,
                     frame_img_name,
@@ -822,8 +825,9 @@ def parse_asi_backtrace(self, thread, bt):
                     frame_offset,
                     frame_file,
                     frame_line,
+                    _,
                 ) = frame_match.groups()
-            elif len(frame_match.groups()) == 8:
+            elif len(frame_match.groups()) == 9:
                 (
                     frame_id,
                     frame_img_name,
@@ -833,8 +837,11 @@ def parse_asi_backtrace(self, thread, bt):
                     frame_file,
                     frame_line,
                     frame_column,
+                    _,
                 ) = frame_match.groups()
 
+            inlined = frame_match.group("inlined") is not None
+
             thread.add_ident(frame_img_name)
             if frame_img_name not in self.crashlog.idents:
                 self.crashlog.idents.append(frame_img_name)
@@ -855,7 +862,9 @@ def parse_asi_backtrace(self, thread, bt):
                         }
 
             thread.frames.append(
-                self.crashlog.Frame(int(frame_id), int(frame_addr, 0), description)
+                self.crashlog.Frame(
+                    int(frame_id), int(frame_addr, 0), description, inlined
+                )
             )
 
         return True
@@ -943,8 +952,11 @@ def get(cls):
                         )?
                        """
 
+            inlined = r"(?:\s+(?P<inlined>\[inlined\]))?"
+
             return re.compile(
-                index + img_name + version + address + symbol, flags=re.VERBOSE
+                index + img_name + version + address + symbol + inlined,
+                flags=re.VERBOSE,
             )
 
     frame_regex = FrameRegex.get()
@@ -1209,18 +1221,19 @@ def parse_thread(self, line):
             frame_addr
         ) = frame_symbol = frame_offset = frame_file = frame_line = frame_column = None
 
-        if len(frame_match.groups()) == 3:
+        if len(frame_match.groups()) == 4:
             # Get the image UUID from the frame image name.
-            (frame_id, frame_img_name, frame_addr) = frame_match.groups()
-        elif len(frame_match.groups()) == 5:
+            frame_id, frame_img_name, frame_addr, _ = frame_match.groups()
+        elif len(frame_match.groups()) == 6:
             (
                 frame_id,
                 frame_img_name,
                 frame_addr,
                 frame_symbol,
                 frame_offset,
+                _,
             ) = frame_match.groups()
-        elif len(frame_match.groups()) == 7:
+        elif len(frame_match.groups()) == 8:
             (
                 frame_id,
                 frame_img_name,
@@ -1229,8 +1242,9 @@ def parse_thread(self, line):
                 frame_offset,
                 frame_file,
                 frame_line,
+                _,
             ) = frame_match.groups()
-        elif len(frame_match.groups()) == 8:
+        elif len(frame_match.groups()) == 9:
             (
                 frame_id,
                 frame_img_name,
@@ -1240,8 +1254,11 @@ def parse_thread(self, line):
                 frame_file,
                 frame_line,
                 frame_column,
+                _,
             ) = frame_match.groups()
 
+        inlined = frame_match.group("inlined") is not None
+
         self.thread.add_ident(frame_img_name)
         if frame_img_name not in self.crashlog.idents:
             self.crashlog.idents.append(frame_img_name)
@@ -1265,7 +1282,7 @@ def parse_thread(self, line):
             )
 
         self.thread.frames.append(
-            self.crashlog.Frame(int(frame_id), int(frame_addr, 0), description)
+            self.crashlog.Frame(int(frame_id), int(frame_addr, 0), description, inlined)
         )
 
         return True
diff --git a/lldb/examples/python/crashlog_scripted_process.py b/lldb/examples/python/crashlog_scripted_process.py
index 6c6eec8d12b96..11b1fe2688200 100644
--- a/lldb/examples/python/crashlog_scripted_process.py
+++ b/lldb/examples/python/crashlog_scripted_process.py
@@ -154,14 +154,28 @@ def create_register_ctx(self):
 
     def resolve_stackframes(thread, addr_mask, target):
         frames = []
-        for frame in thread.frames:
+        for i, frame in enumerate(thread.frames):
             frame_pc = frame.pc & addr_mask
-            pc = frame_pc if frame.index == 0 or frame_pc == 0 else frame_pc - 1
+            # Skip inlined frames so LLDB can reconstruct them from
+            # debug info when it processes the concrete frames we provide.
+            # Use the inlined attribute when available (symbolicated reports),
+            # otherwise fall back to comparing PCs (non-symbolicated reports).
+            if frame.inlined:
+                continue
+            next_frame = thread.frames[i + 1] if i + 1 < len(thread.frames) else None
+            if next_frame and frame_pc == (next_frame.pc & addr_mask):
+                continue
+            # Don't subtract 1 from the first concrete frame (it's the actual
+            # PC, not a return address) or from null PCs.
+            if len(frames) == 0 or frame_pc == 0:
+                pc = frame_pc
+            else:
+                pc = frame_pc - 1
             sym_addr = lldb.SBAddress()
             sym_addr.SetLoadAddress(pc, target)
             if not sym_addr.IsValid():
                 continue
-            frames.append({"idx": frame.index, "pc": pc})
+            frames.append({"idx": len(frames), "pc": pc})
         return frames
 
     def create_stackframes(self):
diff --git a/lldb/include/lldb/Target/StackFrameList.h b/lldb/include/lldb/Target/StackFrameList.h
index f8822a8dadc9b..6e69ff546814e 100644
--- a/lldb/include/lldb/Target/StackFrameList.h
+++ b/lldb/include/lldb/Target/StackFrameList.h
@@ -257,6 +257,12 @@ class StackFrameList : public std::enable_shared_from_this<StackFrameList> {
   void FetchOnlyConcreteFramesUpTo(uint32_t end_idx);
   void SynthesizeTailCallFrames(StackFrame &next_frame);
 
+  /// Synthesize inline frames for \p frame_sp by walking the inlined
+  /// scope chain via GetParentOfInlinedScope and appending frames to the
+  /// list. Returns the number of inline frames created.
+  uint32_t SynthesizeInlineFrames(lldb::StackFrameSP frame_sp,
+                                  lldb::addr_t cfa);
+
   StackFrameList(const StackFrameList &) = delete;
   const StackFrameList &operator=(const StackFrameList &) = delete;
 };
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
index 391137571ab1e..c8790d245b7e1 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
@@ -175,7 +175,8 @@ bool ScriptedThread::LoadArtificialStackFrames() {
         error, LLDBLog::Thread);
 
   auto create_frame_from_dict =
-      [this, arr_sp](size_t idx) -> llvm::Expected<StackFrameSP> {
+      [this, arr_sp](size_t idx,
+                     uint32_t frame_list_idx) -> llvm::Expected<StackFrameSP> {
     Status error;
     std::optional<StructuredData::Dictionary *> maybe_dict =
         arr_sp->GetItemAtIndexAsDictionary(idx);
@@ -206,12 +207,12 @@ bool ScriptedThread::LoadArtificialStackFrames() {
     lldb::addr_t cfa = LLDB_INVALID_ADDRESS;
     bool cfa_is_valid = false;
     const bool artificial = false;
-    const bool behaves_like_zeroth_frame = false;
+    const bool behaves_like_zeroth_frame = (frame_list_idx == 0);
     SymbolContext sc;
     symbol_addr.CalculateSymbolContext(&sc);
 
-    return std::make_shared<StackFrame>(shared_from_this(), idx, idx, cfa,
-                                        cfa_is_valid, pc,
+    return std::make_shared<StackFrame>(shared_from_this(), frame_list_idx, idx,
+                                        cfa, cfa_is_valid, pc,
                                         StackFrame::Kind::Synthetic, artificial,
                                         behaves_like_zeroth_frame, &sc);
   };
@@ -247,11 +248,12 @@ bool ScriptedThread::LoadArtificialStackFrames() {
   };
 
   StackFrameListSP frames = GetStackFrameList();
+  uint32_t frame_list_idx = 0;
 
   for (size_t idx = 0; idx < arr_size; idx++) {
     StackFrameSP synth_frame_sp = nullptr;
 
-    auto frame_from_dict_or_err = create_frame_from_dict(idx);
+    auto frame_from_dict_or_err = create_frame_from_dict(idx, frame_list_idx);
     if (!frame_from_dict_or_err) {
       auto frame_from_script_obj_or_err = create_frame_from_script_object(idx);
 
@@ -270,13 +272,18 @@ bool ScriptedThread::LoadArtificialStackFrames() {
       synth_frame_sp = *frame_from_dict_or_err;
     }
 
-    if (!frames->SetFrameAtIndex(static_cast<uint32_t>(idx), synth_frame_sp))
+    if (!frames->SetFrameAtIndex(frame_list_idx, synth_frame_sp))
       return ScriptedInterface::ErrorWithMessage<bool>(
           LLVM_PRETTY_FUNCTION,
           llvm::Twine("Couldn't add frame (" + llvm::Twine(idx) +
                       llvm::Twine(") to ScriptedThread StackFrameList."))
               .str(),
           error, LLDBLog::Thread);
+    frame_list_idx++;
+
+    // Synthesize inline frames, mirroring StackFrameList::FetchFramesUpTo().
+    frame_list_idx += frames->SynthesizeInlineFrames(
+        synth_frame_sp, /*cfa=*/LLDB_INVALID_ADDRESS);
   }
 
   return true;
diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp
index 675cf84693296..fb3d0c5f31cb8 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -398,6 +398,39 @@ void StackFrameList::SynthesizeTailCallFrames(StackFrame &next_frame) {
     next_frame.SetFrameIndex(m_frames.size());
 }
 
+uint32_t StackFrameList::SynthesizeInlineFrames(StackFrameSP frame_sp,
+                                                addr_t cfa) {
+  SymbolContext unwind_sc =
+      frame_sp->GetSymbolContext(eSymbolContextBlock | eSymbolContextFunction);
+  if (!unwind_sc.block)
+    return 0;
+
+  TargetSP target_sp = m_thread.CalculateTarget();
+  uint32_t concrete_frame_idx = frame_sp->GetConcreteFrameIndex();
+  Address curr_frame_address(frame_sp->GetFrameCodeAddressForSymbolication());
+
+  SymbolContext next_frame_sc;
+  Address next_frame_address;
+  uint32_t num_inlined_frames = 0;
+
+  while (unwind_sc.GetParentOfInlinedScope(curr_frame_address, next_frame_sc,
+                                           next_frame_address)) {
+    next_frame_sc.line_entry.ApplyFileMappings(target_sp);
+    StackFrameSP inline_frame_sp = std::make_shared<StackFrame>(
+        m_thread.shared_from_this(), m_frames.size(), concrete_frame_idx,
+        frame_sp->GetRegisterContextSP(), cfa, next_frame_address,
+        /*behaves_like_zeroth_frame=*/false, &next_frame_sc);
+
+    inline_frame_sp->m_frame_list_id = GetIdentifier();
+    m_frames.push_back(inline_frame_sp);
+    unwind_sc = next_frame_sc;
+    curr_frame_address = next_frame_address;
+    ++num_inlined_frames;
+  }
+
+  return num_inlined_frames;
+}
+
 bool StackFrameList::GetFramesUpTo(uint32_t end_idx,
                                    InterruptionControl allow_interrupt) {
   // GetFramesUpTo is always called with the intent to add frames, so get the
@@ -544,32 +577,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx,
     }
 
     assert(unwind_frame_sp);
-    SymbolContext unwind_sc = unwind_frame_sp->GetSymbolContext(
-        eSymbolContextBlock | eSymbolContextFunction);
-    Block *unwind_block = unwind_sc.block;
-    TargetSP target_sp = m_thread.CalculateTarget();
-    if (unwind_block) {
-      Address curr_frame_address(
-          unwind_frame_sp->GetFrameCodeAddressForSymbolication());
-
-      SymbolContext next_frame_sc;
-      Address next_frame_address;
-
-      while (unwind_sc.GetParentOfInlinedScope(
-          curr_frame_address, next_frame_sc, next_frame_address)) {
-        next_frame_sc.line_entry.ApplyFileMappings(target_sp);
-        behaves_like_zeroth_frame = false;
-        StackFrameSP frame_sp(new StackFrame(
-            m_thread.shared_from_this(), m_frames.size(), idx,
-            unwind_frame_sp->GetRegisterContextSP(), cfa, next_frame_address,
-            behaves_like_zeroth_frame, &next_frame_sc));
-
-        frame_sp->m_frame_list_id = GetIdentifier();
-        m_frames.push_back(frame_sp);
-        unwind_sc = next_frame_sc;
-        curr_frame_address = next_frame_address;
-      }
-    }
+    SynthesizeInlineFrames(unwind_frame_sp, cfa);
   } while (m_frames.size() - 1 < end_idx);
 
   // Don't try to merge till you've calculated all the frames in this stack.
diff --git a/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/Inputs/a.out.inline.crash b/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/Inputs/a.out.inline.crash
new file mode 100644
index 0000000000000..c8579377ca275
--- /dev/null
+++ b/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/Inputs/a.out.inline.crash
@@ -0,0 +1,49 @@
+Process:               a.out [21606]
+Path:                  /private/tmp/a.out
+Identifier:            a.out
+Version:               0
+Code Type:             ARM-64 (Native)
+Parent Process:        fish [88883]
+User ID:               501
+
+Date/Time:             2020-11-11 14:47:34.600 -0800
+OS Version:            macOS 14.0
+Report Version:        12
+Anonymous UUID:        DCEF35CB-68D5-F524-FF13-060901F52EA8
+
+
+Time Awake Since Boot: 400000 seconds
+
+System Integrity Protection: enabled
+
+Crashed Thread:        0  Dispatch queue: com.apple.main-thread
+
+Exception Type:        EXC_BAD_ACCESS (SIGSEGV)
+Exception Codes:       KERN_INVALID_ADDRESS at 0x0000000000000000
+Exception Note:        EXC_CORPSE_NOTIFY
+
+Termination Signal:    Segmentation fault: 11
+Termination Reason:    Namespace SIGNAL, Code 0xb
+Terminating Process:   exc handler [21606]
+
+Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
+0   a.out                         	@bar@ foo + 16 (inline_test.c:3)
+1   a.out                         	@bar@ bar + 16 (inline_test.c:6)
+2   a.out                         	@main@ main + 20 (inline_test.c:8)
+3   libdyld.dylib                 	0x1000000 start + 1
+
+Thread 0 crashed with ARM Thread State (64-bit):
+    x0: 0x0000000000000000   x1: 0x0000000000000000   x2: 0x0000000000000000   x3: 0x0000000000000000
+    x4: 0x0000000000000000   x5: 0x0000000000000000   x6: 0x0000000000000000   x7: 0x0000000000000000
+    x8: 0x0000000000000001   x9: 0x0000000000000000  x10: 0x0000000000000000  x11: 0x0000000000000000
+   x12: 0x0000000000000000  x13: 0x0000000000000000  x14: 0x0000000000000000  x15: 0x0000000000000000
+   x16: 0x0000000000000000  x17: 0x0000000000000000  x18: 0x0000000000000000  x19: 0x0000000000000000
+   x20: 0x0000000000000000  x21: 0x0000000000000000  x22: 0x0000000000000000  x23: 0x0000000000000000
+   x24: 0x0000000000000000  x25: 0x0000000000000000  x26: 0x0000000000000000  x27: 0x0000000000000000
+   x28: 0x0000000000000000   fp: 0x000000016f04ef00   lr: 0x0000000000000000
+    sp: 0x000000016f04eee0   pc: 0x0000000100000354 cpsr: 0x80001000
+   far: 0x0000000000000000  esr: 0x92000046 (Data Abort) byte write Translation fault
+
+Binary Images:
+       0x100000000 -        0x200000000 +a.out (0) <@UUID@> @EXEC@
+               0x0 - 0xffffffffffffffff ??? (*) <00000000-0000-0000-0000-000000000000> ???
diff --git a/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/Inputs/inline_test.c b/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/Inputs/inline_test.c
new file mode 100644
index 0000000000000..8f2db68a03706
--- /dev/null
+++ b/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/Inputs/inline_test.c
@@ -0,0 +1,8 @@
+__attribute__((always_inline)) void foo() {
+  int *i = 0;
+  *i = 1;
+}
+
+void bar() { foo(); }
+
+int main(int argc, char **argv) { bar(); }
diff --git a/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/inline_crashlog.test b/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/inline_crashlog.test
new file mode 100644
index 0000000000000..f588208226e51
--- /dev/null
+++ b/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/inline_crashlog.test
@@ -0,0 +1,19 @@
+# REQUIRES: python, native && system-darwin
+
+# RUN: %clang_host -g %S/Inputs/inline_test.c -o %t.out
+# RUN: cp %S/Inputs/a.out.inline.crash %t.crash
+# RUN: %python %S/patch-crashlog.py --binary %t.out --crashlog %t.crash --offsets '{"main":20, "bar":16}'
+# RUN: %lldb %t.out \
+# RUN:   -o 'command script import lldb.macosx.crashlog' \
+# RUN:   -o 'crashlog -a -i %t.crash' \
+# RUN:   -o 'thread backtrace' 2>&1 | FileCheck %s
+
+# CHECK: "crashlog" {{.*}} commands have been installed, use the "--help" options on these commands
+
+# Verify that the inlined frame (foo) and its containing function (bar) both
+# appear in the backtrace, even though the crashlog listed them at the same PC.
+# CHECK:      (lldb) thread backtrace
+# CHECK-NEXT: * thread #1
+# CHECK-NEXT:   * frame #0: {{.*}}out`foo{{.*}} [synthetic] [inlined]
+# CHECK-NEXT:     frame #1: {{.*}}out`bar
+# CHECK:          frame #{{[0-9]+}}: {{.*}}out`main{{.*}} [synthetic]



More information about the lldb-commits mailing list