[Lldb-commits] [lldb] c819bff - [lldb] Replay the cached error from GetLoadImageUtilityFunction (#226986)
via lldb-commits
lldb-commits at lists.llvm.org
Sun Oct 4 07:04:31 PDT 2026
Author: Yao Qi
Date: 2026-10-04T15:04:21+01:00
New Revision: c819bff6a970e48f6084d6af0841fccc03b46f0b
URL: https://github.com/llvm/llvm-project/commit/c819bff6a970e48f6084d6af0841fccc03b46f0b
DIFF: https://github.com/llvm/llvm-project/commit/c819bff6a970e48f6084d6af0841fccc03b46f0b.diff
LOG: [lldb] Replay the cached error from GetLoadImageUtilityFunction (#226986)
`Process::LoadImage` needs a live process to load a library: on POSIX it
makes the C library's `dlopen` run inside the debuggee, and on Windows
it makes `LoadLibraryExW` run there, both through a JIT-compiled
function call that needs a process to run the call in. On a core file
(or, on Windows, a saved minidump) there is no such process, so the load
fails every time. POSIX and Windows each lose the resulting error
message, but for two different reasons.
On POSIX, only the first failure is reported correctly. Load a corefile,
and load two libraries that do not exist shows this:
```
(lldb) process load /tmp/does-not-exist-1.dylib
error: failed to load '/tmp/does-not-exist-1.dylib': dlopen error: could not make function caller: Can't make a function caller without a process.
(lldb) process load /tmp/does-not-exist-2.dylib
error: failed to load '/tmp/does-not-exist-2.dylib': (null)
```
`Process::GetLoadImageUtilityFunction` builds the `UtilityFunction` that
performs the `dlopen` call, and caches it in the `Process` The cache is
a `llvm::call_once`: only the first caller's factory function actually
runs, and every later caller just gets the same cached pointer back,
whether or not the first attempt succeeded. Each caller had been passing
its own local `Status &error`, populated only when its own factory ran,
so any caller after the first got a `Status` nobody had written to.
On Windows the same missing-process failure loses its message on the
very first call, not just later ones. Loading a saved minidump and
calling `process load` twice shows this:
```
(lldb) process load /tmp/does-not-exist-1.dll
error: failed to load '/tmp/does-not-exist-1.dll': (null)
(lldb) process load /tmp/does-not-exist-2.dll
error: failed to load '/tmp/does-not-exist-2.dll': (null)
```
`process load` decides success or failure from the returned image token,
so it prints "failed to load" here at all; an SB API caller that checks
`error.Fail()` directly sees a false success on both calls. This is a
different bug from the POSIX one above: it is not about the cache
replaying a stale result, it is that `PlatformWindows::DoLoadImage`
never populated its `error` output parameter in the first place. Its
factory wrote the failure into a local `Status status` that nothing ever
read, instead of into the function's own `error` parameter.
Give the `Process` its own `Status` member alongside the cached
`UtilityFunction`, and have `GetLoadImageUtilityFunction` take an
`error` output parameter. The one factory call that runs stores its
result in the member; every call, whether or not it ran the factory,
copies the member back out to its caller. `Status` cannot be copied
directly because it wraps a move-only `llvm::Error`, so this uses
`Status::Clone()`, the pattern already used elsewhere in lldb to stash
an error and hand copies of it to more than one reader.
`DWARFUnit` uses the same technique to cache a load failure:
`ExtractUnitDIEIfNeeded` runs its loading step once per unit, guarded by
the member `m_has_parsed_non_skeleton_unit`, and stores any failure in
the member `m_dwo_error` via `SetDwoError`.
`SymbolFileDWARF::CalculateFrameVariableError` later reads it back with
`GetDwoError()` and clones it for its own caller.
Fix `PlatformWindows::DoLoadImage` the same way, by routing its factory
through the function's `error` parameter instead of the local `status`
that nothing read.
Add a test that saves a core file, loads it back, and calls
`SBProcess::LoadImage` twice with a path that cannot be opened. Both
calls must fail, with the same non-empty message. Add a second test,
using a checked-in Windows minidump in place of a core file, that does
the same thing against `PlatformWindows::DoLoadImage`.
---------
Co-authored-by: Charles Zablit <c_zablit at apple.com>
Added:
lldb/test/API/functionalities/load_unload/process-load-error-postmortem/Makefile
lldb/test/API/functionalities/load_unload/process-load-error-postmortem/TestProcessLoadErrorPostmortem.py
lldb/test/API/functionalities/load_unload/process-load-error-postmortem/main.c
Modified:
lldb/include/lldb/Target/Process.h
lldb/source/Plugins/Platform/POSIX/PlatformPOSIX.cpp
lldb/source/Plugins/Platform/POSIX/PlatformPOSIX.h
lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp
lldb/source/Plugins/Platform/Windows/PlatformWindows.h
lldb/source/Target/Process.cpp
lldb/test/API/functionalities/postmortem/wow64_minidump/TestWow64MiniDump.py
Removed:
################################################################################
diff --git a/lldb/include/lldb/Target/Process.h b/lldb/include/lldb/Target/Process.h
index 207c9cf290035..029f7f422caee 100644
--- a/lldb/include/lldb/Target/Process.h
+++ b/lldb/include/lldb/Target/Process.h
@@ -666,11 +666,12 @@ class Process : public std::enable_shared_from_this<Process>,
/// been initialized yet.
///
/// \return
- /// The cached utility function or null if the platform is not the
- /// same as the target's platform.
- UtilityFunction *GetLoadImageUtilityFunction(
+ /// The cached utility function, or an Error if the platform is not
+ /// the same as the target's platform, or if it could not be created.
+ llvm::Expected<UtilityFunction &> GetLoadImageUtilityFunction(
Platform *platform,
- llvm::function_ref<std::unique_ptr<UtilityFunction>()> factory);
+ llvm::function_ref<llvm::Expected<std::unique_ptr<UtilityFunction>>()>
+ factory);
/// Get the dynamic loader plug-in for this process.
///
@@ -3594,6 +3595,9 @@ void PruneThreadPlans();
std::unique_ptr<UtilityFunction> m_dlopen_utility_func_up;
llvm::once_flag m_dlopen_utility_func_flag_once;
+ /// The error from the one attempt to create m_dlopen_utility_func_up,
+ /// set only if that attempt failed.
+ Status m_dlopen_utility_func_error;
/// Per process source file cache.
SourceManager::SourceFileCache m_source_file_cache;
diff --git a/lldb/source/Plugins/Platform/POSIX/PlatformPOSIX.cpp b/lldb/source/Plugins/Platform/POSIX/PlatformPOSIX.cpp
index 2a5a3d55ec98c..b52d4da817f49 100644
--- a/lldb/source/Plugins/Platform/POSIX/PlatformPOSIX.cpp
+++ b/lldb/source/Plugins/Platform/POSIX/PlatformPOSIX.cpp
@@ -553,9 +553,8 @@ Status PlatformPOSIX::EvaluateLibdlExpression(
return Status();
}
-std::unique_ptr<UtilityFunction>
-PlatformPOSIX::MakeLoadImageUtilityFunction(ExecutionContext &exe_ctx,
- Status &error) {
+llvm::Expected<std::unique_ptr<UtilityFunction>>
+PlatformPOSIX::MakeLoadImageUtilityFunction(ExecutionContext &exe_ctx) {
// Remember to prepend this with the prefix from
// GetLibdlFunctionDeclarations. The returned values are all in
// __lldb_dlopen_result for consistency. The wrapper returns a void * but
@@ -630,23 +629,21 @@ PlatformPOSIX::MakeLoadImageUtilityFunction(ExecutionContext &exe_ctx,
std::move(expr), dlopen_wrapper_name, eLanguageTypeC_plus_plus, exe_ctx);
if (!utility_fn_or_error) {
std::string error_str = llvm::toString(utility_fn_or_error.takeError());
- error = Status::FromErrorStringWithFormat(
+ return llvm::createStringError(
"dlopen error: could not create utility function: %s",
error_str.c_str());
- return nullptr;
}
std::unique_ptr<UtilityFunction> dlopen_utility_func_up =
std::move(*utility_fn_or_error);
Value value;
ValueList arguments;
- FunctionCaller *do_dlopen_function = nullptr;
// Fetch the clang types we will need:
TypeSystemClangSP scratch_ts_sp =
ScratchTypeSystemClang::GetForTarget(process->GetTarget());
if (!scratch_ts_sp)
- return nullptr;
+ return llvm::createStringError("dlopen error: no scratch type system");
CompilerType clang_void_pointer_type =
scratch_ts_sp->GetBasicType(eBasicTypeVoid).GetPointerType();
@@ -663,24 +660,18 @@ PlatformPOSIX::MakeLoadImageUtilityFunction(ExecutionContext &exe_ctx,
arguments.PushValue(value);
arguments.PushValue(value);
arguments.PushValue(value);
-
- do_dlopen_function = dlopen_utility_func_up->MakeFunctionCaller(
+
+ dlopen_utility_func_up->MakeFunctionCaller(
clang_void_pointer_type, arguments, exe_ctx.GetThreadSP(), utility_error);
- if (utility_error.Fail()) {
- error = Status::FromErrorStringWithFormat(
+ if (utility_error.Fail())
+ return llvm::createStringError(
"dlopen error: could not make function caller: %s",
utility_error.AsCString());
- return nullptr;
- }
-
- do_dlopen_function = dlopen_utility_func_up->GetFunctionCaller();
- if (!do_dlopen_function) {
- error =
- Status::FromErrorString("dlopen error: could not get function caller.");
- return nullptr;
- }
-
- // We made a good utility function, so cache it in the process:
+
+ if (!dlopen_utility_func_up->GetFunctionCaller())
+ return llvm::createStringError(
+ "dlopen error: could not get function caller.");
+
return dlopen_utility_func_up;
}
@@ -708,21 +699,20 @@ uint32_t PlatformPOSIX::DoLoadImage(lldb_private::Process *process,
thread_sp->CalculateExecutionContext(exe_ctx);
Status utility_error;
- UtilityFunction *dlopen_utility_func;
ValueList arguments;
- FunctionCaller *do_dlopen_function = nullptr;
// The UtilityFunction is held in the Process. Platforms don't track the
// lifespan of the Targets that use them, we can't put this in the Platform.
- dlopen_utility_func = process->GetLoadImageUtilityFunction(
- this, [&]() -> std::unique_ptr<UtilityFunction> {
- return MakeLoadImageUtilityFunction(exe_ctx, error);
- });
- // If we couldn't make it, the error will be in error, so we can exit here.
- if (!dlopen_utility_func)
+ llvm::Expected<UtilityFunction &> dlopen_utility_func_or_err =
+ process->GetLoadImageUtilityFunction(
+ this, [&]() { return MakeLoadImageUtilityFunction(exe_ctx); });
+ if (!dlopen_utility_func_or_err) {
+ error = Status::FromError(dlopen_utility_func_or_err.takeError());
return LLDB_INVALID_IMAGE_TOKEN;
-
- do_dlopen_function = dlopen_utility_func->GetFunctionCaller();
+ }
+
+ FunctionCaller *do_dlopen_function =
+ dlopen_utility_func_or_err->GetFunctionCaller();
if (!do_dlopen_function) {
error =
Status::FromErrorString("dlopen error: could not get function caller.");
diff --git a/lldb/source/Plugins/Platform/POSIX/PlatformPOSIX.h b/lldb/source/Plugins/Platform/POSIX/PlatformPOSIX.h
index 0a02ecd279cfe..6c27a36b3b1e3 100644
--- a/lldb/source/Plugins/Platform/POSIX/PlatformPOSIX.h
+++ b/lldb/source/Plugins/Platform/POSIX/PlatformPOSIX.h
@@ -86,9 +86,8 @@ class PlatformPOSIX : public lldb_private::RemoteAwarePlatform {
llvm::StringRef expr_prefix,
lldb::ValueObjectSP &result_valobj_sp);
- std::unique_ptr<lldb_private::UtilityFunction>
- MakeLoadImageUtilityFunction(lldb_private::ExecutionContext &exe_ctx,
- lldb_private::Status &error);
+ llvm::Expected<std::unique_ptr<lldb_private::UtilityFunction>>
+ MakeLoadImageUtilityFunction(lldb_private::ExecutionContext &exe_ctx);
virtual
llvm::StringRef GetLibdlFunctionDeclarations(lldb_private::Process *process);
diff --git a/lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp b/lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp
index d977d60b9a766..6764289973088 100644
--- a/lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp
+++ b/lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp
@@ -228,15 +228,15 @@ uint32_t PlatformWindows::DoLoadImage(Process *process,
ExecutionContext context;
thread->CalculateExecutionContext(context);
- Status status;
- UtilityFunction *loader =
- process->GetLoadImageUtilityFunction(this, [&]() -> std::unique_ptr<UtilityFunction> {
- return MakeLoadImageUtilityFunction(context, status);
- });
- if (loader == nullptr)
+ llvm::Expected<UtilityFunction &> loader_or_err =
+ process->GetLoadImageUtilityFunction(
+ this, [&]() { return MakeLoadImageUtilityFunction(context); });
+ if (!loader_or_err) {
+ error = Status::FromError(loader_or_err.takeError());
return LLDB_INVALID_IMAGE_TOKEN;
+ }
- FunctionCaller *invocation = loader->GetFunctionCaller();
+ FunctionCaller *invocation = loader_or_err->GetFunctionCaller();
if (!invocation) {
error = Status::FromErrorString(
"LoadLibrary error: could not get function caller");
@@ -253,6 +253,7 @@ uint32_t PlatformWindows::DoLoadImage(Process *process,
name.emplace_back(L'\0');
/* Inject name paramter into inferior */
+ Status status;
lldb::addr_t injected_name =
process->AllocateMemory(name.size() * sizeof(llvm::UTF16),
ePermissionsReadable | ePermissionsWritable,
@@ -741,9 +742,8 @@ PlatformWindows::GetSoftwareBreakpointTrapOpcode(Target &target,
}
}
-std::unique_ptr<UtilityFunction>
-PlatformWindows::MakeLoadImageUtilityFunction(ExecutionContext &context,
- Status &status) {
+llvm::Expected<std::unique_ptr<UtilityFunction>>
+PlatformWindows::MakeLoadImageUtilityFunction(ExecutionContext &context) {
// FIXME(compnerd) `-fdeclspec` is not passed to the clang instance?
static constexpr const char kLoaderDecls[] = R"(
extern "C" {
@@ -858,16 +858,15 @@ void * __lldb_LoadLibraryHelper(const wchar_t *name, const wchar_t *paths,
context);
if (!function) {
std::string error = llvm::toString(function.takeError());
- status = Status::FromErrorStringWithFormat(
+ return llvm::createStringError(
"LoadLibrary error: could not create utility function: %s",
error.c_str());
- return nullptr;
}
TypeSystemClangSP scratch_ts_sp =
ScratchTypeSystemClang::GetForTarget(target);
if (!scratch_ts_sp)
- return nullptr;
+ return llvm::createStringError("LoadLibrary error: no scratch type system");
CompilerType VoidPtrTy =
scratch_ts_sp->GetBasicType(eBasicTypeVoid).GetPointerType();
@@ -890,18 +889,14 @@ void * __lldb_LoadLibraryHelper(const wchar_t *name, const wchar_t *paths,
std::unique_ptr<UtilityFunction> utility{std::move(*function)};
utility->MakeFunctionCaller(VoidPtrTy, parameters, context.GetThreadSP(),
error);
- if (error.Fail()) {
- status = Status::FromErrorStringWithFormat(
+ if (error.Fail())
+ return llvm::createStringError(
"LoadLibrary error: could not create function caller: %s",
error.AsCString());
- return nullptr;
- }
- if (!utility->GetFunctionCaller()) {
- status = Status::FromErrorString(
+ if (!utility->GetFunctionCaller())
+ return llvm::createStringError(
"LoadLibrary error: could not get function caller");
- return nullptr;
- }
return utility;
}
diff --git a/lldb/source/Plugins/Platform/Windows/PlatformWindows.h b/lldb/source/Plugins/Platform/Windows/PlatformWindows.h
index 6f8c20a8456a2..392ca75f64664 100644
--- a/lldb/source/Plugins/Platform/Windows/PlatformWindows.h
+++ b/lldb/source/Plugins/Platform/Windows/PlatformWindows.h
@@ -85,9 +85,8 @@ class PlatformWindows : public RemoteAwarePlatform {
std::vector<ArchSpec> m_supported_architectures;
private:
- std::unique_ptr<lldb_private::UtilityFunction>
- MakeLoadImageUtilityFunction(lldb_private::ExecutionContext &context,
- lldb_private::Status &status);
+ llvm::Expected<std::unique_ptr<lldb_private::UtilityFunction>>
+ MakeLoadImageUtilityFunction(lldb_private::ExecutionContext &context);
lldb_private::Status EvaluateLoaderExpression(lldb_private::Process *process,
const char *expression,
diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp
index fe58b732a6497..546b676f72f63 100644
--- a/lldb/source/Target/Process.cpp
+++ b/lldb/source/Target/Process.cpp
@@ -6698,14 +6698,25 @@ Status Process::UpdateAutomaticSignalFiltering() {
return Status();
}
-UtilityFunction *Process::GetLoadImageUtilityFunction(
+llvm::Expected<UtilityFunction &> Process::GetLoadImageUtilityFunction(
Platform *platform,
- llvm::function_ref<std::unique_ptr<UtilityFunction>()> factory) {
+ llvm::function_ref<llvm::Expected<std::unique_ptr<UtilityFunction>>()>
+ factory) {
if (platform != GetTarget().GetPlatform().get())
- return nullptr;
- llvm::call_once(m_dlopen_utility_func_flag_once,
- [&] { m_dlopen_utility_func_up = factory(); });
- return m_dlopen_utility_func_up.get();
+ return llvm::createStringError(
+ "the platform requesting the load-image utility function is not "
+ "the target's platform");
+ llvm::call_once(m_dlopen_utility_func_flag_once, [&] {
+ llvm::Expected<std::unique_ptr<UtilityFunction>> factory_result = factory();
+ if (factory_result)
+ m_dlopen_utility_func_up = std::move(*factory_result);
+ else
+ m_dlopen_utility_func_error =
+ Status::FromError(factory_result.takeError());
+ });
+ if (m_dlopen_utility_func_up)
+ return *m_dlopen_utility_func_up;
+ return m_dlopen_utility_func_error.ToError();
}
llvm::Expected<TraceSupportedResponse> Process::TraceSupported() {
diff --git a/lldb/test/API/functionalities/load_unload/process-load-error-postmortem/Makefile b/lldb/test/API/functionalities/load_unload/process-load-error-postmortem/Makefile
new file mode 100644
index 0000000000000..c7256ac7e3263
--- /dev/null
+++ b/lldb/test/API/functionalities/load_unload/process-load-error-postmortem/Makefile
@@ -0,0 +1,4 @@
+C_SOURCES = main.c
+MAKE_DSYM := NO
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/load_unload/process-load-error-postmortem/TestProcessLoadErrorPostmortem.py b/lldb/test/API/functionalities/load_unload/process-load-error-postmortem/TestProcessLoadErrorPostmortem.py
new file mode 100644
index 0000000000000..320953747df56
--- /dev/null
+++ b/lldb/test/API/functionalities/load_unload/process-load-error-postmortem/TestProcessLoadErrorPostmortem.py
@@ -0,0 +1,51 @@
+"""
+Test that Process::LoadImage reports a real error, not an empty/null one, on
+every call after the first, when there is no live process to call dlopen in
+(e.g. debugging a core file).
+"""
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class ProcessLoadErrorPostmortemTestCase(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ @skipIfWindows # Windows does not support "process load" the same way.
+ @skipIfRemote # save-core / core loading is local-target only here.
+ def test_load_image_error_replayed_on_core(self):
+ self.build()
+ exe = self.getBuildArtifact("a.out")
+ core = self.getBuildArtifact("process.core")
+
+ (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
+ self, "// break here", lldb.SBFileSpec("main.c")
+ )
+
+ self.runCmd("process save-core --style=stack " + core)
+ process.Kill()
+ self.dbg.DeleteTarget(target)
+
+ target = self.dbg.CreateTarget(exe)
+ process = target.LoadCore(core)
+ self.assertTrue(process.IsValid(), "Could not load the core file")
+
+ nonexistent = lldb.SBFileSpec(
+ "/NoSuchDir/NoSuchSubdir/not-a-real-library", False
+ )
+
+ messages = []
+ for _ in range(2):
+ error = lldb.SBError()
+ token = process.LoadImage(nonexistent, error)
+ self.assertEqual(token, lldb.LLDB_INVALID_IMAGE_TOKEN)
+ self.assertTrue(error.Fail())
+ message = error.GetCString()
+ self.assertIsNotNone(message)
+ self.assertNotEqual(message, "")
+ self.assertNotIn("(null)", message)
+ messages.append(message)
+
+ self.assertEqual(messages[0], messages[1])
diff --git a/lldb/test/API/functionalities/load_unload/process-load-error-postmortem/main.c b/lldb/test/API/functionalities/load_unload/process-load-error-postmortem/main.c
new file mode 100644
index 0000000000000..fa0d6e8b5f797
--- /dev/null
+++ b/lldb/test/API/functionalities/load_unload/process-load-error-postmortem/main.c
@@ -0,0 +1,4 @@
+int main() {
+ int stack_int = 5;
+ return stack_int; // break here
+}
diff --git a/lldb/test/API/functionalities/postmortem/wow64_minidump/TestWow64MiniDump.py b/lldb/test/API/functionalities/postmortem/wow64_minidump/TestWow64MiniDump.py
index b4c19ee8fc7b1..eb4d7a2eee29e 100644
--- a/lldb/test/API/functionalities/postmortem/wow64_minidump/TestWow64MiniDump.py
+++ b/lldb/test/API/functionalities/postmortem/wow64_minidump/TestWow64MiniDump.py
@@ -65,3 +65,30 @@ def test_stack_info_in_wow64_mini_dump(self):
eip = frame.FindRegister("pc")
self.assertTrue(eip.IsValid())
self.assertEqual(pc, eip.GetValueAsUnsigned())
+
+ @skipIfLLVMTargetMissing("X86")
+ def test_load_image_error_replayed_on_wow64_mini_dump(self):
+ """Test that Process::LoadImage reports a real error, not an empty
+ one, on every call, when debugging a minidump."""
+ self.runCmd("platform select remote-windows")
+ target = self.dbg.CreateTarget("")
+ process = target.LoadCore("fizzbuzz_wow64.dmp")
+ self.assertTrue(process, PROCESS_IS_VALID)
+
+ nonexistent = lldb.SBFileSpec(
+ r"Z:\NoSuchDir\NoSuchSubdir\not-a-real-library.dll", False
+ )
+
+ messages = []
+ for _ in range(2):
+ error = lldb.SBError()
+ token = process.LoadImage(nonexistent, error)
+ self.assertEqual(token, lldb.LLDB_INVALID_IMAGE_TOKEN)
+ self.assertTrue(error.Fail())
+ message = error.GetCString()
+ self.assertIsNotNone(message)
+ self.assertNotEqual(message, "")
+ self.assertNotIn("(null)", message)
+ messages.append(message)
+
+ self.assertEqual(messages[0], messages[1])
More information about the lldb-commits
mailing list