[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
Wed Aug 12 10:02:47 PDT 2026


https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/210041

>From 41acefaf78229d9fb9fa3490c496b9392a9f6214 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Mon, 10 Aug 2026 18:50:48 +0100
Subject: [PATCH] [lldb] Fall back to comparing modules by their canonical
 paths

---
 lldb/source/Core/Module.cpp                   | 24 ++++-
 lldb/source/Target/Target.cpp                 | 22 +++++
 .../breakpoint_via_symlink/Makefile           |  3 +
 .../TestBreakpointViaSymlink.py               | 99 +++++++++++++++++++
 .../breakpoint/breakpoint_via_symlink/main.c  | 10 ++
 .../module_aliased_path/Makefile              |  5 +
 .../TestModuleAliasedPath.py                  | 87 ++++++++++++++++
 .../functionalities/module_aliased_path/foo.c |  3 +
 .../module_aliased_path/main.c                |  3 +
 9 files changed, 255 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
 create mode 100644 lldb/test/API/functionalities/module_aliased_path/Makefile
 create mode 100644 lldb/test/API/functionalities/module_aliased_path/TestModuleAliasedPath.py
 create mode 100644 lldb/test/API/functionalities/module_aliased_path/foo.c
 create mode 100644 lldb/test/API/functionalities/module_aliased_path/main.c

diff --git a/lldb/source/Core/Module.cpp b/lldb/source/Core/Module.cpp
index da3fa21dc225a..8cee132d7bc3a 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 &lhs,
+                                       const FileSpec &rhs) {
+  if (lhs.GetDirectory().empty() || rhs.GetDirectory().empty())
+    return false;
+  FileSystem &fs = FileSystem::Instance();
+  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(lhs_real), FileSpec(rhs_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/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index 12af26b599b30..a9ebcc688474f 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"
 
@@ -3639,6 +3641,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/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..5b7fd6a192cde
--- /dev/null
+++ b/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/TestBreakpointViaSymlink.py
@@ -0,0 +1,99 @@
+"""
+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
+
+    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):
+                os.remove(link_exe)
+            os.symlink(real_exe, link_exe)
+        except (OSError, NotImplementedError) as e:
+            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)
+
+        bkpt = target.BreakpointCreateBySourceRegex(
+            "break here", lldb.SBFileSpec("main.c")
+        )
+        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)
+
+        self.assertEqual(bkpt.GetNumLocations(), 1)
+        self.assertEqual(bkpt.GetNumResolvedLocations(), 1)
+
+        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)
+        )
+
+    @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)
+
+        real_target = self.dbg.CreateTarget(real_exe)
+        self.assertTrue(real_target, VALID_TARGET)
+
+        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
new file mode 100644
index 0000000000000..38a53355e34f8
--- /dev/null
+++ b/lldb/test/API/functionalities/breakpoint/breakpoint_via_symlink/main.c
@@ -0,0 +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
+}
diff --git a/lldb/test/API/functionalities/module_aliased_path/Makefile b/lldb/test/API/functionalities/module_aliased_path/Makefile
new file mode 100644
index 0000000000000..e2607a0b08a7c
--- /dev/null
+++ b/lldb/test/API/functionalities/module_aliased_path/Makefile
@@ -0,0 +1,5 @@
+C_SOURCES := main.c
+DYLIB_C_SOURCES := foo.c
+DYLIB_NAME := foo
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/module_aliased_path/TestModuleAliasedPath.py b/lldb/test/API/functionalities/module_aliased_path/TestModuleAliasedPath.py
new file mode 100644
index 0000000000000..6367c8da0c11e
--- /dev/null
+++ b/lldb/test/API/functionalities/module_aliased_path/TestModuleAliasedPath.py
@@ -0,0 +1,87 @@
+"""
+Test that a module registered with the target through an aliased directory is
+not duplicated when the process loads the same file under its canonical path.
+
+A process reports the canonical path of each image it loads: on Windows the
+LOAD_DLL_DEBUG_EVENT path comes from GetFinalPathNameByHandleW, which resolves
+symbolic links, junctions and subst/mapped drives. The path LLDB already has for
+the same file is whatever it was given, and test harnesses hand it the build
+directory as spelled on the command line -- swift-ci builds under a subst drive,
+so the module is added as `T:\\...\\foo.dll` while the load event reports
+`C:\\Users\\...\\foo.dll`. Matching those textually fails, so LLDB adds a second
+module for a file it already has and the first one is left behind, unloaded,
+with its breakpoint locations unresolved.
+"""
+
+import os
+import subprocess
+import sys
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+ at skipIfTargetDoesNotSupportSharedLibraries()
+class TestModuleAliasedPath(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def make_directory_alias(self, link, target):
+        """Make `link` a second name for the directory `target`, or return False."""
+        if sys.platform == "win32":
+            command = 'mklink /J "%s" "%s"' % (link, target)
+            if subprocess.call(command, shell=True, stdout=subprocess.DEVNULL) != 0:
+                return False
+            self.addTearDownHook(lambda: os.rmdir(link))
+        else:
+            try:
+                os.symlink(target, link)
+            except (OSError, NotImplementedError):
+                return False
+            self.addTearDownHook(lambda: os.unlink(link))
+        return os.path.realpath(link) != link
+
+    @skipIfRemote
+    def test_module_added_through_aliased_path(self):
+        self.build()
+
+        lib_name = self.platformContext.getFullLibName("foo")
+        build_dir = self.getBuildDir()
+        alias_dir = build_dir + ".alias"
+        if not self.make_directory_alias(alias_dir, build_dir):
+            self.skipTest("could not alias the build directory")
+
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target, VALID_TARGET)
+
+        self.assertTrue(
+            target.AddModule(os.path.join(alias_dir, lib_name), None, None).IsValid(),
+            "added the library through the aliased path",
+        )
+
+        bkpt = target.BreakpointCreateBySourceRegex(
+            "break here", lldb.SBFileSpec("foo.c")
+        )
+        self.assertEqual(bkpt.GetNumLocations(), 1, "one location before launch")
+
+        env = []
+        shlib_var = self.platformContext.shlib_environment_var
+        if shlib_var and shlib_var != "PATH":
+            env.append("%s=%s" % (shlib_var, build_dir))
+
+        process = target.LaunchSimple(
+            None, env or None, self.get_process_working_directory()
+        )
+        self.assertState(process.GetState(), lldb.eStateStopped)
+        thread = lldbutil.get_one_thread_stopped_at_breakpoint(process, bkpt)
+        self.assertIsNotNone(thread, "stopped at the breakpoint in the library")
+
+        modules = [
+            m
+            for m in target.module_iter()
+            if m.GetFileSpec().GetFilename() == lib_name
+        ]
+        self.assertEqual(len(modules), 1, "one module for the library")
+        self.assertEqual(bkpt.GetNumLocations(), 1, "one location after launch")
+        self.assertEqual(bkpt.GetNumResolvedLocations(), 1)
diff --git a/lldb/test/API/functionalities/module_aliased_path/foo.c b/lldb/test/API/functionalities/module_aliased_path/foo.c
new file mode 100644
index 0000000000000..375a5207d2654
--- /dev/null
+++ b/lldb/test/API/functionalities/module_aliased_path/foo.c
@@ -0,0 +1,3 @@
+int LLDB_DYLIB_EXPORT foo(void) {
+  return 42; // break here
+}
diff --git a/lldb/test/API/functionalities/module_aliased_path/main.c b/lldb/test/API/functionalities/module_aliased_path/main.c
new file mode 100644
index 0000000000000..d9dddf7df5343
--- /dev/null
+++ b/lldb/test/API/functionalities/module_aliased_path/main.c
@@ -0,0 +1,3 @@
+extern int foo(void);
+
+int main(void) { return foo() == 42 ? 0 : 1; }



More information about the lldb-commits mailing list