[Lldb-commits] [lldb] [lldb] Unload Wasm modules the stub no longer reports (PR #227814)
Jonas Devlieghere via lldb-commits
lldb-commits at lists.llvm.org
Fri Oct 2 13:47:09 PDT 2026
https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/227814
>From f5d047f7bc6692bba7b21da7957e4ded4bc578c9 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Wed, 30 Sep 2026 09:42:52 -0700
Subject: [PATCH] [lldb] Unload Wasm modules the stub no longer reports
When a Wasm engine such as JavaScriptCore reloads a page, the modules
it ran go away and new instances take their place. LLDB kept two kinds
of stale module around.
The first module the stub ever reported stayed in the image list for
good. ProcessGDBRemote::LoadModules never unloads the executable, and
Target::GetExecutableModule took that module for one. Now that the
target no longer guesses its executable, this only needs a test.
A module reloaded under the same name matched the module read from
memory at its old address, and LLDB then moved that module to the new
one. That trips an assertion in ObjectFileWasm::SetLoadAddress, and
otherwise leaves a module whose image came from an instance that is
gone. A module read from memory now only matches a spec at the address
it was read from, like ModuleSpec::Matches already does for two specs.
rdar://175013476
---
lldb/source/Core/Module.cpp | 5 +
.../gdb_remote_client/TestWasm.py | 94 +++++++++++++++++++
2 files changed, 99 insertions(+)
diff --git a/lldb/source/Core/Module.cpp b/lldb/source/Core/Module.cpp
index 2d7cffb7b4398..072ba9bf80c54 100644
--- a/lldb/source/Core/Module.cpp
+++ b/lldb/source/Core/Module.cpp
@@ -1489,6 +1489,11 @@ bool Module::MatchesModuleSpec(const ModuleSpec &module_ref) {
if (object_name != GetObjectName())
return false;
}
+
+ // A module read from memory is the image at the address it was read from.
+ std::optional<lldb::addr_t> load_addr = module_ref.GetLoadAddress();
+ if (load_addr && m_memory_module_addr && *load_addr != *m_memory_module_addr)
+ return false;
return true;
}
diff --git a/lldb/test/API/functionalities/gdb_remote_client/TestWasm.py b/lldb/test/API/functionalities/gdb_remote_client/TestWasm.py
index 1d284be6fd7ed..fb02c4eecbdbb 100644
--- a/lldb/test/API/functionalities/gdb_remote_client/TestWasm.py
+++ b/lldb/test/API/functionalities/gdb_remote_client/TestWasm.py
@@ -12,6 +12,11 @@
MODULE_ID = 16
SECOND_MODULE_ID = 26
+# Ids of the instances the fake stub loads in place of the others when it
+# reloads them. A reloaded module is a new instance, with an id of its own.
+RELOADED_MODULE_ID = 36
+RELOADED_SECOND_MODULE_ID = 46
+
# The address spaces an address can point into, in the bits above the id of the
# instance the address belongs to. The object space holds the module image, so
# that is the space a module is loaded in.
@@ -327,6 +332,22 @@ def qWasmLocal(self, packet):
return "E03"
+class ReloadingResponder(MyResponder):
+ """
+ A fake stub that replaces the modules it has loaded with others when it
+ continues, as an engine does when the page running them reloads, and stops
+ to report the change.
+ """
+
+ def __init__(self, modules, reloaded_modules):
+ MyResponder.__init__(self, modules)
+ self._reloaded_modules = reloaded_modules
+
+ def cont(self):
+ self._modules = self._reloaded_modules
+ return "T05thread:1;library:;"
+
+
class TestWasm(GDBRemoteTestBase):
@skipIfAsan
@skipIfXmlSupportMissing
@@ -1029,6 +1050,79 @@ def test_read_global_legacy_server_other_instance(self):
self.assertPacketLogReceived(["qWasmGlobal:0;0"])
+ def reload_modules(self, modules, reloaded_modules):
+ """
+ Connect to a fake stub holding the given modules, continue it until it
+ has reloaded them as the others, and return its target.
+ """
+ self.server.responder = ReloadingResponder(modules, reloaded_modules)
+ target = self.dbg.CreateTarget("")
+ process = self.connect(target, "wasm")
+ lldbutil.expect_state_changes(
+ self, self.dbg.GetListener(), process, [lldb.eStateStopped]
+ )
+ self.assertSuccess(process.Continue())
+ return target
+
+ def modules_in_target(self, target):
+ """
+ The name of every module in the target, together with the address its
+ image was read from.
+ """
+ return [
+ (
+ module.GetFileSpec().GetFilename(),
+ module.GetObjectFileHeaderAddress().GetLoadAddress(target),
+ )
+ for module in target.modules
+ ]
+
+ @skipIfAsan
+ @skipIfXmlSupportMissing
+ def test_reload_modules(self):
+ """Test that the modules a stub no longer reports leave the target,
+ the first one it loaded included."""
+
+ modules = [
+ self.build_wasm_module("test_wasm"),
+ self.build_wasm_module("second_test_wasm", module_id=SECOND_MODULE_ID),
+ ]
+ reloaded_modules = [
+ self.build_wasm_module("reloaded_test_wasm", module_id=RELOADED_MODULE_ID),
+ self.build_wasm_module(
+ "reloaded_second_test_wasm", module_id=RELOADED_SECOND_MODULE_ID
+ ),
+ ]
+ target = self.reload_modules(modules, reloaded_modules)
+
+ self.assertEqual(
+ self.modules_in_target(target),
+ [(module.name, module.load_address) for module in reloaded_modules],
+ )
+
+ @skipIfAsan
+ @skipIfXmlSupportMissing
+ def test_reload_modules_under_same_name(self):
+ """Test that a module a stub reloads under the name it had is read
+ again from where it was reloaded."""
+
+ modules = [
+ self.build_wasm_module("test_wasm"),
+ self.build_wasm_module("second_test_wasm", module_id=SECOND_MODULE_ID),
+ ]
+ reloaded_modules = [
+ WasmModule(module.obj_path, module.name, module_id=module_id)
+ for module, module_id in zip(
+ modules, [RELOADED_MODULE_ID, RELOADED_SECOND_MODULE_ID]
+ )
+ ]
+ target = self.reload_modules(modules, reloaded_modules)
+
+ self.assertEqual(
+ self.modules_in_target(target),
+ [(module.name, module.load_address) for module in reloaded_modules],
+ )
+
@skipIfXmlSupportMissing
def test_non_wasm_process(self):
"""Test that the plugin falls back to plain GDB remote debugging when
More information about the lldb-commits
mailing list