[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