[Lldb-commits] [lldb] [lldb] Fallback to comparing modules by their canonical paths (PR #210041)
Charles Zablit via lldb-commits
lldb-commits at lists.llvm.org
Fri Jul 17 09:03:20 PDT 2026
https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/210041
>From 9e0557ebd3461fee9f1b97dc32785664d2b41ce1 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Thu, 16 Jul 2026 13:09:40 +0100
Subject: [PATCH 1/3] [lldb] Fallback to comparing Modules by their canonical
paths
---
lldb/source/Core/Module.cpp | 24 ++++++-
.../breakpoint_via_symlink/Makefile | 3 +
.../TestBreakpointViaSymlink.py | 70 +++++++++++++++++++
.../breakpoint/breakpoint_via_symlink/main.c | 3 +
4 files changed, 99 insertions(+), 1 deletion(-)
create mode 100644 lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/Makefile
create mode 100644 lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/TestBreakpointViaSymlink.py
create mode 100644 lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/main.c
diff --git a/lldb/source/Core/Module.cpp b/lldb/source/Core/Module.cpp
index dcdfd67d3e303..1493c0291c572 100644
--- a/lldb/source/Core/Module.cpp
+++ b/lldb/source/Core/Module.cpp
@@ -54,6 +54,7 @@
#endif
#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallString.h"
#include "llvm/Support/Compiler.h"
#include "llvm/Support/DJB.h"
#include "llvm/Support/FileSystem.h"
@@ -1455,6 +1456,25 @@ bool Module::SetLoadAddress(Target &target, lldb::addr_t value,
return false;
}
+// A module's stored path can be an alias of the same on-disk file that a lookup
+// spec refers to:
+// - Windows: a subst/mapped drive or a symbolic link
+// - POSIX: a symbolic link
+// In those cases, MatchesModuleSpec fails even though both name the same file.
+// This is a fall back which compares the canonicalized real paths.
+static bool FileSpecsResolveToSameFile(const FileSpec &pattern,
+ const FileSpec &file) {
+ if (!pattern.GetDirectory() || !file)
+ return false;
+ FileSystem &fs = FileSystem::Instance();
+ llvm::SmallString<256> pattern_real, file_real;
+ if (fs.GetRealPath(pattern.GetPath(), pattern_real) ||
+ fs.GetRealPath(file.GetPath(), file_real))
+ return false;
+ return FileSpec::Equal(FileSpec(pattern_real), FileSpec(file_real),
+ /*full=*/true);
+}
+
bool Module::MatchesModuleSpec(const ModuleSpec &module_ref) {
const UUID &uuid = module_ref.GetUUID();
@@ -1465,7 +1485,9 @@ bool Module::MatchesModuleSpec(const ModuleSpec &module_ref) {
const FileSpec &file_spec = module_ref.GetFileSpec();
if (!FileSpec::Match(file_spec, m_file) &&
- !FileSpec::Match(file_spec, m_platform_file))
+ !FileSpec::Match(file_spec, m_platform_file) &&
+ !FileSpecsResolveToSameFile(file_spec, m_file) &&
+ !FileSpecsResolveToSameFile(file_spec, m_platform_file))
return false;
const FileSpec &platform_file_spec = module_ref.GetPlatformFileSpec();
diff --git a/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/Makefile b/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/Makefile
new file mode 100644
index 0000000000000..10495940055b6
--- /dev/null
+++ b/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/TestBreakpointViaSymlink.py b/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/TestBreakpointViaSymlink.py
new file mode 100644
index 0000000000000..defb23cc16f37
--- /dev/null
+++ b/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/TestBreakpointViaSymlink.py
@@ -0,0 +1,70 @@
+"""
+Test that creating a target through a symlinked (or otherwise aliased)
+executable does not create a duplicate module at launch.
+
+When a target is created via a path that is not the file's canonical path (a
+symlink, or on Windows a subst/mapped drive), the running process reports its
+*real* image path at launch. Module matching needs to compare the canonicalized
+paths.
+"""
+
+import os
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestBreakpointViaSymlink(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ @skipIfRemote
+ def test_breakpoint_resolves_once_via_symlink(self):
+ self.build()
+ real_exe = self.getBuildArtifact("a.out")
+
+ # A symlink whose real path differs from the path used to create the
+ # target.
+ link_exe = self.getBuildArtifact("a.out.symlink")
+ try:
+ if os.path.lexists(link_exe):
+ os.remove(link_exe)
+ os.symlink(real_exe, link_exe)
+ except (OSError, NotImplementedError) as e:
+ # Windows needs Developer Mode / SeCreateSymbolicLinkPrivilege.
+ self.skipTest("could not create a symlink: %s" % e)
+ self.addTearDownHook(lambda: os.remove(link_exe))
+ self.assertNotEqual(os.path.realpath(link_exe), link_exe)
+
+ target = self.dbg.CreateTarget(link_exe)
+ self.assertTrue(target, VALID_TARGET)
+
+ bkpt = target.BreakpointCreateBySourceRegex(
+ "break here", lldb.SBFileSpec("main.c")
+ )
+ # Resolves against the preloaded module: exactly one location.
+ self.assertEqual(
+ bkpt.GetNumLocations(), 1, "one breakpoint location before launch"
+ )
+
+ process = target.LaunchSimple(None, None, self.get_process_working_directory())
+ self.assertState(process.GetState(), lldb.eStateStopped)
+
+ # After launch the running image may report its real path. With module
+ # matching canonicalizing paths, the loaded module is the one lldb already
+ # preloaded, so the breakpoint keeps exactly one resolved location.
+ self.assertEqual(bkpt.GetNumLocations(), 1)
+ self.assertEqual(bkpt.GetNumResolvedLocations(), 1)
+
+ # The executable appears as a single module (no duplicate). The module
+ # keeps the path it was created with (the symlink), so compare by real
+ # path.
+ exe_real = os.path.realpath(real_exe)
+ matches = [
+ m
+ for m in target.module_iter()
+ if os.path.realpath(m.GetFileSpec().fullpath) == exe_real
+ ]
+ self.assertEqual(
+ len(matches), 1, "exactly one executable module, got %d" % len(matches)
+ )
diff --git a/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/main.c b/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/main.c
new file mode 100644
index 0000000000000..9ba28de2ae01c
--- /dev/null
+++ b/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/main.c
@@ -0,0 +1,3 @@
+int main(int argc, char **argv) {
+ return 0; // break here
+}
>From 336ff8ba486285619d46ab359a08e3cb63f07677 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Thu, 16 Jul 2026 13:31:40 +0100
Subject: [PATCH 2/3] fixup! [lldb] Fallback to comparing Modules by their
canonical paths
---
lldb/source/Core/Module.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lldb/source/Core/Module.cpp b/lldb/source/Core/Module.cpp
index 1493c0291c572..21bbf7f474a32 100644
--- a/lldb/source/Core/Module.cpp
+++ b/lldb/source/Core/Module.cpp
@@ -1464,7 +1464,7 @@ bool Module::SetLoadAddress(Target &target, lldb::addr_t value,
// This is a fall back which compares the canonicalized real paths.
static bool FileSpecsResolveToSameFile(const FileSpec &pattern,
const FileSpec &file) {
- if (!pattern.GetDirectory() || !file)
+ if (pattern.GetDirectory().empty() || !file)
return false;
FileSystem &fs = FileSystem::Instance();
llvm::SmallString<256> pattern_real, file_real;
>From 88a36c2abd886938a07ed129d02a530599467e07 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Fri, 17 Jul 2026 17:03:07 +0100
Subject: [PATCH 3/3] address comments
---
lldb/source/Core/Module.cpp | 14 ++---
lldb/source/Target/Target.cpp | 22 ++++++++
.../TestBreakpointViaSymlink.py | 53 ++++++++++++++++---
.../breakpoint/breakpoint_via_symlink/main.c | 7 +++
4 files changed, 82 insertions(+), 14 deletions(-)
diff --git a/lldb/source/Core/Module.cpp b/lldb/source/Core/Module.cpp
index 21bbf7f474a32..c085c28acc02d 100644
--- a/lldb/source/Core/Module.cpp
+++ b/lldb/source/Core/Module.cpp
@@ -1462,16 +1462,16 @@ bool Module::SetLoadAddress(Target &target, lldb::addr_t value,
// - POSIX: a symbolic link
// In those cases, MatchesModuleSpec fails even though both name the same file.
// This is a fall back which compares the canonicalized real paths.
-static bool FileSpecsResolveToSameFile(const FileSpec &pattern,
- const FileSpec &file) {
- if (pattern.GetDirectory().empty() || !file)
+static bool FileSpecsResolveToSameFile(const FileSpec &lhs,
+ const FileSpec &rhs) {
+ if (lhs.GetDirectory().empty() || rhs.GetDirectory().empty())
return false;
FileSystem &fs = FileSystem::Instance();
- llvm::SmallString<256> pattern_real, file_real;
- if (fs.GetRealPath(pattern.GetPath(), pattern_real) ||
- fs.GetRealPath(file.GetPath(), file_real))
+ llvm::SmallString<256> lhs_real, rhs_real;
+ if (fs.GetRealPath(lhs.GetPath(), lhs_real) ||
+ fs.GetRealPath(rhs.GetPath(), rhs_real))
return false;
- return FileSpec::Equal(FileSpec(pattern_real), FileSpec(file_real),
+ return FileSpec::Equal(FileSpec(lhs_real), FileSpec(rhs_real),
/*full=*/true);
}
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index e851495a81f38..cddacab059b93 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -33,6 +33,7 @@
#include "lldb/Expression/REPL.h"
#include "lldb/Expression/UserExpression.h"
#include "lldb/Expression/UtilityFunction.h"
+#include "lldb/Host/FileSystem.h"
#include "lldb/Host/Host.h"
#include "lldb/Host/PosixApi.h"
#include "lldb/Host/StreamFile.h"
@@ -75,6 +76,7 @@
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/SetVector.h"
+#include "llvm/ADT/SmallString.h"
#include "llvm/Support/ErrorExtras.h"
#include "llvm/Support/ThreadPool.h"
@@ -3627,6 +3629,26 @@ Status Target::Launch(ProcessLaunchInfo &launch_info, Stream *stream) {
FinalizeFileActions(launch_info);
+ // Preserve the path the user used to create this target when computing
+ // argv[0].
+ if (platform_sp && platform_sp->IsHost()) {
+ if (llvm::StringRef arg0 = GetArg0(); !arg0.empty()) {
+ FileSpec arg0_spec(arg0);
+ FileSpec exe_spec = launch_info.GetExecutableFile();
+ if (exe_spec && arg0_spec != exe_spec) {
+ FileSystem &fs = FileSystem::Instance();
+ llvm::SmallString<256> arg0_real, exe_real;
+ if (!fs.GetRealPath(arg0_spec.GetPath(), arg0_real) &&
+ !fs.GetRealPath(exe_spec.GetPath(), exe_real) &&
+ arg0_real == exe_real) {
+ launch_info.SetExecutableFile(arg0_spec, /*add_as_first_arg=*/false);
+ if (launch_info.GetArguments().GetArgumentCount() > 0)
+ launch_info.GetArguments().ReplaceArgumentAtIndex(0, arg0);
+ }
+ }
+ }
+ }
+
if (state == eStateConnected) {
if (launch_info.GetFlags().Test(eLaunchFlagLaunchInTTY))
return Status::FromErrorString(
diff --git a/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/TestBreakpointViaSymlink.py b/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/TestBreakpointViaSymlink.py
index defb23cc16f37..c52d587283b2f 100644
--- a/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/TestBreakpointViaSymlink.py
+++ b/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/TestBreakpointViaSymlink.py
@@ -18,13 +18,9 @@
class TestBreakpointViaSymlink(TestBase):
NO_DEBUG_INFO_TESTCASE = True
- @skipIfRemote
- def test_breakpoint_resolves_once_via_symlink(self):
- self.build()
- real_exe = self.getBuildArtifact("a.out")
-
- # A symlink whose real path differs from the path used to create the
- # target.
+ def make_symlink(self, real_exe):
+ """Create a symlink to real_exe whose real path differs, skipping the
+ test if the platform can't create one."""
link_exe = self.getBuildArtifact("a.out.symlink")
try:
if os.path.lexists(link_exe):
@@ -35,6 +31,13 @@ def test_breakpoint_resolves_once_via_symlink(self):
self.skipTest("could not create a symlink: %s" % e)
self.addTearDownHook(lambda: os.remove(link_exe))
self.assertNotEqual(os.path.realpath(link_exe), link_exe)
+ return link_exe
+
+ @skipIfRemote
+ def test_breakpoint_resolves_once_via_symlink(self):
+ self.build()
+ real_exe = self.getBuildArtifact("a.out")
+ link_exe = self.make_symlink(real_exe)
target = self.dbg.CreateTarget(link_exe)
self.assertTrue(target, VALID_TARGET)
@@ -68,3 +71,39 @@ def test_breakpoint_resolves_once_via_symlink(self):
self.assertEqual(
len(matches), 1, "exactly one executable module, got %d" % len(matches)
)
+
+ @skipIfRemote
+ def test_arg0_preserved_when_module_reused_via_symlink(self):
+ """Canonical-path module matching lets a second target reuse the module
+ of a first target that named the same file differently. That must not
+ change how the second target is launched: argv[0] has to stay the path
+ the user created *that* target with (multicall binaries dispatch on it).
+ """
+ self.build()
+ real_exe = self.getBuildArtifact("a.out")
+ link_exe = self.make_symlink(real_exe)
+
+ # Create a target with the *real* path first.
+ real_target = self.dbg.CreateTarget(real_exe)
+ self.assertTrue(real_target, VALID_TARGET)
+
+ # Create a second target via the *symlink*.
+ link_target = self.dbg.CreateTarget(link_exe)
+ self.assertTrue(link_target, VALID_TARGET)
+
+ wd = self.get_process_working_directory()
+ process = link_target.LaunchSimple(None, None, wd)
+ self.assertTrue(process, PROCESS_IS_VALID)
+ self.assertState(process.GetState(), lldb.eStateExited)
+ arg0 = lldbutil.read_file_from_process_wd(self, "arg0.txt").strip()
+
+ self.assertEqual(
+ os.path.basename(arg0),
+ os.path.basename(link_exe),
+ "argv[0] should be the symlink used to create the target, got %r" % arg0,
+ )
+ self.assertNotEqual(
+ os.path.basename(arg0),
+ os.path.basename(real_exe),
+ "argv[0] leaked the reused module's real path: %r" % arg0,
+ )
diff --git a/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/main.c b/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/main.c
index 9ba28de2ae01c..38a53355e34f8 100644
--- a/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/main.c
+++ b/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/main.c
@@ -1,3 +1,10 @@
+#include <stdio.h>
+
int main(int argc, char **argv) {
+ FILE *f = fopen("arg0.txt", "w");
+ if (f) {
+ fputs(argc > 0 ? argv[0] : "", f);
+ fclose(f);
+ }
return 0; // break here
}
More information about the lldb-commits
mailing list