Skip to content

[lldb] Guard stale SBProcess target access#192842

Closed
sfu2 wants to merge 5 commits into
llvm:mainfrom
sfu2:lldb-stale-sbprocess-target
Closed

[lldb] Guard stale SBProcess target access#192842
sfu2 wants to merge 5 commits into
llvm:mainfrom
sfu2:lldb-stale-sbprocess-target

Conversation

@sfu2
Copy link
Copy Markdown
Contributor

@sfu2 sfu2 commented Apr 19, 2026

Description

SBSaveCoreOptions can retain a ProcessSP after DeleteTarget destroys the Target, leaving Process with a weak_ptr<Target> whose pointee has already been destroyed. Stale SBProcess handles then reach through Process::GetTarget() and crash while dereferencing that expired target relationship.

Reproduction

  1. Create an SBDebugger.
  2. Load a stopped Linux core file through the SB API to obtain an SBTarget and SBProcess.
  3. Store the process in SBSaveCoreOptions. This retains the underlying ProcessSP even after the target is deleted.
  4. Call SBDebugger::DeleteTarget(target). At this point the public SBTarget is invalid, but the retained Process may still outlive it.
  5. Call process.GetTarget().
    Pre-fix, this can crash because SBProcess::GetTarget() reaches Process::GetTarget(), which dereferences an expired weak target relationship after the Target has already been destroyed.
  6. Call process.SaveCore(options).
    Pre-fix, this can also crash because SBProcess::SaveCore() takes the target API mutex through process_sp->GetTarget().GetAPIMutex(), which dereferences the same expired target relationship.

The new regression test SBProcessDeleteTargetTest.GetTargetAndSaveCoreFailSafelyAfterDelete exercises this sequence with a core-backed SBProcess retained through SBSaveCoreOptions. It reuses the checked-in core-file fixtures lldb/test/API/tools/lldb-dap/coreFile/linux-x86_64.out and lldb/test/API/tools/lldb-dap/coreFile/linux-x86_64.core, resolving them directly from the source tree at runtime.

Note: Google Test filter = SBProcessDeleteTargetTest.*
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from SBProcessDeleteTargetTest
[ RUN      ] SBProcessDeleteTargetTest.GetTargetAndSaveCoreFailSafelyAfterDelete
Segmentation fault (core dumped)
EXIT_STATUS:139

Fix

Add a nullable Process::GetTargetSP() helper for SB API callers, use it to make SBProcess::GetTarget() return an invalid target and make SBProcess::SaveCore() fail with an error when the target has already been deleted. Add an API regression test that keeps a core-backed process alive through SBSaveCoreOptions, deletes the target, and verifies both paths fail safely instead of crashing.

@sfu2 sfu2 requested a review from JDevlieghere as a code owner April 19, 2026 11:07
@llvmbot llvmbot added the lldb label Apr 19, 2026
@llvmbot
Copy link
Copy Markdown
Member

llvmbot commented Apr 19, 2026

@llvm/pr-subscribers-flang-fir-hlfir
@llvm/pr-subscribers-backend-directx
@llvm/pr-subscribers-lto
@llvm/pr-subscribers-flang-openmp
@llvm/pr-subscribers-llvm-selectiondag
@llvm/pr-subscribers-mlgo
@llvm/pr-subscribers-backend-spir-v
@llvm/pr-subscribers-compiler-rt-sanitizer
@llvm/pr-subscribers-llvm-ir
@llvm/pr-subscribers-github-workflow
@llvm/pr-subscribers-backend-loongarch
@llvm/pr-subscribers-vectorizers
@llvm/pr-subscribers-pgo
@llvm/pr-subscribers-flang-driver
@llvm/pr-subscribers-llvm-support
@llvm/pr-subscribers-hlsl
@llvm/pr-subscribers-lld-elf
@llvm/pr-subscribers-platform-windows
@llvm/pr-subscribers-llvm-mc
@llvm/pr-subscribers-lld-macho
@llvm/pr-subscribers-clang-driver
@llvm/pr-subscribers-llvm-globalisel
@llvm/pr-subscribers-backend-powerpc
@llvm/pr-subscribers-backend-sparc
@llvm/pr-subscribers-backend-webassembly
@llvm/pr-subscribers-clang-codegen
@llvm/pr-subscribers-backend-amdgpu
@llvm/pr-subscribers-clang-static-analyzer-1
@llvm/pr-subscribers-backend-risc-v
@llvm/pr-subscribers-clang-format
@llvm/pr-subscribers-libunwind
@llvm/pr-subscribers-clang-modules
@llvm/pr-subscribers-clang-tools-extra
@llvm/pr-subscribers-clangd
@llvm/pr-subscribers-backend-x86
@llvm/pr-subscribers-backend-mips
@llvm/pr-subscribers-backend-msp430
@llvm/pr-subscribers-backend-hexagon
@llvm/pr-subscribers-libcxx
@llvm/pr-subscribers-infrastructure

