From 271a08889b3e408037db15491b0390e472f003dc Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere Date: Fri, 3 Apr 2026 10:04:47 -0700 Subject: [PATCH] [lldb] Load scripts from code signed dSYM bundles (#189444) 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/ModuleList.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 ed00c44df4bd..6c2cabbf55c7 100644 --- a/lldb/include/lldb/Host/macosx/HostInfoMacOSX.h +++ b/lldb/include/lldb/Host/macosx/HostInfoMacOSX.h @@ -58,6 +58,10 @@ public: 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 6bdaf10ef071..c94f6e84ff88 100644 --- a/lldb/include/lldb/Target/Platform.h +++ b/lldb/include/lldb/Target/Platform.h @@ -296,6 +296,11 @@ public: 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 77b8f04a4b3b..2e9ee1de3c45 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/ModuleList.cpp b/lldb/source/Core/ModuleList.cpp index 67ebe58cdb87..34d609bb22d8 100644 --- a/lldb/source/Core/ModuleList.cpp +++ b/lldb/source/Core/ModuleList.cpp @@ -1376,16 +1376,22 @@ bool ModuleList::LoadScriptingResourceInTargetForModule(Module &module, 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(module)) + 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}" @@ -1394,10 +1400,10 @@ To run all discovered debug scripts in this session: settings set target.load-script-from-symbol-file true )", + // clang-format on module.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 1d7573335b8e..9304f23c019b 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 fd0c11197fc6..2214678d392b 100644 --- a/lldb/source/Host/macosx/objcxx/HostInfoMacOSX.mm +++ b/lldb/source/Host/macosx/objcxx/HostInfoMacOSX.mm @@ -44,6 +44,7 @@ #include #include #include +#include #include #if defined(MAC_OS_X_VERSION_MIN_REQUIRED) && \ MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_12_0 @@ -1096,3 +1097,30 @@ bool HostInfoMacOSX::SharedCacheIndexFiles(FileSpec &filepath, UUID &uuid, uuid, filepath.GetPath()); return false; } + +bool HostInfoMacOSX::IsBundleCodeSignTrusted(const FileSpec &bundle_path) { + std::string path = bundle_path.GetPath(); + CFURLRef url = CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, reinterpret_cast(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 c4865c466465..21be75c5a25d 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 #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/. + 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 3c98d420dde8..fd5207e82b6d 100644 --- a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h +++ b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h @@ -71,6 +71,8 @@ public: 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 *old_modules, diff --git a/lldb/source/Target/Platform.cpp b/lldb/source/Target/Platform.cpp index 83f4fdf33d4d..57c30f2c95eb 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 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 96c1a2400dd7..0168c7d686e3 100644 --- a/lldb/source/Target/Target.cpp +++ b/lldb/source/Target/Target.cpp @@ -4371,6 +4371,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 2361314d506a..f8b51ad8558b 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 000000000000..10495940055b --- /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 000000000000..002343ab8c8f --- /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 + + +@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 000000000000..2bc2887de75e --- /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 000000000000..78f2de106c92 --- /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 450b65a80c5c..d0d8b9f4ddab 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.