Skip to content

[lldb] Guard SBFrame/SBThread methods against running processes #152020

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

felipepiovezan
Copy link
Contributor

Prior to this patch, SBFrame/SBThread methods exhibit racy behavior if called while the process is running, because they do not lock the Process::RetRunLock mutex. If they did, they would fail, correctly identifying that the process is not running.

Some methods attempt to protect against this with the pattern:

ExecutionContext exe_ctx(m_opaque_sp.get(), lock); // this is a different lock
Process *process = exe_ctx.GetProcessPtr();
if (process) {
  Process::StopLocker stop_locker;
  if (stop_locker.TryLock(&process->GetRunLock()))
        .... do work ...

However, this is also racy: the constructor of ExecutionContext will access the frame list, which is something that can only be done once the process is stopped.

With this patch:

  1. The constructor of ExecutionContext now expects a ProcessRunLock as an argument. It attempts to lock the run lock, and only fills in information about frames and threads if the lock can be acquired. Callers of the constructor are expected to check the lock.
  2. All uses of ExecutionContext are adjusted to conform to the above.
  3. The SBThread.cpp-defined helper function ResumeNewPlan now expects a locked ProcessRunLock as proof that the execution is stopped. It will unlock the mutex prior to resuming the process.

This commit exposes many opportunities for early-returns, but these would increase the diff of this patch and distract from the important changes, so we opt not to do it here.

@llvmbot
Copy link
Member

llvmbot commented Aug 4, 2025

@llvm/pr-subscribers-lldb

Author: Felipe de Azevedo Piovezan (felipepiovezan)

Changes

Prior to this patch, SBFrame/SBThread methods exhibit racy behavior if called while the process is running, because they do not lock the Process::RetRunLock mutex. If they did, they would fail, correctly identifying that the process is not running.

Some methods attempt to protect against this with the pattern:

ExecutionContext exe_ctx(m_opaque_sp.get(), lock); // this is a different lock
Process *process = exe_ctx.GetProcessPtr();
if (process) {
  Process::StopLocker stop_locker;
  if (stop_locker.TryLock(&process->GetRunLock()))
        .... do work ...

However, this is also racy: the constructor of ExecutionContext will access the frame list, which is something that can only be done once the process is stopped.

With this patch:

  1. The constructor of ExecutionContext now expects a ProcessRunLock as an argument. It attempts to lock the run lock, and only fills in information about frames and threads if the lock can be acquired. Callers of the constructor are expected to check the lock.
  2. All uses of ExecutionContext are adjusted to conform to the above.
  3. The SBThread.cpp-defined helper function ResumeNewPlan now expects a locked ProcessRunLock as proof that the execution is stopped. It will unlock the mutex prior to resuming the process.

This commit exposes many opportunities for early-returns, but these would increase the diff of this patch and distract from the important changes, so we opt not to do it here.


Patch is 65.73 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/152020.diff

7 Files Affected:

  • (modified) lldb/include/lldb/API/SBFrame.h (+4)
  • (modified) lldb/include/lldb/Host/ProcessRunLock.h (+5)
  • (modified) lldb/include/lldb/Target/ExecutionContext.h (+6-3)
  • (modified) lldb/source/API/SBFrame.cpp (+291-242)
  • (modified) lldb/source/API/SBThread.cpp (+193-136)
  • (modified) lldb/source/Target/ExecutionContext.cpp (+8-4)
  • (modified) lldb/test/API/python_api/run_locker/TestRunLocker.py (+1-1)
diff --git a/lldb/include/lldb/API/SBFrame.h b/lldb/include/lldb/API/SBFrame.h
index 3635ee5a537ad..35198068814d0 100644
--- a/lldb/include/lldb/API/SBFrame.h
+++ b/lldb/include/lldb/API/SBFrame.h
@@ -233,6 +233,10 @@ class LLDB_API SBFrame {
 
   void SetFrameSP(const lldb::StackFrameSP &lldb_object_sp);
 
+  /// Return an SBValue with an error message warning that the process is not
+  /// currently stopped.
+  static SBValue CreateProcessIsRunningExprEvalError();
+
   lldb::ExecutionContextRefSP m_opaque_sp;
 };
 
diff --git a/lldb/include/lldb/Host/ProcessRunLock.h b/lldb/include/lldb/Host/ProcessRunLock.h
index 94683673024d5..5efa46154a61e 100644
--- a/lldb/include/lldb/Host/ProcessRunLock.h
+++ b/lldb/include/lldb/Host/ProcessRunLock.h
@@ -41,9 +41,14 @@ class ProcessRunLock {
   class ProcessRunLocker {
   public:
     ProcessRunLocker() = default;
+    ProcessRunLocker(ProcessRunLocker &&other) : m_lock(other.m_lock) {
+      other.m_lock = nullptr;
+    }
 
     ~ProcessRunLocker() { Unlock(); }
 
+    bool IsLocked() const { return m_lock; }
+
     // Try to lock the read lock, but only do so if there are no writers.
     bool TryLock(ProcessRunLock *lock) {
       if (m_lock) {
diff --git a/lldb/include/lldb/Target/ExecutionContext.h b/lldb/include/lldb/Target/ExecutionContext.h
index aebd0d5308e72..ae6051f576fac 100644
--- a/lldb/include/lldb/Target/ExecutionContext.h
+++ b/lldb/include/lldb/Target/ExecutionContext.h
@@ -11,6 +11,7 @@
 
 #include <mutex>
 
+#include "lldb/Host/ProcessRunLock.h"
 #include "lldb/Target/StackID.h"
 #include "lldb/lldb-private.h"
 
@@ -318,11 +319,13 @@ class ExecutionContext {
   // These two variants take in a locker, and grab the target, lock the API
   // mutex into locker, then fill in the rest of the shared pointers.
   ExecutionContext(const ExecutionContextRef &exe_ctx_ref,
-                   std::unique_lock<std::recursive_mutex> &locker)
-      : ExecutionContext(&exe_ctx_ref, locker) {}
+                   std::unique_lock<std::recursive_mutex> &locker,
+                   ProcessRunLock::ProcessRunLocker &stop_locker)
+      : ExecutionContext(&exe_ctx_ref, locker, stop_locker) {}
 
   ExecutionContext(const ExecutionContextRef *exe_ctx_ref,
-                   std::unique_lock<std::recursive_mutex> &locker);
+                   std::unique_lock<std::recursive_mutex> &locker,
+                   ProcessRunLock::ProcessRunLocker &stop_locker);
   // Create execution contexts from execution context scopes
   ExecutionContext(ExecutionContextScope *exe_scope);
   ExecutionContext(ExecutionContextScope &exe_scope);
diff --git a/lldb/source/API/SBFrame.cpp b/lldb/source/API/SBFrame.cpp
index 5b69cf1ee2641..b6fd5e7d3524b 100644
--- a/lldb/source/API/SBFrame.cpp
+++ b/lldb/source/API/SBFrame.cpp
@@ -99,14 +99,15 @@ SBFrame::operator bool() const {
   LLDB_INSTRUMENT_VA(this);
 
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return false;
 
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock()))
-      return GetFrameSP().get() != nullptr;
+    return GetFrameSP().get() != nullptr;
   }
 
   // Without a target & process we can't have a valid stack frame.
@@ -118,16 +119,16 @@ SBSymbolContext SBFrame::GetSymbolContext(uint32_t resolve_scope) const {
 
   SBSymbolContext sb_sym_ctx;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return sb_sym_ctx;
   SymbolContextItem scope = static_cast<SymbolContextItem>(resolve_scope);
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      if (StackFrame *frame = exe_ctx.GetFramePtr())
-        sb_sym_ctx = frame->GetSymbolContext(scope);
-    }
+    if (StackFrame *frame = exe_ctx.GetFramePtr())
+      sb_sym_ctx = frame->GetSymbolContext(scope);
   }
 
   return sb_sym_ctx;
@@ -139,19 +140,19 @@ SBModule SBFrame::GetModule() const {
   SBModule sb_module;
   ModuleSP module_sp;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return sb_module;
 
   StackFrame *frame = nullptr;
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      frame = exe_ctx.GetFramePtr();
-      if (frame) {
-        module_sp = frame->GetSymbolContext(eSymbolContextModule).module_sp;
-        sb_module.SetSP(module_sp);
-      }
+    frame = exe_ctx.GetFramePtr();
+    if (frame) {
+      module_sp = frame->GetSymbolContext(eSymbolContextModule).module_sp;
+      sb_module.SetSP(module_sp);
     }
   }
 
@@ -163,19 +164,19 @@ SBCompileUnit SBFrame::GetCompileUnit() const {
 
   SBCompileUnit sb_comp_unit;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return sb_comp_unit;
 
   StackFrame *frame = nullptr;
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      frame = exe_ctx.GetFramePtr();
-      if (frame) {
-        sb_comp_unit.reset(
-            frame->GetSymbolContext(eSymbolContextCompUnit).comp_unit);
-      }
+    frame = exe_ctx.GetFramePtr();
+    if (frame) {
+      sb_comp_unit.reset(
+          frame->GetSymbolContext(eSymbolContextCompUnit).comp_unit);
     }
   }
 
@@ -187,19 +188,19 @@ SBFunction SBFrame::GetFunction() const {
 
   SBFunction sb_function;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return sb_function;
 
   StackFrame *frame = nullptr;
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      frame = exe_ctx.GetFramePtr();
-      if (frame) {
-        sb_function.reset(
-            frame->GetSymbolContext(eSymbolContextFunction).function);
-      }
+    frame = exe_ctx.GetFramePtr();
+    if (frame) {
+      sb_function.reset(
+          frame->GetSymbolContext(eSymbolContextFunction).function);
     }
   }
 
@@ -211,18 +212,18 @@ SBSymbol SBFrame::GetSymbol() const {
 
   SBSymbol sb_symbol;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return sb_symbol;
 
   StackFrame *frame = nullptr;
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      frame = exe_ctx.GetFramePtr();
-      if (frame) {
-        sb_symbol.reset(frame->GetSymbolContext(eSymbolContextSymbol).symbol);
-      }
+    frame = exe_ctx.GetFramePtr();
+    if (frame) {
+      sb_symbol.reset(frame->GetSymbolContext(eSymbolContextSymbol).symbol);
     }
   }
 
@@ -234,18 +235,18 @@ SBBlock SBFrame::GetBlock() const {
 
   SBBlock sb_block;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return sb_block;
 
   StackFrame *frame = nullptr;
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      frame = exe_ctx.GetFramePtr();
-      if (frame)
-        sb_block.SetPtr(frame->GetSymbolContext(eSymbolContextBlock).block);
-    }
+    frame = exe_ctx.GetFramePtr();
+    if (frame)
+      sb_block.SetPtr(frame->GetSymbolContext(eSymbolContextBlock).block);
   }
   return sb_block;
 }
