Merge pull request #2535 from EylonKrause/fix/preprocess-truncated-callable-macro
[preprocessor] Fix crash/hang on truncated callable macro invocation
diff --git a/.github/bin/smoke-test.sh b/.github/bin/smoke-test.sh
index ff9d1af..5532187 100755
--- a/.github/bin/smoke-test.sh
+++ b/.github/bin/smoke-test.sh
@@ -126,7 +126,7 @@
ExpectedFailCount[syntax:sv-tests]=74
ExpectedFailCount[lint:sv-tests]=73
ExpectedFailCount[project:sv-tests]=176
-ExpectedFailCount[preprocessor:sv-tests]=128
+ExpectedFailCount[preprocessor:sv-tests]=129
ExpectedFailCount[syntax:caliptra-rtl]=41
ExpectedFailCount[lint:caliptra-rtl]=40
diff --git a/verible/verilog/preprocessor/verilog-preprocess.cc b/verible/verilog/preprocessor/verilog-preprocess.cc
index 2f77111..1d847fb 100644
--- a/verible/verilog/preprocessor/verilog-preprocess.cc
+++ b/verible/verilog/preprocessor/verilog-preprocess.cc
@@ -245,11 +245,15 @@
if ((*token_iter)->text() == "(") {
token_iter = GenerateBypassWhiteSpaces(generator); // skip the "("
} else {
+ preprocess_data_.errors.emplace_back(
+ **token_iter,
+ "Error it is illegal to call a callable macro without ().");
return absl::InvalidArgumentError(
"Error it is illegal to call a callable macro without ().");
}
while (parameters_size > 0) {
+ if ((*token_iter)->isEOF()) break; // truncated call; stop scanning args
if ((*token_iter)->token_enum() == MacroArg) {
macro_call->positional_arguments.emplace_back(**token_iter);
token_iter = GenerateBypassWhiteSpaces(generator);
@@ -342,6 +346,10 @@
lexer.DoNextToken()) {
lexed_sequence.push_back(lexer.GetLastToken());
}
+ // Retain the EOF token as an end sentinel so a truncated callable-macro
+ // invocation stops at EOF in GenerateBypassWhiteSpaces instead of
+ // dereferencing past the end of the stream view.
+ lexed_sequence.push_back(lexer.GetLastToken());
verible::TokenStreamView lexed_streamview;
// Initializing the lexed token stream view.
InitTokenStreamView(lexed_sequence, &lexed_streamview);
@@ -352,6 +360,7 @@
// Token-pulling loop.
for (auto iter = iter_generator(); iter != end; iter = iter_generator()) {
auto &last_token = **iter;
+ if (last_token.isEOF()) break; // end sentinel; nothing to forward
// TODO: handle lexical error
if (lexer.GetLastToken().token_enum() == TK_SPACE) {
continue; // don't forward spaces
@@ -396,6 +405,8 @@
lexer.DoNextToken()) {
lexed_sequence.push_back(lexer.GetLastToken());
}
+ // Retain EOF end sentinel (see ExpandText).
+ lexed_sequence.push_back(lexer.GetLastToken());
verible::TokenStreamView lexed_streamview;
// Initializing the lexed token stream view.
InitTokenStreamView(lexed_sequence, &lexed_streamview);
@@ -407,6 +418,7 @@
for (auto iter = iter_generator(); iter != end; iter = iter_generator()) {
// TODO: handle lexical error
auto &last_token = **iter;
+ if (last_token.isEOF()) break; // end sentinel; nothing to forward
if (last_token.token_enum() == TK_SPACE) continue; // don't forward spaces
// If the expanded token is another macro identifier that needs to be
// expanded.
@@ -635,6 +647,9 @@
lexer.DoNextToken()) {
included_sequence.push_back(lexer.GetLastToken());
}
+ // Retain EOF end sentinel; the child ScanStream expects an EOF-terminated
+ // stream.
+ included_sequence.push_back(lexer.GetLastToken());
// Preprocessing the included file tokens.
verible::TokenStreamView lexed_streamview;
@@ -657,8 +672,11 @@
preprocess_data_.included_text_structure.push_back(std::move(u));
}
- // Forwarding the included preprocessed view.
+ // Forwarding the included preprocessed view. The EOF end sentinel appended
+ // above is consumed by the child ScanStream and must not be spliced into the
+ // middle of the parent's token stream.
for (const auto &u : child_preprocessed_data.preprocessed_token_stream) {
+ if (u->isEOF()) continue;
preprocess_data_.preprocessed_token_stream.push_back(u);
}
diff --git a/verible/verilog/preprocessor/verilog-preprocess.h b/verible/verilog/preprocessor/verilog-preprocess.h
index 2d134ae..0d932f0 100644
--- a/verible/verilog/preprocessor/verilog-preprocess.h
+++ b/verible/verilog/preprocessor/verilog-preprocess.h
@@ -164,9 +164,11 @@
absl::Status HandleElse(TokenStreamView::const_iterator else_pos);
absl::Status HandleEndif(TokenStreamView::const_iterator endif_pos);
- static absl::Status ConsumeAndParseMacroCall(
- TokenStreamView::const_iterator, const StreamIteratorGenerator &,
- verible::MacroCall *, const verible::MacroDefinition &);
+ // Non-static so it can record diagnostics into preprocess_data_.errors.
+ absl::Status ConsumeAndParseMacroCall(TokenStreamView::const_iterator,
+ const StreamIteratorGenerator &,
+ verible::MacroCall *,
+ const verible::MacroDefinition &);
// The following functions return nullptr when there is no error:
absl::Status ConsumeMacroDefinition(const StreamIteratorGenerator &,
diff --git a/verible/verilog/preprocessor/verilog-preprocess_test.cc b/verible/verilog/preprocessor/verilog-preprocess_test.cc
index ae33567..a8e41d9 100644
--- a/verible/verilog/preprocessor/verilog-preprocess_test.cc
+++ b/verible/verilog/preprocessor/verilog-preprocess_test.cc
@@ -1033,5 +1033,30 @@
<< error.error_message;
}
+// Regression: a callable-macro invocation truncated at end-of-stream (no '(',
+// or '(' with no matching ')') must not crash or hang the preprocessor. Before
+// the fix these inputs dereferenced past the end of the token stream view
+// (SIGSEGV) or spun forever scanning arguments. With error-surfacing enabled
+// the no-'(' cases also report a preprocessor diagnostic.
+TEST(VerilogPreprocessTest, TruncatedCallableMacroDoesNotCrash) {
+ constexpr std::string_view kNoParenInputs[] = {
+ "`define A(x) hello `A\n`A(1)\n", // truncated callable ref in macro body
+ "`define A(x) x\n`A\n", // truncated callable ref at top level
+ };
+ for (std::string_view input : kNoParenInputs) {
+ PreprocessorTester tester(
+ input, VerilogPreprocess::Config({.expand_macros = true}));
+ EXPECT_FALSE(tester.Status().ok()) << input;
+ EXPECT_GE(tester.PreprocessorData().errors.size(), 1) << input;
+ }
+
+ // '(' with no matching ')': must terminate (was an infinite loop). The
+ // residue is rejected downstream, so only assert non-OK here.
+ PreprocessorTester open_paren(
+ "`define C(z) z\n`define A(x) hello `C(\n`A(1)\n",
+ VerilogPreprocess::Config({.expand_macros = true}));
+ EXPECT_FALSE(open_paren.Status().ok());
+}
+
} // namespace
} // namespace verilog
diff --git a/verible/verilog/tools/preprocessor/verilog-preprocessor.cc b/verible/verilog/tools/preprocessor/verilog-preprocessor.cc
index c23fdf7..26907dc 100644
--- a/verible/verilog/tools/preprocessor/verilog-preprocessor.cc
+++ b/verible/verilog/tools/preprocessor/verilog-preprocessor.cc
@@ -122,13 +122,17 @@
// source code just like it was, but with conditionals filtered.
lexed_sequence.push_back(lexer.GetLastToken());
}
+ lexed_sequence.push_back(lexer.GetLastToken()); // EOF end sentinel
verible::TokenStreamView lexed_streamview;
// Initializing the lexed token stream view.
InitTokenStreamView(lexed_sequence, &lexed_streamview);
verilog::VerilogPreprocessData preprocessed_data =
preprocessor.ScanStream(lexed_streamview);
auto &preprocessed_stream = preprocessed_data.preprocessed_token_stream;
- for (auto u : preprocessed_stream) outs << u->text();
+ for (auto u : preprocessed_stream) {
+ if (u->isEOF()) continue; // end sentinel, not part of the source
+ outs << u->text();
+ }
for (auto &u : preprocessed_data.errors) outs << u.error_message << '\n';
if (!preprocessed_data.errors.empty()) {
return absl::InvalidArgumentError("Error: The preprocessing has failed.");