From 7ac8bfed2323f8fd3965eabf051963e5288e3cec Mon Sep 17 00:00:00 2001 From: Jonas Devlieghere 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/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 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/ModuleList.cpp b/lldb/source/Core/ModuleList.cpp index 67ebe58cdb872..34d609bb22d8f 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 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 #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 @@ 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(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 #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 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 *old_modules, diff --git a/lldb/source/Target/Platform.cpp b/lldb/source/Target/Platform.cpp index 83f4fdf33d4d2..57c30f2c95eb2 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 96c1a2400dd78..0168c7d686e37 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 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 + + +@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.