@llvm/pr-subscribers-lldb

Author: Henry (sfu2)

Changes

Description

SBSaveCoreOptions can retain a ProcessSP after DeleteTarget destroys the Target, leaving Process with a weak_ptr&lt;Target&gt; whose pointee has already been destroyed. Stale SBProcess handles then reach through Process::GetTarget() and crash while dereferencing that expired target relationship.

Reproduction

  1. Create an SBDebugger.
  2. Load a stopped Linux core file through the SB API to obtain an SBTarget and SBProcess.
  3. Store the process in SBSaveCoreOptions. This retains the underlying ProcessSP even after the target is deleted.
  4. Call SBDebugger::DeleteTarget(target). At this point the public SBTarget is invalid, but the retained Process may still outlive it.
  5. Call process.GetTarget().
    Pre-fix, this can crash because SBProcess::GetTarget() reaches Process::GetTarget(), which dereferences an expired weak target relationship after the Target has already been destroyed.
  6. Call process.SaveCore(options).
    Pre-fix, this can also crash because SBProcess::SaveCore() takes the target API mutex through process_sp-&gt;GetTarget().GetAPIMutex(), which dereferences the same expired target relationship.

The new regression test SBProcessDeleteTargetTest.GetTargetAndSaveCoreFailSafelyAfterDelete exercises this sequence with a core-backed SBProcess retained through SBSaveCoreOptions. It reuses the checked-in core-file fixtures lldb/test/API/tools/lldb-dap/coreFile/linux-x86_64.out and lldb/test/API/tools/lldb-dap/coreFile/linux-x86_64.core, resolving them directly from the source tree at runtime.

Note: Google Test filter = SBProcessDeleteTargetTest.*
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from SBProcessDeleteTargetTest
[ RUN      ] SBProcessDeleteTargetTest.GetTargetAndSaveCoreFailSafelyAfterDelete
Segmentation fault (core dumped)
EXIT_STATUS:139

Fix

Add a nullable Process::GetTargetSP() helper for SB API callers, use it to make SBProcess::GetTarget() return an invalid target and make SBProcess::SaveCore() fail with an error when the target has already been deleted. Add an API regression test that keeps a core-backed process alive through SBSaveCoreOptions, deletes the target, and verifies both paths fail safely instead of crashing.


Full diff: https://github.com/llvm/llvm-project/pull/192842.diff

4 Files Affected:

  • (modified) lldb/include/lldb/Target/Process.h (+5)
  • (modified) lldb/source/API/SBProcess.cpp (+16-7)
  • (modified) lldb/unittests/API/CMakeLists.txt (+1)
  • (added) lldb/unittests/API/SBProcessDeleteTargetTest.cpp (+101)
diff --git a/lldb/include/lldb/Target/Process.h b/lldb/include/lldb/Target/Process.h
index 19b5ae3041826..ca53f4633f4d5 100644
--- a/lldb/include/lldb/Target/Process.h
+++ b/lldb/include/lldb/Target/Process.h
@@ -1244,6 +1244,11 @@ class Process : public std::enable_shared_from_this<Process>,
 
   /// Get the target object pointer for this module.
   ///
+  /// \return
+  ///     A Target object pointer to the target that owns this
+  ///     module.
+  lldb::TargetSP GetTargetSP() const { return m_target_wp.lock(); }
+
   /// \return
   ///     A Target object pointer to the target that owns this
   ///     module.
diff --git a/lldb/source/API/SBProcess.cpp b/lldb/source/API/SBProcess.cpp
index 14ce236b4f1b5..ca8d2100f4075 100644
--- a/lldb/source/API/SBProcess.cpp
+++ b/lldb/source/API/SBProcess.cpp
@@ -241,12 +241,15 @@ SBTarget SBProcess::GetTarget() const {
   LLDB_INSTRUMENT_VA(this);
 
   SBTarget sb_target;
-  TargetSP target_sp;
   ProcessSP process_sp(GetSP());
-  if (process_sp) {
-    target_sp = process_sp->GetTarget().shared_from_this();
-    sb_target.SetSP(target_sp);
-  }
+  if (!process_sp)
+    return sb_target;
+
+  TargetSP target_sp = process_sp->GetTargetSP();
+  if (!target_sp)
+    return sb_target;
+
+  sb_target.SetSP(target_sp);
 
   return sb_target;
 }
