[clang] [clang][Lex] Collapse relative extern module paths when recursing to prevent unbounded path length growth. (PR #193691)

Tom Murray via cfe-commits cfe-commits at lists.llvm.org
Tue Apr 28 03:07:28 PDT 2026


https://github.com/TomMurray updated https://github.com/llvm/llvm-project/pull/193691

>From 425f1b8c616926a1d202b1c5232d17dd9a909c1d Mon Sep 17 00:00:00 2001
From: Tom Murray <tom.paul.murray at gmail.com>
Date: Thu, 23 Apr 2026 09:49:37 +0100
Subject: [PATCH 1/2] Remove dots from relative extern module paths before
 trying to open them

---
 clang/lib/Lex/ModuleMap.cpp | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/clang/lib/Lex/ModuleMap.cpp b/clang/lib/Lex/ModuleMap.cpp
index 56ae51ada7148..a164798db5314 100644
--- a/clang/lib/Lex/ModuleMap.cpp
+++ b/clang/lib/Lex/ModuleMap.cpp
@@ -2001,6 +2001,13 @@ void ModuleMapLoader::handleExternModuleDecl(
   if (llvm::sys::path::is_relative(FileNameRef)) {
     ModuleMapFileName += Directory.getName();
     llvm::sys::path::append(ModuleMapFileName, EMD.Path);
+    // As extern module declarations are parsed recursively, relative paths
+    // to those modules can become arbitrarily long.
+    // If the OS name length limit is exceeded when trying to get the file ref
+    // we can silently fail to find an extern module that exists.
+    // To mitigate this, collapse relative paths containing '../' for when
+    // constructing the name of each module file referenced as an extern module.
+    llvm::sys::path::remove_dots(ModuleMapFileName, /*remove_dot_dot=*/true);
     FileNameRef = ModuleMapFileName;
   }
   if (auto File = SourceMgr.getFileManager().getOptionalFileRef(FileNameRef))

>From 42bacef4443ed995bec4d9f6ddf51235b0861593 Mon Sep 17 00:00:00 2001
From: Tom Murray <tom.paul.murray at gmail.com>
Date: Tue, 28 Apr 2026 10:45:19 +0100
Subject: [PATCH 2/2] Add a unit test for normalizing path behaviour in
 ModuleMap

---
 clang/unittests/Lex/CMakeLists.txt    |   1 +
 clang/unittests/Lex/ModuleMapTest.cpp | 132 ++++++++++++++++++++++++++
 2 files changed, 133 insertions(+)
 create mode 100644 clang/unittests/Lex/ModuleMapTest.cpp

diff --git a/clang/unittests/Lex/CMakeLists.txt b/clang/unittests/Lex/CMakeLists.txt
index fa5e58f5a8932..cf338eef0e92c 100644
--- a/clang/unittests/Lex/CMakeLists.txt
+++ b/clang/unittests/Lex/CMakeLists.txt
@@ -5,6 +5,7 @@ add_clang_unittest(LexTests
   LexerTest.cpp
   LexHLSLRootSignatureTest.cpp
   ModuleDeclStateTest.cpp
+  ModuleMapTest.cpp
   NoTrivialPPDirectiveTracerTest.cpp
   PPCallbacksTest.cpp
   PPConditionalDirectiveRecordTest.cpp
diff --git a/clang/unittests/Lex/ModuleMapTest.cpp b/clang/unittests/Lex/ModuleMapTest.cpp
new file mode 100644
index 0000000000000..7dd00199cb7d6
--- /dev/null
+++ b/clang/unittests/Lex/ModuleMapTest.cpp
@@ -0,0 +1,132 @@
+//===- unittests/Lex/ModuleMapTest.cpp - PPCallbacks tests ------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===--------------------------------------------------------------===//
+//
+
+#include "clang/Lex/ModuleMap.h"
+#include "clang/Basic/SourceManager.h"
+#include "clang/Basic/TargetInfo.h"
+#include "clang/Basic/TargetOptions.h"
+#include "clang/Lex/HeaderSearch.h"
+#include "clang/Lex/HeaderSearchOptions.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/VirtualFileSystem.h"
+#include "llvm/TargetParser/Host.h"
+#include "gtest/gtest.h"
+
+#include <vector>
+
+namespace clang {
+namespace {
+
+struct InterceptorFS : llvm::vfs::ProxyFileSystem {
+  // Record the paths looked up by a ModuleMap.
+  std::vector<std::string> StatPaths;
+
+  InterceptorFS(IntrusiveRefCntPtr<llvm::vfs::FileSystem> UnderlyingFS)
+      : ProxyFileSystem(UnderlyingFS) {}
+
+  llvm::ErrorOr<llvm::vfs::Status> status(const Twine &Path) override {
+    StatPaths.emplace_back(Path.str());
+    return ProxyFileSystem::status(Path);
+  }
+};
+
+class ModuleMapTest : public ::testing::Test {
+protected:
+  ModuleMapTest()
+      : InMemFS(llvm::makeIntrusiveRefCnt<llvm::vfs::InMemoryFileSystem>()),
+        VFS(llvm::makeIntrusiveRefCnt<InterceptorFS>(InMemFS)),
+        FileMgr(FileMgrOpts, VFS),
+        Diagnostics(DiagnosticIDs::create(), DiagnosticOpts,
+                    new IgnoringDiagConsumer()),
+        SrcMgr(Diagnostics, FileMgr), TargetOpts(new TargetOptions),
+        HdrSearch(HdrSearchOpts, SrcMgr, Diagnostics, LangOpts,
+                  /* Target = */ nullptr),
+        Map(SrcMgr, Diagnostics, LangOpts, /* Target= */ nullptr, HdrSearch) {
+    TargetOpts->Triple = "x86_64-apple-darwin11.1.0";
+    Target = TargetInfo::CreateTargetInfo(Diagnostics, *TargetOpts);
+    Map.setTarget(*Target);
+  }
+
+  void addFile(llvm::StringRef Path, llvm::StringRef Content) {
+    InMemFS->addFile(Path, 0, llvm::MemoryBuffer::getMemBufferCopy(Content));
+  }
+
+  bool loadRoot(llvm::StringRef Path) {
+    auto File = FileMgr.getOptionalFileRef(Path);
+    if (!File) {
+      // parseAndLoadModuleMapFile returns false on success, true on error.
+      return true;
+    }
+    return Map.parseAndLoadModuleMapFile(*File, /* IsSystem = */ false,
+                                         /* ImplicitlyDiscovered = */ false,
+                                         File->getDir());
+  }
+
+  IntrusiveRefCntPtr<llvm::vfs::InMemoryFileSystem> InMemFS;
+  IntrusiveRefCntPtr<InterceptorFS> VFS;
+  FileSystemOptions FileMgrOpts;
+  FileManager FileMgr;
+  DiagnosticOptions DiagnosticOpts;
+  DiagnosticsEngine Diagnostics;
+  SourceManager SrcMgr;
+  LangOptions LangOpts;
+  std::shared_ptr<TargetOptions> TargetOpts;
+  llvm::IntrusiveRefCntPtr<TargetInfo> Target;
+  HeaderSearchOptions HdrSearchOpts;
+  HeaderSearch HdrSearch;
+
+  ModuleMap Map;
+};
+
+// Regression test for
+// https://github.com/llvm/llvm-project/issues/147220.
+//
+// These tests specifically aim to validate that ModuleMap paths do not grow
+// in an unbounded fashion in the presence of chained relative paths to extern
+// modules.
+//
+TEST_F(ModuleMapTest, ExternModuleRelativeLookupPathIsNormalized) {
+  // Root module as entry point, chained extern module references:
+  // A -> B -> C
+  addFile("/root/A.cppmap", R"(
+module A {}
+extern module B "../root/B.cppmap"
+  )");
+  addFile("/root/B.cppmap", R"(
+module B {}
+extern module C "../root/C.cppmap"
+  )");
+  // Leaf module
+  addFile("/root/C.cppmap", "module C {}");
+
+  ASSERT_FALSE(loadRoot("/root/A.cppmap"));
+
+  // Now check paths used are normalised.
+  llvm::SmallSet<llvm::StringRef, 4> Seen;
+  for (const std::string &Path : VFS->StatPaths) {
+    llvm::StringRef BaseName;
+    for (llvm::StringRef Component : llvm::make_range(
+             llvm::sys::path::begin(Path), llvm::sys::path::end(Path))) {
+      // As `/root/A.cppmap` is absolute, there should be no relative paths at
+      // lookup time.
+      EXPECT_NE(Component, "..");
+      // Last component is basename
+      BaseName = Component;
+    }
+    Seen.insert(BaseName);
+  }
+
+  // Ensure the path check covered all the modules we expected.
+  ASSERT_TRUE(Seen.contains("A.cppmap"));
+  ASSERT_TRUE(Seen.contains("B.cppmap"));
+  ASSERT_TRUE(Seen.contains("C.cppmap"));
+}
+
+} // namespace
+} // namespace clang



More information about the cfe-commits mailing list