[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.
This commit is contained in:
Jonas Devlieghere 2026-04-03 10:04:47 -07:00 committed by GitHub
parent 7da3a66c06
commit 271a08889b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 195 additions and 13 deletions

View File

@ -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,

View File

@ -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.
///

View File

@ -56,7 +56,8 @@ enum InlineStrategy {
enum LoadScriptFromSymFile {
eLoadScriptFromSymFileTrue,
eLoadScriptFromSymFileFalse,
eLoadScriptFromSymFileWarn
eLoadScriptFromSymFileWarn,
eLoadScriptFromSymFileTrusted,
};
enum LoadCWDlldbinitFile {

View File

@ -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;
}

View File

@ -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

View File

@ -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 @@ 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<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;
}

View File

@ -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) {

View File

@ -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<lldb::ModuleSP> *old_modules,

View File

@ -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) {

View File

@ -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[] = {

View File

@ -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)">,

View File

@ -0,0 +1,3 @@
C_SOURCES := main.c
include Makefile.rules

View File

@ -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",
)

View File

@ -0,0 +1,5 @@
import lldb
def __lldb_init_module(debugger, internal_dict):
lldb._dsym_codesign_test_loaded = True

View File

@ -0,0 +1 @@
int main(void) { return 0; }

View File

@ -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.