[Lldb-commits] [lldb] [WIP][lldb] Add mechanism for auto-loading Python scripts from pre-configured paths (PR #187031)

Michael Buch via lldb-commits lldb-commits at lists.llvm.org
Wed Mar 18 06:15:05 PDT 2026


https://github.com/Michael137 updated https://github.com/llvm/llvm-project/pull/187031

>From b76f9ce3cb7562563787c1b5c8c5c57c5b5a5b7a Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Wed, 18 Mar 2026 09:31:45 +0000
Subject: [PATCH 1/2] [lldb][Platform][NFC] Move SanitizedScriptingModuleName
 into ScriptInterpreter

In preparation for https://github.com/llvm/llvm-project/pull/187031

The `SanitizedScriptingModuleName` will be re-used from `Platform` (in
addition to `PlatformDarwin` where it currently lives). To do that we'll
need to move it to a common place. `ScriptInterpreter` seems like the
most natural place for this to live.

I introduced a new virtual `GetSanitizedScriptingModuleName` that
`ScriptInterpreter`s can override in the future if they have their own
sanitization logic. I made the default implementation the one we've been
using for Python because that way the unit-tests that currently mock the
`ScriptInterpreter` don't need to copy the implementation.

Also made `WarnIfInvalidUnsanitizedScriptExists` a protected static
function on `Platform` because it didn't seem right for that to also
live in `ScriptInterpreter`.
---
 .../lldb/Interpreter/ScriptInterpreter.h      |  45 ++++++++
 lldb/include/lldb/Target/Platform.h           |   9 ++
 lldb/source/Interpreter/ScriptInterpreter.cpp |  25 +++++
 .../Platform/MacOSX/PlatformDarwin.cpp        | 103 ++----------------
 lldb/source/Target/Platform.cpp               |  31 ++++++
 5 files changed, 117 insertions(+), 96 deletions(-)

diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 557d73a415452..dac74cc8ab1da 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -572,6 +572,51 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  /// Holds an lldb_private::Module name and a "sanitized" version
+  /// of it for the purposes of loading a script of that name by
+  /// the relevant ScriptInterpreter.
+  ///
+  /// E.g., for Python the sanitized name can't include:
+  /// * Special characters: '-', ' ', '.'
+  /// * Python keywords
+  class SanitizedScriptingModuleName {
+  public:
+    SanitizedScriptingModuleName(std::string name, std::string sanitized_name,
+                                 std::string conflicting_keyword)
+        : m_original_name(std::move(name)),
+          m_sanitized_name(std::move(sanitized_name)),
+          m_conflicting_keyword(std::move(conflicting_keyword)) {}
+
+    /// Returns \c true if this name is a keyword in the associated scripting
+    /// language.
+    bool IsKeyword() const { return !m_conflicting_keyword.empty(); }
+
+    /// Returns \c true if the original name has been sanitized (i.e., required
+    /// changes).
+    bool RequiredSanitization() const {
+      return m_sanitized_name != m_original_name;
+    }
+
+    llvm::StringRef GetSanitizedName() const { return m_sanitized_name; }
+    llvm::StringRef GetOriginalName() const { return m_original_name; }
+    llvm::StringRef GetConflictingKeyword() const {
+      return m_conflicting_keyword;
+    }
+
+  private:
+    std::string m_original_name;
+    std::string m_sanitized_name;
+
+    /// If the m_sanitized_name conflicts with a keyword for the
+    /// ScriptInterpreter language associated with this
+    /// SanitizedScriptingModuleName, is set to the conflicting keyword. Empty
+    /// otherwise.
+    std::string m_conflicting_keyword;
+  };
+
+  virtual SanitizedScriptingModuleName
+  GetSanitizedScriptingModuleName(llvm::StringRef name);
+
   lldb::DataExtractorSP
   GetDataExtractorFromSBData(const lldb::SBData &data) const;
 
diff --git a/lldb/include/lldb/Target/Platform.h b/lldb/include/lldb/Target/Platform.h
index 1ba7516f0102c..1c30bc1003ec9 100644
--- a/lldb/include/lldb/Target/Platform.h
+++ b/lldb/include/lldb/Target/Platform.h
@@ -21,6 +21,7 @@
 #include "lldb/Core/UserSettingsController.h"
 #include "lldb/Host/File.h"
 #include "lldb/Interpreter/Options.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Target/StopInfo.h"
 #include "lldb/Utility/ArchSpec.h"
 #include "lldb/Utility/ConstString.h"
@@ -1060,6 +1061,14 @@ class Platform : public PluginInterface {
 
   virtual const char *GetCacheHostname();
 
+  /// If we did some replacements of reserved characters, and a
+  /// file with the untampered name exists, then warn the user
+  /// that the file as-is shall not be loaded.
+  static void WarnIfInvalidUnsanitizedScriptExists(
+      Stream &os,
+      const ScriptInterpreter::SanitizedScriptingModuleName &sanitized_name,
+      const FileSpec &original_fspec, const FileSpec &fspec);
+
 private:
   typedef std::function<Status(const ModuleSpec &)> ModuleResolver;
 
diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp
index 5e8478c2670bb..b00f4db528ce3 100644
--- a/lldb/source/Interpreter/ScriptInterpreter.cpp
+++ b/lldb/source/Interpreter/ScriptInterpreter.cpp
@@ -214,6 +214,31 @@ ScriptInterpreter::AcquireInterpreterLock() {
   return std::make_unique<ScriptInterpreterLocker>();
 }
 
+ScriptInterpreter::SanitizedScriptingModuleName
+ScriptInterpreter::GetSanitizedScriptingModuleName(llvm::StringRef name) {
+  std::string sanitized_name(name);
+  std::string conflicting_keyword;
+
+  // FIXME: for Python, don't allow certain characters in imported module
+  // filenames. Theoretically, different scripting languages may have
+  // different sets of forbidden tokens in filenames, and that should
+  // be dealt with by each ScriptInterpreter. For now, just replace dots
+  // with underscores. In order to support anything other than Python
+  // this will need to be reworked.
+  llvm::replace(sanitized_name, '.', '_');
+  llvm::replace(sanitized_name, ' ', '_');
+  llvm::replace(sanitized_name, '-', '_');
+  llvm::replace(sanitized_name, '+', 'x');
+
+  if (IsReservedWord(sanitized_name.c_str())) {
+    conflicting_keyword = sanitized_name;
+    sanitized_name.insert(sanitized_name.begin(), '_');
+  }
+
+  return ScriptInterpreter::SanitizedScriptingModuleName(
+      name.str(), std::move(sanitized_name), std::move(conflicting_keyword));
+}
+
 static void ReadThreadBytesReceived(void *baton, const void *src,
                                     size_t src_len) {
   if (src && src_len) {
diff --git a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
index 2f2eed3359a93..39abd9a8a1176 100644
--- a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
+++ b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
@@ -80,97 +80,6 @@ static Status ExceptionMaskValidator(const char *string, void *unused) {
   return {};
 }
 
-namespace {
-/// Holds an lldb_private::Module name and a "sanitized" version
-/// of it for the purposes of loading a script of that name by
-/// the relevant ScriptInterpreter.
-///
-/// E.g., for Python the sanitized name can't include:
-/// * Special characters: '-', ' ', '.'
-/// * Python keywords
-class SanitizedScriptingModuleName {
-public:
-  SanitizedScriptingModuleName(llvm::StringRef name,
-                               ScriptInterpreter &script_interpreter)
-      : m_original_name(name), m_sanitized_name(name.str()) {
-    // FIXME: for Python, don't allow certain characters in imported module
-    // filenames. Theoretically, different scripting languages may have
-    // different sets of forbidden tokens in filenames, and that should
-    // be dealt with by each ScriptInterpreter. For now, just replace dots
-    // with underscores. In order to support anything other than Python
-    // this will need to be reworked.
-    llvm::replace(m_sanitized_name, '.', '_');
-    llvm::replace(m_sanitized_name, ' ', '_');
-    llvm::replace(m_sanitized_name, '-', '_');
-    llvm::replace(m_sanitized_name, '+', 'x');
-
-    if (script_interpreter.IsReservedWord(m_sanitized_name.c_str())) {
-      m_conflicting_keyword = m_sanitized_name;
-      m_sanitized_name.insert(m_sanitized_name.begin(), '_');
-    }
-  }
-
-  /// Returns \c true if this name is a keyword in the associated scripting
-  /// language.
-  bool IsKeyword() const { return !m_conflicting_keyword.empty(); }
-
-  /// Returns \c true if the original name has been sanitized (i.e., required
-  /// changes).
-  bool RequiredSanitization() const {
-    return m_sanitized_name != m_original_name;
-  }
-
-  llvm::StringRef GetSanitizedName() const { return m_sanitized_name; }
-  llvm::StringRef GetOriginalName() const { return m_original_name; }
-  llvm::StringRef GetConflictingKeyword() const {
-    return m_conflicting_keyword;
-  }
-
-  /// If we did some replacements of reserved characters, and a
-  /// file with the untampered name exists, then warn the user
-  /// that the file as-is shall not be loaded.
-  void WarnIfInvalidUnsanitizedScriptExists(Stream &os,
-                                            const FileSpec &original_fspec,
-                                            const FileSpec &fspec) const {
-    if (!RequiredSanitization())
-      return;
-
-    // Path to unsanitized script name doesn't exist. Nothing to warn about.
-    if (!FileSystem::Instance().Exists(original_fspec))
-      return;
-
-    std::string reason_for_complaint =
-        IsKeyword() ? llvm::formatv("conflicts with the keyword '{0}'",
-                                    GetConflictingKeyword())
-                          .str()
-                    : "contains reserved characters";
-
-    if (FileSystem::Instance().Exists(fspec))
-       os.Format(
-            "debug script '{0}' cannot be loaded because '{1}' {2}. "
-            "Ignoring '{1}' and loading '{3}' instead.\n",
-            original_fspec.GetPath(), original_fspec.GetFilename(),
-            std::move(reason_for_complaint), fspec.GetFilename());
-    else
-      os.Format(
-            "debug script '{0}' cannot be loaded because '{1}' {2}. "
-            "If you intend to have this script loaded, please rename it to "
-            "'{3}' and retry.\n",
-            original_fspec.GetPath(), original_fspec.GetFilename(),
-            std::move(reason_for_complaint), fspec.GetFilename());
-  }
-
-private:
-  llvm::StringRef m_original_name;
-  std::string m_sanitized_name;
-
-  /// If the m_sanitized_name conflicts with a keyword for the ScriptInterpreter
-  /// language associated with this SanitizedScriptingModuleName, is set to the
-  /// conflicting keyword. Empty otherwise.
-  std::string m_conflicting_keyword;
-};
-} // namespace
-
 /// Destructor.
 ///
 /// The destructor is virtual since this class is designed to be
@@ -297,9 +206,11 @@ FileSpecList PlatformDarwin::LocateExecutableScriptingResourcesFromDSYM(
 
   FileSpecList file_list;
   while (module_spec.GetFilename()) {
-    SanitizedScriptingModuleName sanitized_name(
-        module_spec.GetFilename().GetStringRef(),
-        *target.GetDebugger().GetScriptInterpreter());
+    ScriptInterpreter::SanitizedScriptingModuleName sanitized_name =
+        target.GetDebugger()
+            .GetScriptInterpreter()
+            ->GetSanitizedScriptingModuleName(
+                module_spec.GetFilename().GetStringRef());
 
     StreamString path_string;
     StreamString original_path_string;
@@ -319,8 +230,8 @@ FileSpecList PlatformDarwin::LocateExecutableScriptingResourcesFromDSYM(
     FileSpec orig_script_fspec(original_path_string.GetString());
     FileSystem::Instance().Resolve(orig_script_fspec);
 
-    sanitized_name.WarnIfInvalidUnsanitizedScriptExists(
-        feedback_stream, orig_script_fspec, script_fspec);
+    WarnIfInvalidUnsanitizedScriptExists(feedback_stream, sanitized_name,
+                                         orig_script_fspec, script_fspec);
 
     if (FileSystem::Instance().Exists(script_fspec)) {
       file_list.Append(script_fspec);
diff --git a/lldb/source/Target/Platform.cpp b/lldb/source/Target/Platform.cpp
index 647f8389dc41b..e159632a446fd 100644
--- a/lldb/source/Target/Platform.cpp
+++ b/lldb/source/Target/Platform.cpp
@@ -2111,6 +2111,37 @@ Platform::LocateModuleCallback Platform::GetLocateModuleCallback() const {
   return m_locate_module_callback;
 }
 
+void Platform::WarnIfInvalidUnsanitizedScriptExists(
+    Stream &os,
+    const ScriptInterpreter::SanitizedScriptingModuleName &sanitized_name,
+    const FileSpec &original_fspec, const FileSpec &fspec) {
+  if (!sanitized_name.RequiredSanitization())
+    return;
+
+  // Path to unsanitized script name doesn't exist. Nothing to warn about.
+  if (!FileSystem::Instance().Exists(original_fspec))
+    return;
+
+  std::string reason_for_complaint =
+      sanitized_name.IsKeyword()
+          ? llvm::formatv("conflicts with the keyword '{0}'",
+                          sanitized_name.GetConflictingKeyword())
+                .str()
+          : "contains reserved characters";
+
+  if (FileSystem::Instance().Exists(fspec))
+    os.Format("debug script '{0}' cannot be loaded because '{1}' {2}. "
+              "Ignoring '{1}' and loading '{3}' instead.\n",
+              original_fspec.GetPath(), original_fspec.GetFilename(),
+              std::move(reason_for_complaint), fspec.GetFilename());
+  else
+    os.Format("debug script '{0}' cannot be loaded because '{1}' {2}. "
+              "If you intend to have this script loaded, please rename it to "
+              "'{3}' and retry.\n",
+              original_fspec.GetPath(), original_fspec.GetFilename(),
+              std::move(reason_for_complaint), fspec.GetFilename());
+}
+
 PlatformSP PlatformList::GetOrCreate(llvm::StringRef name) {
   std::lock_guard<std::recursive_mutex> guard(m_mutex);
   for (const PlatformSP &platform_sp : m_platforms) {

>From 4065e8126ed68540d93320a20ad2e442638d5c79 Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Tue, 10 Mar 2026 14:17:59 +0000
Subject: [PATCH 2/2] [lldb] Add mechanism for auto-loading Python scripts from
 pre-configured paths

Depends on:
* https://github.com/llvm/llvm-project/pull/187229

(only last commit is relevant)

This patch implements the base infrastructure described in this [RFC re. Moving libc++ data-formatters out of LLDB](https://discourse.llvm.org/t/rfc-lldb-moving-libc-data-formatters-out-of-lldb/89591)

The intention is to provide vendors with a way to pre-configure a set of paths that LLDB can automatically ingest formatter scripts from.

Three main changes
---
 lldb/include/lldb/Core/Debugger.h             |   5 +
 lldb/include/lldb/Target/Platform.h           |  17 +-
 lldb/source/Core/CMakeLists.txt               |  18 +
 lldb/source/Core/CoreProperties.td            |   4 +
 lldb/source/Core/Debugger.cpp                 |  51 ++
 lldb/source/Core/Module.cpp                   |   3 +
 lldb/source/Core/SafeAutoloadPaths.inc.in     |   1 +
 .../Platform/MacOSX/PlatformDarwin.cpp        |   3 +-
 .../Plugins/Platform/MacOSX/PlatformDarwin.h  |   4 +-
 lldb/source/Target/Platform.cpp               |  73 ++-
 lldb/source/Target/TargetProperties.td        |   4 +
 .../Darwin/dsym-takes-priority.test           |  31 ++
 .../safe-path-fallback-no-dsym-script.test    |  24 +
 .../SafeAutoLoad/UNIX/basic-load.test         |  24 +
 .../SafeAutoLoad/UNIX/last-path-wins.test     |  31 ++
 .../UNIX/no-match-nested-dir.test             |  24 +
 .../UNIX/no-match-wrong-dirname.test          |  24 +
 .../UNIX/no-match-wrong-scriptname.test       |  24 +
 .../UNIX/no-paths-configured.test             |  22 +
 .../UNIX/special-chars-sanitized.test         |  24 +
 .../SafeAutoLoad/UNIX/submodule-import.test   |  30 ++
 .../SafeAutoLoad/UNIX/subpackage-import.test  |  30 ++
 lldb/unittests/Platform/PlatformTest.cpp      | 471 ++++++++++++++++++
 23 files changed, 935 insertions(+), 7 deletions(-)
 create mode 100644 lldb/source/Core/SafeAutoloadPaths.inc.in
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/Darwin/dsym-takes-priority.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/Darwin/safe-path-fallback-no-dsym-script.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/basic-load.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/last-path-wins.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-nested-dir.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-dirname.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-scriptname.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-paths-configured.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/special-chars-sanitized.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/submodule-import.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/subpackage-import.test

diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h
index 96f586a6f1989..e8f830337d26a 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -76,6 +76,9 @@ struct TestingProperties : public Properties {
   TestingProperties();
   bool GetInjectVarLocListError() const;
   static TestingProperties &GetGlobalTestingProperties();
+  void SetSafeAutoLoadPaths(FileSpecList paths);
+  void AppendSafeAutoLoadPaths(FileSpec path);
+  FileSpecList GetSafeAutoLoadPaths() const;
 };
 #endif
 
@@ -131,6 +134,8 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
   static void AssertCallback(llvm::StringRef message, llvm::StringRef backtrace,
                              llvm::StringRef prompt);
 
+  static FileSpecList GetSafeAutoLoadPaths();
+
   void Clear();
 
   void DispatchClientTelemetry(const lldb_private::StructuredDataImpl &entry);
diff --git a/lldb/include/lldb/Target/Platform.h b/lldb/include/lldb/Target/Platform.h
index 1c30bc1003ec9..d150cc35a1bd1 100644
--- a/lldb/include/lldb/Target/Platform.h
+++ b/lldb/include/lldb/Target/Platform.h
@@ -275,9 +275,22 @@ class Platform : public PluginInterface {
   //
   // Locating the file should happen only on the local computer or using the
   // current computers global settings.
+  FileSpecList LocateExecutableScriptingResources(Target *target,
+                                                  Module &module,
+                                                  Stream &feedback_stream);
+
   virtual FileSpecList
-  LocateExecutableScriptingResources(Target *target, Module &module,
-                                     Stream &feedback_stream);
+  LocateExecutableScriptingResourcesImpl(Target *target, Module &module,
+                                         Stream &feedback_stream);
+
+  /// Helper function for \c LocateExecutableScriptingResources
+  /// which gathers FileSpecs for executable scripts from
+  /// pre-configured "safe" auto-load paths.
+  ///
+  /// Looks for a script at:
+  ///   \c <safe-path>/<module-name>/<module-name>.py
+  static FileSpecList LocateExecutableScriptingResourcesFromSafePaths(
+      Stream &feedback_stream, FileSpec module_spec, const Target &target);
 
   /// \param[in] module_spec
   ///     The ModuleSpec of a binary to find.
diff --git a/lldb/source/Core/CMakeLists.txt b/lldb/source/Core/CMakeLists.txt
index df35bd5c025f3..8770039ae412e 100644
--- a/lldb/source/Core/CMakeLists.txt
+++ b/lldb/source/Core/CMakeLists.txt
@@ -85,6 +85,24 @@ add_lldb_library(lldbCore NO_PLUGIN_DEPENDENCIES
     clangDriver
   )
 
+set(LLDB_SAFE_AUTO_LOAD_PATHS "" CACHE STRING
+    "Semicolon-separated list of paths that LLDB will automatically load scripting resources from.")
+
+# Turn list of paths into a comma-separated list to be #include'd
+# into a C-array.
+set(_entries "")
+foreach(path IN LISTS LLDB_SAFE_AUTO_LOAD_PATHS)
+  string(REPLACE "\\" "\\\\" escaped_path "${path}")
+  list(APPEND _entries "\"${escaped_path}\",")
+endforeach()
+string(JOIN "\n" SAFE_PATH_ENTRIES "${_entries}")
+
+configure_file(
+  ${CMAKE_CURRENT_SOURCE_DIR}/SafeAutoloadPaths.inc.in
+  ${CMAKE_CURRENT_BINARY_DIR}/SafeAutoloadPaths.inc
+  @ONLY
+)
+
 add_dependencies(lldbCore
   LLDBCorePropertiesGen
   LLDBCorePropertiesEnumGen)
diff --git a/lldb/source/Core/CoreProperties.td b/lldb/source/Core/CoreProperties.td
index 03326b130e067..8cea0931868aa 100644
--- a/lldb/source/Core/CoreProperties.td
+++ b/lldb/source/Core/CoreProperties.td
@@ -60,6 +60,10 @@ let Definition = "testing", Path = "testing" in {
         Global,
         DefaultFalse,
         Desc<"Used for testing LLDB only. Hide locations of local variables.">;
+  def SafeAutoloadPaths : Property<"safe-auto-load-paths", "FileSpecList">,
+                          DefaultStringValue<"">,
+                          Desc<"List of paths that LLDB will automatically "
+                               "load scripting resources from.">;
 }
 #endif
 
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index fc099891443df..3d4bd5b55b434 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -32,6 +32,7 @@
 #include "lldb/Interpreter/CommandInterpreter.h"
 #include "lldb/Interpreter/CommandReturnObject.h"
 #include "lldb/Interpreter/OptionValue.h"
+#include "lldb/Interpreter/OptionValueFileSpecList.h"
 #include "lldb/Interpreter/OptionValueLanguage.h"
 #include "lldb/Interpreter/OptionValueProperties.h"
 #include "lldb/Interpreter/OptionValueSInt64.h"
@@ -212,6 +213,27 @@ enum {
 };
 #endif
 
+static const FileSpecList &GetDefaultSafeAutoLoadPaths() {
+  static const FileSpecList sSafePaths = [] {
+    // FIXME: in c++20 this could be a std::array (with CTAD deduced size)
+    // and we could statically assert that all members are non-empty.
+    const llvm::SmallVector<llvm::StringRef> kVendorSafePaths = {
+#include "SafeAutoloadPaths.inc"
+    };
+    FileSpecList fspecs;
+    for (auto path : kVendorSafePaths) {
+      assert(!path.empty());
+      LLDB_LOG(GetLog(SystemLog::System), "Safe auto-load path configured: {0}",
+               path);
+      fspecs.EmplaceBack(path);
+    }
+
+    return fspecs;
+  }();
+
+  return sSafePaths;
+}
+
 #ifndef NDEBUG
 TestingProperties::TestingProperties() {
   m_collection_sp = std::make_shared<OptionValueProperties>("testing");
@@ -228,6 +250,27 @@ TestingProperties &TestingProperties::GetGlobalTestingProperties() {
   static TestingProperties g_testing_properties;
   return g_testing_properties;
 }
+
+void TestingProperties::SetSafeAutoLoadPaths(FileSpecList paths) {
+  const uint32_t idx = ePropertySafeAutoloadPaths;
+  OptionValueFileSpecList *option_value =
+      m_collection_sp->GetPropertyAtIndexAsOptionValueFileSpecList(idx);
+  assert(option_value);
+  option_value->SetCurrentValue(std::move(paths));
+}
+
+void TestingProperties::AppendSafeAutoLoadPaths(FileSpec path) {
+  const uint32_t idx = ePropertySafeAutoloadPaths;
+  OptionValueFileSpecList *option_value =
+      m_collection_sp->GetPropertyAtIndexAsOptionValueFileSpecList(idx);
+  assert(option_value);
+  option_value->AppendCurrentValue(path);
+}
+
+FileSpecList TestingProperties::GetSafeAutoLoadPaths() const {
+  const uint32_t idx = ePropertySafeAutoloadPaths;
+  return GetPropertyAtIndexAs<FileSpecList>(idx, GetDefaultSafeAutoLoadPaths());
+}
 #endif
 
 LoadPluginCallbackType Debugger::g_load_plugin_callback = nullptr;
@@ -2547,3 +2590,11 @@ StructuredData::DictionarySP Debugger::GetBuildConfiguration() {
   AddLLVMTargets(*config_up);
   return config_up;
 }
+
+FileSpecList Debugger::GetSafeAutoLoadPaths() {
+#ifndef NDEBUG
+  return TestingProperties::GetGlobalTestingProperties().GetSafeAutoLoadPaths();
+#else
+  return GetDefaultSafeAutoLoadPaths();
+#endif
+}
diff --git a/lldb/source/Core/Module.cpp b/lldb/source/Core/Module.cpp
index bbab89ff8db4f..eb0345158dd2a 100644
--- a/lldb/source/Core/Module.cpp
+++ b/lldb/source/Core/Module.cpp
@@ -1489,6 +1489,9 @@ To run all discovered debug scripts in this session:
       return false;
     }
 
+    LLDB_LOG(GetLog(LLDBLog::Modules), "Auto-loading {0}",
+             scripting_fspec.GetPath());
+
     StreamString scripting_stream;
     scripting_fspec.Dump(scripting_stream.AsRawOstream());
     LoadScriptOptions options;
diff --git a/lldb/source/Core/SafeAutoloadPaths.inc.in b/lldb/source/Core/SafeAutoloadPaths.inc.in
new file mode 100644
index 0000000000000..4312239c10a63
--- /dev/null
+++ b/lldb/source/Core/SafeAutoloadPaths.inc.in
@@ -0,0 +1 @@
+ at SAFE_PATH_ENTRIES@
diff --git a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
index 39abd9a8a1176..960616b2fef1c 100644
--- a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
+++ b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
@@ -251,7 +251,7 @@ FileSpecList PlatformDarwin::LocateExecutableScriptingResourcesFromDSYM(
   return file_list;
 }
 
-FileSpecList PlatformDarwin::LocateExecutableScriptingResources(
+FileSpecList PlatformDarwin::LocateExecutableScriptingResourcesImpl(
     Target *target, Module &module, Stream &feedback_stream) {
   if (!target)
     return {};
@@ -268,7 +268,6 @@ FileSpecList PlatformDarwin::LocateExecutableScriptingResources(
   // stripped while leaving "this.binary.file" as-is.
 
   const FileSpec &module_spec = module.GetFileSpec();
-
   if (!module_spec)
     return {};
 
diff --git a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h
index e884bcba5c2cc..f17636e8b2e42 100644
--- a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h
+++ b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h
@@ -68,8 +68,8 @@ class PlatformDarwin : public PlatformPOSIX {
                            FileSpec &sym_file) override;
 
   FileSpecList
-  LocateExecutableScriptingResources(Target *target, Module &module,
-                                     Stream &feedback_stream) override;
+  LocateExecutableScriptingResourcesImpl(Target *target, Module &module_spec,
+                                         Stream &feedback_stream) override;
 
   Status GetSharedModule(const ModuleSpec &module_spec, Process *process,
                          lldb::ModuleSP &module_sp,
diff --git a/lldb/source/Target/Platform.cpp b/lldb/source/Target/Platform.cpp
index e159632a446fd..c0933b7e78f40 100644
--- a/lldb/source/Target/Platform.cpp
+++ b/lldb/source/Target/Platform.cpp
@@ -27,6 +27,7 @@
 #include "lldb/Interpreter/OptionValueFileSpec.h"
 #include "lldb/Interpreter/OptionValueProperties.h"
 #include "lldb/Interpreter/Property.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Symbol/ObjectFile.h"
 #include "lldb/Target/ModuleCache.h"
 #include "lldb/Target/Platform.h"
@@ -41,6 +42,7 @@
 #include "lldb/Utility/StructuredData.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/FileSystem.h"
+#include "llvm/Support/FormatVariadic.h"
 #include "llvm/Support/Path.h"
 
 // Define these constants from POSIX mman.h rather than include the file so
@@ -155,10 +157,79 @@ Status Platform::GetFileWithUUID(const FileSpec &platform_file,
   return Status();
 }
 
+FileSpecList Platform::LocateExecutableScriptingResourcesFromSafePaths(
+    Stream &feedback_stream, FileSpec module_spec, const Target &target) {
+  assert(module_spec);
+  assert(target.GetDebugger().GetScriptInterpreter());
+
+  // For now only Python scripts supported for auto-loading.
+  if (target.GetDebugger().GetScriptLanguage() != eScriptLanguagePython)
+    return {};
+
+  ScriptInterpreter::SanitizedScriptingModuleName sanitized_name =
+      target.GetDebugger()
+          .GetScriptInterpreter()
+          ->GetSanitizedScriptingModuleName(
+              module_spec.GetFileNameStrippingExtension().GetStringRef());
+
+  FileSpecList file_list;
+  FileSpecList paths = Debugger::GetSafeAutoLoadPaths();
+
+  // Iterate in reverse so we consider the latest appended path first.
+  for (FileSpec path : llvm::reverse(paths)) {
+    path.AppendPathComponent(sanitized_name.GetOriginalName());
+
+    if (!FileSystem::Instance().Exists(path))
+      continue;
+
+    FileSpec script_fspec = path;
+    script_fspec.AppendPathComponent(
+        llvm::formatv("{0}.py", sanitized_name.GetSanitizedName()).str());
+    FileSystem::Instance().Resolve(script_fspec);
+
+    FileSpec orig_script_fspec = path;
+    orig_script_fspec.AppendPathComponent(
+        llvm::formatv("{0}.py", sanitized_name.GetOriginalName()).str());
+    FileSystem::Instance().Resolve(orig_script_fspec);
+
+    WarnIfInvalidUnsanitizedScriptExists(feedback_stream, sanitized_name,
+                                         orig_script_fspec, script_fspec);
+
+    if (FileSystem::Instance().Exists(script_fspec))
+      file_list.Append(script_fspec);
+
+    // If we successfully found a directory in a safe auto-load path
+    // stop looking at any other paths.
+    break;
+  }
+
+  return file_list;
+}
+
+FileSpecList
+Platform::LocateExecutableScriptingResourcesImpl(Target *target, Module &module,
+                                                 Stream &feedback_stream) {
+  return {};
+}
+
 FileSpecList
 Platform::LocateExecutableScriptingResources(Target *target, Module &module,
                                              Stream &feedback_stream) {
-  return FileSpecList();
+  if (!target)
+    return {};
+
+  // Give derived platforms a chance to locate scripting resources.
+  if (FileSpecList fspecs = LocateExecutableScriptingResourcesImpl(
+          target, module, feedback_stream);
+      !fspecs.IsEmpty())
+    return fspecs;
+
+  const FileSpec &module_spec = module.GetFileSpec();
+  if (!module_spec)
+    return {};
+
+  return LocateExecutableScriptingResourcesFromSafePaths(feedback_stream,
+                                                         module_spec, *target);
 }
 
 Status Platform::GetSharedModule(
diff --git a/lldb/source/Target/TargetProperties.td b/lldb/source/Target/TargetProperties.td
index 2361314d506ac..a4e18f5b59eea 100644
--- a/lldb/source/Target/TargetProperties.td
+++ b/lldb/source/Target/TargetProperties.td
@@ -180,6 +180,10 @@ let Definition = "target", Path = "target" in {
     DefaultEnumValue<"eLoadScriptFromSymFileWarn">,
     EnumValues<"OptionEnumValues(g_load_script_from_sym_file_values)">,
     Desc<"Allow LLDB to load scripting resources embedded in symbol files when available.">;
+  def SafeLoadPaths
+      : Property<"safe-load-paths", "String">,
+        DefaultStringValue<"">,
+        Desc<"Paths that LLDB can auto-load scripting resources from.">;
   def LoadCWDlldbinitFile: Property<"load-cwd-lldbinit", "Enum">,
     DefaultEnumValue<"eLoadCWDlldbinitWarn">,
     EnumValues<"OptionEnumValues(g_load_cwd_lldbinit_values)">,
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/dsym-takes-priority.test b/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/dsym-takes-priority.test
new file mode 100644
index 0000000000000..60b875a643c88
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/dsym-takes-priority.test
@@ -0,0 +1,31 @@
+# REQUIRES: python, asserts, system-darwin
+#
+# Test that when both a dSYM script and a safe-path script exist,
+# the dSYM script takes priority.
+
+# RUN: split-file %s %t
+# RUN: %clang_host -g %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/TestModule.out.dSYM/Contents/Resources/Python
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/dsym_script.py %t/TestModule.out.dSYM/Contents/Resources/Python/TestModule.py
+# RUN: cp %t/safe_script.py %t/safe-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s --implicit-check-not=SAFE_PATH_SCRIPT
+
+# CHECK: DSYM_SCRIPT
+
+#--- main.c
+int main() { return 0; }
+
+#--- dsym_script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("DSYM_SCRIPT", file=sys.stderr)
+
+#--- safe_script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SAFE_PATH_SCRIPT", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/safe-path-fallback-no-dsym-script.test b/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/safe-path-fallback-no-dsym-script.test
new file mode 100644
index 0000000000000..b00484ecdbe6d
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/safe-path-fallback-no-dsym-script.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, system-darwin
+#
+# Test that when a dSYM exists but has no Python script inside,
+# the safe-path script is loaded as a fallback.
+
+# RUN: split-file %s %t
+# RUN: %clang_host -g %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK: SAFE_PATH_FALLBACK
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SAFE_PATH_FALLBACK", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/basic-load.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/basic-load.test
new file mode 100644
index 0000000000000..0ee57c14130ae
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/basic-load.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+
+# Test that LLDB auto-loads <safe-path>/<module-name>/<module-name>.py on
+# module load.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK: AUTOLOAD_SUCCESS
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("AUTOLOAD_SUCCESS", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/last-path-wins.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/last-path-wins.test
new file mode 100644
index 0000000000000..160b771fda024
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/last-path-wins.test
@@ -0,0 +1,31 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that the last appended safe-auto-load path takes priority.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/first-path/TestModule
+# RUN: mkdir -p %t/second-path/TestModule
+
+# RUN: cp %t/first.py %t/first-path/TestModule/TestModule.py
+# RUN: cp %t/second.py %t/second-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/first-path' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/second-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s --implicit-check-not=FIRST_PATH
+
+# CHECK: SECOND_PATH
+
+#--- main.c
+int main() { return 0; }
+
+#--- first.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("FIRST_PATH", file=sys.stderr)
+
+#--- second.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SECOND_PATH", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-nested-dir.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-nested-dir.test
new file mode 100644
index 0000000000000..aeee2e8cd473d
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-nested-dir.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that LLDB does not load scripts from nested subdirectories inside the
+# module directory.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule/nested
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/nested/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK-NOT: SHOULD_NOT_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SHOULD_NOT_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-dirname.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-dirname.test
new file mode 100644
index 0000000000000..345613a378a19
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-dirname.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that LLDB does not load scripts when the directory name doesn't match
+# the module name.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/WrongName
+
+# RUN: cp %t/script.py %t/safe-path/WrongName/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK-NOT: SHOULD_NOT_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SHOULD_NOT_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-scriptname.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-scriptname.test
new file mode 100644
index 0000000000000..f7fb13878d9a8
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-scriptname.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that LLDB does not load scripts when the script filename inside the
+# module directory doesn't match <module-name>.py.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/other.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK-NOT: SHOULD_NOT_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SHOULD_NOT_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-paths-configured.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-paths-configured.test
new file mode 100644
index 0000000000000..14e713aee5caa
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-paths-configured.test
@@ -0,0 +1,22 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that nothing loads when no safe-auto-load-paths are configured.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK-NOT: SHOULD_NOT_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SHOULD_NOT_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/special-chars-sanitized.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/special-chars-sanitized.test
new file mode 100644
index 0000000000000..c2ad4a96c1bd4
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/special-chars-sanitized.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that a module name with special characters (dashes, dots, spaces) is
+# sanitized and the sanitized <module-name>.py is loaded.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o "%t/Test- Module.1.out"
+# RUN: mkdir -p "%t/safe-path/Test- Module.1"
+
+# RUN: cp %t/script.py "%t/safe-path/Test- Module.1/Test__Module_1.py"
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create "%t/Test- Module.1.out"' 2>&1 | FileCheck %s
+
+# CHECK: SANITIZED_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SANITIZED_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/submodule-import.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/submodule-import.test
new file mode 100644
index 0000000000000..a6fa1856aa588
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/submodule-import.test
@@ -0,0 +1,30 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that an auto-loaded script can import a sibling Python submodule
+# from the same directory.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: cp %t/helper.py %t/safe-path/TestModule/helper.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK: HELPER_LOADED
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+import helper
+def __lldb_init_module(debugger, internal_dict):
+    print(helper.get_marker(), file=sys.stderr)
+
+#--- helper.py
+def get_marker():
+    return "HELPER_LOADED"
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/subpackage-import.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/subpackage-import.test
new file mode 100644
index 0000000000000..754d6482b4d87
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/subpackage-import.test
@@ -0,0 +1,30 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that an auto-loaded script can import a sibling Python package
+# (a subdirectory with an __init__.py).
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule/helpers
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: cp %t/init.py %t/safe-path/TestModule/helpers/__init__.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK: PACKAGE_LOADED
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+import helpers
+def __lldb_init_module(debugger, internal_dict):
+    print(helpers.get_marker(), file=sys.stderr)
+
+#--- init.py
+def get_marker():
+    return "PACKAGE_LOADED"
diff --git a/lldb/unittests/Platform/PlatformTest.cpp b/lldb/unittests/Platform/PlatformTest.cpp
index 6299197791fc4..be20baf5d1f33 100644
--- a/lldb/unittests/Platform/PlatformTest.cpp
+++ b/lldb/unittests/Platform/PlatformTest.cpp
@@ -8,8 +8,12 @@
 
 #include "gtest/gtest.h"
 
+#include "TestUtils.h"
+
 #include "Plugins/Platform/POSIX/PlatformPOSIX.h"
 #include "TestingSupport/SubsystemRAII.h"
+#include "TestingSupport/TestUtilities.h"
+#include "lldb/Core/Debugger.h"
 #include "lldb/Core/PluginManager.h"
 #include "lldb/Host/FileSystem.h"
 #include "lldb/Host/HostInfo.h"
@@ -163,3 +167,470 @@ TEST_F(PlatformTest, CreateUnknown) {
   ASSERT_EQ(list.Create("unknown-platform-name"), nullptr);
   ASSERT_EQ(list.GetOrCreate("dummy"), nullptr);
 }
+
+struct PlatformLocateSafePathTest : public PlatformTest {
+protected:
+  void SetUp() override {
+    std::call_once(TestUtilities::g_debugger_initialize_flag,
+                   []() { Debugger::Initialize(nullptr); });
+
+    ArchSpec arch("x86_64-apple-macosx-");
+    m_platform_sp = std::make_shared<PlatformArm>();
+    Platform::SetHostPlatform(m_platform_sp);
+
+    m_debugger_sp = Debugger::CreateInstance();
+
+    m_debugger_sp->GetTargetList().CreateTarget(*m_debugger_sp, "", arch,
+                                                lldb_private::eLoadDependentsNo,
+                                                m_platform_sp, m_target_sp);
+
+    ASSERT_TRUE(m_target_sp);
+    ASSERT_TRUE(m_platform_sp);
+
+    ASSERT_FALSE(llvm::sys::fs::createUniqueDirectory(
+        "locate-scripts-from-safe-paths-test", m_tmp_root_dir))
+        << "Failed to create test directory.";
+  };
+
+  void TearDown() override {
+    llvm::sys::fs::remove_directories(m_tmp_root_dir);
+    TestingProperties::GetGlobalTestingProperties().SetSafeAutoLoadPaths({});
+  }
+
+  DebuggerSP m_debugger_sp;
+  PlatformSP m_platform_sp;
+  TargetSP m_target_sp;
+
+  /// Root directory for m_tmp_dsym_dwarf_dir and m_tmp_dsym_python_dir
+  llvm::SmallString<128> m_tmp_root_dir;
+
+  SubsystemRAII<MockScriptInterpreterPython> subsystems;
+};
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_NoSetting) {
+  // Tests LocateScriptingResourcesFromSafePaths finds no script if we don't set
+  // the safe path setting.
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("TestModule.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  ASSERT_EQ(file_specs.GetSize(), 0u);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_NoMatch) {
+  // Tests LocateScriptingResourcesFromSafePaths finds no directory to load
+  // from.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  // Directory name doesn't match the module name.
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule1");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("TestModule1.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  ASSERT_EQ(file_specs.GetSize(), 0u);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_Match) {
+  // Tests LocateScriptingResourcesFromSafePaths locates the
+  // <module-name>/<module-name>.py script correctly.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("TestModule.py", module_dir);
+  // Other files should be ignored.
+  CreateFile("helper.py", module_dir);
+  CreateFile("not_a_script.txt", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "TestModule.py");
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_NestedDir) {
+  // Tests that a matching Python file nested inside a subdirectory is not
+  // picked up. Only <module-name>/<module-name>.py at the top level matters.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  // Create a nested directory that contains the matching Python file.
+  llvm::SmallString<128> nested_dir(module_dir);
+  llvm::sys::path::append(nested_dir, "nested");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(nested_dir));
+
+  CreateFile("TestModule.py", nested_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_MultiplePaths) {
+  // Tests LocateScriptingResourcesFromSafePaths locates the script from the
+  // last appended auto-load path.
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> path1(m_tmp_root_dir);
+  llvm::sys::path::append(path1, "AnotherSafePath");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path1));
+
+  llvm::SmallString<128> path2(m_tmp_root_dir);
+  llvm::sys::path::append(path2, "AnotherAnotherSafePath");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path2));
+
+  llvm::SmallString<128> path3(m_tmp_root_dir);
+  llvm::sys::path::append(path3, "EmptySafePath");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path3));
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  llvm::SmallString<128> path1_module_dir(path1);
+  llvm::sys::path::append(path1_module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path1_module_dir));
+
+  llvm::SmallString<128> path2_module_dir(path2);
+  llvm::sys::path::append(path2_module_dir, "NotTheTestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path2_module_dir));
+
+  llvm::SmallString<128> path3_module_dir(path3);
+  llvm::sys::path::append(path3_module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path3_module_dir));
+
+  // Place the correctly named script in each module directory.
+  CreateFile("TestModule.py", module_dir);
+  CreateFile("TestModule.py", path1_module_dir);
+  CreateFile("TestModule.py", path2_module_dir);
+  // Keep path3 (EmptySafePath) empty.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(path1));
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(path2));
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  // path1 was the last appended path with a matching directory.
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_TRUE(llvm::StringRef(file_specs.GetFileSpecAtIndex(0).GetPath())
+                  .contains("AnotherSafePath"));
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "TestModule.py");
+
+  // Now add another safe path with a valid module directory but no
+  // TestModule.py inside. LLDB shouldn't fall back to other matching safe
+  // paths.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(path3));
+
+  file_specs = Platform::LocateExecutableScriptingResourcesFromSafePaths(
+      ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+
+  // Now place the correctly named script in path3.
+  CreateFile("TestModule.py", path3_module_dir);
+
+  file_specs = Platform::LocateExecutableScriptingResourcesFromSafePaths(
+      ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_TRUE(llvm::StringRef(file_specs.GetFileSpecAtIndex(0).GetPath())
+                  .contains("EmptySafePath"));
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "TestModule.py");
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_SpecialChars_NoMatch) {
+  // Module name has special characters. The directory exists but only contains
+  // a script with the original (unsanitized) name. No match.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule-1.1 1.o
+  FileSpec module_fspec(CreateFile("TestModule-1.1 1.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule-1.1 1");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  // Only the unsanitized name exists.
+  FileSpec orig_fspec(CreateFile("TestModule-1.1 1.py", module_dir));
+  ASSERT_TRUE(orig_fspec);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+
+  std::string expected = llvm::formatv(
+      "debug script '{0}' cannot be loaded because"
+      " 'TestModule-1.1 1.py' contains reserved characters. If you intend to"
+      " have this script loaded, please rename it to 'TestModule_1_1_1.py' and "
+      "retry.\n",
+      orig_fspec.GetPath());
+  EXPECT_EQ(ss.GetString(), expected);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_SpecialChars_Match_Warning) {
+  // Module name has special characters. Both the original and sanitized scripts
+  // exist. LLDB loads the sanitized one and warns.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule-1.1 1.o
+  FileSpec module_fspec(CreateFile("TestModule-1.1 1.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule-1.1 1");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  FileSpec orig_fspec(CreateFile("TestModule-1.1 1.py", module_dir));
+  ASSERT_TRUE(orig_fspec);
+
+  CreateFile("TestModule_1_1_1.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(),
+            "TestModule_1_1_1.py");
+
+  std::string expected = llvm::formatv(
+      "debug script '{0}' cannot be loaded because"
+      " 'TestModule-1.1 1.py' contains reserved characters. Ignoring"
+      " 'TestModule-1.1 1.py' and loading 'TestModule_1_1_1.py' instead.\n",
+      orig_fspec.GetPath());
+  EXPECT_EQ(ss.GetString(), expected);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_SpecialChars_Match_NoWarning) {
+  // Module name has special characters. Only the sanitized script exists.
+  // No warning.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule-1.1 1.o
+  FileSpec module_fspec(CreateFile("TestModule-1.1 1.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule-1.1 1");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("TestModule_1_1_1.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(),
+            "TestModule_1_1_1.py");
+  EXPECT_TRUE(ss.GetString().empty());
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_Keyword_NoMatch) {
+  // Module name is a reserved keyword. Only the original script exists.
+  // Warns and returns nothing.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/import.o
+  FileSpec module_fspec(CreateFile("import.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "import");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  FileSpec orig_fspec(CreateFile("import.py", module_dir));
+  ASSERT_TRUE(orig_fspec);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+
+  std::string expected = llvm::formatv(
+      "debug script '{0}' cannot be loaded because 'import.py' "
+      "conflicts with the keyword 'import'. If you intend to have this script "
+      "loaded, please rename it to '_import.py' and retry.\n",
+      orig_fspec.GetPath());
+  EXPECT_EQ(ss.GetString(), expected);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_Keyword_Match) {
+  // Module name is a reserved keyword. Both original and sanitized scripts
+  // exist. Loads the sanitized one and warns.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/import.o
+  FileSpec module_fspec(CreateFile("import.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "import");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  FileSpec orig_fspec(CreateFile("import.py", module_dir));
+  ASSERT_TRUE(orig_fspec);
+
+  CreateFile("_import.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "_import.py");
+
+  std::string expected =
+      llvm::formatv("debug script '{0}' cannot be loaded because 'import.py' "
+                    "conflicts with the keyword 'import'. Ignoring 'import.py' "
+                    "and loading '_import.py' instead.\n",
+                    orig_fspec.GetPath());
+  EXPECT_EQ(ss.GetString(), expected);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_Keyword_Match_NoWarning) {
+  // Module name is a reserved keyword. Only the sanitized script exists.
+  // No warning.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/import.o
+  FileSpec module_fspec(CreateFile("import.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "import");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("_import.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "_import.py");
+  EXPECT_TRUE(ss.GetString().empty());
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_InnerDirectoryHasModuleName) {
+  // Test a directory structure like
+  // <safe-path>/TestModule/TestModule/TestModule.py. LLDB should not load that
+  // inner script.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> inner_dir(m_tmp_root_dir);
+  llvm::sys::path::append(inner_dir, "TestModule", "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directories(inner_dir));
+
+  CreateFile("TestModule.py", inner_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+  EXPECT_TRUE(ss.GetString().empty());
+}



More information about the lldb-commits mailing list