[Lldb-commits] [lldb] [lldb] Replay the cached error from GetLoadImageUtilityFunction (PR #226986)
Yao Qi via lldb-commits
lldb-commits at lists.llvm.org
Mon Sep 28 07:55:27 PDT 2026
https://github.com/qiyao updated https://github.com/llvm/llvm-project/pull/226986
>From 6c0f3d15c89bca38c1bd65869e07bcafb58fd0bf Mon Sep 17 00:00:00 2001
From: Yao Qi <yao_qi at apple.com>
Date: Sat, 26 Sep 2026 08:41:37 +0100
Subject: [PATCH 1/2] [lldb] Replay the cached error from
GetLoadImageUtilityFunction
`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`.
---
lldb/include/lldb/Target/Process.h | 10 +++-
.../Plugins/Platform/POSIX/PlatformPOSIX.cpp | 6 ++-
.../Platform/Windows/PlatformWindows.cpp | 10 ++--
lldb/source/Target/Process.cpp | 10 ++--
.../process-load-error-postmortem/Makefile | 4 ++
.../TestProcessLoadErrorPostmortem.py | 51 +++++++++++++++++++
.../process-load-error-postmortem/main.c | 4 ++
.../wow64_minidump/TestWow64MiniDump.py | 27 ++++++++++
8 files changed, 111 insertions(+), 11 deletions(-)
create mode 100644 lldb/test/API/functionalities/load_unload/process-load-error-postmortem/Makefile
create mode 100644 lldb/test/API/functionalities/load_unload/process-load-error-postmortem/TestProcessLoadErrorPostmortem.py
create mode 100644 lldb/test/API/functionalities/load_unload/process-load-error-postmortem/main.c
diff --git a/lldb/include/lldb/Target/Process.h b/lldb/include/lldb/Target/Process.h
index 984a4ef880800e..3c6a62e43a66ae 100644
--- a/lldb/include/lldb/Target/Process.h
+++ b/lldb/include/lldb/Target/Process.h
@@ -664,13 +664,16 @@ class Process : public std::enable_shared_from_this<Process>,
/// A function that will be called only once per-process in a
/// thread-safe way to create the UtilityFunction if it has not
/// been initialized yet.
+ /// \param[out] error
+ /// If the UtilityFunction could not be created, the reason why.
///
/// \return
/// The cached utility function or null if the platform is not the
- /// same as the target's platform.
+ /// same as the target's platform, or if it could not be created.
UtilityFunction *GetLoadImageUtilityFunction(
Platform *platform,
- llvm::function_ref<std::unique_ptr<UtilityFunction>()> factory);
+ llvm::function_ref<std::unique_ptr<UtilityFunction>()> factory,
+ Status &error);
/// Get the dynamic loader plug-in for this process.
///
@@ -3590,6 +3593,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,
+ /// success or failure.
+ 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 2a5a3d55ec98c8..9e2921690fc2cc 100644
--- a/lldb/source/Plugins/Platform/POSIX/PlatformPOSIX.cpp
+++ b/lldb/source/Plugins/Platform/POSIX/PlatformPOSIX.cpp
@@ -715,9 +715,11 @@ uint32_t PlatformPOSIX::DoLoadImage(lldb_private::Process *process,
// 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> {
+ this,
+ [&]() -> std::unique_ptr<UtilityFunction> {
return MakeLoadImageUtilityFunction(exe_ctx, error);
- });
+ },
+ error);
// If we couldn't make it, the error will be in error, so we can exit here.
if (!dlopen_utility_func)
return LLDB_INVALID_IMAGE_TOKEN;
diff --git a/lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp b/lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp
index 0e1938b080bba7..2c56a077b9a88c 100644
--- a/lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp
+++ b/lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp
@@ -229,10 +229,12 @@ uint32_t PlatformWindows::DoLoadImage(Process *process,
thread->CalculateExecutionContext(context);
Status status;
- UtilityFunction *loader =
- process->GetLoadImageUtilityFunction(this, [&]() -> std::unique_ptr<UtilityFunction> {
- return MakeLoadImageUtilityFunction(context, status);
- });
+ UtilityFunction *loader = process->GetLoadImageUtilityFunction(
+ this,
+ [&]() -> std::unique_ptr<UtilityFunction> {
+ return MakeLoadImageUtilityFunction(context, error);
+ },
+ error);
if (loader == nullptr)
return LLDB_INVALID_IMAGE_TOKEN;
diff --git a/lldb/source/Target/Process.cpp b/lldb/source/Target/Process.cpp
index c86160ae0432d6..27be64355a5b38 100644
--- a/lldb/source/Target/Process.cpp
+++ b/lldb/source/Target/Process.cpp
@@ -6712,11 +6712,15 @@ Status Process::UpdateAutomaticSignalFiltering() {
UtilityFunction *Process::GetLoadImageUtilityFunction(
Platform *platform,
- llvm::function_ref<std::unique_ptr<UtilityFunction>()> factory) {
+ llvm::function_ref<std::unique_ptr<UtilityFunction>()> factory,
+ Status &error) {
if (platform != GetTarget().GetPlatform().get())
return nullptr;
- llvm::call_once(m_dlopen_utility_func_flag_once,
- [&] { m_dlopen_utility_func_up = factory(); });
+ llvm::call_once(m_dlopen_utility_func_flag_once, [&] {
+ m_dlopen_utility_func_up = factory();
+ m_dlopen_utility_func_error = error.Clone();
+ });
+ error = m_dlopen_utility_func_error.Clone();
return m_dlopen_utility_func_up.get();
}
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 00000000000000..c7256ac7e32634
--- /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 00000000000000..320953747df567
--- /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 00000000000000..fa0d6e8b5f7974
--- /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 b4c19ee8fc7b12..eb4d7a2eee29e3 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])
>From 2566538a18c26f39ef402285095d7c44bfdf0db5 Mon Sep 17 00:00:00 2001
From: Yao Qi <qiyaoltc at gmail.com>
Date: Mon, 28 Sep 2026 15:55:10 +0100
Subject: [PATCH 2/2] Apply batched suggestions from code review
Co-authored-by: Charles Zablit <c_zablit at apple.com>
---
lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp b/lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp
index 2c56a077b9a88c..a8d22cf2a235b3 100644
--- a/lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp
+++ b/lldb/source/Plugins/Platform/Windows/PlatformWindows.cpp
@@ -228,7 +228,6 @@ uint32_t PlatformWindows::DoLoadImage(Process *process,
ExecutionContext context;
thread->CalculateExecutionContext(context);
- Status status;
UtilityFunction *loader = process->GetLoadImageUtilityFunction(
this,
[&]() -> std::unique_ptr<UtilityFunction> {
@@ -255,6 +254,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,
More information about the lldb-commits
mailing list