-
Notifications
You must be signed in to change notification settings - Fork 15.6k
[Clang][Lex] Fix __has_include_next to return false when no valid next dir #173717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release/21.x
Are you sure you want to change the base?
Conversation
|
@llvm/pr-subscribers-clang Author: Zachary Fogg (zfogg) ChangesSummaryFix ProblemWhen a header file is included using an absolute path (e.g., via
Real-World Impact (macOS + Clang 21)This bug was discovered when building LibTooling-based tools on macOS. The macOS SDK's #if __has_include_next(<stdbool.h>)
#include_next <stdbool.h>
#endifWhen source files were passed to a LibTooling tool with include paths like
SolutionRefactor the token consumption logic into a separate
The primary file case is excluded to preserve existing behavior. TestAdded Test Planninja check-clang-preprocessor
# or
llvm-lit clang/test/Preprocessor/has_include_next_absolute.cFull diff: https://github.com/llvm/llvm-project/pull/173717.diff 3 Files Affected:
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; }
|
|
cc @jansvoboda11 @AaronBallman - This is a backport of a fix for |
6125b45 to
77331cd
Compare
Which commit fixed this bug? |
|
I need to verify this more carefully. Looking at the code, My original observation may have been incorrect - the bug might exist in both versions. I'll verify whether this fix is also needed for The issue occurs when:
If the bug exists in main, I'll create a PR targeting main first, then cherry-pick to 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)
77331cd to
cd3279b
Compare
Summary
Fix
__has_include_nextto returnfalsewhen 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_nextcall within that header would search from the beginning of the include path instead of returningfalse. This caused:clang-tidyand custom LibTooling-based source transformers would crash when parsing files included via absolute pathReal-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:When source files were passed to a LibTooling tool with include paths like
-I/path/to/project/lib, clang would:<stdbool.h>in/path/to/project/lib/stdbool.h(due to the bug searching from start)fatal error: cannot open file '/path/to/project/lib/stdbool.h': No such file or directorySolution
Refactor the token consumption logic into a separate
ConsumeHasIncludeTokens()helper function that returns aHasIncludeResultstruct. This allowsEvaluateHasIncludeNext()to:getIncludeNextStart()The primary file case is excluded to preserve existing behavior.
Test
Added
has_include_next_absolute.cwhich tests that__has_include_nextreturnsfalsefor 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