@@ -1274,8 +1277,14 @@ lldb::SBError SBProcess::SaveCore(SBSaveCoreOptions &options) {
     return error;
   }
 
-  std::lock_guard<std::recursive_mutex> guard(
-      process_sp->GetTarget().GetAPIMutex());
+  TargetSP target_sp = process_sp->GetTargetSP();
+  if (!target_sp) {
+    error = Status::FromErrorString(
+        "SBProcess is invalid because its target has been deleted");
+    return error;
+  }
+
+  std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
 
   if (process_sp->GetState() != eStateStopped) {
     error = Status::FromErrorString("the process is not stopped");
diff --git a/lldb/unittests/API/CMakeLists.txt b/lldb/unittests/API/CMakeLists.txt
index b86054fb353f7..19a6302021d8e 100644
--- a/lldb/unittests/API/CMakeLists.txt
+++ b/lldb/unittests/API/CMakeLists.txt
@@ -3,6 +3,7 @@ add_lldb_unittest(APITests
   SBLineEntryTest.cpp
   SBMutexTest.cpp
   SBBreakpointClearConditionTest.cpp
+  SBProcessDeleteTargetTest.cpp
 
   SBAPITEST
 
diff --git a/lldb/unittests/API/SBProcessDeleteTargetTest.cpp b/lldb/unittests/API/SBProcessDeleteTargetTest.cpp
new file mode 100644
index 0000000000000..078d5aa53ed6f
--- /dev/null
+++ b/lldb/unittests/API/SBProcessDeleteTargetTest.cpp
@@ -0,0 +1,101 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// Use the umbrella header for -Wdocumentation.
+#include "lldb/API/LLDB.h"
+
+#include "TestingSupport/SubsystemRAII.h"
+#include "gtest/gtest.h"
+#include "llvm/ADT/SmallString.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/Path.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+namespace {
+
+class SBProcessDeleteTargetTest : public testing::Test {
+protected:
+  void SetUp() override {
+    debugger = SBDebugger::Create(/*source_init_files=*/false);
+  }
+
+  void TearDown() override { SBDebugger::Destroy(debugger); }
+
+  static bool DebuggerSupportsLLVMTarget(llvm::StringRef target) {
+    SBStructuredData data = SBDebugger::GetBuildConfiguration()
+                                .GetValueForKey("targets")
+                                .GetValueForKey("value");
+    for (size_t i = 0; i < data.GetSize(); ++i) {
+      char buf[100] = {0};
+      size_t size = data.GetItemAtIndex(i).GetStringValue(buf, sizeof(buf));
+      if (llvm::StringRef(buf, size) == target)
+        return true;
+    }
+    return false;
+  }
+
+  static std::string GetCoreFixturePath(llvm::StringRef name) {
+    llvm::SmallString<256> path(__FILE__);
+    llvm::sys::path::remove_filename(path);
+    llvm::sys::path::append(path, "..", "..", "test", "API");
+    llvm::sys::path::append(path, "tools", "lldb-dap", "coreFile", name);
+    return std::string(path.str());
+  }
+
+  void LoadCore(SBTarget &target, SBProcess &process) {
+    std::string binary_path = GetCoreFixturePath("linux-x86_64.out");
+    std::string core_path = GetCoreFixturePath("linux-x86_64.core");
+    ASSERT_TRUE(llvm::sys::fs::exists(binary_path));
+    ASSERT_TRUE(llvm::sys::fs::exists(core_path));
+
+    SBError error;
+    target = debugger.CreateTarget(binary_path.c_str(), /*target_triple=*/"",
+                                   /*platform_name=*/"",
+                                   /*add_dependent_modules=*/false, error);
+    EXPECT_TRUE(target);
+    EXPECT_TRUE(error.Success()) << error.GetCString();
+    debugger.SetSelectedTarget(target);
+
+    process = target.LoadCore(core_path.c_str());
+    EXPECT_TRUE(process);
+  }
+
+  SubsystemRAII<lldb::SBDebugger> subsystems;
+  SBDebugger debugger;
+};
+
+} // namespace
+
+TEST_F(SBProcessDeleteTargetTest, GetTargetAndSaveCoreFailSafelyAfterDelete) {
+  if (!DebuggerSupportsLLVMTarget("X86"))
+    GTEST_SKIP() << "X86 target is not enabled";
+
+  SBTarget target;
+  SBProcess process;
+  LoadCore(target, process);
+  ASSERT_TRUE(target.IsValid());
+  ASSERT_TRUE(process.IsValid());
+  ASSERT_EQ(process.GetState(), eStateStopped);
+
+  SBSaveCoreOptions options;
+  options.SetProcess(process);
+
+  ASSERT_TRUE(debugger.DeleteTarget(target));
+  ASSERT_FALSE(target.IsValid());
+  EXPECT_FALSE(process.IsValid());
+
+  EXPECT_FALSE(process.GetTarget().IsValid());
+
+  SBError error = process.SaveCore(options);
+  EXPECT_TRUE(error.Fail());
+  EXPECT_STREQ("SBProcess is invalid because its target has been deleted",
+               error.GetCString());
+}

@github-actions
Copy link
Copy Markdown

⚠️ We detected that you are using a GitHub private e-mail address to contribute to the repo.
Please turn off Keep my email addresses private setting in your account.
See LLVM Developer Policy and LLVM Discourse for more information.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 19, 2026

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

@sfu2 sfu2 force-pushed the lldb-stale-sbprocess-target branch from 6c49089 to 1459b97 Compare April 19, 2026 11:14
SBSaveCoreOptions can retain a ProcessSP after DeleteTarget destroys
the Target, leaving Process with a weak_ptr<Target> whose pointee has
already been destroyed. Stale SBProcess handles then reach through
Process::GetTarget() and crash while dereferencing that expired target
relationship.

Add a nullable Process::GetTargetSP() helper for SB API callers, use it
to make SBProcess::GetTarget() return an invalid target and make
SBProcess::SaveCore() fail with an error when the target has already been
deleted. Add an API regression test that keeps a core-backed process
alive through SBSaveCoreOptions, deletes the target, and verifies both
paths fail safely instead of crashing.
@sfu2 sfu2 force-pushed the lldb-stale-sbprocess-target branch from 1459b97 to 14bf8a7 Compare April 19, 2026 11:20
@JDevlieghere JDevlieghere requested a review from jimingham April 20, 2026 18:59
@JDevlieghere
Copy link
Copy Markdown
Member

@jimingham My understanding was that a Process always has a Target. I would expect that deleting the target causes the process to be delated as well. Can you confirm whether that was the intended design?

@jimingham
Copy link
Copy Markdown
Contributor

We did not intend that holding onto an SBProcess should keep its SBTarget alive. The SBProcess and the SBTarget both hold weak pointers to their lldb_private counterpart. An SBProcess whose weak pointer "lock's" successfully should always have a valid TargetSP.

If we wanted to do the error-handling more completely, when we find we have an invalid Target in SBProcess::GetTarget, we should assert that the Process WP locks to an empty shared pointer. If the process is also invalid, then it's correct to hand out an empty SBTarget.

Anything else would indicate a programming error.

Copy link
Copy Markdown
Contributor

@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 looks right.

For extra credit, you might check the case where you have a valid SBProcess (i.e. its process_wp locks to a valid sp) but it doesn't have a valid Target.

Having an SBProcess whose underlying lldb_private Process & Target have gone away is a thing that can happen, but having an lldb_private Process whose Target has gone away should not be possible.

@sfu2
Copy link
Copy Markdown
Contributor Author

sfu2 commented Apr 22, 2026

Basically, this patch fixes the stale retained-ProcessSP case after DeleteTarget. I don’t currently see a normal path for a live non-finalized lldb_private::Process to lose its Target though.

@jimingham
Copy link
Copy Markdown
Contributor

The SBProcess holds a ProcessWP, so it doesn't keep the process alive. The Target holds a ProcessSP, but it resets that in Finalize and in the Destructor.

The situation you are protecting against is that the Target has been destroyed (so that Process's TargetWP wouldn't resolve to a valid SP) but the ProcessWP in SBProcess still resolves to a valid ProcessSP. That can only happen if something else must have been holding onto a ProcessSP for the process in question as well as the Target.

The ProcessSP might be still alive but it has been finalized and isn't really useable at that stage, so I don't think we're doing any good handing out SBProcess objects with actual but invalid Processes.

I wonder if a more complete fix here might be to have SBProcess::GetSP() also return an empty shared pointer if the ProcessSP that you get back from locking the weak pointer had been Finalized (that's what Process::IsValid() checks). That would mean that in the only cases where you might hand out a valid SBProcess it would always have a Target to hand out as well.

@sfu2
Copy link
Copy Markdown
Contributor Author

sfu2 commented Apr 23, 2026

Sure. The fix was updated so the stale retained-ProcessSP case after DeleteTarget is handled centrally in SBProcess::GetSP().

In a325beb, the regression was tightened to expect stale finalized processes to behave as invalid SBProcesss;
In bed4e59, the fix was implemented by making SBProcess::GetSP() return empty if the locked Process has already been finalized, so those stale processes no longer surface as usable SBProcess objects.

Copy link
Copy Markdown
Contributor

@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.

LGTM

Thanks for cleaning this up.

@llvmbot llvmbot added clang-tidy clang:driver 'clang' and 'clang++' user-facing binaries. Not 'clang-cl' clang-format clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:headers Headers provided by Clang, e.g. for intrinsics clang:modules C++20 modules and Clang Header Modules clang:codegen IR generation bugs: mangling, exceptions, etc. clang:as-a-library libclang and C++ API clang:static analyzer compiler-rt:asan Address sanitizer compiler-rt:builtins compiler-rt:fuzzer compiler-rt:ubsan Undefined behavior sanitizer compiler-rt:scudo Scudo Hardened Allocator llvm:codegen libc llvm:regalloc debuginfo llvm:mc Machine (object) code lld:MachO lld:ELF llvm:globalisel lld:wasm labels Apr 25, 2026
@sfu2
Copy link
Copy Markdown
Contributor Author

sfu2 commented Apr 25, 2026

I am terribly sorry for the noise — I accidentally clicked GitHub's sync/update button on my fork branch, which temporarily pulled a large unrelated upstream diff into this PR and caused unrelated code-owner/subscriber review requests.

I have force-pushed the branch back to the intended LLDB-only changes. The remaining diff should now be limited to the SBProcess stale-target fix and regression test.

I do not have permission to remove the unrelated reviewers/subscriber requests or labels. Could someone with maintainer permission please kindly clear the accidental unrelated metadata if needed? I could also open a new PR referencing this one if that's better.

Sincere apologies again for the noise to everyone.

Comment thread lldb/include/lldb/Target/Process.h Outdated
Comment thread lldb/source/API/SBProcess.cpp
Fix a typo in comment.

Co-authored-by: Sergei Barannikov <barannikov88@gmail.com>
Comment thread lldb/source/API/SBProcess.cpp Outdated
TargetSP target_sp = process_sp->GetTargetSP();
assert(target_sp && "valid SBProcess should have a target");
if (!target_sp) {
error = Status::FromErrorString("SBProcess is invalid");
Copy link
Copy Markdown
Contributor

@s-barannikov s-barannikov Apr 25, 2026

Choose a reason for hiding this comment

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

Is there a typo in the error string? We're in SBProcess method and process_sp is valid at this point (checked at the beginning of this function).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The original wording was meant at the SB API level: this handle is no longer usable as an SBProcess, even though the local process_sp is still non-null. But I agree that reads as ambiguous here. I'll fix it in the next commit.


TargetSP target_sp = process_sp->GetTargetSP();
assert(target_sp && "valid SBProcess should have a target");
if (!target_sp)
Copy link
Copy Markdown
Contributor

@s-barannikov s-barannikov Apr 25, 2026

Choose a reason for hiding this comment

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

I guess same applies here (but error reporting is missing?).
I feel a bit uncomfortable about reporting a user error while it is actually reports a violation of an internal contract, if I'm reading what @jimingham wrote correctly. The bug will be there forever because no one will find it unless they use a debug build of lldb. Maybe this should be lldbassert in the end. I'd like someone on the lldb team confirm or refute my reasoning (I'm new to lldb).

@HazardyKnusperkeks
Copy link
Copy Markdown
Contributor

I am terribly sorry for the noise — I accidentally clicked GitHub's sync/update button on my fork branch, which temporarily pulled a large unrelated upstream diff into this PR and caused unrelated code-owner/subscriber review requests.

I have force-pushed the branch back to the intended LLDB-only changes. The remaining diff should now be limited to the SBProcess stale-target fix and regression test.

I do not have permission to remove the unrelated reviewers/subscriber requests or labels. Could someone with maintainer permission please kindly clear the accidental unrelated metadata if needed? I could also open a new PR referencing this one if that's better.

Sincere apologies again for the noise to everyone.

Most people just abandon the PR and create a new one, because now you spam everyone until they have the chance to unsubscribe manually. (Which I will now do.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

9 participants