[Lldb-commits] [lldb] [lldb] Match runtime linkage names without indexing unrelated symbols (PR #232694)

Karim Alweheshy via lldb-commits lldb-commits at lists.llvm.org
Fri Oct 16 04:58:36 PDT 2026


https://github.com/karim-alweheshy updated https://github.com/llvm/llvm-project/pull/232694

>From 2888d6d184552bee1b635bbc575968b12360e79a Mon Sep 17 00:00:00 2001
From: Karim Alweheshy <karim.alweheshy at reddit.com>
Date: Fri, 16 Oct 2026 11:15:06 +0200
Subject: [PATCH 1/2] [lldb] Match runtime linkage names without indexing
 unrelated symbols

Add an opt-in exact linkage-name path that preserves symbol order, types and linker annotations while avoiding demangled-name indexes. Use it for BoundsSafety detection and breakpoint activation, retaining existing DWARF lookup and default name matching.

Assisted-by: OpenAI Codex
---
 .../lldb/Breakpoint/BreakpointResolverName.h  |   9 +
 lldb/include/lldb/Core/Module.h               |   8 +-
 lldb/include/lldb/Core/ModuleList.h           |   3 +-
 lldb/include/lldb/Symbol/Symtab.h             |   8 +-
 .../Breakpoint/BreakpointResolverName.cpp     |  24 +-
 lldb/source/Core/Module.cpp                   |  13 +-
 lldb/source/Core/ModuleList.cpp               |   6 +-
 .../InstrumentationRuntimeBoundsSafety.cpp    |  60 +++--
 lldb/source/Symbol/Symtab.cpp                 |  50 ++++-
 .../BoundsSafety/runtime_detection/Makefile   |  16 ++
 .../TestBoundsSafetyRuntimeDetection.py       | 160 +++++++++++++
 .../runtime_detection/cxx_runtime.cpp         |   2 +
 .../runtime_detection/late_runtime.cpp        |   2 +
 .../runtime_detection/library_runtime.c       |   2 +
 .../BoundsSafety/runtime_detection/main.c     |  30 +++
 .../BoundsSafety/runtime_detection/runtime.c  |  25 +++
 .../runtime_detection/unrelated.cpp           |   5 +
 lldb/unittests/Symbol/SymtabTest.cpp          | 211 ++++++++++++++++++
 lldb/unittests/Target/CMakeLists.txt          |   2 +
 .../Target/InstrumentationRuntimeTest.cpp     | 145 ++++++++++++
 20 files changed, 748 insertions(+), 33 deletions(-)
 create mode 100644 lldb/test/API/lang/BoundsSafety/runtime_detection/Makefile
 create mode 100644 lldb/test/API/lang/BoundsSafety/runtime_detection/TestBoundsSafetyRuntimeDetection.py
 create mode 100644 lldb/test/API/lang/BoundsSafety/runtime_detection/cxx_runtime.cpp
 create mode 100644 lldb/test/API/lang/BoundsSafety/runtime_detection/late_runtime.cpp
 create mode 100644 lldb/test/API/lang/BoundsSafety/runtime_detection/library_runtime.c
 create mode 100644 lldb/test/API/lang/BoundsSafety/runtime_detection/main.c
 create mode 100644 lldb/test/API/lang/BoundsSafety/runtime_detection/runtime.c
 create mode 100644 lldb/test/API/lang/BoundsSafety/runtime_detection/unrelated.cpp
 create mode 100644 lldb/unittests/Target/InstrumentationRuntimeTest.cpp

diff --git a/lldb/include/lldb/Breakpoint/BreakpointResolverName.h b/lldb/include/lldb/Breakpoint/BreakpointResolverName.h
index 33b5db966c9742..e4da5c6609c1b2 100644
--- a/lldb/include/lldb/Breakpoint/BreakpointResolverName.h
+++ b/lldb/include/lldb/Breakpoint/BreakpointResolverName.h
@@ -58,6 +58,14 @@ class BreakpointResolverName : public BreakpointResolver {
 
   ~BreakpointResolverName() override = default;
 
+  /// Match exact symbol-table linkage names without building demangled-name
+  /// indexes. DWARF lookup and pruning retain their source-name behavior.
+  /// This option applies only to exact-name resolvers.
+  void SetMatchLinkageNames(bool match) {
+    assert(!match || m_match_type == Breakpoint::Exact);
+    m_match_linkage_names = match;
+  }
+
   Searcher::CallbackReturn SearchCallback(SearchFilter &filter,
                                           SymbolContext &context,
                                           Address *addr) override;
@@ -85,6 +93,7 @@ class BreakpointResolverName : public BreakpointResolver {
   Breakpoint::MatchType m_match_type;
   lldb::LanguageType m_language;
   bool m_skip_prologue;
+  bool m_match_linkage_names = false;
 
   void AddNameLookup(llvm::StringRef name,
                      lldb::FunctionNameType name_type_mask);
diff --git a/lldb/include/lldb/Core/Module.h b/lldb/include/lldb/Core/Module.h
index e71a29e3e413b9..468198572a18ac 100644
--- a/lldb/include/lldb/Core/Module.h
+++ b/lldb/include/lldb/Core/Module.h
@@ -69,6 +69,9 @@ struct ModuleFunctionSearchOptions {
   bool include_symbols = false;
   /// Include inlined functions.
   bool include_inlines = false;
+  /// Match exact symbol-table linkage spellings without demangling unrelated
+  /// symbols. DWARF function lookup retains its source-name behavior.
+  bool match_linkage_names = false;
 };
 
 /// \class Module Module.h "lldb/Core/Module.h"
@@ -262,9 +265,12 @@ class Module : public std::enable_shared_from_this<Module>,
   const Symbol *FindFirstSymbolWithNameAndType(
       ConstString name, lldb::SymbolType symbol_type = lldb::eSymbolTypeAny);
 
+  /// When match_linkage_names is true, match exact linkage spellings and linker
+  /// annotation aliases without building the demangled name indexes.
   void FindSymbolsWithNameAndType(ConstString name,
                                   lldb::SymbolType symbol_type,
-                                  SymbolContextList &sc_list);
+                                  SymbolContextList &sc_list,
+                                  bool match_linkage_names = false);
 
   void FindSymbolsMatchingRegExAndType(
       const RegularExpression &regex, lldb::SymbolType symbol_type,
diff --git a/lldb/include/lldb/Core/ModuleList.h b/lldb/include/lldb/Core/ModuleList.h
index 61259b54d7e39b..4cfe57dda6c6a9 100644
--- a/lldb/include/lldb/Core/ModuleList.h
+++ b/lldb/include/lldb/Core/ModuleList.h
@@ -389,7 +389,8 @@ class ModuleList {
 
   void FindSymbolsWithNameAndType(ConstString name,
                                   lldb::SymbolType symbol_type,
-                                  SymbolContextList &sc_list) const;
+                                  SymbolContextList &sc_list,
+                                  bool match_linkage_names = false) const;
 
   void FindSymbolsMatchingRegExAndType(const RegularExpression &regex,
                                        lldb::SymbolType symbol_type,
diff --git a/lldb/include/lldb/Symbol/Symtab.h b/lldb/include/lldb/Symbol/Symtab.h
index 224de9e775859f..fa5a86f64e2054 100644
--- a/lldb/include/lldb/Symbol/Symtab.h
+++ b/lldb/include/lldb/Symbol/Symtab.h
@@ -101,7 +101,8 @@ class Symtab {
           Mangled::NamePreference::ePreferDemangled);
   void FindAllSymbolsWithNameAndType(ConstString name,
                                      lldb::SymbolType symbol_type,
-                                     std::vector<uint32_t> &symbol_indexes);
+                                     std::vector<uint32_t> &symbol_indexes,
+                                     bool match_linkage_names = false);
   void FindAllSymbolsWithNameAndType(ConstString name,
                                      lldb::SymbolType symbol_type,
                                      Debug symbol_debug_type,
@@ -120,8 +121,11 @@ class Symtab {
   Symbol *FindSymbolContainingFileAddress(lldb::addr_t file_addr);
   void ForEachSymbolContainingFileAddress(
       lldb::addr_t file_addr, std::function<bool(Symbol *)> const &callback);
+  /// When match_linkage_names is true, match only the exact linkage spelling,
+  /// including linker annotation aliases, without building demangled indexes.
   void FindFunctionSymbols(ConstString name, uint32_t name_type_mask,
-                           SymbolContextList &sc_list);
+                           SymbolContextList &sc_list,
+                           bool match_linkage_names = false);
 
   void SortSymbolIndexesByValue(std::vector<uint32_t> &indexes,
                                 bool remove_duplicates) const;
diff --git a/lldb/source/Breakpoint/BreakpointResolverName.cpp b/lldb/source/Breakpoint/BreakpointResolverName.cpp
index 981b6fba708d6a..b1a03ffd7aff55 100644
--- a/lldb/source/Breakpoint/BreakpointResolverName.cpp
+++ b/lldb/source/Breakpoint/BreakpointResolverName.cpp
@@ -24,6 +24,8 @@
 using namespace lldb;
 using namespace lldb_private;
 
+static constexpr llvm::StringLiteral MatchLinkageNames("MatchLinkageNames");
+
 BreakpointResolverName::BreakpointResolverName(
     const BreakpointSP &bkpt, const char *name_cstr,
     FunctionNameType name_type_mask, LanguageType language,
@@ -84,7 +86,8 @@ BreakpointResolverName::BreakpointResolverName(
                          rhs.GetOffset(), rhs.GetOffsetIsInsnCount()),
       m_lookups(rhs.m_lookups), m_regex(rhs.m_regex),
       m_match_type(rhs.m_match_type), m_language(rhs.m_language),
-      m_skip_prologue(rhs.m_skip_prologue) {}
+      m_skip_prologue(rhs.m_skip_prologue),
+      m_match_linkage_names(rhs.m_match_linkage_names) {}
 
 BreakpointResolverSP BreakpointResolverName::CreateFromStructuredData(
     const StructuredData::Dictionary &options_dict, Status &error) {
@@ -117,10 +120,24 @@ BreakpointResolverSP BreakpointResolverName::CreateFromStructuredData(
     return nullptr;
   }
 
+  bool match_linkage_names = false;
+  if (options_dict.HasKey(MatchLinkageNames) &&
+      !options_dict.GetValueForKeyAsBoolean(MatchLinkageNames,
+                                            match_linkage_names)) {
+    error = Status::FromErrorString(
+        "BRN::CFSD: MatchLinkageNames entry is not a boolean.");
+    return nullptr;
+  }
+
   llvm::StringRef regex_text;
   success = options_dict.GetValueForKeyAsString(
       GetKey(OptionNames::RegexString), regex_text);
   if (success) {
+    if (match_linkage_names) {
+      error = Status::FromErrorString(
+          "BRN::CFSD: Linkage names require an exact-name resolver.");
+      return nullptr;
+    }
     return std::make_shared<BreakpointResolverName>(
         nullptr, RegularExpression(regex_text), language, offset,
         skip_prologue);
@@ -182,6 +199,7 @@ BreakpointResolverSP BreakpointResolverName::CreateFromStructuredData(
     for (size_t i = 1; i < num_elem; i++) {
       resolver_sp->AddNameLookup(names[i], name_masks[i]);
     }
+    resolver_sp->SetMatchLinkageNames(match_linkage_names);
     return resolver_sp;
 }
 
@@ -211,6 +229,9 @@ StructuredData::ObjectSP BreakpointResolverName::SerializeToStructuredData() {
   options_dict_sp->AddBooleanItem(GetKey(OptionNames::SkipPrologue),
                                   m_skip_prologue);
 
+  if (m_match_linkage_names)
+    options_dict_sp->AddBooleanItem(MatchLinkageNames, true);
+
   return WrapOptionsDict(options_dict_sp);
 }
 
@@ -264,6 +285,7 @@ BreakpointResolverName::SearchCallback(SearchFilter &filter,
   ModuleFunctionSearchOptions function_options;
   function_options.include_symbols = !filter_by_cu;
   function_options.include_inlines = true;
+  function_options.match_linkage_names = m_match_linkage_names;
 
   switch (m_match_type) {
   case Breakpoint::Exact:
diff --git a/lldb/source/Core/Module.cpp b/lldb/source/Core/Module.cpp
index 7eab74e57766e9..00b518ddcb9794 100644
--- a/lldb/source/Core/Module.cpp
+++ b/lldb/source/Core/Module.cpp
@@ -817,8 +817,11 @@ void Module::FindFunctions(llvm::ArrayRef<Module::LookupInfo> lookup_infos,
                            options.include_inlines, sc_list);
     if (options.include_symbols)
       if (Symtab *symtab = symbols->GetSymtab())
-        symtab->FindFunctionSymbols(lookup_info.GetLookupName(),
-                                    lookup_info.GetNameTypeMask(), sc_list);
+        symtab->FindFunctionSymbols(options.match_linkage_names
+                                        ? lookup_info.GetName()
+                                        : lookup_info.GetLookupName(),
+                                    lookup_info.GetNameTypeMask(), sc_list,
+                                    options.match_linkage_names);
   }
 }
 
@@ -1306,12 +1309,14 @@ void Module::FindFunctionSymbols(ConstString name, uint32_t name_type_mask,
 
 void Module::FindSymbolsWithNameAndType(ConstString name,
                                         SymbolType symbol_type,
-                                        SymbolContextList &sc_list) {
+                                        SymbolContextList &sc_list,
+                                        bool match_linkage_names) {
   // No need to protect this call using m_mutex all other method calls are
   // already thread safe.
   if (Symtab *symtab = GetSymtab()) {
     std::vector<uint32_t> symbol_indexes;
-    symtab->FindAllSymbolsWithNameAndType(name, symbol_type, symbol_indexes);
+    symtab->FindAllSymbolsWithNameAndType(name, symbol_type, symbol_indexes,
+                                          match_linkage_names);
     SymbolIndicesToSymbolContextList(symtab, symbol_indexes, sc_list);
   }
 }
diff --git a/lldb/source/Core/ModuleList.cpp b/lldb/source/Core/ModuleList.cpp
index 9f5614538a9874..7e76e17eabf8a3 100644
--- a/lldb/source/Core/ModuleList.cpp
+++ b/lldb/source/Core/ModuleList.cpp
@@ -560,10 +560,12 @@ void ModuleList::FindGlobalVariables(const RegularExpression &regex,
 
 void ModuleList::FindSymbolsWithNameAndType(ConstString name,
                                             SymbolType symbol_type,
-                                            SymbolContextList &sc_list) const {
+                                            SymbolContextList &sc_list,
+                                            bool match_linkage_names) const {
   std::lock_guard<std::recursive_mutex> guard(m_modules_mutex);
   for (const ModuleSP &module_sp : m_modules)
-    module_sp->FindSymbolsWithNameAndType(name, symbol_type, sc_list);
+    module_sp->FindSymbolsWithNameAndType(name, symbol_type, sc_list,
+                                          match_linkage_names);
 }
 
 void ModuleList::FindSymbolsMatchingRegExAndType(
diff --git a/lldb/source/Plugins/InstrumentationRuntime/BoundsSafety/InstrumentationRuntimeBoundsSafety.cpp b/lldb/source/Plugins/InstrumentationRuntime/BoundsSafety/InstrumentationRuntimeBoundsSafety.cpp
index 506785ad15e1b4..e7bfc4c08cb428 100644
--- a/lldb/source/Plugins/InstrumentationRuntime/BoundsSafety/InstrumentationRuntimeBoundsSafety.cpp
+++ b/lldb/source/Plugins/InstrumentationRuntime/BoundsSafety/InstrumentationRuntimeBoundsSafety.cpp
@@ -9,13 +9,17 @@
 #include "InstrumentationRuntimeBoundsSafety.h"
 
 #include "Plugins/Process/Utility/HistoryThread.h"
+#include "lldb/Breakpoint/BreakpointResolverName.h"
 #include "lldb/Breakpoint/StoppointCallbackContext.h"
 #include "lldb/Core/Debugger.h"
 #include "lldb/Core/Module.h"
 #include "lldb/Core/PluginManager.h"
+#include "lldb/Core/SearchFilter.h"
 #include "lldb/Symbol/Block.h"
+#include "lldb/Symbol/ObjectFile.h"
 #include "lldb/Symbol/Symbol.h"
 #include "lldb/Symbol/SymbolContext.h"
+#include "lldb/Symbol/Symtab.h"
 #include "lldb/Symbol/Variable.h"
 #include "lldb/Symbol/VariableList.h"
 #include "lldb/Target/InstrumentationRuntimeStopInfo.h"
@@ -29,6 +33,7 @@
 #include "clang/CodeGen/ModuleBuilder.h"
 
 #include <memory>
+#include <mutex>
 #include <type_traits>
 
 using namespace lldb;
@@ -386,15 +391,31 @@ InstrumentationRuntimeBoundsSafety::GetPatternForRuntimeLibrary() {
 bool InstrumentationRuntimeBoundsSafety::CheckIfRuntimeIsValid(
     const lldb::ModuleSP module_sp) {
   Log *log_category = GetLog(LLDBLog::InstrumentationRuntime);
-  for (const auto &SoftTrapFunc : getBoundsSafetySoftTrapRuntimeFuncs()) {
-    ConstString test_sym(SoftTrapFunc);
-
-    if (module_sp->FindFirstSymbolWithNameAndType(test_sym,
-                                                  lldb::eSymbolTypeAny)) {
-      LLDB_LOG(log_category, "found \"{0}\" in {1}",
-               test_sym.AsCString("<unknown symbol>"),
-               module_sp->GetFileSpec().GetPath());
-      return true;
+  if (Symtab *symtab = module_sp->GetSymtab()) {
+    const ConstString minimal(BoundsSafetySoftTrapMinimal);
+    const ConstString with_str(BoundsSafetySoftTrapStr);
+    std::lock_guard<std::recursive_mutex> guard(symtab->GetMutex());
+    const size_t num_symbols = symtab->GetNumSymbols();
+    for (size_t i = 0; i < num_symbols; ++i) {
+      Symbol *symbol = symtab->SymbolAtIndex(i);
+      if (symbol->IsTrampoline())
+        continue;
+      // These runtime entry points have C linkage. Their presence does not
+      // require indexing or demangling every other symbol in the module.
+      ConstString name = symbol->GetMangled().GetName(Mangled::ePreferMangled);
+      bool matches = name == minimal || name == with_str;
+      if (!matches && symbol->ContainsLinkerAnnotations()) {
+        llvm::StringRef stripped =
+            symtab->GetObjectFile()->StripLinkerSymbolAnnotations(
+                name.GetStringRef());
+        matches = stripped == BoundsSafetySoftTrapMinimal ||
+                  stripped == BoundsSafetySoftTrapStr;
+      }
+      if (matches && !symbol->IsSyntheticWithAutoGeneratedName()) {
+        LLDB_LOG(log_category, "found \"{0}\" in {1}", name,
+                 module_sp->GetFileSpec().GetPath());
+        return true;
+      }
     }
   }
   LLDB_LOG(log_category,
@@ -451,14 +472,19 @@ void InstrumentationRuntimeBoundsSafety::Activate() {
   for (auto &breakpoint_func : getBoundsSafetySoftTrapRuntimeFuncs())
     breakpoints.emplace_back(breakpoint_func);
 
-  BreakpointSP breakpoint = process_sp->GetTarget().CreateBreakpoint(
-      /*containingModules=*/nullptr,
-      /*containingSourceFiles=*/nullptr, breakpoints, eFunctionNameTypeFull,
-      eLanguageTypeUnknown,
-      /*m_offset=*/0,
-      /*skip_prologue*/ eLazyBoolNo,
-      /*internal=*/true,
-      /*request_hardware*/ false);
+  Target &target = process_sp->GetTarget();
+  SearchFilterSP filter =
+      std::make_shared<SearchFilterForUnconstrainedSearches>(
+          target.shared_from_this());
+  auto resolver = std::make_shared<BreakpointResolverName>(
+      nullptr, breakpoints, eFunctionNameTypeFull,
+      target.GetLanguage().AsLanguageType(),
+      /*offset=*/0, /*skip_prologue=*/false);
+  resolver->SetMatchLinkageNames(true);
+  BreakpointResolverSP resolver_sp = resolver;
+  BreakpointSP breakpoint = target.CreateBreakpoint(
+      filter, resolver_sp, /*internal=*/true, /*hardware=*/false,
+      /*resolve_indirect_symbols=*/true);
 
   if (!breakpoint)
     return LogBeforeReturn<void>("failed to create breakpoint");
diff --git a/lldb/source/Symbol/Symtab.cpp b/lldb/source/Symbol/Symtab.cpp
index efea0339d2ef66..8b42f9b24617af 100644
--- a/lldb/source/Symbol/Symtab.cpp
+++ b/lldb/source/Symbol/Symtab.cpp
@@ -810,12 +810,30 @@ Symbol *Symtab::FindSymbolWithType(SymbolType symbol_type,
   return nullptr;
 }
 
-void
-Symtab::FindAllSymbolsWithNameAndType(ConstString name,
-                                      SymbolType symbol_type,
-                                      std::vector<uint32_t> &symbol_indexes) {
+void Symtab::FindAllSymbolsWithNameAndType(
+    ConstString name, SymbolType symbol_type,
+    std::vector<uint32_t> &symbol_indexes, bool match_linkage_names) {
   std::lock_guard<std::recursive_mutex> guard(m_mutex);
 
+  if (match_linkage_names) {
+    if (!name)
+      return;
+    for (uint32_t i = 0; i < m_symbols.size(); ++i) {
+      Symbol &symbol = m_symbols[i];
+      if (symbol_type != eSymbolTypeAny && symbol.GetType() != symbol_type)
+        continue;
+      ConstString linkage_name =
+          symbol.GetMangled().GetName(Mangled::ePreferMangled);
+      bool matches = linkage_name == name;
+      if (!matches && symbol.ContainsLinkerAnnotations())
+        matches = m_objfile->StripLinkerSymbolAnnotations(
+                      linkage_name.GetStringRef()) == name.GetStringRef();
+      if (matches && !symbol.IsSyntheticWithAutoGeneratedName())
+        symbol_indexes.push_back(i);
+    }
+    return;
+  }
+
   // Initialize all of the lookup by name indexes before converting NAME to a
   // uniqued string NAME_STR below.
   InitNameIndexes();
@@ -1085,9 +1103,31 @@ void Symtab::SymbolIndicesToSymbolContextList(
 }
 
 void Symtab::FindFunctionSymbols(ConstString name, uint32_t name_type_mask,
-                                 SymbolContextList &sc_list) {
+                                 SymbolContextList &sc_list,
+                                 bool match_linkage_names) {
   std::vector<uint32_t> symbol_indexes;
 
+  if (match_linkage_names) {
+    std::lock_guard<std::recursive_mutex> guard(m_mutex);
+    std::vector<uint32_t> matching_indexes;
+    FindAllSymbolsWithNameAndType(name, eSymbolTypeAny, matching_indexes,
+                                  /*match_linkage_names=*/true);
+    for (uint32_t i : matching_indexes) {
+      switch (m_symbols[i].GetType()) {
+      case eSymbolTypeCode:
+      case eSymbolTypeResolver:
+      case eSymbolTypeReExported:
+      case eSymbolTypeAbsolute:
+        symbol_indexes.push_back(i);
+        break;
+      default:
+        break;
+      }
+    }
+    SymbolIndicesToSymbolContextList(symbol_indexes, sc_list);
+    return;
+  }
+
   // eFunctionNameTypeAuto should be pre-resolved by a call to
   // Module::LookupInfo::LookupInfo()
   assert((name_type_mask & eFunctionNameTypeAuto) == 0);
diff --git a/lldb/test/API/lang/BoundsSafety/runtime_detection/Makefile b/lldb/test/API/lang/BoundsSafety/runtime_detection/Makefile
new file mode 100644
index 00000000000000..3e28c7d677ad3a
--- /dev/null
+++ b/lldb/test/API/lang/BoundsSafety/runtime_detection/Makefile
@@ -0,0 +1,16 @@
+C_SOURCES := main.c
+DYLIB_CXX_SOURCES := unrelated.cpp
+DYLIB_NAME := Unrelated
+
+ifeq ($(RUNTIME_LOCATION),main)
+C_SOURCES += runtime.c
+else ifeq ($(RUNTIME_LOCATION),dylib)
+DYLIB_C_SOURCES := library_runtime.c
+else ifeq ($(RUNTIME_LOCATION),cxx)
+DYLIB_CXX_SOURCES += cxx_runtime.cpp
+else ifeq ($(RUNTIME_LOCATION),both)
+C_SOURCES += runtime.c
+DYLIB_CXX_SOURCES += cxx_runtime.cpp
+endif
+
+include Makefile.rules
diff --git a/lldb/test/API/lang/BoundsSafety/runtime_detection/TestBoundsSafetyRuntimeDetection.py b/lldb/test/API/lang/BoundsSafety/runtime_detection/TestBoundsSafetyRuntimeDetection.py
new file mode 100644
index 00000000000000..b5ebaf98460d6d
--- /dev/null
+++ b/lldb/test/API/lang/BoundsSafety/runtime_detection/TestBoundsSafetyRuntimeDetection.py
@@ -0,0 +1,160 @@
+import lldb
+from lldbsuite.test.decorators import skipIf, skipIfWindows, skipUnlessDarwin
+from lldbsuite.test.lldbtest import TestBase
+
+
+ at skipIfWindows
+class TestBoundsSafetyRuntimeDetection(TestBase):
+    SHARED_BUILD_TESTCASE = False
+
+    def start(
+        self,
+        runtime_location=None,
+        with_string=False,
+        static=False,
+        without_debug_info=False,
+        late=False,
+        strip_runtime_symbols=False,
+        unrelated_dsym=False,
+    ):
+        flags = []
+        if with_string:
+            flags.append("-DWITH_STRING_TRAP")
+        if static:
+            flags.append("-DSTATIC_RUNTIME")
+        if runtime_location in ("main", "both"):
+            flags.append("-DCALL_MAIN_RUNTIME")
+        if runtime_location in ("dylib", "cxx", "both"):
+            flags.append("-DCALL_LIBRARY_RUNTIME")
+        if late:
+            self.build(
+                dictionary={
+                    "DYLIB_ONLY": "YES",
+                    "DYLIB_NAME": "Late",
+                    "DYLIB_CXX_SOURCES": "late_runtime.cpp",
+                }
+            )
+            flags.append("-DLATE_LOAD")
+        dictionary = {
+            "RUNTIME_LOCATION": runtime_location or "none",
+            "CFLAGS_EXTRAS": " ".join(flags),
+        }
+        if without_debug_info:
+            dictionary["DEBUG_INFO_FLAG"] = "-g0"
+        self.build(dictionary=dictionary)
+        if unrelated_dsym and self.platformIsDarwin():
+            library = self.getBuildArtifact(
+                self.platformContext.getFullLibName("Unrelated")
+            )
+            self.runBuildCommand(
+                ["xcrun", "dsymutil", "-o", library + ".dSYM", library]
+            )
+        if strip_runtime_symbols:
+            library = self.getBuildArtifact(
+                self.platformContext.getFullLibName("Unrelated")
+            )
+            self.runBuildCommand(["xcrun", "strip", "-x", library])
+            self.runBuildCommand(["codesign", "-f", "-s", "-", library])
+        self.runCmd("settings set target.preload-symbols false")
+        self.runCmd("settings set target.load-script-from-symbol-file false")
+        self.log = self.getBuildArtifact("instrumentation.log")
+        self.runCmd(f'log enable lldb instrumentation-runtime -f "{self.log}"')
+        target = self.createTestTarget(load_dependent_modules=False)
+        breakpoint = target.BreakpointCreateByName(
+            "main", target.GetExecutable().GetFilename()
+        )
+        self.assertEqual(breakpoint.GetNumLocations(), 1)
+        arguments = (
+            [self.getBuildArtifact(self.platformContext.getFullLibName("Late"))]
+            if late
+            else None
+        )
+        process = target.LaunchSimple(
+            arguments, None, self.get_process_working_directory()
+        )
+        self.assertState(process.GetState(), lldb.eStateStopped)
+        if runtime_location is not None:
+            self.assertTrue(
+                process.IsInstrumentationRuntimePresent(
+                    lldb.eInstrumentationRuntimeTypeBoundsSafety
+                )
+            )
+        return target
+
+    def check_trap(self, target, module_name, with_string=False, raw_symbol=True):
+        process = target.GetProcess()
+        self.assertSuccess(process.Continue())
+        self.assertState(process.GetState(), lldb.eStateStopped)
+        thread = process.GetSelectedThread()
+        self.assertEqual(thread.GetStopReason(), lldb.eStopReasonInstrumentation)
+        frame = thread.GetFrameAtIndex(0)
+        expected_name = (
+            "__bounds_safety_soft_trap_s"
+            if with_string
+            else "__bounds_safety_soft_trap"
+        )
+        if raw_symbol:
+            self.assertEqual(frame.GetSymbol().GetName(), expected_name)
+        else:
+            self.assertIn(
+                frame.GetFunctionName(),
+                (expected_name, expected_name + "()", "::" + expected_name + "()"),
+            )
+        self.assertIn(module_name, frame.GetModule().GetFileSpec().GetFilename())
+
+    def test_non_runtime_library_is_rejected(self):
+        target = self.start(unrelated_dsym=True)
+        self.runCmd("log disable lldb instrumentation-runtime")
+        library = self.platformContext.getFullLibName("Unrelated")
+        with open(self.log) as log:
+            self.assertTrue(
+                any(
+                    "did not find BoundsSafety soft trap functions in module" in line
+                    and library in line
+                    for line in log
+                )
+            )
+        process = target.GetProcess()
+        self.assertSuccess(process.Continue())
+        self.assertState(process.GetState(), lldb.eStateExited)
+        self.assertEqual(process.GetExitStatus(), 42)
+
+    def test_hidden_minimal_runtime_in_executable(self):
+        target = self.start("main")
+        self.check_trap(target, target.GetExecutable().GetFilename())
+
+    def test_hidden_string_runtime_in_executable(self):
+        target = self.start("main", with_string=True)
+        self.check_trap(target, target.GetExecutable().GetFilename(), with_string=True)
+
+    def test_hidden_minimal_runtime_in_unrelated_library(self):
+        target = self.start("dylib")
+        self.check_trap(target, "Unrelated")
+
+    def test_hidden_string_runtime_in_unrelated_library(self):
+        target = self.start("dylib", with_string=True)
+        self.check_trap(target, "Unrelated", with_string=True)
+
+    def test_extern_c_runtime_in_cxx_library(self):
+        target = self.start("cxx")
+        self.check_trap(target, "Unrelated")
+
+    def test_static_runtime_copies_in_multiple_images(self):
+        target = self.start("both", static=True)
+        self.check_trap(target, target.GetExecutable().GetFilename())
+        self.check_trap(target, "Unrelated")
+
+    def test_runtime_without_dwarf(self):
+        target = self.start("dylib", without_debug_info=True)
+        self.check_trap(target, "Unrelated")
+
+    def test_late_loaded_runtime_copy(self):
+        target = self.start("main", late=True)
+        self.check_trap(target, target.GetExecutable().GetFilename())
+        self.check_trap(target, "Late")
+
+    @skipUnlessDarwin
+    @skipIf(debug_info="dwarf")
+    def test_extern_c_runtime_with_dwarf_only(self):
+        target = self.start("cxx", strip_runtime_symbols=True)
+        self.check_trap(target, "Unrelated", raw_symbol=False)
diff --git a/lldb/test/API/lang/BoundsSafety/runtime_detection/cxx_runtime.cpp b/lldb/test/API/lang/BoundsSafety/runtime_detection/cxx_runtime.cpp
new file mode 100644
index 00000000000000..213108252bfae9
--- /dev/null
+++ b/lldb/test/API/lang/BoundsSafety/runtime_detection/cxx_runtime.cpp
@@ -0,0 +1,2 @@
+#define CALL_RUNTIME_NAME call_library_trap
+#include "runtime.c"
diff --git a/lldb/test/API/lang/BoundsSafety/runtime_detection/late_runtime.cpp b/lldb/test/API/lang/BoundsSafety/runtime_detection/late_runtime.cpp
new file mode 100644
index 00000000000000..17e80adeb1b70c
--- /dev/null
+++ b/lldb/test/API/lang/BoundsSafety/runtime_detection/late_runtime.cpp
@@ -0,0 +1,2 @@
+#define CALL_RUNTIME_NAME call_late_trap
+#include "runtime.c"
diff --git a/lldb/test/API/lang/BoundsSafety/runtime_detection/library_runtime.c b/lldb/test/API/lang/BoundsSafety/runtime_detection/library_runtime.c
new file mode 100644
index 00000000000000..213108252bfae9
--- /dev/null
+++ b/lldb/test/API/lang/BoundsSafety/runtime_detection/library_runtime.c
@@ -0,0 +1,2 @@
+#define CALL_RUNTIME_NAME call_library_trap
+#include "runtime.c"
diff --git a/lldb/test/API/lang/BoundsSafety/runtime_detection/main.c b/lldb/test/API/lang/BoundsSafety/runtime_detection/main.c
new file mode 100644
index 00000000000000..2e74e156b2767b
--- /dev/null
+++ b/lldb/test/API/lang/BoundsSafety/runtime_detection/main.c
@@ -0,0 +1,30 @@
+extern int unrelated(void);
+#ifdef CALL_MAIN_RUNTIME
+extern void call_main_trap(void);
+#endif
+#ifdef CALL_LIBRARY_RUNTIME
+extern void call_library_trap(void);
+#endif
+#ifdef LATE_LOAD
+#include <assert.h>
+#include <dlfcn.h>
+#endif
+
+int main(int argc, char **argv) {
+  int result = unrelated();
+#ifdef CALL_MAIN_RUNTIME
+  call_main_trap();
+#endif
+#ifdef CALL_LIBRARY_RUNTIME
+  call_library_trap();
+#endif
+#ifdef LATE_LOAD
+  void *library = dlopen(argv[1], RTLD_NOW);
+  assert(library);
+  void (*call_late_trap)(void) =
+      (void (*)(void))dlsym(library, "call_late_trap");
+  assert(call_late_trap);
+  call_late_trap();
+#endif
+  return result;
+}
diff --git a/lldb/test/API/lang/BoundsSafety/runtime_detection/runtime.c b/lldb/test/API/lang/BoundsSafety/runtime_detection/runtime.c
new file mode 100644
index 00000000000000..ca09d8dc5f4289
--- /dev/null
+++ b/lldb/test/API/lang/BoundsSafety/runtime_detection/runtime.c
@@ -0,0 +1,25 @@
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifdef STATIC_RUNTIME
+#define TRAP_LINKAGE static __attribute__((noinline, used))
+#else
+#define TRAP_LINKAGE __attribute__((visibility("hidden"), noinline, used))
+#endif
+
+#ifndef CALL_RUNTIME_NAME
+#define CALL_RUNTIME_NAME call_main_trap
+#endif
+
+#ifdef WITH_STRING_TRAP
+TRAP_LINKAGE void __bounds_safety_soft_trap_s(const char *reason) {}
+void CALL_RUNTIME_NAME(void) { __bounds_safety_soft_trap_s("fixture"); }
+#else
+TRAP_LINKAGE void __bounds_safety_soft_trap(void) {}
+void CALL_RUNTIME_NAME(void) { __bounds_safety_soft_trap(); }
+#endif
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/lldb/test/API/lang/BoundsSafety/runtime_detection/unrelated.cpp b/lldb/test/API/lang/BoundsSafety/runtime_detection/unrelated.cpp
new file mode 100644
index 00000000000000..9c19c4e77d1167
--- /dev/null
+++ b/lldb/test/API/lang/BoundsSafety/runtime_detection/unrelated.cpp
@@ -0,0 +1,5 @@
+namespace Unrelated {
+int value() { return 42; }
+} // namespace Unrelated
+
+extern "C" int unrelated(void) { return Unrelated::value(); }
diff --git a/lldb/unittests/Symbol/SymtabTest.cpp b/lldb/unittests/Symbol/SymtabTest.cpp
index 0236b6caa97068..c023d4409ca4fc 100644
--- a/lldb/unittests/Symbol/SymtabTest.cpp
+++ b/lldb/unittests/Symbol/SymtabTest.cpp
@@ -16,9 +16,11 @@
 
 #include "lldb/Core/DataFileCache.h"
 #include "lldb/Core/Module.h"
+#include "lldb/Core/ModuleList.h"
 #include "lldb/Host/FileSystem.h"
 #include "lldb/Host/HostInfo.h"
 #include "lldb/Symbol/Symbol.h"
+#include "lldb/Symbol/SymbolContext.h"
 #include "lldb/Symbol/Symtab.h"
 #include "lldb/Utility/DataEncoder.h"
 #include "lldb/Utility/DataExtractor.h"
@@ -844,3 +846,212 @@ TEST_F(SymtabTest, TestSymbolTableCreatedOnDemand) {
   Symtab *cached_symtab = symbol_file->GetSymtab(/*can_create=*/false);
   ASSERT_EQ(symtab, cached_symtab);
 }
+
+TEST_F(SymtabTest, LinkageFunctionLookupDoesNotIndexOtherNames) {
+  auto image = TestFile::fromYaml(R"(
+--- !ELF
+FileHeader:
+  Class: ELFCLASS64
+  Data: ELFDATA2LSB
+  Type: ET_DYN
+  Machine: EM_X86_64
+Sections:
+  - Name: .text
+    Type: SHT_PROGBITS
+    Flags: [SHF_ALLOC, SHF_EXECINSTR]
+    Address: 0x1000
+    Content: C3
+)");
+  ASSERT_THAT_EXPECTED(image, llvm::Succeeded());
+  auto module = std::make_shared<Module>(image->moduleSpec());
+  ObjectFile *object = module->GetObjectFile();
+  ASSERT_NE(object, nullptr);
+  Symtab *symtab = object->GetSymtab();
+  uint32_t id = 0;
+  auto add_symbol = [&](llvm::StringRef name, SymbolType type, bool trampoline,
+                        bool annotations) {
+    const uint32_t symbol_id = id++;
+    symtab->AddSymbol(Symbol(symbol_id, name, type, /*external=*/false,
+                             /*is_debug=*/false, trampoline,
+                             /*is_artificial=*/false, SectionSP(), id, 0,
+                             /*size_is_valid=*/false, annotations, 0));
+  };
+  add_symbol("runtime", eSymbolTypeCode, false, false);
+  add_symbol("runtime at VERSION", eSymbolTypeResolver, false, true);
+  add_symbol("runtime", eSymbolTypeReExported, false, false);
+  add_symbol("runtime", eSymbolTypeAbsolute, false, false);
+  add_symbol("runtime", eSymbolTypeUndefined, false, false);
+  add_symbol("runtime", eSymbolTypeData, false, false);
+  // IsTrampoline uses the symbol type; the constructor's legacy bool is unused.
+  add_symbol("runtime", eSymbolTypeTrampoline, true, false);
+  add_symbol("_Z7runtimev", eSymbolTypeCode, false, false);
+  add_symbol("$s4Test7runtimeyyF", eSymbolTypeCode, false, false);
+  for (unsigned i = 0; i < 1024; ++i)
+    add_symbol("_Z9unrelatedILi" + std::to_string(i) + "EEvv", eSymbolTypeCode,
+               false, false);
+
+  SymbolContextList matches;
+  symtab->FindFunctionSymbols(ConstString("runtime"), eFunctionNameTypeFull,
+                              matches, /*match_linkage_names=*/true);
+  ASSERT_EQ(matches.GetSize(), 4u);
+  for (unsigned i = 0; i < matches.GetSize(); ++i) {
+    SymbolContext context;
+    ASSERT_TRUE(matches.GetContextAtIndex(i, context));
+    ASSERT_NE(context.symbol, nullptr);
+    EXPECT_EQ(context.symbol->GetID(), i);
+  }
+  EXPECT_EQ(module->GetSymtabIndexTime().get().count(), 0.0);
+
+  SymbolContextList absent;
+  symtab->FindFunctionSymbols(ConstString("absent"), eFunctionNameTypeFull,
+                              absent, /*match_linkage_names=*/true);
+  EXPECT_EQ(absent.GetSize(), 0u);
+  EXPECT_EQ(module->GetSymtabIndexTime().get().count(), 0.0);
+
+  // The default still builds the general indexes and finds the C++ basename.
+  SymbolContextList normal;
+  symtab->FindFunctionSymbols(ConstString("runtime"), eFunctionNameTypeBase,
+                              normal);
+  EXPECT_GT(normal.GetSize(), matches.GetSize());
+  EXPECT_GT(module->GetSymtabIndexTime().get().count(), 0.0);
+}
+
+TEST_F(SymtabTest, LinkageSymbolLookupPreservesTypesOrderAndAnnotations) {
+  auto image = TestFile::fromYaml(R"(
+--- !ELF
+FileHeader:
+  Class: ELFCLASS64
+  Data: ELFDATA2LSB
+  Type: ET_DYN
+  Machine: EM_X86_64
+Sections:
+  - Name: .text
+    Type: SHT_PROGBITS
+    Flags: [SHF_ALLOC, SHF_EXECINSTR]
+    Address: 0x1000
+    Content: C3
+)");
+  ASSERT_THAT_EXPECTED(image, llvm::Succeeded());
+  auto module = std::make_shared<Module>(image->moduleSpec());
+  Symtab *symtab = module->GetObjectFile()->GetSymtab();
+  ASSERT_NE(symtab, nullptr);
+  auto add = [&](user_id_t id, llvm::StringRef name, SymbolType type,
+                 bool external, bool trampoline, bool annotations) {
+    symtab->AddSymbol(Symbol(id, name, type, external, false, trampoline, false,
+                             SectionSP(), id, 0, false, annotations, 0));
+  };
+  add(0, "dispatch_queue_offsets", eSymbolTypeData, false, false, false);
+  add(1, "dispatch_queue_offsets at VERSION", eSymbolTypeData, true, false, true);
+  add(2, "dispatch_queue_offsets", eSymbolTypeReExported, true, false, false);
+  add(3, "dispatch_queue_offsets", eSymbolTypeUndefined, true, false, false);
+  add(4, "dispatch_queue_offsets", eSymbolTypeTrampoline, true, true, false);
+  add(5, "_Z22dispatch_queue_offsetsv", eSymbolTypeCode, true, false, false);
+  add(6, "_Z18other_wire_requestv", eSymbolTypeCode, true, false, false);
+  Symbol named_synthetic(7, "dispatch_queue_offsets", eSymbolTypeData, false,
+                         false, false, false, SectionSP(), 7, 0, false, false,
+                         0);
+  named_synthetic.SetIsSynthetic(true);
+  symtab->AddSymbol(named_synthetic);
+  ConstString synthetic_name("_Z30qsymbol_synthetic_unrelated_06v");
+  Symbol unrelated_synthetic(8, synthetic_name.GetStringRef(), eSymbolTypeCode,
+                             false, false, false, false, SectionSP(), 8, 0,
+                             false, false, 0);
+  unrelated_synthetic.SetIsSynthetic(true);
+  symtab->AddSymbol(unrelated_synthetic);
+  ConstString counterpart;
+  ASSERT_FALSE(synthetic_name.GetMangledCounterpart(counterpart));
+
+  std::vector<uint32_t> matches;
+  symtab->FindAllSymbolsWithNameAndType(ConstString("dispatch_queue_offsets"),
+                                        eSymbolTypeAny, matches,
+                                        /*match_linkage_names=*/true);
+  ASSERT_EQ(matches.size(), 6u);
+  const user_id_t all_ids[] = {0, 1, 2, 3, 4, 7};
+  for (auto [index, id] : llvm::zip(matches, all_ids))
+    EXPECT_EQ(symtab->SymbolAtIndex(index)->GetID(), id);
+  // Checking autogenerated-name status on every named synthetic symbol would
+  // demangle this unrelated name even though no index is built.
+  EXPECT_FALSE(synthetic_name.GetMangledCounterpart(counterpart));
+
+  matches.clear();
+  symtab->FindAllSymbolsWithNameAndType(ConstString("dispatch_queue_offsets"),
+                                        eSymbolTypeData, matches,
+                                        /*match_linkage_names=*/true);
+  ASSERT_EQ(matches.size(), 3u);
+  const user_id_t data_ids[] = {0, 1, 7};
+  for (auto [index, id] : llvm::zip(matches, data_ids))
+    EXPECT_EQ(symtab->SymbolAtIndex(index)->GetID(), id);
+  EXPECT_EQ(module->GetSymtabIndexTime().get().count(), 0.0);
+
+  matches.clear();
+  symtab->FindAllSymbolsWithNameAndType(ConstString("dispatch_queue_offsets"),
+                                        eSymbolTypeTrampoline, matches,
+                                        /*match_linkage_names=*/true);
+  ASSERT_EQ(matches.size(), 1u);
+  EXPECT_EQ(symtab->SymbolAtIndex(matches.front())->GetID(), 4u);
+  EXPECT_EQ(module->GetSymtabIndexTime().get().count(), 0.0);
+
+  matches.clear();
+  symtab->FindAllSymbolsWithNameAndType(ConstString(), eSymbolTypeAny, matches,
+                                        /*match_linkage_names=*/true);
+  EXPECT_TRUE(matches.empty());
+  EXPECT_EQ(module->GetSymtabIndexTime().get().count(), 0.0);
+
+  // Unrelated qSymbol requests still use the established name index, including
+  // a C++ function's unqualified basename.
+  symtab->FindAllSymbolsWithNameAndType(ConstString("other_wire_request"),
+                                        eSymbolTypeAny, matches);
+  ASSERT_EQ(matches.size(), 1u);
+  EXPECT_EQ(symtab->SymbolAtIndex(matches.front())->GetID(), 6u);
+  EXPECT_GT(module->GetSymtabIndexTime().get().count(), 0.0);
+}
+
+TEST_F(SymtabTest, LinkageModuleListLookupPreservesModuleOrderAndTrampolines) {
+  auto image = TestFile::fromYaml(R"(
+--- !ELF
+FileHeader:
+  Class: ELFCLASS64
+  Data: ELFDATA2LSB
+  Type: ET_DYN
+  Machine: EM_X86_64
+Sections:
+  - Name: .text
+    Type: SHT_PROGBITS
+    Flags: [SHF_ALLOC, SHF_EXECINSTR]
+    Address: 0x1000
+    Content: C3
+)");
+  ASSERT_THAT_EXPECTED(image, llvm::Succeeded());
+  auto first = std::make_shared<Module>(image->moduleSpec());
+  auto second = std::make_shared<Module>(image->moduleSpec());
+  for (const auto &module : {first, second}) {
+    Symtab *symtab = module->GetObjectFile()->GetSymtab();
+    ASSERT_NE(symtab, nullptr);
+    // Keep the symtab provider available independently of the candidate type.
+    symtab->AddSymbol(Symbol(0, "unrelated", eSymbolTypeCode, false, false,
+                             false, false, SectionSP(), 0, 0, false, false, 0));
+    symtab->AddSymbol(Symbol(
+        1, "runtime", module == first ? eSymbolTypeTrampoline : eSymbolTypeData,
+        false, false, false, false, SectionSP(), 0, 0, false, false, 0));
+  }
+  ModuleList modules;
+  modules.Append(first);
+  modules.Append(second);
+  SymbolContextList matches;
+  modules.FindSymbolsWithNameAndType(ConstString("runtime"), eSymbolTypeAny,
+                                     matches, /*match_linkage_names=*/true);
+  ASSERT_EQ(matches.GetSize(), 2u);
+  EXPECT_EQ(matches[0].module_sp, first);
+  EXPECT_EQ(matches[0].symbol->GetType(), eSymbolTypeTrampoline);
+  EXPECT_EQ(matches[1].module_sp, second);
+  EXPECT_EQ(matches[1].symbol->GetType(), eSymbolTypeData);
+  EXPECT_EQ(first->GetSymtabIndexTime().get().count(), 0.0);
+  EXPECT_EQ(second->GetSymtabIndexTime().get().count(), 0.0);
+
+  // The default name indexes keep their existing trampoline exclusion.
+  matches.Clear();
+  modules.FindSymbolsWithNameAndType(ConstString("runtime"), eSymbolTypeAny,
+                                     matches);
+  ASSERT_EQ(matches.GetSize(), 1u);
+  EXPECT_EQ(matches[0].module_sp, second);
+}
diff --git a/lldb/unittests/Target/CMakeLists.txt b/lldb/unittests/Target/CMakeLists.txt
index 7cf36f12cdede4..8f661198ac2572 100644
--- a/lldb/unittests/Target/CMakeLists.txt
+++ b/lldb/unittests/Target/CMakeLists.txt
@@ -3,6 +3,7 @@ add_lldb_unittest(TargetTests
   DynamicRegisterInfoTest.cpp
   ExecutableModuleTest.cpp
   ExecutionContextTest.cpp
+  InstrumentationRuntimeTest.cpp
   LanguageTest.cpp
   LocateModuleCallbackTest.cpp
   MemoryRegionInfoTest.cpp
@@ -27,6 +28,7 @@ add_lldb_unittest(TargetTests
   LINK_LIBS
       lldbCore
       lldbHost
+      lldbPluginInstrumentationRuntimeBoundsSafety
       lldbPluginObjectFileBreakpad
       lldbPluginObjectFileELF
       lldbPluginObjectFileMachO
diff --git a/lldb/unittests/Target/InstrumentationRuntimeTest.cpp b/lldb/unittests/Target/InstrumentationRuntimeTest.cpp
new file mode 100644
index 00000000000000..72dc09a7409a94
--- /dev/null
+++ b/lldb/unittests/Target/InstrumentationRuntimeTest.cpp
@@ -0,0 +1,145 @@
+//===-- InstrumentationRuntimeTest.cpp -----------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "Plugins/InstrumentationRuntime/BoundsSafety/InstrumentationRuntimeBoundsSafety.h"
+#include "Plugins/ObjectFile/ELF/ObjectFileELF.h"
+#include "Plugins/SymbolFile/Symtab/SymbolFileSymtab.h"
+#include "TestingSupport/SubsystemRAII.h"
+#include "TestingSupport/TestUtilities.h"
+#include "lldb/Breakpoint/BreakpointResolverName.h"
+#include "lldb/Core/Module.h"
+#include "lldb/Core/ModuleList.h"
+#include "lldb/Host/FileSystem.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Symbol/ObjectFile.h"
+#include "lldb/Symbol/Symbol.h"
+#include "lldb/Symbol/Symtab.h"
+#include "gtest/gtest.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+namespace {
+class InstrumentationRuntimeTest : public testing::Test {
+  SubsystemRAII<FileSystem, HostInfo, ObjectFileELF, SymbolFileSymtab>
+      subsystem;
+};
+
+constexpr llvm::StringLiteral EmptyImage = R"(
+--- !ELF
+FileHeader:
+  Class: ELFCLASS64
+  Data: ELFDATA2LSB
+  Type: ET_DYN
+  Machine: EM_X86_64
+Sections:
+  - Name: .text
+    Type: SHT_PROGBITS
+    Flags: [SHF_ALLOC, SHF_EXECINSTR]
+    Address: 0x1000
+    Content: C3
+)";
+
+} // namespace
+
+TEST_F(InstrumentationRuntimeTest, BoundsSafetyProbeUsesExactCNames) {
+  auto image = TestFile::fromYaml(EmptyImage);
+  ASSERT_THAT_EXPECTED(image, llvm::Succeeded());
+
+  auto probe = [&](llvm::StringRef name, SymbolType type, bool trampoline,
+                   bool synthetic, bool expected, bool annotations = false) {
+    SCOPED_TRACE(name.str());
+    auto module = std::make_shared<Module>(image->moduleSpec());
+    auto sentinel = std::make_shared<Module>(image->moduleSpec());
+    ASSERT_NE(module->GetObjectFile(), nullptr);
+    ASSERT_NE(sentinel->GetObjectFile(), nullptr);
+    module->SetFileSpecAndObjectName(FileSpec("unrelated.so"), ConstString());
+    sentinel->SetFileSpecAndObjectName(FileSpec("sentinel.so"), ConstString());
+    Symtab *symtab = module->GetObjectFile()->GetSymtab();
+    // Give the symtab provider a function even when the candidate is an
+    // undefined symbol, so Module::GetSymtab retains the provider.
+    symtab->AddSymbol(Symbol(1, "unrelated", eSymbolTypeCode, false, false,
+                             false, false, SectionSP(), 0, 0, false, false, 0));
+    Symbol symbol(0, name, type, /*external=*/false, /*is_debug=*/false,
+                  trampoline, /*is_artificial=*/false, SectionSP(), 0, 0,
+                  /*size_is_valid=*/false, annotations, 0);
+    // Trampoline classification uses the type, not the legacy constructor flag.
+    ASSERT_EQ(symbol.IsTrampoline(), trampoline);
+    symbol.SetIsSynthetic(synthetic);
+    symtab->AddSymbol(symbol);
+
+    ModuleList modules;
+    modules.Append(module);
+    modules.Append(sentinel);
+    auto runtime = InstrumentationRuntimeBoundsSafety::CreateInstance(nullptr);
+    runtime->ModulesDidLoad(modules);
+
+    // A match stops the module walk even though activation has no process.
+    // A negative probe reaches the sentinel and creates its raw symbol table.
+    EXPECT_EQ(sentinel->GetObjectFile()->GetSymtab(/*can_create=*/false) ==
+                  nullptr,
+              expected);
+    EXPECT_EQ(module->GetSymtabIndexTime().get().count(), 0.0);
+  };
+
+  for (auto name : {"__bounds_safety_soft_trap", "__bounds_safety_soft_trap_s"})
+    for (auto type :
+         {eSymbolTypeCode, eSymbolTypeResolver, eSymbolTypeReExported,
+          eSymbolTypeAbsolute, eSymbolTypeUndefined})
+      probe(name, type, false, false, true);
+
+  probe("__bounds_safety_soft_trap at VERSION", eSymbolTypeCode, false, false,
+        true, true);
+  probe("__bounds_safety_soft_trap", eSymbolTypeTrampoline, true, false, false);
+  probe("", eSymbolTypeCode, false, true, false);
+  probe("_Z25__bounds_safety_soft_trapv", eSymbolTypeCode, false, false, false);
+  probe("$s4Test25__bounds_safety_soft_trapyyF", eSymbolTypeCode, false, false,
+        false);
+}
+
+TEST_F(InstrumentationRuntimeTest, LinkageResolverSerialization) {
+  auto resolver = std::make_shared<BreakpointResolverName>(
+      nullptr, "runtime", eFunctionNameTypeFull, eLanguageTypeUnknown,
+      Breakpoint::Exact, 0, false, false);
+  auto serialized = resolver->SerializeToStructuredData();
+  auto *dictionary = serialized->GetAsDictionary();
+  ASSERT_NE(dictionary, nullptr);
+  StructuredData::Dictionary *options = nullptr;
+  ASSERT_TRUE(dictionary->GetValueForKeyAsDictionary("Options", options));
+  ASSERT_NE(options, nullptr);
+  EXPECT_FALSE(options->HasKey("MatchLinkageNames"));
+
+  Status error;
+  EXPECT_NE(BreakpointResolver::CreateFromStructuredData(*dictionary, error),
+            nullptr);
+  EXPECT_TRUE(error.Success());
+
+  resolver->SetMatchLinkageNames(true);
+  serialized = resolver->SerializeToStructuredData();
+  dictionary = serialized->GetAsDictionary();
+  auto restored =
+      BreakpointResolver::CreateFromStructuredData(*dictionary, error);
+  ASSERT_NE(restored, nullptr);
+  auto reserialized = restored->SerializeToStructuredData();
+  ASSERT_TRUE(reserialized->GetAsDictionary()->GetValueForKeyAsDictionary(
+      "Options", options));
+  bool match_linkage_names = false;
+  EXPECT_TRUE(options->GetValueForKeyAsBoolean("MatchLinkageNames",
+                                               match_linkage_names));
+  EXPECT_TRUE(match_linkage_names);
+
+  options->AddStringItem("MatchLinkageNames", "invalid");
+  EXPECT_EQ(BreakpointResolverName::CreateFromStructuredData(*options, error),
+            nullptr);
+  EXPECT_TRUE(error.Fail());
+  options->AddBooleanItem("MatchLinkageNames", true);
+  options->AddStringItem("Regex", "runtime");
+  EXPECT_EQ(BreakpointResolverName::CreateFromStructuredData(*options, error),
+            nullptr);
+  EXPECT_TRUE(error.Fail());
+}

>From fd29549179b2985f65c0ec77b10552bf11827aca Mon Sep 17 00:00:00 2001
From: Karim Alweheshy <karim.alweheshy at reddit.com>
Date: Fri, 16 Oct 2026 13:56:51 +0200
Subject: [PATCH 2/2] [lldb] Register the Bounds runtime fixture shared library

Supply the harness library search environment when launching, so Linux can load Unrelated before reaching main.

Assisted-by: OpenAI Codex
---
 .../runtime_detection/TestBoundsSafetyRuntimeDetection.py      | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lldb/test/API/lang/BoundsSafety/runtime_detection/TestBoundsSafetyRuntimeDetection.py b/lldb/test/API/lang/BoundsSafety/runtime_detection/TestBoundsSafetyRuntimeDetection.py
index b5ebaf98460d6d..45a343659fe58d 100644
--- a/lldb/test/API/lang/BoundsSafety/runtime_detection/TestBoundsSafetyRuntimeDetection.py
+++ b/lldb/test/API/lang/BoundsSafety/runtime_detection/TestBoundsSafetyRuntimeDetection.py
@@ -69,8 +69,9 @@ def start(
             if late
             else None
         )
+        environment = self.registerSharedLibrariesWithTarget(target, ["Unrelated"])
         process = target.LaunchSimple(
-            arguments, None, self.get_process_working_directory()
+            arguments, environment, self.get_process_working_directory()
         )
         self.assertState(process.GetState(), lldb.eStateStopped)
         if runtime_location is not None:



More information about the lldb-commits mailing list