Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@zfogg
Copy link
Contributor

@zfogg zfogg commented Dec 27, 2025

Summary

Fix __has_include_next to return false when the current file was found via absolute path rather than incorrectly searching from the start of the include path.

Problem

When a header file is included using an absolute path (e.g., via -include /full/path/to/header.h), any __has_include_next call within that header would search from the beginning of the include path instead of returning false. This caused:

  1. False positives: Headers that shouldn't be "found" were found because the search started from the beginning
  2. Fatal errors in LibTooling: Tools like clang-tidy and custom LibTooling-based source transformers would crash when parsing files included via absolute path

Real-World Impact (macOS + Clang 21)

This bug was discovered when building LibTooling-based tools on macOS. The macOS SDK's <stdbool.h> uses __has_include_next:

#if __has_include_next(<stdbool.h>)
#include_next <stdbool.h>
#endif

When source files were passed to a LibTooling tool with include paths like -I/path/to/project/lib, clang would:

  1. Look for <stdbool.h> in /path/to/project/lib/stdbool.h (due to the bug searching from start)
  2. Fail with: fatal error: cannot open file '/path/to/project/lib/stdbool.h': No such file or directory

Solution

Refactor the token consumption logic into a separate ConsumeHasIncludeTokens() helper function that returns a HasIncludeResult struct. This allows EvaluateHasIncludeNext() to:

  1. Get the include-next start location from getIncludeNextStart()
  2. Check if there is no valid "next" search location
  3. Consume the tokens (maintaining preprocessor state)
  4. Return false early without performing the file lookup

The primary file case is excluded to preserve existing behavior.

Test

Added has_include_next_absolute.c which tests that __has_include_next returns false for a nonexistent header when the current file was found via absolute path.

Test Plan

ninja check-clang-preprocessor
# or
llvm-lit clang/test/Preprocessor/has_include_next_absolute.c

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Dec 27, 2025
@llvmbot
Copy link
Member

llvmbot commented Dec 27, 2025

@llvm/pr-subscribers-clang

Author: Zachary Fogg (zfogg)

Changes

Summary

Fix __has_include_next to return false when the current file was found via absolute path rather than incorrectly searching from the start of the include path.

Problem

When a header file is included using an absolute path (e.g., via -include /full/path/to/header.h), any __has_include_next call within that header would search from the beginning of the include path instead of returning false. This caused:

  1. False positives: Headers that shouldn't be "found" were found because the search started from the beginning
  2. Fatal errors in LibTooling: Tools like clang-tidy and custom LibTooling-based source transformers would crash when parsing files included via absolute path

Real-World Impact (macOS + Clang 21)

This bug was discovered when building LibTooling-based tools on macOS. The macOS SDK's &lt;stdbool.h&gt; uses __has_include_next:

#if __has_include_next(&lt;stdbool.h&gt;)
#include_next &lt;stdbool.h&gt;
#endif

When source files were passed to a LibTooling tool with include paths like -I/path/to/project/lib, clang would:

  1. Look for &lt;stdbool.h&gt; in /path/to/project/lib/stdbool.h (due to the bug searching from start)
  2. Fail with: fatal error: cannot open file '/path/to/project/lib/stdbool.h': No such file or directory

Solution

Refactor the token consumption logic into a separate ConsumeHasIncludeTokens() helper function that returns a HasIncludeResult struct. This allows EvaluateHasIncludeNext() to:

  1. Get the include-next start location from getIncludeNextStart()
  2. Check if there is no valid "next" search location
  3. Consume the tokens (maintaining preprocessor state)
  4. Return false early without performing the file lookup

The primary file case is excluded to preserve existing behavior.

Test

Added has_include_next_absolute.c which tests that __has_include_next returns false for a nonexistent header when the current file was found via absolute path.

Test Plan

ninja check-clang-preprocessor
# or
llvm-lit clang/test/Preprocessor/has_include_next_absolute.c

Full diff: https://github.com/llvm/llvm-project/pull/173717.diff

3 Files Affected:

  • (modified) clang/lib/Lex/PPMacroExpansion.cpp (+59-18)
  • (added) clang/test/Preprocessor/Inputs/has-include-next-absolute/test_header.h (+10)
  • (added) clang/test/Preprocessor/has_include_next_absolute.c (+17)
diff --git a/clang/lib/Lex/PPMacroExpansion.cpp b/clang/lib/Lex/PPMacroExpansion.cpp
index 890567cfd3246..0d9ff87476f6a 100644
--- a/clang/lib/Lex/PPMacroExpansion.cpp
+++ b/clang/lib/Lex/PPMacroExpansion.cpp
@@ -1131,13 +1131,23 @@ static bool HasExtension(const Preprocessor &PP, StringRef Extension) {
 #undef EXTENSION
 }
 
-/// EvaluateHasIncludeCommon - Process a '__has_include("path")'
+/// Result of consuming __has_include/__has_include_next tokens.
+struct HasIncludeResult {
+  bool Valid;              // Whether token parsing succeeded.
+  StringRef Filename;      // The parsed filename.
+  SourceLocation FileLoc;  // Location of the filename token.
+  bool IsAngled;           // True for <...>, false for "...".
+};
+
+/// ConsumeHasIncludeTokens - Consume the tokens for a '__has_include("path")'
 /// or '__has_include_next("path")' expression.