@@ -255,18 +256,18 @@ SBBlock SBFrame::GetFrameBlock() const {
 
   SBBlock sb_block;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return sb_block;
 
   StackFrame *frame = nullptr;
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      frame = exe_ctx.GetFramePtr();
-      if (frame)
-        sb_block.SetPtr(frame->GetFrameBlock());
-    }
+    frame = exe_ctx.GetFramePtr();
+    if (frame)
+      sb_block.SetPtr(frame->GetFrameBlock());
   }
   return sb_block;
 }
@@ -276,19 +277,19 @@ SBLineEntry SBFrame::GetLineEntry() const {
 
   SBLineEntry sb_line_entry;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return sb_line_entry;
 
   StackFrame *frame = nullptr;
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      frame = exe_ctx.GetFramePtr();
-      if (frame) {
-        sb_line_entry.SetLineEntry(
-            frame->GetSymbolContext(eSymbolContextLineEntry).line_entry);
-      }
+    frame = exe_ctx.GetFramePtr();
+    if (frame) {
+      sb_line_entry.SetLineEntry(
+          frame->GetSymbolContext(eSymbolContextLineEntry).line_entry);
     }
   }
   return sb_line_entry;
@@ -300,7 +301,10 @@ uint32_t SBFrame::GetFrameID() const {
   uint32_t frame_idx = UINT32_MAX;
 
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return frame_idx;
 
   StackFrame *frame = exe_ctx.GetFramePtr();
   if (frame)
@@ -313,7 +317,10 @@ lldb::addr_t SBFrame::GetCFA() const {
   LLDB_INSTRUMENT_VA(this);
 
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return LLDB_INVALID_ADDRESS;
 
   StackFrame *frame = exe_ctx.GetFramePtr();
   if (frame)
@@ -326,19 +333,19 @@ addr_t SBFrame::GetPC() const {
 
   addr_t addr = LLDB_INVALID_ADDRESS;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return addr;
 
   StackFrame *frame = nullptr;
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      frame = exe_ctx.GetFramePtr();
-      if (frame) {
-        addr = frame->GetFrameCodeAddress().GetOpcodeLoadAddress(
-            target, AddressClass::eCode);
-      }
+    frame = exe_ctx.GetFramePtr();
+    if (frame) {
+      addr = frame->GetFrameCodeAddress().GetOpcodeLoadAddress(
+          target, AddressClass::eCode);
     }
   }
 
@@ -350,17 +357,17 @@ bool SBFrame::SetPC(addr_t new_pc) {
 
   bool ret_val = false;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return ret_val;
 
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      if (StackFrame *frame = exe_ctx.GetFramePtr()) {
-        if (RegisterContextSP reg_ctx_sp = frame->GetRegisterContext()) {
-          ret_val = reg_ctx_sp->SetPC(new_pc);
-        }
+    if (StackFrame *frame = exe_ctx.GetFramePtr()) {
+      if (RegisterContextSP reg_ctx_sp = frame->GetRegisterContext()) {
+        ret_val = reg_ctx_sp->SetPC(new_pc);
       }
     }
   }
@@ -373,17 +380,17 @@ addr_t SBFrame::GetSP() const {
 
   addr_t addr = LLDB_INVALID_ADDRESS;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return addr;
 
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      if (StackFrame *frame = exe_ctx.GetFramePtr()) {
-        if (RegisterContextSP reg_ctx_sp = frame->GetRegisterContext()) {
-          addr = reg_ctx_sp->GetSP();
-        }
+    if (StackFrame *frame = exe_ctx.GetFramePtr()) {
+      if (RegisterContextSP reg_ctx_sp = frame->GetRegisterContext()) {
+        addr = reg_ctx_sp->GetSP();
       }
     }
   }
@@ -396,17 +403,17 @@ addr_t SBFrame::GetFP() const {
 
   addr_t addr = LLDB_INVALID_ADDRESS;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return addr;
 
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      if (StackFrame *frame = exe_ctx.GetFramePtr()) {
-        if (RegisterContextSP reg_ctx_sp = frame->GetRegisterContext()) {
-          addr = reg_ctx_sp->GetFP();
-        }
+    if (StackFrame *frame = exe_ctx.GetFramePtr()) {
+      if (RegisterContextSP reg_ctx_sp = frame->GetRegisterContext()) {
+        addr = reg_ctx_sp->GetFP();
       }
     }
   }
@@ -419,18 +426,18 @@ SBAddress SBFrame::GetPCAddress() const {
 
   SBAddress sb_addr;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return sb_addr;
 
   StackFrame *frame = exe_ctx.GetFramePtr();
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      frame = exe_ctx.GetFramePtr();
-      if (frame)
-        sb_addr.SetAddress(frame->GetFrameCodeAddress());
-    }
+    frame = exe_ctx.GetFramePtr();
+    if (frame)
+      sb_addr.SetAddress(frame->GetFrameCodeAddress());
   }
   return sb_addr;
 }
@@ -446,7 +453,10 @@ lldb::SBValue SBFrame::GetValueForVariablePath(const char *var_path) {
 
   SBValue sb_value;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return sb_value;
 
   StackFrame *frame = exe_ctx.GetFramePtr();
   Target *target = exe_ctx.GetTargetPtr();
@@ -468,25 +478,25 @@ lldb::SBValue SBFrame::GetValueForVariablePath(const char *var_path,
   }
 
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return sb_value;
 
   StackFrame *frame = nullptr;
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      frame = exe_ctx.GetFramePtr();
-      if (frame) {
-        VariableSP var_sp;
-        Status error;
-        ValueObjectSP value_sp(frame->GetValueForVariableExpressionPath(
-            var_path, eNoDynamicValues,
-            StackFrame::eExpressionPathOptionCheckPtrVsMember |
-                StackFrame::eExpressionPathOptionsAllowDirectIVarAccess,
-            var_sp, error));
-        sb_value.SetSP(value_sp, use_dynamic);
-      }
+    frame = exe_ctx.GetFramePtr();
+    if (frame) {
+      VariableSP var_sp;
+      Status error;
+      ValueObjectSP value_sp(frame->GetValueForVariableExpressionPath(
+          var_path, eNoDynamicValues,
+          StackFrame::eExpressionPathOptionCheckPtrVsMember |
+              StackFrame::eExpressionPathOptionsAllowDirectIVarAccess,
+          var_sp, error));
+      sb_value.SetSP(value_sp, use_dynamic);
     }
   }
   return sb_value;
@@ -497,7 +507,10 @@ SBValue SBFrame::FindVariable(const char *name) {
 
   SBValue value;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return value;
 
   StackFrame *frame = exe_ctx.GetFramePtr();
   Target *target = exe_ctx.GetTargetPtr();
@@ -522,21 +535,21 @@ SBValue SBFrame::FindVariable(const char *name,
 
   ValueObjectSP value_sp;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return sb_value;
 
   StackFrame *frame = nullptr;
   Target *target = exe_ctx.GetTargetPtr();
   Process *process = exe_ctx.GetProcessPtr();
   if (target && process) {
-    Process::StopLocker stop_locker;
-    if (stop_locker.TryLock(&process->GetRunLock())) {
-      frame = exe_ctx.GetFramePtr();
-      if (frame) {
-        value_sp = frame->FindVariable(ConstString(name));
+    frame = exe_ctx.GetFramePtr();
+    if (frame) {
+      value_sp = frame->FindVariable(ConstString(name));
 
-        if (value_sp)
-          sb_value.SetSP(value_sp, use_dynamic);
-      }
+      if (value_sp)
+        sb_value.SetSP(value_sp, use_dynamic);
     }
   }
 
@@ -548,7 +561,10 @@ SBValue SBFrame::FindValue(const char *name, ValueType value_type) {
 
   SBValue value;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
+  if (!stop_locker.IsLocked())
+    return value;
 
   StackFrame *frame = exe_ctx.GetFramePtr();
   Target *target = exe_ctx.GetTargetPtr();
@@ -572,14 +588,14 @@ SBValue SBFrame::FindValue(const char *name, ValueType value_type,
 
   ValueObjectSP value_sp;
   std::unique_lock<std::recursive_mutex> lock;
-  ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
+  Process::StopLocker stop_locker;
+  Executi...
[truncated]

Copy link

github-actions bot commented Aug 4, 2025

✅ With the latest revision this PR passed the Python code formatter.

Copy link

github-actions bot commented Aug 4, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

Prior to this patch, SBFrame/SBThread methods exhibit racy behavior if
called while the process is running, because they do not lock the
`Process::RetRunLock` mutex. If they did, they would fail, correctly
identifying that the process is not running.

Some methods _attempt_ to protect against this with the pattern:

```
ExecutionContext exe_ctx(m_opaque_sp.get(), lock); // this is a different lock
Process *process = exe_ctx.GetProcessPtr();
if (process) {
  Process::StopLocker stop_locker;
  if (stop_locker.TryLock(&process->GetRunLock()))
        .... do work ...
```

However, this is also racy: the constructor of `ExecutionContext` will
access the frame list, which is something that can only be done once the
process is stopped.

With this patch:

1. The constructor of `ExecutionContext` now expects a `ProcessRunLock`
   as an argument. It attempts to lock the run lock, and only fills in
   information about frames and threads if the lock can be acquired.
   Callers of the constructor are expected to check the lock.
2. All uses of ExecutionContext are adjusted to conform to the above.
3. The SBThread.cpp-defined helper function ResumeNewPlan now expects a
   locked ProcessRunLock as _proof_ that the execution is stopped. It
   will unlock the mutex prior to resuming the process.

This commit exposes many opportunities for early-returns, but these
would increase the diff of this patch and distract from the important
changes, so we opt not to do it here.
@felipepiovezan felipepiovezan force-pushed the felipe/fix_racy_sbmethods branch from 301d8ac to 9be92f8 Compare August 4, 2025 20:59
@felipepiovezan
Copy link
Contributor Author

This linux test failed, but I'm not yet sure if it is related, as it doesn't fail locally for me:

2025-08-04T21:05:44.7103932Z   File "/home/gha/actions-runner/_work/llvm-project/llvm-project/lldb/test/API/python_api/default-constructor/TestDefaultConstructorForAPIObjects.py", line 328, in test_SBThread
2025-08-04T21:05:44.7105222Z     sb_thread.fuzz_obj(obj)
2025-08-04T21:05:44.7106213Z   File "/home/gha/actions-runner/_work/llvm-project/llvm-project/lldb/test/API/python_api/default-constructor/sb_thread.py", line 12, in fuzz_obj
2025-08-04T21:05:44.7107258Z     obj.GetStopDescription(256)
2025-08-04T21:05:44.7108355Z   File "/home/gha/actions-runner/_work/llvm-project/llvm-project/build/local/lib/python3.12/dist-packages/lldb/__init__.py", line 14194, in GetStopDescription
2025-08-04T21:05:44.7109675Z     return _lldb.SBThread_GetStopDescription(self, dst_or_null)
2025-08-04T21:05:44.7110225Z            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
2025-08-04T21:05:44.7111017Z SystemError: <built-in function SBThread_GetStopDescription> returned NULL without setting an exception
2025-08-04T21:05:44.7112033Z Config=x86_64-/home/gha/actions-runner/_work/llvm-project/llvm-project/build/bin/clang

std::unique_lock<std::recursive_mutex> &lock)
ExecutionContext::ExecutionContext(
const ExecutionContextRef *exe_ctx_ref_ptr,
std::unique_lock<std::recursive_mutex> &lock,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that there are two locks, you might want to give this one a more descriptive name?

@@ -99,14 +99,15 @@ SBFrame::operator bool() const {
LLDB_INSTRUMENT_VA(this);

std::unique_lock<std::recursive_mutex> lock;
ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
Process::StopLocker stop_locker;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why this function didn't start off with:

if (!m_opaque_sp) return false;

But given we're doing more work now, it might be good to add that short-circuit.

Copy link
Member

@JDevlieghere JDevlieghere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this PR is updating all call sites of this ctor. We're always passing a unique_lock and the StopLocker. I understand they do different things and we need both, but if we always need both, should we abstract this away a bit more. Like a UniqueStopLocker or something, which also holds onto the unique_lock RAII style?

Comment on lines +236 to +237
/// Return an SBValue with an error message warning that the process is not
/// currently stopped.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The "error message warning" is somewhat confusing. How about:

Suggested change
/// Return an SBValue with an error message warning that the process is not
/// currently stopped.
/// Return an SBValue containing an error message that warns the process is not currently stopped.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the SB API level, if you are getting the ExecutionContext from an ExecutionContextRef represented by a Target, Process, Thread or Frame you should also hold the API mutex. There are some cases where we don't actually require a process. For instance SBTarget::CreateValueFromAddress can work on either a live process that's stopped or to look at static data on a non-running target. But those seem like they are in the minority, and in that case, for instance, you could easily do:

if (!process || stop_locker.IsLocked())

so that wouldn't be particularly awkward.

I can't think of a good reason why you would want to drop the API lock before the stop lock or vice versa. There's the one place where Felipe swaps the stop_locker originally set for another, so long as this doesn't make that particularly awkward, this seems doable.

Copy link
Collaborator

@jimingham jimingham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty close.

In the SBThread modifications there were a bunch of places where you were returning empty SBError's or not filling in the error you were passed in with an informative message. We should fill in error's we're passed everywhere.

I think this would look nicer if the ExecutionContext constructor was:

ExecutionContext(ExecutionContextRef *, API_Locker, StopLocker, Status *status = nullptr)

Then in all the code that returns or is passed an SBError, you could just pass its opaque pointer to the constructor as well, and you wouldn't either be as likely to forget to do it or have to do it by hand in a whole bunch of places.

Other than that I just had a couple comments about comments.

@@ -318,11 +319,13 @@ class ExecutionContext {
// These two variants take in a locker, and grab the target, lock the API
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment isn't correct anymore.
You should mention that IF the ExecutionContextRef coming in has a process, it will try to lock the stop locker and won't fill anything in below the process. But if the exe_ctx_ref doesn't have a process, the stop_locker won't get locked. That's fairly obvious, but still worth making clear.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also worth pointing out that IF the stop_locker is locked, the process and target have to be valid, so you don't need to check for that after checking the stop_locker...

ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
Process::StopLocker stop_locker;
ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
if (!stop_locker.IsLocked())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree you shouldn't do code simplifications in this pass. But if the stop_locker is locked, then you must have a target and a process. Since this function, for instance, doesn't actually use the target & process pointers except to check for null, that code could be eliminated.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be another cleanup pass...

@@ -782,27 +809,26 @@ SBValueList SBFrame::GetVariables(const lldb::SBVariablesOptions &options) {

SBValueList value_list;
std::unique_lock<std::recursive_mutex> lock;
ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
Process::StopLocker stop_locker;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, GetVariables didn't take the stop locker originally???

ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
Process::StopLocker stop_locker;
ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
if (stop_locker.IsLocked()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you early returned here, there would actually be fewer differences, and it would match the pattern you've set up in all the other methods. I'd do that here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, no I see. Lots more code would have to be outdented below if you did that. Okay, that's fine for a second pass. But it does look odd to have mixed early return and not here.

: m_target_sp(), m_process_sp(), m_thread_sp(), m_frame_sp() {
if (exe_ctx_ref_ptr) {
m_target_sp = exe_ctx_ref_ptr->GetTargetSP();
if (m_target_sp) {
lock = std::unique_lock<std::recursive_mutex>(m_target_sp->GetAPIMutex());

m_process_sp = exe_ctx_ref_ptr->GetProcessSP();
m_thread_sp = exe_ctx_ref_ptr->GetThreadSP();
m_frame_sp = exe_ctx_ref_ptr->GetFrameSP();
if (m_process_sp && stop_locker.TryLock(&m_process_sp->GetRunLock())) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be useful to log (to the API log channel) the times that someone called in here and there WAS a process but it was not stopped. If you saw a flurry of those in an API log it would be a strong indication that the client was not doing the right thing.

Process::StopLocker stop_locker;
ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
if (!stop_locker.IsLocked())
return;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this one.
Maybe we should make the ExecutionContext constructor take an optional Status * object, and if that's not empty we can put a standard error in it? Then you could just pass in these errors, and then you wouldn't have to set the error directly each time.
You could also use that for your SBValue error return; I'm pretty sure there's a way to make a ValueObject just from a Status.

ExecutionContext exe_ctx(m_opaque_sp.get(), lock);
Process::StopLocker stop_locker;
ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
if (!stop_locker.IsLocked())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one also has an error.

Process::StopLocker stop_locker;
ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
if (!stop_locker.IsLocked())
return sb_error;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't return an empty error when we know the error... Again, if the ExecutionContext constructor took an optional error, that would be simple to do.

Process::StopLocker stop_locker;
ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
if (!stop_locker.IsLocked())
return error;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, a better error here.

Process::StopLocker stop_locker;
ExecutionContext exe_ctx(m_opaque_sp.get(), lock, stop_locker);
if (!stop_locker.IsLocked())
return sb_error;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants