[Lldb-commits] [lldb] [lldb] Add ScriptedSymbolLocator plugin for source file resolution (PR #181334)

via lldb-commits lldb-commits at lists.llvm.org
Fri Feb 13 00:49:29 PST 2026


https://github.com/rchamala created https://github.com/llvm/llvm-project/pull/181334

## Summary                                                        
                                                                    
  Adds a new `SymbolLocatorScripted` plugin that allows Python scripts to implement custom symbol and source file resolution logic. This enables downstream users to build custom symbol servers, source file remapping, and build artifact resolution entirely in Python.
                                                                    
  ### Changes

  - Adds `LocateSourceFile()` to the SymbolLocator plugin interface, called during source path resolution with a fully loaded `ModuleSP`, so the plugin has access to the module's UUID, file paths, and symbols.
  - Adds `SymbolLocatorScripted` plugin that delegates all four SymbolLocator methods (`LocateExecutableObjectFile`, `LocateExecutableSymbolFile`, `DownloadObjectAndSymbolFile`, `LocateSourceFile`) to a user-provided Python class.
  - Adds `ScriptedSymbolLocatorPythonInterface` to bridge C++ calls to Python, with proper GIL management and error handling.
  - Results for `LocateSourceFile` are cached per (module UUID, source file) pair.
  - The Python class is configured via: `settings set plugin.symbol-locator.scripted.script-class module.ClassName`

  ### Python class interface

  ```python
  class MyLocator:
      def __init__(self, exe_ctx, args): ...
      def locate_source_file(self, module, original_source_file):
  ...
      def locate_executable_object_file(self, module_spec): ...
      def locate_executable_symbol_file(self, module_spec,
  default_search_paths): ...
      def download_object_and_symbol_file(self, module_spec,
  force_lookup, copy_executable): ...
```

  ### Test plan
```
  Added TestScriptedSymbolLocator.py with 3 test cases:
  - test_locate_source_file — verifies the locator resolves source
  files, receives a valid SBModule with UUID, and remaps paths correctly
  - test_locate_source_file_none_fallthrough — verifies returning
  None falls through to default LLDB resolution, and that having no script
  class set works normally
  - test_invalid_script_class — verifies graceful handling of
  invalid class names without crashing
```

>From 6594ea6be6dc4ef4075de168233543c46b3bf8f3 Mon Sep 17 00:00:00 2001
From: Rahul Reddy Chamala <rachamal at fb.com>
Date: Thu, 12 Feb 2026 16:58:25 -0800
Subject: [PATCH] Add ScriptedSymbolLocator plugin for source file resolution

---
 lldb/include/lldb/Core/PluginManager.h        |   4 +
 .../ScriptedSymbolLocatorInterface.h          |  56 ++++
 .../lldb/Interpreter/ScriptInterpreter.h      |   6 +
 lldb/include/lldb/Symbol/LineEntry.h          |   2 +-
 lldb/include/lldb/lldb-forward.h              |   3 +
 lldb/include/lldb/lldb-private-interfaces.h   |   2 +
 lldb/source/Core/Module.cpp                   |   2 +-
 lldb/source/Core/PluginManager.cpp            |  24 +-
 .../Python/Interfaces/CMakeLists.txt          |   3 +-
 .../ScriptInterpreterPythonInterfaces.h       |   1 +
 .../Interfaces/ScriptedPythonInterface.h      |   4 +
 .../ScriptedSymbolLocatorPythonInterface.cpp  | 280 ++++++++++++++++++
 .../ScriptedSymbolLocatorPythonInterface.h    |  69 +++++
 .../Python/ScriptInterpreterPython.cpp        |   5 +
 .../Python/ScriptInterpreterPythonImpl.h      |   7 +-
 .../Plugins/SymbolLocator/CMakeLists.txt      |   1 +
 .../Debuginfod/SymbolLocatorDebuginfod.cpp    |   2 +-
 .../SymbolLocator/Scripted/CMakeLists.txt     |  24 ++
 .../Scripted/SymbolLocatorScripted.cpp        | 268 +++++++++++++++++
 .../Scripted/SymbolLocatorScripted.h          |  57 ++++
 .../SymbolLocatorScriptedProperties.td        |   7 +
 lldb/source/Symbol/LineEntry.cpp              |  22 +-
 lldb/source/Target/StackFrame.cpp             |   2 +-
 lldb/source/Target/StackFrameList.cpp         |   2 +-
 lldb/source/Target/ThreadPlanStepRange.cpp    |   8 +-
 .../scripted_symbol_locator/Makefile          |   4 +
 .../TestScriptedSymbolLocator.py              | 160 ++++++++++
 .../scripted_symbol_locator/main.c            |   7 +
 .../scripted_symbol_locator/source_locator.py |  51 ++++
 29 files changed, 1067 insertions(+), 16 deletions(-)
 create mode 100644 lldb/include/lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.cpp
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.h
 create mode 100644 lldb/source/Plugins/SymbolLocator/Scripted/CMakeLists.txt
 create mode 100644 lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.cpp
 create mode 100644 lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.h
 create mode 100644 lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScriptedProperties.td
 create mode 100644 lldb/test/API/functionalities/scripted_symbol_locator/Makefile
 create mode 100644 lldb/test/API/functionalities/scripted_symbol_locator/TestScriptedSymbolLocator.py
 create mode 100644 lldb/test/API/functionalities/scripted_symbol_locator/main.c
 create mode 100644 lldb/test/API/functionalities/scripted_symbol_locator/source_locator.py

diff --git a/lldb/include/lldb/Core/PluginManager.h b/lldb/include/lldb/Core/PluginManager.h
index 4d116f52460ff..3fd3e6177afa0 100644
--- a/lldb/include/lldb/Core/PluginManager.h
+++ b/lldb/include/lldb/Core/PluginManager.h
@@ -455,6 +455,7 @@ class PluginManager {
       SymbolLocatorDownloadObjectAndSymbolFile download_object_symbol_file =
           nullptr,
       SymbolLocatorFindSymbolFileInBundle find_symbol_file_in_bundle = nullptr,
+      SymbolLocatorLocateSourceFile locate_source_file = nullptr,
       DebuggerInitializeCallback debugger_init_callback = nullptr);
 
   static bool UnregisterPlugin(SymbolLocatorCreateInstance create_callback);
@@ -479,6 +480,9 @@ class PluginManager {
                                          const UUID *uuid,
                                          const ArchSpec *arch);
 
+  static FileSpec LocateSourceFile(const lldb::ModuleSP &module_sp,
+                                   const FileSpec &original_source_file);
+
   // Trace
   static bool RegisterPlugin(
       llvm::StringRef name, llvm::StringRef description,
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h
new file mode 100644
index 0000000000000..26fcb68aa598d
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h
@@ -0,0 +1,56 @@
+//===-- ScriptedSymbolLocatorInterface.h -------------------------*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_INTERPRETER_INTERFACES_SCRIPTEDSYMBOLLOCATORINTERFACE_H
+#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDSYMBOLLOCATORINTERFACE_H
+
+#include "lldb/Core/ModuleSpec.h"
+#include "lldb/Core/StructuredDataImpl.h"
+#include "lldb/Interpreter/Interfaces/ScriptedInterface.h"
+#include "lldb/Utility/Status.h"
+
+#include "lldb/lldb-private.h"
+
+#include <optional>
+#include <string>
+
+namespace lldb_private {
+class ScriptedSymbolLocatorInterface : virtual public ScriptedInterface {
+public:
+  virtual llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, ExecutionContext &exe_ctx,
+                     StructuredData::DictionarySP args_sp,
+                     StructuredData::Generic *script_obj = nullptr) = 0;
+
+  virtual std::optional<ModuleSpec>
+  LocateExecutableObjectFile(const ModuleSpec &module_spec, Status &error) {
+    return {};
+  }
+
+  virtual std::optional<FileSpec>
+  LocateExecutableSymbolFile(const ModuleSpec &module_spec,
+                             const FileSpecList &default_search_paths,
+                             Status &error) {
+    return {};
+  }
+
+  virtual bool DownloadObjectAndSymbolFile(ModuleSpec &module_spec,
+                                           Status &error, bool force_lookup,
+                                           bool copy_executable) {
+    return false;
+  }
+
+  virtual std::optional<FileSpec>
+  LocateSourceFile(const lldb::ModuleSP &module_sp,
+                   const FileSpec &original_source_file, Status &error) {
+    return {};
+  }
+};
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDSYMBOLLOCATORINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 557d73a415452..7675ba838c75d 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -34,6 +34,7 @@
 #include "lldb/Interpreter/Interfaces/ScriptedPlatformInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedProcessInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedThreadInterface.h"
+#include "lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h"
 #include "lldb/Interpreter/ScriptObject.h"
 #include "lldb/Symbol/SymbolContext.h"
 #include "lldb/Utility/Broadcaster.h"
@@ -545,6 +546,11 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  virtual lldb::ScriptedSymbolLocatorInterfaceSP
+  CreateScriptedSymbolLocatorInterface() {
+    return {};
+  }
+
   virtual lldb::ScriptedThreadPlanInterfaceSP
   CreateScriptedThreadPlanInterface() {
     return {};
diff --git a/lldb/include/lldb/Symbol/LineEntry.h b/lldb/include/lldb/Symbol/LineEntry.h
index adf2e989e3e34..e023eda6d89a4 100644
--- a/lldb/include/lldb/Symbol/LineEntry.h
+++ b/lldb/include/lldb/Symbol/LineEntry.h
@@ -128,7 +128,7 @@ struct LineEntry {
   ///
   /// \param[in] target_sp
   ///     Shared pointer to the target this LineEntry belongs to.
-  void ApplyFileMappings(lldb::TargetSP target_sp);
+  void ApplyFileMappings(lldb::TargetSP target_sp, const Address &address);
 
   /// Helper to access the file.
   const FileSpec &GetFile() const { return file_sp->GetSpecOnly(); }
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index ccfe5efa19e1d..c0f65b09616a3 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -196,6 +196,7 @@ class ScriptedProcessInterface;
 class ScriptedStopHookInterface;
 class ScriptedThreadInterface;
 class ScriptedThreadPlanInterface;
+class ScriptedSymbolLocatorInterface;
 class ScriptedSyntheticChildren;
 class SearchFilter;
 class Section;
@@ -431,6 +432,8 @@ typedef std::shared_ptr<lldb_private::ScriptedThreadPlanInterface>
     ScriptedThreadPlanInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedBreakpointInterface>
     ScriptedBreakpointInterfaceSP;
+typedef std::shared_ptr<lldb_private::ScriptedSymbolLocatorInterface>
+    ScriptedSymbolLocatorInterfaceSP;
 typedef std::shared_ptr<lldb_private::Section> SectionSP;
 typedef std::unique_ptr<lldb_private::SectionList> SectionListUP;
 typedef std::weak_ptr<lldb_private::Section> SectionWP;
diff --git a/lldb/include/lldb/lldb-private-interfaces.h b/lldb/include/lldb/lldb-private-interfaces.h
index a87e01769c555..6d71b8d671b71 100644
--- a/lldb/include/lldb/lldb-private-interfaces.h
+++ b/lldb/include/lldb/lldb-private-interfaces.h
@@ -110,6 +110,8 @@ typedef std::optional<FileSpec> (*SymbolLocatorLocateExecutableSymbolFile)(
 typedef bool (*SymbolLocatorDownloadObjectAndSymbolFile)(
     ModuleSpec &module_spec, Status &error, bool force_lookup,
     bool copy_executable);
+typedef std::optional<FileSpec> (*SymbolLocatorLocateSourceFile)(
+    const lldb::ModuleSP &module_sp, const FileSpec &original_source_file);
 using BreakpointHitCallback =
     std::function<bool(void *baton, StoppointCallbackContext *context,
                        lldb::user_id_t break_id, lldb::user_id_t break_loc_id)>;
diff --git a/lldb/source/Core/Module.cpp b/lldb/source/Core/Module.cpp
index 659190833c20d..fc17daf86a901 100644
--- a/lldb/source/Core/Module.cpp
+++ b/lldb/source/Core/Module.cpp
@@ -476,7 +476,7 @@ uint32_t Module::ResolveSymbolContextForAddress(
           symfile->ResolveSymbolContext(so_addr, resolve_scope, sc);
 
       if ((resolve_scope & eSymbolContextLineEntry) && sc.line_entry.IsValid())
-        sc.line_entry.ApplyFileMappings(sc.target_sp);
+        sc.line_entry.ApplyFileMappings(sc.target_sp, so_addr);
     }
 
     // Resolve the symbol if requested, but don't re-look it up if we've
diff --git a/lldb/source/Core/PluginManager.cpp b/lldb/source/Core/PluginManager.cpp
index 64130d700a006..5e6bfe343b6a1 100644
--- a/lldb/source/Core/PluginManager.cpp
+++ b/lldb/source/Core/PluginManager.cpp
@@ -1476,18 +1476,21 @@ struct SymbolLocatorInstance
       SymbolLocatorLocateExecutableSymbolFile locate_executable_symbol_file,
       SymbolLocatorDownloadObjectAndSymbolFile download_object_symbol_file,
       SymbolLocatorFindSymbolFileInBundle find_symbol_file_in_bundle,
+      SymbolLocatorLocateSourceFile locate_source_file,
       DebuggerInitializeCallback debugger_init_callback)
       : PluginInstance<SymbolLocatorCreateInstance>(
             name, description, create_callback, debugger_init_callback),
         locate_executable_object_file(locate_executable_object_file),
         locate_executable_symbol_file(locate_executable_symbol_file),
         download_object_symbol_file(download_object_symbol_file),
-        find_symbol_file_in_bundle(find_symbol_file_in_bundle) {}
+        find_symbol_file_in_bundle(find_symbol_file_in_bundle),
+        locate_source_file(locate_source_file) {}
 
   SymbolLocatorLocateExecutableObjectFile locate_executable_object_file;
   SymbolLocatorLocateExecutableSymbolFile locate_executable_symbol_file;
   SymbolLocatorDownloadObjectAndSymbolFile download_object_symbol_file;
   SymbolLocatorFindSymbolFileInBundle find_symbol_file_in_bundle;
+  SymbolLocatorLocateSourceFile locate_source_file;
 };
 typedef PluginInstances<SymbolLocatorInstance> SymbolLocatorInstances;
 
@@ -1503,11 +1506,13 @@ bool PluginManager::RegisterPlugin(
     SymbolLocatorLocateExecutableSymbolFile locate_executable_symbol_file,
     SymbolLocatorDownloadObjectAndSymbolFile download_object_symbol_file,
     SymbolLocatorFindSymbolFileInBundle find_symbol_file_in_bundle,
+    SymbolLocatorLocateSourceFile locate_source_file,
     DebuggerInitializeCallback debugger_init_callback) {
   return GetSymbolLocatorInstances().RegisterPlugin(
       name, description, create_callback, locate_executable_object_file,
       locate_executable_symbol_file, download_object_symbol_file,
-      find_symbol_file_in_bundle, debugger_init_callback);
+      find_symbol_file_in_bundle, locate_source_file,
+      debugger_init_callback);
 }
 
 bool PluginManager::UnregisterPlugin(
@@ -1591,6 +1596,21 @@ FileSpec PluginManager::FindSymbolFileInBundle(const FileSpec &symfile_bundle,
   return {};
 }
 
+FileSpec PluginManager::LocateSourceFile(
+    const lldb::ModuleSP &module_sp,
+    const FileSpec &original_source_file) {
+  auto instances = GetSymbolLocatorInstances().GetSnapshot();
+  for (auto &instance : instances) {
+    if (instance.locate_source_file) {
+      std::optional<FileSpec> result =
+          instance.locate_source_file(module_sp, original_source_file);
+      if (result)
+        return *result;
+    }
+  }
+  return {};
+}
+
 #pragma mark Trace
 
 struct TraceInstance
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
index 50569cdefaafa..ddffff08095fb 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
@@ -26,6 +26,7 @@ add_lldb_library(lldbPluginScriptInterpreterPythonInterfaces PLUGIN
   ScriptedFrameProviderPythonInterface.cpp
   ScriptedPlatformPythonInterface.cpp
   ScriptedProcessPythonInterface.cpp
+  ScriptedSymbolLocatorPythonInterface.cpp
   ScriptedPythonInterface.cpp
   ScriptedStopHookPythonInterface.cpp
   ScriptedBreakpointPythonInterface.cpp
@@ -42,5 +43,3 @@ add_lldb_library(lldbPluginScriptInterpreterPythonInterfaces PLUGIN
     ${Python3_LIBRARIES}
     ${LLDB_LIBEDIT_LIBS}
   )
-
-
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
index 721902ec1e253..52827d01b2495 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
@@ -22,6 +22,7 @@
 #include "ScriptedPlatformPythonInterface.h"
 #include "ScriptedProcessPythonInterface.h"
 #include "ScriptedStopHookPythonInterface.h"
+#include "ScriptedSymbolLocatorPythonInterface.h"
 #include "ScriptedThreadPlanPythonInterface.h"
 
 namespace lldb_private {
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index 4aadee584b2e2..82e6f239a3b68 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -632,6 +632,10 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     return python::SWIGBridge::ToSWIGWrapper(arg);
   }
 
+  python::PythonObject Transform(lldb::ModuleSP arg) {
+    return python::SWIGBridge::ToSWIGWrapper(arg);
+  }
+
   python::PythonObject Transform(Event *arg) {
     return python::SWIGBridge::ToSWIGWrapper(arg);
   }
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.cpp
new file mode 100644
index 0000000000000..53922d1710318
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.cpp
@@ -0,0 +1,280 @@
+//===-- ScriptedSymbolLocatorPythonInterface.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 "lldb/Host/Config.h"
+#include "lldb/Target/ExecutionContext.h"
+#include "lldb/Utility/Log.h"
+#include "lldb/lldb-enumerations.h"
+
+#if LLDB_ENABLE_PYTHON
+
+// clang-format off
+// LLDB Python header must be included first
+#include "../lldb-python.h"
+// clang-format on
+
+#include "../SWIGPythonBridge.h"
+#include "../ScriptInterpreterPythonImpl.h"
+#include "ScriptedSymbolLocatorPythonInterface.h"
+
+#include "lldb/API/SBFileSpec.h"
+#include "lldb/API/SBModuleSpec.h"
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::python;
+using Locker = ScriptInterpreterPythonImpl::Locker;
+
+ScriptedSymbolLocatorPythonInterface::ScriptedSymbolLocatorPythonInterface(
+    ScriptInterpreterPythonImpl &interpreter)
+    : ScriptedSymbolLocatorInterface(), ScriptedPythonInterface(interpreter) {}
+
+llvm::Expected<StructuredData::GenericSP>
+ScriptedSymbolLocatorPythonInterface::CreatePluginObject(
+    const llvm::StringRef class_name, ExecutionContext &exe_ctx,
+    StructuredData::DictionarySP args_sp, StructuredData::Generic *script_obj) {
+  ExecutionContextRefSP exe_ctx_ref_sp =
+      std::make_shared<ExecutionContextRef>(exe_ctx);
+  StructuredDataImpl sd_impl(args_sp);
+  return ScriptedPythonInterface::CreatePluginObject(class_name, script_obj,
+                                                     exe_ctx_ref_sp, sd_impl);
+}
+
+/// Helper to convert an internal ModuleSpec to a Python SBModuleSpec object.
+/// Must be called with the GIL held.
+static PythonObject ToSWIGModuleSpec(const ModuleSpec &module_spec) {
+  // Build an SBModuleSpec using public API setters since the constructor
+  // from ModuleSpec is private.
+  SBModuleSpec sb_module_spec;
+
+  const UUID &uuid = module_spec.GetUUID();
+  if (uuid.IsValid())
+    sb_module_spec.SetUUIDBytes(uuid.GetBytes().data(),
+                                uuid.GetBytes().size());
+
+  const FileSpec &file = module_spec.GetFileSpec();
+  if (file)
+    sb_module_spec.SetFileSpec(SBFileSpec(file.GetPath().c_str(), false));
+
+  const FileSpec &platform_file = module_spec.GetPlatformFileSpec();
+  if (platform_file)
+    sb_module_spec.SetPlatformFileSpec(
+        SBFileSpec(platform_file.GetPath().c_str(), false));
+
+  const FileSpec &symbol_file = module_spec.GetSymbolFileSpec();
+  if (symbol_file)
+    sb_module_spec.SetSymbolFileSpec(
+        SBFileSpec(symbol_file.GetPath().c_str(), false));
+
+  const ArchSpec &arch = module_spec.GetArchitecture();
+  if (arch.IsValid())
+    sb_module_spec.SetTriple(arch.GetTriple().getTriple().c_str());
+
+  ConstString object_name = module_spec.GetObjectName();
+  if (object_name)
+    sb_module_spec.SetObjectName(object_name.GetCString());
+
+  sb_module_spec.SetObjectOffset(module_spec.GetObjectOffset());
+  sb_module_spec.SetObjectSize(module_spec.GetObjectSize());
+
+  return SWIGBridge::ToSWIGWrapper(
+      std::make_unique<SBModuleSpec>(sb_module_spec));
+}
+
+/// Helper to convert an internal FileSpec to a Python SBFileSpec object.
+/// Must be called with the GIL held.
+static PythonObject ToSWIGFileSpec(const FileSpec &file_spec) {
+  return SWIGBridge::ToSWIGWrapper(
+      std::make_unique<SBFileSpec>(file_spec.GetPath().c_str(), false));
+}
+
+std::optional<ModuleSpec>
+ScriptedSymbolLocatorPythonInterface::LocateExecutableObjectFile(
+    const ModuleSpec &module_spec, Status &error) {
+  if (!m_object_instance_sp)
+    return {};
+
+  // Acquire the GIL before creating any Python objects.
+  Locker py_lock(&m_interpreter, Locker::AcquireLock | Locker::NoSTDIN,
+                 Locker::FreeLock);
+
+  PythonObject implementor(PyRefType::Borrowed,
+                           (PyObject *)m_object_instance_sp->GetValue());
+  if (!implementor.IsAllocated())
+    return {};
+
+  PythonObject py_module_spec = ToSWIGModuleSpec(module_spec);
+
+  auto expected = implementor.CallMethod("locate_executable_object_file",
+                                         py_module_spec);
+  if (!expected) {
+    // Consume the PythonException while the GIL is held. Converting to string
+    // forces PythonException destruction before the GIL is released.
+    std::string msg = llvm::toString(expected.takeError());
+    error = Status(msg);
+    return {};
+  }
+
+  PythonObject py_return = std::move(*expected);
+  if (!py_return.IsAllocated() || py_return.IsNone())
+    return {};
+
+  auto obj = py_return.CreateStructuredObject();
+  if (!obj)
+    return {};
+
+  llvm::StringRef value = obj->GetStringValue();
+  if (value.empty())
+    return {};
+
+  ModuleSpec result_spec(module_spec);
+  result_spec.GetFileSpec().SetPath(value);
+  return result_spec;
+}
+
+std::optional<FileSpec>
+ScriptedSymbolLocatorPythonInterface::LocateExecutableSymbolFile(
+    const ModuleSpec &module_spec,
+    const FileSpecList &default_search_paths, Status &error) {
+  if (!m_object_instance_sp)
+    return {};
+
+  // Acquire the GIL before creating any Python objects.
+  Locker py_lock(&m_interpreter, Locker::AcquireLock | Locker::NoSTDIN,
+                 Locker::FreeLock);
+
+  PythonObject implementor(PyRefType::Borrowed,
+                           (PyObject *)m_object_instance_sp->GetValue());
+  if (!implementor.IsAllocated())
+    return {};
+
+  PythonObject py_module_spec = ToSWIGModuleSpec(module_spec);
+
+  // Convert FileSpecList to a Python list of SBFileSpec.
+  PythonList py_paths(default_search_paths.GetSize());
+  for (size_t i = 0; i < default_search_paths.GetSize(); i++) {
+    py_paths.SetItemAtIndex(
+        i, ToSWIGFileSpec(default_search_paths.GetFileSpecAtIndex(i)));
+  }
+
+  auto expected = implementor.CallMethod("locate_executable_symbol_file",
+                                         py_module_spec, py_paths);
+  if (!expected) {
+    std::string msg = llvm::toString(expected.takeError());
+    error = Status(msg);
+    return {};
+  }
+
+  PythonObject py_return = std::move(*expected);
+  if (!py_return.IsAllocated() || py_return.IsNone())
+    return {};
+
+  auto obj = py_return.CreateStructuredObject();
+  if (!obj)
+    return {};
+
+  llvm::StringRef value = obj->GetStringValue();
+  if (value.empty())
+    return {};
+
+  FileSpec file_spec;
+  file_spec.SetPath(value);
+  return file_spec;
+}
+
+bool ScriptedSymbolLocatorPythonInterface::DownloadObjectAndSymbolFile(
+    ModuleSpec &module_spec, Status &error, bool force_lookup,
+    bool copy_executable) {
+  if (!m_object_instance_sp)
+    return false;
+
+  // Acquire the GIL before creating any Python objects.
+  Locker py_lock(&m_interpreter, Locker::AcquireLock | Locker::NoSTDIN,
+                 Locker::FreeLock);
+
+  PythonObject implementor(PyRefType::Borrowed,
+                           (PyObject *)m_object_instance_sp->GetValue());
+  if (!implementor.IsAllocated())
+    return false;
+
+  PythonObject py_module_spec = ToSWIGModuleSpec(module_spec);
+
+  auto expected = implementor.CallMethod("download_object_and_symbol_file",
+                                         py_module_spec,
+                                         PythonBoolean(force_lookup),
+                                         PythonBoolean(copy_executable));
+  if (!expected) {
+    std::string msg = llvm::toString(expected.takeError());
+    error = Status(msg);
+    return false;
+  }
+
+  PythonObject py_return = std::move(*expected);
+  if (!py_return.IsAllocated() || py_return.IsNone())
+    return false;
+
+  auto obj = py_return.CreateStructuredObject();
+  if (!obj)
+    return false;
+
+  return obj->GetBooleanValue();
+}
+
+std::optional<FileSpec>
+ScriptedSymbolLocatorPythonInterface::LocateSourceFile(
+    const lldb::ModuleSP &module_sp, const FileSpec &original_source_file,
+    Status &error) {
+  if (!m_object_instance_sp)
+    return {};
+
+  std::optional<FileSpec> result;
+
+  {
+    // Acquire the GIL before creating any Python objects. All Python objects
+    // (including error objects) must be destroyed within this scope.
+    Locker py_lock(&m_interpreter, Locker::AcquireLock | Locker::NoSTDIN,
+                   Locker::FreeLock);
+
+    PythonObject implementor(PyRefType::Borrowed,
+                             (PyObject *)m_object_instance_sp->GetValue());
+    if (!implementor.IsAllocated())
+      return {};
+
+    PythonObject py_module = SWIGBridge::ToSWIGWrapper(module_sp);
+    std::string source_path = original_source_file.GetPath();
+    PythonString py_source_path(source_path);
+
+    auto expected = implementor.CallMethod("locate_source_file", py_module,
+                                           py_source_path);
+    if (!expected) {
+      // Consume the error (which may contain PythonException) while the GIL
+      // is still held. Convert to string to force PythonException destruction
+      // before the GIL is released.
+      std::string msg = llvm::toString(expected.takeError());
+      error = Status(msg);
+      return {};
+    }
+
+    PythonObject py_return = std::move(*expected);
+    if (py_return.IsAllocated() && !py_return.IsNone()) {
+      auto obj = py_return.CreateStructuredObject();
+      if (obj) {
+        llvm::StringRef value = obj->GetStringValue();
+        if (!value.empty()) {
+          FileSpec file_spec;
+          file_spec.SetPath(value);
+          result = file_spec;
+        }
+      }
+    }
+  } // GIL released here, after all Python objects are destroyed.
+
+  return result;
+}
+
+#endif // LLDB_ENABLE_PYTHON
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.h
new file mode 100644
index 0000000000000..d7d98db9e8ea3
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.h
@@ -0,0 +1,69 @@
+//===-- ScriptedSymbolLocatorPythonInterface.h -------------------*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDSYMBOLLOCATORPYTHONINTERFACE_H
+#define LLDB_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDSYMBOLLOCATORPYTHONINTERFACE_H
+
+#include "lldb/Host/Config.h"
+#include "lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h"
+
+#if LLDB_ENABLE_PYTHON
+
+#include "ScriptedPythonInterface.h"
+
+#include <optional>
+
+namespace lldb_private {
+class ScriptedSymbolLocatorPythonInterface
+    : public ScriptedSymbolLocatorInterface,
+      public ScriptedPythonInterface,
+      public PluginInterface {
+public:
+  ScriptedSymbolLocatorPythonInterface(
+      ScriptInterpreterPythonImpl &interpreter);
+
+  llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(const llvm::StringRef class_name,
+                     ExecutionContext &exe_ctx,
+                     StructuredData::DictionarySP args_sp,
+                     StructuredData::Generic *script_obj = nullptr) override;
+
+  llvm::SmallVector<AbstractMethodRequirement>
+  GetAbstractMethodRequirements() const override {
+    return llvm::SmallVector<AbstractMethodRequirement>(
+        {{"locate_source_file", 2}});
+  }
+
+  std::optional<ModuleSpec>
+  LocateExecutableObjectFile(const ModuleSpec &module_spec,
+                             Status &error) override;
+
+  std::optional<FileSpec>
+  LocateExecutableSymbolFile(const ModuleSpec &module_spec,
+                             const FileSpecList &default_search_paths,
+                             Status &error) override;
+
+  bool DownloadObjectAndSymbolFile(ModuleSpec &module_spec, Status &error,
+                                   bool force_lookup,
+                                   bool copy_executable) override;
+
+  std::optional<FileSpec>
+  LocateSourceFile(const lldb::ModuleSP &module_sp,
+                   const FileSpec &original_source_file,
+                   Status &error) override;
+
+  static llvm::StringRef GetPluginNameStatic() {
+    return "ScriptedSymbolLocatorPythonInterface";
+  }
+
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+};
+} // namespace lldb_private
+
+#endif // LLDB_ENABLE_PYTHON
+#endif // LLDB_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDSYMBOLLOCATORPYTHONINTERFACE_H
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index 35a772c1454df..1346f496b0e07 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
@@ -1532,6 +1532,11 @@ ScriptInterpreterPythonImpl::CreateScriptedFrameProviderInterface() {
   return std::make_shared<ScriptedFrameProviderPythonInterface>(*this);
 }
 
+ScriptedSymbolLocatorInterfaceSP
+ScriptInterpreterPythonImpl::CreateScriptedSymbolLocatorInterface() {
+  return std::make_shared<ScriptedSymbolLocatorPythonInterface>(*this);
+}
+
 ScriptedThreadPlanInterfaceSP
 ScriptInterpreterPythonImpl::CreateScriptedThreadPlanInterface() {
   return std::make_shared<ScriptedThreadPlanPythonInterface>(*this);
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
index 1eac78e6360f2..b301add60d754 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
@@ -104,6 +104,9 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
   lldb::ScriptedFrameProviderInterfaceSP
   CreateScriptedFrameProviderInterface() override;
 
+  lldb::ScriptedSymbolLocatorInterfaceSP
+  CreateScriptedSymbolLocatorInterface() override;
+
   lldb::ScriptedThreadPlanInterfaceSP
   CreateScriptedThreadPlanInterface() override;
 
@@ -202,7 +205,7 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
 
   bool GetLongHelpForCommandObject(StructuredData::GenericSP cmd_obj_sp,
                                    std::string &dest) override;
-                                   
+
   StructuredData::ObjectSP
   GetOptionsForCommandObject(StructuredData::GenericSP cmd_obj_sp) override;
 
@@ -211,7 +214,7 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
 
   bool SetOptionValueForCommandObject(StructuredData::GenericSP cmd_obj_sp,
                                       ExecutionContext *exe_ctx,
-                                      llvm::StringRef long_option, 
+                                      llvm::StringRef long_option,
                                       llvm::StringRef value) override;
 
   void OptionParsingStartedForCommandObject(
diff --git a/lldb/source/Plugins/SymbolLocator/CMakeLists.txt b/lldb/source/Plugins/SymbolLocator/CMakeLists.txt
index 3b466f71dca58..bf7f6046eed9d 100644
--- a/lldb/source/Plugins/SymbolLocator/CMakeLists.txt
+++ b/lldb/source/Plugins/SymbolLocator/CMakeLists.txt
@@ -7,6 +7,7 @@ set_property(DIRECTORY PROPERTY LLDB_PLUGIN_KIND SymbolLocator)
 # provider.
 add_subdirectory(Debuginfod)
 add_subdirectory(Default)
+add_subdirectory(Scripted)
 if (CMAKE_SYSTEM_NAME MATCHES "Darwin")
   add_subdirectory(DebugSymbols)
 endif()
diff --git a/lldb/source/Plugins/SymbolLocator/Debuginfod/SymbolLocatorDebuginfod.cpp b/lldb/source/Plugins/SymbolLocator/Debuginfod/SymbolLocatorDebuginfod.cpp
index a09bb356e3a8c..bdef57f0671e1 100644
--- a/lldb/source/Plugins/SymbolLocator/Debuginfod/SymbolLocatorDebuginfod.cpp
+++ b/lldb/source/Plugins/SymbolLocator/Debuginfod/SymbolLocatorDebuginfod.cpp
@@ -111,7 +111,7 @@ void SymbolLocatorDebuginfod::Initialize() {
     PluginManager::RegisterPlugin(
         GetPluginNameStatic(), GetPluginDescriptionStatic(), CreateInstance,
         LocateExecutableObjectFile, LocateExecutableSymbolFile, nullptr,
-        nullptr, SymbolLocatorDebuginfod::DebuggerInitialize);
+        nullptr, nullptr, SymbolLocatorDebuginfod::DebuggerInitialize);
     llvm::HTTPClient::initialize();
   });
 }
diff --git a/lldb/source/Plugins/SymbolLocator/Scripted/CMakeLists.txt b/lldb/source/Plugins/SymbolLocator/Scripted/CMakeLists.txt
new file mode 100644
index 0000000000000..79eff43dd5e22
--- /dev/null
+++ b/lldb/source/Plugins/SymbolLocator/Scripted/CMakeLists.txt
@@ -0,0 +1,24 @@
+lldb_tablegen(SymbolLocatorScriptedProperties.inc -gen-lldb-property-defs
+  SOURCE SymbolLocatorScriptedProperties.td
+  TARGET LLDBPluginSymbolLocatorScriptedPropertiesGen)
+
+lldb_tablegen(SymbolLocatorScriptedPropertiesEnum.inc -gen-lldb-property-enum-defs
+  SOURCE SymbolLocatorScriptedProperties.td
+  TARGET LLDBPluginSymbolLocatorScriptedPropertiesEnumGen)
+
+set_property(DIRECTORY PROPERTY LLDB_PLUGIN_KIND SymbolLocator)
+
+add_lldb_library(lldbPluginSymbolLocatorScripted PLUGIN
+  SymbolLocatorScripted.cpp
+
+  LINK_LIBS
+    lldbCore
+    lldbHost
+    lldbInterpreter
+    lldbSymbol
+    lldbUtility
+  )
+
+add_dependencies(lldbPluginSymbolLocatorScripted
+  LLDBPluginSymbolLocatorScriptedPropertiesGen
+  LLDBPluginSymbolLocatorScriptedPropertiesEnumGen)
diff --git a/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.cpp b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.cpp
new file mode 100644
index 0000000000000..82e8c7a6734a5
--- /dev/null
+++ b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.cpp
@@ -0,0 +1,268 @@
+//===-- SymbolLocatorScripted.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 "SymbolLocatorScripted.h"
+
+#include "lldb/Core/Debugger.h"
+#include "lldb/Core/Module.h"
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Interpreter/OptionValueString.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/Log.h"
+
+#include <unordered_map>
+
+using namespace lldb;
+using namespace lldb_private;
+
+LLDB_PLUGIN_DEFINE(SymbolLocatorScripted)
+
+namespace {
+
+#define LLDB_PROPERTIES_symbollocatorscripted
+#include "SymbolLocatorScriptedProperties.inc"
+
+enum {
+#define LLDB_PROPERTIES_symbollocatorscripted
+#include "SymbolLocatorScriptedPropertiesEnum.inc"
+};
+
+class PluginProperties : public Properties {
+public:
+  static llvm::StringRef GetSettingName() {
+    return SymbolLocatorScripted::GetPluginNameStatic();
+  }
+
+  PluginProperties() {
+    m_collection_sp = std::make_shared<OptionValueProperties>(GetSettingName());
+    m_collection_sp->Initialize(g_symbollocatorscripted_properties);
+
+    m_collection_sp->SetValueChangedCallback(
+        ePropertyScriptClass, [this] { ScriptClassChangedCallback(); });
+  }
+
+  llvm::StringRef GetScriptClassName() const {
+    const OptionValueString *s =
+        m_collection_sp->GetPropertyAtIndexAsOptionValueString(
+            ePropertyScriptClass);
+    if (s)
+      return s->GetCurrentValueAsRef();
+    return {};
+  }
+
+  ScriptedSymbolLocatorInterfaceSP GetInterface() const {
+    return m_interface_sp;
+  }
+
+  void SetInterface(ScriptedSymbolLocatorInterfaceSP interface_sp) {
+    m_interface_sp = interface_sp;
+  }
+
+  /// Look up a previously cached source file resolution result.
+  /// Returns true if a cached entry exists (even if the result is nullopt).
+  bool LookupSourceFileCache(const std::string &key,
+                             std::optional<FileSpec> &result) {
+    auto it = m_source_file_cache.find(key);
+    if (it != m_source_file_cache.end()) {
+      result = it->second;
+      return true;
+    }
+    return false;
+  }
+
+  void InsertSourceFileCache(const std::string &key,
+                             const std::optional<FileSpec> &result) {
+    m_source_file_cache[key] = result;
+  }
+
+private:
+  void ScriptClassChangedCallback() {
+    // Invalidate the cached interface and source file cache when the user
+    // changes the script class.
+    m_interface_sp.reset();
+    m_source_file_cache.clear();
+  }
+
+  ScriptedSymbolLocatorInterfaceSP m_interface_sp;
+  std::unordered_map<std::string, std::optional<FileSpec>> m_source_file_cache;
+};
+
+} // namespace
+
+static PluginProperties &GetGlobalPluginProperties() {
+  static PluginProperties g_settings;
+  return g_settings;
+}
+
+static ScriptedSymbolLocatorInterfaceSP GetOrCreateInterface() {
+  PluginProperties &props = GetGlobalPluginProperties();
+
+  llvm::StringRef class_name = props.GetScriptClassName();
+  if (class_name.empty())
+    return {};
+
+  // Return the cached interface if available.
+  auto interface_sp = props.GetInterface();
+  if (interface_sp)
+    return interface_sp;
+
+  // Find a debugger with a script interpreter.
+  DebuggerSP debugger_sp = Debugger::GetDebuggerAtIndex(0);
+  if (!debugger_sp)
+    return {};
+
+  ScriptInterpreter *interpreter = debugger_sp->GetScriptInterpreter();
+  if (!interpreter)
+    return {};
+
+  interface_sp = interpreter->CreateScriptedSymbolLocatorInterface();
+  if (!interface_sp)
+    return {};
+
+  // Create the Python script object from the user's class.
+  ExecutionContext exe_ctx;
+  StructuredData::DictionarySP args_sp;
+  auto obj_or_err =
+      interface_sp->CreatePluginObject(class_name, exe_ctx, args_sp);
+
+  if (!obj_or_err) {
+    Log *log = GetLog(LLDBLog::Symbols);
+    LLDB_LOG_ERROR(log, obj_or_err.takeError(),
+                   "SymbolLocatorScripted: failed to create Python object for "
+                   "class '{0}': {1}",
+                   class_name);
+    return {};
+  }
+
+  props.SetInterface(interface_sp);
+  return interface_sp;
+}
+
+SymbolLocatorScripted::SymbolLocatorScripted() : SymbolLocator() {}
+
+void SymbolLocatorScripted::Initialize() {
+  PluginManager::RegisterPlugin(
+      GetPluginNameStatic(), GetPluginDescriptionStatic(), CreateInstance,
+      LocateExecutableObjectFile, LocateExecutableSymbolFile,
+      DownloadObjectAndSymbolFile, nullptr, LocateSourceFile,
+      SymbolLocatorScripted::DebuggerInitialize);
+}
+
+void SymbolLocatorScripted::Terminate() {
+  GetGlobalPluginProperties().SetInterface(nullptr);
+  PluginManager::UnregisterPlugin(CreateInstance);
+}
+
+void SymbolLocatorScripted::DebuggerInitialize(Debugger &debugger) {
+  if (!PluginManager::GetSettingForSymbolLocatorPlugin(
+          debugger, PluginProperties::GetSettingName())) {
+    const bool is_global_setting = true;
+    PluginManager::CreateSettingForSymbolLocatorPlugin(
+        debugger, GetGlobalPluginProperties().GetValueProperties(),
+        "Properties for the Scripted Symbol Locator plug-in.",
+        is_global_setting);
+  }
+}
+
+llvm::StringRef SymbolLocatorScripted::GetPluginDescriptionStatic() {
+  return "Scripted symbol locator plug-in.";
+}
+
+SymbolLocator *SymbolLocatorScripted::CreateInstance() {
+  return new SymbolLocatorScripted();
+}
+
+std::optional<ModuleSpec> SymbolLocatorScripted::LocateExecutableObjectFile(
+    const ModuleSpec &module_spec) {
+  auto interface_sp = GetOrCreateInterface();
+  if (!interface_sp)
+    return {};
+  Status error;
+  auto result = interface_sp->LocateExecutableObjectFile(module_spec, error);
+  if (!error.Success()) {
+    Log *log = GetLog(LLDBLog::Symbols);
+    LLDB_LOG(log, "SymbolLocatorScripted: locate_executable_object_file "
+                  "failed: {0}",
+             error);
+  }
+  return result;
+}
+
+std::optional<FileSpec> SymbolLocatorScripted::LocateExecutableSymbolFile(
+    const ModuleSpec &module_spec, const FileSpecList &default_search_paths) {
+  auto interface_sp = GetOrCreateInterface();
+  if (!interface_sp)
+    return {};
+  Status error;
+  auto result = interface_sp->LocateExecutableSymbolFile(
+      module_spec, default_search_paths, error);
+  if (!error.Success()) {
+    Log *log = GetLog(LLDBLog::Symbols);
+    LLDB_LOG(log, "SymbolLocatorScripted: locate_executable_symbol_file "
+                  "failed: {0}",
+             error);
+  }
+  return result;
+}
+
+bool SymbolLocatorScripted::DownloadObjectAndSymbolFile(
+    ModuleSpec &module_spec, Status &error, bool force_lookup,
+    bool copy_executable) {
+  auto interface_sp = GetOrCreateInterface();
+  if (!interface_sp)
+    return false;
+  return interface_sp->DownloadObjectAndSymbolFile(module_spec, error,
+                                                    force_lookup,
+                                                    copy_executable);
+}
+
+std::optional<FileSpec> SymbolLocatorScripted::LocateSourceFile(
+    const lldb::ModuleSP &module_sp, const FileSpec &original_source_file) {
+  if (!module_sp)
+    return {};
+
+  PluginProperties &props = GetGlobalPluginProperties();
+
+  // Cache resolved source files to avoid repeated Python calls for the same
+  // (module, source_file) pair.
+  std::string cache_key =
+      module_sp->GetUUID().GetAsString() + ":" +
+      original_source_file.GetPath();
+
+  std::optional<FileSpec> cached;
+  if (props.LookupSourceFileCache(cache_key, cached))
+    return cached;
+
+  auto interface_sp = GetOrCreateInterface();
+  if (!interface_sp) {
+    props.InsertSourceFileCache(cache_key, std::nullopt);
+    return {};
+  }
+
+  Status error;
+  auto located =
+      interface_sp->LocateSourceFile(module_sp, original_source_file, error);
+
+  if (!error.Success()) {
+    Log *log = GetLog(LLDBLog::Symbols);
+    LLDB_LOG(log, "SymbolLocatorScripted: locate_source_file failed: {0}",
+             error);
+  }
+
+  props.InsertSourceFileCache(cache_key, located);
+
+  if (located) {
+    Log *log = GetLog(LLDBLog::Symbols);
+    LLDB_LOGF(log,
+              "SymbolLocatorScripted::%s: resolved source file '%s' to '%s'",
+              __FUNCTION__, original_source_file.GetPath().c_str(),
+              located->GetPath().c_str());
+  }
+  return located;
+}
diff --git a/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.h b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.h
new file mode 100644
index 0000000000000..845200c2c715f
--- /dev/null
+++ b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.h
@@ -0,0 +1,57 @@
+//===-- SymbolLocatorScripted.h --------------------------------*- C++ -*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_SOURCE_PLUGINS_SYMBOLLOCATOR_SCRIPTED_SYMBOLLOCATORSCRIPTED_H
+#define LLDB_SOURCE_PLUGINS_SYMBOLLOCATOR_SCRIPTED_SYMBOLLOCATORSCRIPTED_H
+
+#include "lldb/Core/Debugger.h"
+#include "lldb/Symbol/SymbolLocator.h"
+#include "lldb/lldb-private.h"
+
+namespace lldb_private {
+
+class SymbolLocatorScripted : public SymbolLocator {
+public:
+  SymbolLocatorScripted();
+
+  static void Initialize();
+  static void Terminate();
+  static void DebuggerInitialize(Debugger &debugger);
+
+  static llvm::StringRef GetPluginNameStatic() { return "scripted"; }
+  static llvm::StringRef GetPluginDescriptionStatic();
+
+  static lldb_private::SymbolLocator *CreateInstance();
+
+  /// PluginInterface protocol.
+  /// \{
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+  /// \}
+
+  // Locate the executable file given a module specification.
+  static std::optional<ModuleSpec>
+  LocateExecutableObjectFile(const ModuleSpec &module_spec);
+
+  // Locate the symbol file given a module specification.
+  static std::optional<FileSpec>
+  LocateExecutableSymbolFile(const ModuleSpec &module_spec,
+                             const FileSpecList &default_search_paths);
+
+  static bool DownloadObjectAndSymbolFile(ModuleSpec &module_spec,
+                                          Status &error, bool force_lookup,
+                                          bool copy_executable);
+
+  // Locate the source file given a module and original source file path.
+  static std::optional<FileSpec>
+  LocateSourceFile(const lldb::ModuleSP &module_sp,
+                   const FileSpec &original_source_file);
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_SYMBOLLOCATOR_SCRIPTED_SYMBOLLOCATORSCRIPTED_H
diff --git a/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScriptedProperties.td b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScriptedProperties.td
new file mode 100644
index 0000000000000..bfc612cb962b2
--- /dev/null
+++ b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScriptedProperties.td
@@ -0,0 +1,7 @@
+include "../../../../include/lldb/Core/PropertiesBase.td"
+
+let Definition = "symbollocatorscripted" in {
+  def ScriptClass : Property<"script-class", "String">,
+    DefaultStringValue<"">,
+    Desc<"The name of the Python class that implements the scripted symbol locator. The class should implement methods like locate_source_file(module_spec, original_source_file) to resolve source files.">;
+}
diff --git a/lldb/source/Symbol/LineEntry.cpp b/lldb/source/Symbol/LineEntry.cpp
index dcfbac8789863..d246b4f82efc8 100644
--- a/lldb/source/Symbol/LineEntry.cpp
+++ b/lldb/source/Symbol/LineEntry.cpp
@@ -7,6 +7,9 @@
 //===----------------------------------------------------------------------===//
 
 #include "lldb/Symbol/LineEntry.h"
+#include "lldb/Core/Address.h"
+#include "lldb/Core/Module.h"
+#include "lldb/Core/PluginManager.h"
 #include "lldb/Symbol/CompileUnit.h"
 #include "lldb/Target/Process.h"
 #include "lldb/Target/Target.h"
@@ -242,8 +245,25 @@ AddressRange LineEntry::GetSameLineContiguousAddressRange(
   return complete_line_range;
 }
 
-void LineEntry::ApplyFileMappings(lldb::TargetSP target_sp) {
+void LineEntry::ApplyFileMappings(lldb::TargetSP target_sp,
+                                  const Address &address) {
   if (target_sp) {
+    // Try to resolve the source file via SymbolLocator plugins (e.g.,
+    // ScriptedSymbolLocator). This allows users to fetch source files
+    // by build ID from remote servers.
+    // Use Address::GetModule() directly to avoid re-entering
+    // ResolveSymbolContextForAddress which would cause infinite recursion.
+    lldb::ModuleSP module_sp = address.GetModule();
+    if (module_sp) {
+      FileSpec resolved = PluginManager::LocateSourceFile(
+          module_sp, original_file_sp->GetSpecOnly());
+      if (resolved) {
+        original_file_sp = std::make_shared<SupportFile>(resolved);
+        file_sp = std::make_shared<SupportFile>(resolved);
+        return;
+      }
+    }
+
     // Apply any file remappings to our file.
     if (auto new_file_spec = target_sp->GetSourcePathMap().FindFile(
             original_file_sp->GetSpecOnly())) {
diff --git a/lldb/source/Target/StackFrame.cpp b/lldb/source/Target/StackFrame.cpp
index 340607e14abed..656e68a9dd511 100644
--- a/lldb/source/Target/StackFrame.cpp
+++ b/lldb/source/Target/StackFrame.cpp
@@ -412,7 +412,7 @@ StackFrame::GetSymbolContext(SymbolContextItem resolve_scope) {
         if ((resolved & eSymbolContextLineEntry) &&
             !m_sc.line_entry.IsValid()) {
           m_sc.line_entry = sc.line_entry;
-          m_sc.line_entry.ApplyFileMappings(m_sc.target_sp);
+          m_sc.line_entry.ApplyFileMappings(m_sc.target_sp, lookup_addr);
         }
       }
     } else {
diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp
index 4f4b06f30460b..a8649a0eb3ecd 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -562,7 +562,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx,
 
       while (unwind_sc.GetParentOfInlinedScope(
           curr_frame_address, next_frame_sc, next_frame_address)) {
-        next_frame_sc.line_entry.ApplyFileMappings(target_sp);
+        next_frame_sc.line_entry.ApplyFileMappings(target_sp, curr_frame_address);
         behaves_like_zeroth_frame = false;
         StackFrameSP frame_sp(new StackFrame(
             m_thread.shared_from_this(), m_frames.size(), idx,
diff --git a/lldb/source/Target/ThreadPlanStepRange.cpp b/lldb/source/Target/ThreadPlanStepRange.cpp
index 3a9deb6f5c6fd..6c0345d35430f 100644
--- a/lldb/source/Target/ThreadPlanStepRange.cpp
+++ b/lldb/source/Target/ThreadPlanStepRange.cpp
@@ -106,7 +106,7 @@ bool ThreadPlanStepRange::InRange() {
 
   size_t num_ranges = m_address_ranges.size();
   for (size_t i = 0; i < num_ranges; i++) {
-    ret_value = 
+    ret_value =
         m_address_ranges[i].ContainsLoadAddress(pc_load_addr, &GetTarget());
     if (ret_value)
       break;
@@ -340,7 +340,7 @@ bool ThreadPlanStepRange::SetNextBranchBreakpoint() {
 
   // clear the m_found_calls, we'll rediscover it for this range.
   m_found_calls = false;
-  
+
   lldb::addr_t cur_addr = GetThread().GetRegisterContext()->GetPC();
   // Find the current address in our address ranges, and fetch the disassembly
   // if we haven't already:
@@ -433,7 +433,7 @@ bool ThreadPlanStepRange::SetNextBranchBreakpoint() {
             top_most_line_entry.range = range;
             top_most_line_entry.file_sp = std::make_shared<SupportFile>();
             top_most_line_entry.ApplyFileMappings(
-                GetThread().CalculateTarget());
+                GetThread().CalculateTarget(), range.GetBaseAddress());
             if (!top_most_line_entry.file_sp->GetSpecOnly())
               top_most_line_entry.file_sp =
                   top_most_line_entry.original_file_sp;
@@ -551,7 +551,7 @@ bool ThreadPlanStepRange::IsPlanStale() {
       lldb::addr_t addr = GetThread().GetRegisterContext()->GetPC() - 1;
       size_t num_ranges = m_address_ranges.size();
       for (size_t i = 0; i < num_ranges; i++) {
-        bool in_range = 
+        bool in_range =
             m_address_ranges[i].ContainsLoadAddress(addr, &GetTarget());
         if (in_range) {
           SetPlanComplete();
diff --git a/lldb/test/API/functionalities/scripted_symbol_locator/Makefile b/lldb/test/API/functionalities/scripted_symbol_locator/Makefile
new file mode 100644
index 0000000000000..36728cd2e597b
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/Makefile
@@ -0,0 +1,4 @@
+C_SOURCES := main.c
+USE_SYSTEM_STDLIB := 1
+LD_EXTRAS := -Wl,--build-id
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/scripted_symbol_locator/TestScriptedSymbolLocator.py b/lldb/test/API/functionalities/scripted_symbol_locator/TestScriptedSymbolLocator.py
new file mode 100644
index 0000000000000..46ce53438c283
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/TestScriptedSymbolLocator.py
@@ -0,0 +1,160 @@
+"""
+Test the ScriptedSymbolLocator plugin for source file resolution.
+"""
+
+import os
+import shutil
+import tempfile
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class ScriptedSymbolLocatorTestCase(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def setUp(self):
+        TestBase.setUp(self)
+        self.main_source_file = lldb.SBFileSpec("main.c")
+
+    def import_locator(self):
+        self.runCmd(
+            "command script import "
+            + os.path.join(self.getSourceDir(), "source_locator.py")
+        )
+
+    def set_locator_class(self, class_name):
+        self.runCmd(
+            "settings set plugin.symbol-locator.scripted.script-class " + class_name
+        )
+
+    def clear_locator_class(self):
+        self.runCmd('settings set plugin.symbol-locator.scripted.script-class ""')
+
+    def script(self, expr):
+        """Execute a Python expression in LLDB's script interpreter and return
+        the result as a string."""
+        ret = lldb.SBCommandReturnObject()
+        self.dbg.GetCommandInterpreter().HandleCommand(
+            "script " + expr, ret
+        )
+        return ret.GetOutput().strip() if ret.Succeeded() else ""
+
+    @skipUnlessPlatform(["linux", "freebsd"])
+    def test_locate_source_file(self):
+        """Test that the scripted locator resolves source files and receives
+        an SBModule with a valid UUID."""
+        self.build()
+
+        # Copy main.c to a temp directory so the locator can "resolve" to it.
+        tmp_dir = tempfile.mkdtemp()
+        self.addTearDownHook(lambda: shutil.rmtree(tmp_dir))
+        shutil.copy(os.path.join(self.getSourceDir(), "main.c"), tmp_dir)
+
+        # Create the target BEFORE setting the script class, so module loading
+        # (which may run on worker threads) does not trigger the Python locator.
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target and target.IsValid(), VALID_TARGET)
+
+        # Now set up the scripted locator. LocateSourceFile will only be called
+        # from the main thread when we access a frame's line entry.
+        self.import_locator()
+        self.script(
+            "source_locator.SourceLocator.resolved_dir = '%s'" % tmp_dir
+        )
+        self.set_locator_class("source_locator.SourceLocator")
+        self.addTearDownHook(lambda: self.clear_locator_class())
+
+        bp = target.BreakpointCreateByName("func")
+        self.assertTrue(bp and bp.IsValid(), "Breakpoint is valid")
+        self.assertEqual(bp.GetNumLocations(), 1)
+
+        # Launch and stop at the breakpoint so ApplyFileMappings runs on
+        # the main thread via StackFrame::GetSymbolContext.
+        process = target.LaunchSimple(None, None, os.getcwd())
+        self.assertIsNotNone(process)
+        self.assertState(process.GetState(), lldb.eStateStopped)
+
+        thread = process.GetSelectedThread()
+        frame = thread.GetSelectedFrame()
+        line_entry = frame.GetLineEntry()
+        self.assertTrue(line_entry and line_entry.IsValid(), "Line entry is valid")
+        self.assertEqual(line_entry.GetFileSpec().GetFilename(), "main.c")
+
+        # Verify the resolved path points to our temp directory.
+        resolved_dir = line_entry.GetFileSpec().GetDirectory()
+        self.assertEqual(resolved_dir, tmp_dir)
+
+        # Verify the locator was called with a valid UUID by reading calls
+        # from LLDB's Python namespace.
+        calls_str = self.script("source_locator.SourceLocator.calls")
+        self.assertIn("main.c", calls_str, "Locator should have been called")
+
+        self.dbg.DeleteTarget(target)
+
+    @skipUnlessPlatform(["linux", "freebsd"])
+    def test_locate_source_file_none_fallthrough(self):
+        """Test that returning None falls through to normal LLDB resolution,
+        and that having no script class set also works normally."""
+        self.build()
+
+        # First: test with NoneLocator -- should fall through.
+        self.import_locator()
+        self.set_locator_class("source_locator.NoneLocator")
+        self.addTearDownHook(lambda: self.clear_locator_class())
+
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target and target.IsValid(), VALID_TARGET)
+
+        bp = target.BreakpointCreateByName("func")
+        self.assertTrue(bp and bp.IsValid(), "Breakpoint is valid")
+        self.assertEqual(bp.GetNumLocations(), 1)
+
+        loc = bp.GetLocationAtIndex(0)
+        line_entry = loc.GetAddress().GetLineEntry()
+        self.assertTrue(line_entry and line_entry.IsValid(), "Line entry is valid")
+        self.assertEqual(line_entry.GetFileSpec().GetFilename(), "main.c")
+
+        self.dbg.DeleteTarget(target)
+
+        # Second: test with no script class set -- should also work normally.
+        self.clear_locator_class()
+
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target and target.IsValid(), VALID_TARGET)
+
+        bp = target.BreakpointCreateByName("func")
+        self.assertTrue(bp and bp.IsValid(), "Breakpoint is valid")
+        self.assertEqual(bp.GetNumLocations(), 1)
+
+        loc = bp.GetLocationAtIndex(0)
+        line_entry = loc.GetAddress().GetLineEntry()
+        self.assertTrue(line_entry and line_entry.IsValid(), "Line entry is valid")
+        self.assertEqual(line_entry.GetFileSpec().GetFilename(), "main.c")
+
+        self.dbg.DeleteTarget(target)
+
+    @skipUnlessPlatform(["linux", "freebsd"])
+    def test_invalid_script_class(self):
+        """Test that an invalid script class name is handled gracefully
+        without crashing, and breakpoints still resolve."""
+        self.build()
+
+        self.set_locator_class("nonexistent_module.NonexistentClass")
+        self.addTearDownHook(lambda: self.clear_locator_class())
+
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target and target.IsValid(), VALID_TARGET)
+
+        # Should not crash -- breakpoint should still resolve via normal path.
+        bp = target.BreakpointCreateByName("func")
+        self.assertTrue(bp and bp.IsValid(), "Breakpoint is valid")
+        self.assertEqual(bp.GetNumLocations(), 1)
+
+        loc = bp.GetLocationAtIndex(0)
+        line_entry = loc.GetAddress().GetLineEntry()
+        self.assertTrue(line_entry and line_entry.IsValid(), "Line entry is valid")
+
+        self.dbg.DeleteTarget(target)
diff --git a/lldb/test/API/functionalities/scripted_symbol_locator/main.c b/lldb/test/API/functionalities/scripted_symbol_locator/main.c
new file mode 100644
index 0000000000000..beef030966265
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/main.c
@@ -0,0 +1,7 @@
+int func(int argc) {
+  return argc + 1; // break here
+}
+
+int main(int argc, const char *argv[]) {
+  return func(argc);
+}
diff --git a/lldb/test/API/functionalities/scripted_symbol_locator/source_locator.py b/lldb/test/API/functionalities/scripted_symbol_locator/source_locator.py
new file mode 100644
index 0000000000000..67e2a76de5ada
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/source_locator.py
@@ -0,0 +1,51 @@
+import os
+
+import lldb
+
+
+class SourceLocator:
+    """Test locator that records calls and returns a configured resolved path."""
+
+    calls = []
+    resolved_dir = None
+
+    def __init__(self, exe_ctx, args):
+        SourceLocator.calls = []
+
+    def locate_source_file(self, module, original_source_file):
+        uuid = module.GetUUIDString()
+        SourceLocator.calls.append((uuid, original_source_file))
+        if SourceLocator.resolved_dir:
+            basename = os.path.basename(original_source_file)
+            return os.path.join(SourceLocator.resolved_dir, basename)
+        return None
+
+    def locate_executable_object_file(self, module_spec):
+        return None
+
+    def locate_executable_symbol_file(self, module_spec, default_search_paths):
+        return None
+
+    def download_object_and_symbol_file(self, module_spec, force_lookup,
+                                        copy_executable):
+        return False
+
+
+class NoneLocator:
+    """Locator that always returns None."""
+
+    def __init__(self, exe_ctx, args):
+        pass
+
+    def locate_source_file(self, module, original_source_file):
+        return None
+
+    def locate_executable_object_file(self, module_spec):
+        return None
+
+    def locate_executable_symbol_file(self, module_spec, default_search_paths):
+        return None
+
+    def download_object_and_symbol_file(self, module_spec, force_lookup,
+                                        copy_executable):
+        return False



More information about the lldb-commits mailing list