[lldb] [llvm] [lldb] Load scripts from code signed dSYM bundles (PR #189444)

Jonas Devlieghere via llvm-commits llvm-commits at lists.llvm.org
Wed Apr 1 15:53:25 PDT 2026


https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/189444

>From c23dcf23c7e4da9bebf5db430790a9d20d755be9 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Mon, 30 Mar 2026 11:09:49 -0700
Subject: [PATCH] [lldb] Load scripts from code signed dSYM bundles

LLDB automatically discovers, but doesn't automatically load, scripts in
the dSYM bundle. This is to prevent running untrusted code. Users can
choose to import the script manually or toggle a global setting to
override this policy. This isn't a great user experience: the former
quickly becomes tedious and the latter leads to decreased security.

This PR offers a middle ground that allows LLDB to automatically load
scripts from trusted dSYM bundles. Trusted here means that the bundle
was signed  with a certificate trusted by the system. This can be a
locally created certificate (but not an ad-hoc certificate) or a
certificate from a trusted vendor.
---
 .../include/lldb/Host/macosx/HostInfoMacOSX.h |  4 +
 lldb/include/lldb/Target/Platform.h           |  5 ++
 lldb/include/lldb/Target/Target.h             |  3 +-
 lldb/source/Core/Module.cpp                   | 18 +++--
 lldb/source/Host/macosx/objcxx/CMakeLists.txt |  4 +-
 .../Host/macosx/objcxx/HostInfoMacOSX.mm      | 28 +++++++
 .../Platform/MacOSX/PlatformDarwin.cpp        | 35 +++++++++
 .../Plugins/Platform/MacOSX/PlatformDarwin.h  |  2 +
 lldb/source/Target/Platform.cpp               |  2 +
 lldb/source/Target/Target.cpp                 |  6 ++
 lldb/source/Target/TargetProperties.td        | 10 ++-
 lldb/test/API/macosx/dsym_codesign/Makefile   |  3 +
 .../macosx/dsym_codesign/TestdSYMCodesign.py  | 78 +++++++++++++++++++
 .../API/macosx/dsym_codesign/dsym_script.py   |  5 ++
 lldb/test/API/macosx/dsym_codesign/main.c     |  1 +
 llvm/docs/ReleaseNotes.md                     |  4 +-
 16 files changed, 195 insertions(+), 13 deletions(-)
 create mode 100644 lldb/test/API/macosx/dsym_codesign/Makefile
 create mode 100644 lldb/test/API/macosx/dsym_codesign/TestdSYMCodesign.py
 create mode 100644 lldb/test/API/macosx/dsym_codesign/dsym_script.py
 create mode 100644 lldb/test/API/macosx/dsym_codesign/main.c

diff --git a/lldb/include/lldb/Host/macosx/HostInfoMacOSX.h b/lldb/include/lldb/Host/macosx/HostInfoMacOSX.h
index ed00c44df4bdc..6c2cabbf55c74 100644
--- a/lldb/include/lldb/Host/macosx/HostInfoMacOSX.h
+++ b/lldb/include/lldb/Host/macosx/HostInfoMacOSX.h
@@ -58,6 +58,10 @@ class HostInfoMacOSX : public HostInfoPosix {
   static bool SharedCacheIndexFiles(FileSpec &filepath, UUID &uuid,
                                     lldb::SymbolSharedCacheUse sc_mode);
 
+  /// Check whether a bundle at the given path has a valid code signature that
+  /// chains to a trusted anchor in the system trust store.
+  static bool IsBundleCodeSignTrusted(const FileSpec &bundle_path);
+
 protected:
   static bool ComputeSupportExeDirectory(FileSpec &file_spec);
   static void ComputeHostArchitectureSupport(ArchSpec &arch_32,
diff --git a/lldb/include/lldb/Target/Platform.h b/lldb/include/lldb/Target/Platform.h
index 6bdaf10ef0713..c94f6e84ff889 100644
--- a/lldb/include/lldb/Target/Platform.h
+++ b/lldb/include/lldb/Target/Platform.h
@@ -296,6 +296,11 @@ class Platform : public PluginInterface {
                                                   FileSpec module_spec,
                                                   const Target &target);
 
+  /// Returns true if the module's symbol file (e.g. a dSYM bundle) is
+  /// code-signed with a trusted signature. Used to decide whether to
+  /// auto-loaded scripts.
+  virtual bool IsSymbolFileTrusted(Module &module);
+
   /// \param[in] module_spec
   ///     The ModuleSpec of a binary to find.
   ///
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index 77b8f04a4b3b8..2e9ee1de3c456 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -56,7 +56,8 @@ enum InlineStrategy {
 enum LoadScriptFromSymFile {
   eLoadScriptFromSymFileTrue,
   eLoadScriptFromSymFileFalse,
-  eLoadScriptFromSymFileWarn
+  eLoadScriptFromSymFileWarn,
+  eLoadScriptFromSymFileTrusted,
 };
 
 enum LoadCWDlldbinitFile {
diff --git a/lldb/source/Core/Module.cpp b/lldb/source/Core/Module.cpp
index 808c8a347e9b2..3d134fa2a3336 100644
--- a/lldb/source/Core/Module.cpp
+++ b/lldb/source/Core/Module.cpp
@@ -1467,16 +1467,22 @@ bool Module::LoadScriptingResourceInTarget(Target *target, Status &error) {
     debugger.ReportWarning(feedback_stream.GetString().str(), debugger.GetID());
 
   for (const auto &[scripting_fspec, load_style] : file_specs) {
-    if (load_style == eLoadScriptFromSymFileFalse)
-      continue;
-
     if (!FileSystem::Instance().Exists(scripting_fspec))
       continue;
 
-    if (load_style == eLoadScriptFromSymFileWarn) {
-      // clang-format off
+    switch (load_style) {
+    case eLoadScriptFromSymFileFalse:
+      continue;
+    case eLoadScriptFromSymFileTrue:
+      break;
+    case eLoadScriptFromSymFileTrusted:
+      if (!platform_sp->IsSymbolFileTrusted(*this))
+        continue;
+      break;
+    case eLoadScriptFromSymFileWarn:
       debugger.ReportWarning(
           llvm::formatv(
+              // clang-format off
 R"('{0}' contains a debug script. To run this script in this debug session:
 
     command script import "{1}"
@@ -1485,10 +1491,10 @@ To run all discovered debug scripts in this session:
 
     settings set target.load-script-from-symbol-file true
 )",
+              // clang-format on
               GetFileSpec().GetFileNameStrippingExtension(),
               scripting_fspec.GetPath()),
           debugger.GetID());
-      // clang-format on
 
       return false;
     }
diff --git a/lldb/source/Host/macosx/objcxx/CMakeLists.txt b/lldb/source/Host/macosx/objcxx/CMakeLists.txt
index 1d7573335b8ec..9304f23c019b4 100644
--- a/lldb/source/Host/macosx/objcxx/CMakeLists.txt
+++ b/lldb/source/Host/macosx/objcxx/CMakeLists.txt
@@ -1,7 +1,8 @@
-
 remove_module_flags()
 include_directories(..)
 
+find_library(SECURITY_FRAMEWORK Security)
+
 add_lldb_library(lldbHostMacOSXObjCXX NO_PLUGIN_DEPENDENCIES
   Host.mm
   HostInfoMacOSX.mm
@@ -16,6 +17,7 @@ add_lldb_library(lldbHostMacOSXObjCXX NO_PLUGIN_DEPENDENCIES
   LINK_LIBS
     lldbUtility
     ${EXTRA_LIBS}
+    ${SECURITY_FRAMEWORK}
   )
 
 target_compile_options(lldbHostMacOSXObjCXX PRIVATE
diff --git a/lldb/source/Host/macosx/objcxx/HostInfoMacOSX.mm b/lldb/source/Host/macosx/objcxx/HostInfoMacOSX.mm
index fd0c11197fc66..2214678d392b5 100644
--- a/lldb/source/Host/macosx/objcxx/HostInfoMacOSX.mm
+++ b/lldb/source/Host/macosx/objcxx/HostInfoMacOSX.mm
@@ -44,6 +44,7 @@
 #include <AvailabilityMacros.h>
 #include <CoreFoundation/CoreFoundation.h>
 #include <Foundation/Foundation.h>
+#include <Security/Security.h>
 #include <mach-o/dyld.h>
 #if defined(MAC_OS_X_VERSION_MIN_REQUIRED) && \
     MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_12_0
@@ -1096,3 +1097,30 @@ static dispatch_data_t (*g_dyld_image_segment_data_4HWTrace)(
         uuid, filepath.GetPath());
   return false;
 }
+
+bool HostInfoMacOSX::IsBundleCodeSignTrusted(const FileSpec &bundle_path) {
+  std::string path = bundle_path.GetPath();
+  CFURLRef url = CFURLCreateFromFileSystemRepresentation(
+      kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(path.data()),
+      path.size(), /*isDirectory=*/true);
+  if (!url)
+    return false;
+  auto url_cleanup = llvm::make_scope_exit([&]() { CFRelease(url); });
+
+  SecStaticCodeRef static_code = nullptr;
+  if (SecStaticCodeCreateWithPath(url, kSecCSDefaultFlags, &static_code) !=
+      errSecSuccess)
+    return false;
+  auto code_cleanup = llvm::make_scope_exit([&]() { CFRelease(static_code); });
+
+  // Check that the signature chains to a trusted root CA.
+  SecRequirementRef requirement = nullptr;
+  if (SecRequirementCreateWithString(CFSTR("anchor trusted"),
+                                     kSecCSDefaultFlags,
+                                     &requirement) != errSecSuccess)
+    return false;
+  auto req_cleanup = llvm::make_scope_exit([&]() { CFRelease(requirement); });
+
+  return SecStaticCodeCheckValidity(static_code, kSecCSDefaultFlags,
+                                    requirement) == errSecSuccess;
+}
diff --git a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
index c4865c4664651..21be75c5a25da 100644
--- a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
+++ b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
@@ -50,6 +50,7 @@
 #include "llvm/Support/VersionTuple.h"
 
 #if defined(__APPLE__)
+#include "lldb/Host/macosx/HostInfoMacOSX.h"
 #include <TargetConditionals.h>
 #endif
 
@@ -295,6 +296,40 @@ PlatformDarwin::LocateExecutableScriptingResourcesForPlatform(
   return empty;
 }
 
+bool PlatformDarwin::IsSymbolFileTrusted(Module &module) {
+#if defined(__APPLE__)
+  SymbolFile *symfile = module.GetSymbolFile();
+  if (!symfile)
+    return false;
+
+  ObjectFile *objfile = symfile->GetObjectFile();
+  if (!objfile)
+    return false;
+
+  std::string symfile_path = objfile->GetFileSpec().GetPath();
+  llvm::StringRef path_ref(symfile_path);
+
+  // Find the .dSYM bundle root from the symfile path, which is typically
+  // .dSYM/Contents/Resources/DWARF/<name>.
+  auto pos = path_ref.find(".dSYM/");
+  if (pos == llvm::StringRef::npos)
+    return false;
+
+  FileSpec bundle_spec(path_ref.substr(0, pos + 5));
+
+  if (HostInfoMacOSX::IsBundleCodeSignTrusted(bundle_spec)) {
+    LLDB_LOG(GetLog(LLDBLog::Modules),
+             "dSYM bundle '{0}' has valid trusted code signature",
+             bundle_spec.GetPath());
+    return true;
+  }
+
+  return false;
+#else
+  return false;
+#endif
+}
+
 Status PlatformDarwin::ResolveSymbolFile(Target &target,
                                          const ModuleSpec &sym_spec,
                                          FileSpec &sym_file) {
diff --git a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h
index 3c98d420dde8c..fd5207e82b6db 100644
--- a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h
+++ b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h
@@ -71,6 +71,8 @@ class PlatformDarwin : public PlatformPOSIX {
   LocateExecutableScriptingResourcesForPlatform(
       Target *target, Module &module_spec, Stream &feedback_stream) override;
 
+  bool IsSymbolFileTrusted(Module &module) override;
+
   Status GetSharedModule(const ModuleSpec &module_spec, Process *process,
                          lldb::ModuleSP &module_sp,
                          llvm::SmallVectorImpl<lldb::ModuleSP> *old_modules,
diff --git a/lldb/source/Target/Platform.cpp b/lldb/source/Target/Platform.cpp
index 55eaa88b562d4..cdd57aca1664a 100644
--- a/lldb/source/Target/Platform.cpp
+++ b/lldb/source/Target/Platform.cpp
@@ -157,6 +157,8 @@ Status Platform::GetFileWithUUID(const FileSpec &platform_file,
   return Status();
 }
 
+bool Platform::IsSymbolFileTrusted(Module &module) { return false; }
+
 llvm::SmallDenseMap<FileSpec, LoadScriptFromSymFile>
 Platform::LocateExecutableScriptingResourcesFromSafePaths(
     Stream &feedback_stream, FileSpec module_spec, const Target &target) {
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index 2a3832225f9b7..df2679116d3d0 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -4380,6 +4380,12 @@ static constexpr OptionEnumValueElement g_load_script_from_sym_file_values[] = {
         "warn",
         "Warn about debug scripts inside symbol files but do not load them.",
     },
+    {
+        eLoadScriptFromSymFileTrusted,
+        "trusted",
+        "Load debug scripts inside trusted symbol files, and warn about "
+        "scripts from untrusted symbol files.",
+    },
 };
 
 static constexpr OptionEnumValueElement g_load_cwd_lldbinit_values[] = {
diff --git a/lldb/source/Target/TargetProperties.td b/lldb/source/Target/TargetProperties.td
index 2361314d506ac..f8b51ad8558b9 100644
--- a/lldb/source/Target/TargetProperties.td
+++ b/lldb/source/Target/TargetProperties.td
@@ -176,10 +176,12 @@ let Definition = "target", Path = "target" in {
   def UseFastStepping: Property<"use-fast-stepping", "Boolean">,
     DefaultTrue,
     Desc<"Use a fast stepping algorithm based on running from branch to branch rather than instruction single-stepping.">;
-  def LoadScriptFromSymbolFile: Property<"load-script-from-symbol-file", "Enum">,
-    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 LoadScriptFromSymbolFile
+      : Property<"load-script-from-symbol-file", "Enum">,
+        DefaultEnumValue<"eLoadScriptFromSymFileTrusted">,
+        EnumValues<"OptionEnumValues(g_load_script_from_sym_file_values)">,
+        Desc<"Allow LLDB to load scripting resources embedded in symbol files "
+             "when available.">;
   def LoadCWDlldbinitFile: Property<"load-cwd-lldbinit", "Enum">,
     DefaultEnumValue<"eLoadCWDlldbinitWarn">,
     EnumValues<"OptionEnumValues(g_load_cwd_lldbinit_values)">,
diff --git a/lldb/test/API/macosx/dsym_codesign/Makefile b/lldb/test/API/macosx/dsym_codesign/Makefile
new file mode 100644
index 0000000000000..10495940055b6
--- /dev/null
+++ b/lldb/test/API/macosx/dsym_codesign/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
diff --git a/lldb/test/API/macosx/dsym_codesign/TestdSYMCodesign.py b/lldb/test/API/macosx/dsym_codesign/TestdSYMCodesign.py
new file mode 100644
index 0000000000000..002343ab8c8ff
--- /dev/null
+++ b/lldb/test/API/macosx/dsym_codesign/TestdSYMCodesign.py
@@ -0,0 +1,78 @@
+import os
+import shutil
+import subprocess
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+
+
+def has_lldb_codesign():
+    """Check if the lldb_codesign certificate is available."""
+    try:
+        result = subprocess.run(
+            [
+                "security",
+                "find-certificate",
+                "-c",
+                "lldb_codesign",
+                "/Library/Keychains/System.keychain",
+            ],
+            capture_output=True,
+        )
+        return result.returncode == 0
+    except FileNotFoundError:
+        return False
+
+
+ at skipUnlessDarwin
+class TestdSYMCodesign(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+    SHARED_BUILD_TESTCASE = False
+
+    def build_dsym_with_script(self):
+        self.build(debug_info="dsym")
+        exe = self.getBuildArtifact("a.out")
+        dsym = self.getBuildArtifact("a.out.dSYM")
+        python_dir = os.path.join(dsym, "Contents", "Resources", "Python")
+        os.makedirs(python_dir, exist_ok=True)
+        shutil.copy(
+            os.path.join(self.getSourceDir(), "dsym_script.py"),
+            os.path.join(python_dir, "a.py"),
+        )
+        return exe, dsym
+
+    def test_adhoc_signed_dsym(self):
+        """An ad-hoc signed dSYM should not be loaded because the
+        signature doesn't chain to a trusted root CA."""
+        exe, dsym = self.build_dsym_with_script()
+        subprocess.check_call(["codesign", "-f", "-s", "-", dsym])
+
+        self.runCmd("settings set target.load-script-from-symbol-file trusted")
+        self.createTestTarget(file_path=exe)
+
+        self.expect(
+            "script -- print('SENTINEL')",
+            substrs=["SENTINEL"],
+        )
+        # The script should NOT have been loaded.
+        self.assertFalse(
+            hasattr(lldb, "_dsym_codesign_test_loaded"),
+            "Script should not auto-load from ad-hoc signed dSYM",
+        )
+
+    @unittest.skipUnless(has_lldb_codesign(), "requires lldb_codesign certificate")
+    def test_trusted_signed_dsym_auto_loads(self):
+        """A dSYM signed with the trusted lldb_codesign certificate should
+        auto-load scripts."""
+        exe, dsym = self.build_dsym_with_script()
+        subprocess.check_call(["codesign", "-f", "-s", "lldb_codesign", dsym])
+
+        self.runCmd("settings set target.load-script-from-symbol-file trusted")
+        self.createTestTarget(file_path=exe)
+
+        # The script sets a marker attribute on the lldb module.
+        self.assertTrue(
+            getattr(lldb, "_dsym_codesign_test_loaded", False),
+            "Script should auto-load from trusted signed dSYM",
+        )
diff --git a/lldb/test/API/macosx/dsym_codesign/dsym_script.py b/lldb/test/API/macosx/dsym_codesign/dsym_script.py
new file mode 100644
index 0000000000000..2bc2887de75ed
--- /dev/null
+++ b/lldb/test/API/macosx/dsym_codesign/dsym_script.py
@@ -0,0 +1,5 @@
+import lldb
+
+
+def __lldb_init_module(debugger, internal_dict):
+    lldb._dsym_codesign_test_loaded = True
diff --git a/lldb/test/API/macosx/dsym_codesign/main.c b/lldb/test/API/macosx/dsym_codesign/main.c
new file mode 100644
index 0000000000000..78f2de106c92b
--- /dev/null
+++ b/lldb/test/API/macosx/dsym_codesign/main.c
@@ -0,0 +1 @@
+int main(void) { return 0; }
diff --git a/llvm/docs/ReleaseNotes.md b/llvm/docs/ReleaseNotes.md
index 450b65a80c5cd..d0d8b9f4ddabc 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -244,6 +244,8 @@ Changes to LLDB
 * ``SBTarget::GetDataByteSize()``, ``SBTarget::GetCodeByteSize()``, and ``SBSection::GetTargetByteSize()``
   have been deprecated. They always return 1, as before.
 * A new ``webinspector-wasm`` platform was added to list and attach to WebAssebly processes in Safari.
+* The default for `load-script-from-symbol-file` was changed from `warn` to `trusted`. This means that scripts from
+  code signed dSYM bundles are now loaded automatically, while untrusted bundles continue to produce a warning.
 
 ### FreeBSD
 
@@ -256,7 +258,7 @@ Changes to LLDB
 #### Kernel Debugging
 
 * The plugin that analyzes FreeBSD kernel core dump and live core has been renamed from `freebsd-kernel` to
- `freebsd-kernel-core`. Remote kernel debugging is still handled by the `gdb-remote` plugin. 
+ `freebsd-kernel-core`. Remote kernel debugging is still handled by the `gdb-remote` plugin.
 * Support for libfbsdvmcore has been removed. As a result, FreeBSD kernel dump debugging is now only
   available on FreeBSD hosts. Live kernel debugging through the GDB remote protocol is still available
   from any platform.



More information about the llvm-commits mailing list