-/// Returns true if successful.
-static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
-                                     Preprocessor &PP,
-                                     ConstSearchDirIterator LookupFrom,
-                                     const FileEntry *LookupFromFile) {
+/// Returns a HasIncludeResult indicating whether parsing succeeded and
+/// providing the filename if so.
+static HasIncludeResult ConsumeHasIncludeTokens(Token &Tok, IdentifierInfo *II,
+                                                Preprocessor &PP,
+                                                SmallString<128> &FilenameBuffer) {
+  HasIncludeResult Result = {false, StringRef(), SourceLocation(), false};
+
   // Save the location of the current token.  If a '(' is later found, use
   // that location.  If not, use the end of this location instead.
   SourceLocation LParenLoc = Tok.getLocation();
@@ -1148,13 +1158,13 @@ static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
     // Return a valid identifier token.
     assert(Tok.is(tok::identifier));
     Tok.setIdentifierInfo(II);
-    return false;
+    return Result;
   }
 
   // Get '('. If we don't have a '(', try to form a header-name token.
   do {
     if (PP.LexHeaderName(Tok))
-      return false;
+      return Result;
   } while (Tok.getKind() == tok::comment);
 
   // Ensure we have a '('.
@@ -1165,25 +1175,24 @@ static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
     // If the next token looks like a filename or the start of one,
     // assume it is and process it as such.
     if (Tok.isNot(tok::header_name))
-      return false;
+      return Result;
   } else {
     // Save '(' location for possible missing ')' message.
     LParenLoc = Tok.getLocation();
     if (PP.LexHeaderName(Tok))
-      return false;
+      return Result;
   }
 
   if (Tok.isNot(tok::header_name)) {
     PP.Diag(Tok.getLocation(), diag::err_pp_expects_filename);
-    return false;
+    return Result;
   }
 
   // Reserve a buffer to get the spelling.
-  SmallString<128> FilenameBuffer;
   bool Invalid = false;
   StringRef Filename = PP.getSpelling(Tok, FilenameBuffer, &Invalid);
   if (Invalid)
-    return false;
+    return Result;
 
   SourceLocation FilenameLoc = Tok.getLocation();
 
@@ -1195,13 +1204,33 @@ static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
     PP.Diag(PP.getLocForEndOfToken(FilenameLoc), diag::err_pp_expected_after)
         << II << tok::r_paren;
     PP.Diag(LParenLoc, diag::note_matching) << tok::l_paren;
-    return false;
+    return Result;
   }
 
-  bool isAngled = PP.GetIncludeFilenameSpelling(Tok.getLocation(), Filename);
+  bool IsAngled = PP.GetIncludeFilenameSpelling(Tok.getLocation(), Filename);
   // If GetIncludeFilenameSpelling set the start ptr to null, there was an
   // error.
   if (Filename.empty())
