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 <[email protected]> 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 <[email protected]> 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 <[email protected]> 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 } _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
