[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 01:15:04 PST 2026


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

>From 21bbc191ee8d490c391611f6149941d40a882c6d 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          |  57 ++++
 .../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            |  22 +-
 .../Python/Interfaces/CMakeLists.txt          |   3 +-
 .../ScriptInterpreterPythonInterfaces.h       |   1 +
 .../Interfaces/ScriptedPythonInterface.h      |   4 +
 .../ScriptedSymbolLocatorPythonInterface.cpp  | 278 ++++++++++++++++++
 .../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        | 271 +++++++++++++++++
 .../Scripted/SymbolLocatorScripted.h          |  57 ++++
 .../SymbolLocatorScriptedProperties.td        |   7 +
 lldb/source/Symbol/LineEntry.cpp              |  22 +-
 lldb/source/Target/StackFrame.cpp             |   2 +-
 lldb/source/Target/StackFrameList.cpp         |   3 +-
 lldb/source/Target/ThreadPlanStepRange.cpp    |  10 +-
 .../scripted_symbol_locator/Makefile          |   4 +
 .../TestScriptedSymbolLocator.py              | 156 ++++++++++
 .../scripted_symbol_locator/main.c            |   5 +
 .../scripted_symbol_locator/source_locator.py |  53 ++++
 29 files changed, 1065 insertions(+), 17 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..a9edb23d164c0
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h
@@ -0,0 +1,57 @@
+//===-- 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..bbd669fd767d3 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -33,6 +33,7 @@
 #include "lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedPlatformInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedProcessInterface.h"
+#include "lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedThreadInterface.h"
 #include "lldb/Interpreter/ScriptObject.h"
 #include "lldb/Symbol/SymbolContext.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..5b8bcc7cc68ef 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,12 @@ 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 +1595,20 @@ 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..f0027bb7d386c
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.cpp
@@ -0,0 +1,278 @@
+//===-- 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..35b02a0e56c65
--- /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..34b46cf0b6f4c
--- /dev/null
+++ b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.cpp
@@ -0,0 +1,271 @@
+//===-- 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..b46916a9af35e 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -562,7 +562,8 @@ 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..a536d0ace7e53 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:
@@ -432,8 +432,8 @@ bool ThreadPlanStepRange::SetNextBranchBreakpoint() {
                 std::make_shared<SupportFile>(call_site_file_spec);
             top_most_line_entry.range = range;
             top_most_line_entry.file_sp = std::make_shared<SupportFile>();
-            top_most_line_entry.ApplyFileMappings(
-                GetThread().CalculateTarget());
+            top_most_line_entry.ApplyFileMappings(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..7bded9bdbab96
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/TestScriptedSymbolLocator.py
@@ -0,0 +1,156 @@
+"""
+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..71a79cd90f5b2
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/main.c
@@ -0,0 +1,5 @@
+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..0d314f3914766
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/source_locator.py
@@ -0,0 +1,53 @@
+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