+    return Result;
+
+  Result.Valid = true;
+  Result.Filename = Filename;
+  Result.FileLoc = FilenameLoc;
+  Result.IsAngled = IsAngled;
+  return Result;
+}
+
+/// EvaluateHasIncludeCommon - Process a '__has_include("path")'
+/// or '__has_include_next("path")' expression.
+/// Returns true if the file exists.
+static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
+                                     Preprocessor &PP,
+                                     ConstSearchDirIterator LookupFrom,
+                                     const FileEntry *LookupFromFile) {
+  SmallString<128> FilenameBuffer;
+  HasIncludeResult ParseResult =
+      ConsumeHasIncludeTokens(Tok, II, PP, FilenameBuffer);
+  if (!ParseResult.Valid)
     return false;
 
   // Passing this to LookupFile forces header search to check whether the found
@@ -1211,14 +1240,16 @@ static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
 
   // Search include directories.
   OptionalFileEntryRef File =
-      PP.LookupFile(FilenameLoc, Filename, isAngled, LookupFrom, LookupFromFile,
-                    nullptr, nullptr, nullptr, &KH, nullptr, nullptr);
+      PP.LookupFile(ParseResult.FileLoc, ParseResult.Filename,
+                    ParseResult.IsAngled, LookupFrom, LookupFromFile, nullptr,
+                    nullptr, nullptr, &KH, nullptr, nullptr);
 
   if (PPCallbacks *Callbacks = PP.getPPCallbacks()) {
     SrcMgr::CharacteristicKind FileType = SrcMgr::C_User;
     if (File)
       FileType = PP.getHeaderSearchInfo().getFileDirFlavor(*File);
-    Callbacks->HasInclude(FilenameLoc, Filename, isAngled, File, FileType);
+    Callbacks->HasInclude(ParseResult.FileLoc, ParseResult.Filename,
+                          ParseResult.IsAngled, File, FileType);
   }
 
   // Get the result value.  A result of true means the file exists.
@@ -1333,6 +1364,16 @@ bool Preprocessor::EvaluateHasIncludeNext(Token &Tok, IdentifierInfo *II) {
   const FileEntry *LookupFromFile;
   std::tie(Lookup, LookupFromFile) = getIncludeNextStart(Tok);
 
+  // If there's no valid "next" search location (file was found via absolute
+  // path), consume the tokens but return false - there's no "next" to find.
+  // Primary file case is excluded to preserve existing behavior.
+  if (!Lookup && !LookupFromFile && !isInPrimaryFile()) {
+    // Still need to consume the tokens to maintain preprocessor state.
+    SmallString<128> FilenameBuffer;
+    ConsumeHasIncludeTokens(Tok, II, *this, FilenameBuffer);
+    return false;
+  }
+
   return EvaluateHasIncludeCommon(Tok, II, *this, Lookup, LookupFromFile);
 }
 
diff --git a/clang/test/Preprocessor/Inputs/has-include-next-absolute/test_header.h b/clang/test/Preprocessor/Inputs/has-include-next-absolute/test_header.h
new file mode 100644
index 0000000000000..5142adb6dfa50
--- /dev/null
+++ b/clang/test/Preprocessor/Inputs/has-include-next-absolute/test_header.h
@@ -0,0 +1,10 @@
+// Test header for __has_include_next with absolute path
+// When this header is found via absolute path (not through search directories),
+// __has_include_next should return false instead of searching from the start
+// of the include path.
+
+#if __has_include_next(<nonexistent_header.h>)
+#error "__has_include_next should return false for nonexistent header"
+#endif
+
+#define TEST_HEADER_INCLUDED 1
diff --git a/clang/test/Preprocessor/has_include_next_absolute.c b/clang/test/Preprocessor/has_include_next_absolute.c
new file mode 100644
index 0000000000000..35fd4bd6594fd
--- /dev/null
+++ b/clang/test/Preprocessor/has_include_next_absolute.c
@@ -0,0 +1,17 @@
+// RUN: %clang_cc1 -E -include %S/Inputs/has-include-next-absolute/test_header.h \
+// RUN:   -verify %s
+
+// Test that __has_include_next returns false when the current file was found
+// via absolute path (not through the search directories). Previously, this
+// would incorrectly search from the start of the include path, which could
+// cause false positives or fatal errors when it tried to open non-existent
+// files.
+
+// expected-warning@Inputs/has-include-next-absolute/test_header.h:6 {{#include_next in file found relative to primary source file or found by absolute path; will search from start of include path}}
+
+// Verify the header was included correctly
+#ifndef TEST_HEADER_INCLUDED
+#error "test_header.h was not included"
+#endif
+
+int main(void) { return 0; }

@zfogg
Copy link
Contributor Author

zfogg commented Dec 27, 2025

cc @jansvoboda11 @AaronBallman - This is a backport of a fix for __has_include_next behavior when a file is found via absolute path. Would appreciate a review when you have time.

@zfogg zfogg force-pushed the backport-has-include-next-fix-21.x branch from 6125b45 to 77331cd Compare December 27, 2025 12:30
@llvmbot llvmbot added the cmake Build system in general and CMake in particular label Dec 27, 2025
@zwuis
Copy link
Contributor

zwuis commented Dec 27, 2025

bug not present in current git llvm22

Which commit fixed this bug?

@zfogg
Copy link
Contributor Author

zfogg commented Dec 27, 2025

I need to verify this more carefully. Looking at the code, EvaluateHasIncludeNext is identical between release/21.x and main.

My original observation may have been incorrect - the bug might exist in both versions. I'll verify whether this fix is also needed for main and create a separate PR if so.

The issue occurs when:

  1. A file is included via absolute path (not through a search directory)
  2. That file contains __has_include_next()
  3. getIncludeNextStart() returns {nullptr, LookupFromFile} because there's no search directory
  4. EvaluateHasIncludeCommon should return false in this case, but instead tries to look up the file

If the bug exists in main, I'll create a PR targeting main first, then cherry-pick to 21.x.

@zfogg
Copy link
Contributor Author

zfogg commented Dec 27, 2025

Created #173721 targeting main with the same fix. This PR (#173717) is the backport to release/21.x.

…t dir

When a header file is included using an absolute path, any __has_include_next
call within that header would search from the beginning of the include path
instead of returning false. This caused false positives and fatal errors in
LibTooling-based tools on macOS when the SDK's stdbool.h (which uses
__has_include_next) was processed.

The fix refactors the token consumption logic into a separate
ConsumeHasIncludeTokens() helper function that returns a HasIncludeResult
struct. This allows EvaluateHasIncludeNext() to:
1. Get the include-next start location from getIncludeNextStart()
2. Check if there is no valid "next" search location
3. Consume the tokens (maintaining preprocessor state)
4. Return false early without performing the file lookup

The primary file case is excluded to preserve existing behavior.

(cherry picked from commit 5cbf464)
@zfogg zfogg force-pushed the backport-has-include-next-fix-21.x branch from 77331cd to cd3279b Compare December 27, 2025 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category cmake Build system in general and CMake in particular

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants