Skip to content

Commit debe772

Browse files
committed
[preprocessor] Fix crash/hang on truncated callable macro invocation
GenerateBypassWhiteSpaces dereferences the stream iterator (**iterator) with no end guard, relying on every stream ending in a kept EOF token. The streams re-lexed for macro expansion in ExpandText, ExpandMacro, HandleInclude and the standalone tool strip the EOF (loop stops on !isEOF()), so a callable macro invocation truncated at end-of-stream (e.g. `define A(x) hello `A followed by `A(1)) makes the streamer return the view's end() iterator and the deref reads past the end -> SIGSEGV. An included file ending in a callable macro crashes the same way; a '(' with no ')' spins forever. Restore the kept-EOF sentinel on each re-lexed stream so the whitespace-skip loop stops at EOF and callers return a diagnostic instead of dereferencing past the end: append the EOF sentinel in all four stream builders; break on it in the two token-pulling loops so it is not forwarded; skip it when splicing an included child stream into the parent; and break on EOF in the argument-scanning loop (the one caller that did not handle a mid-scan EOF, which otherwise hangs on a '(' without ')'). Also record the "callable macro without ()" error in preprocess_data_.errors instead of silently swallowing it (requires making ConsumeAndParseMacroCall non-static). Adds TruncatedCallableMacroDoesNotCrash. Existing preprocessor and analyzer test suites pass. Unbounded macro self-recursion is a separate pre-existing bug and is not addressed here. Signed-off-by: Eylon Krause <eylon1909@gmail.com>
1 parent 873f559 commit debe772

4 files changed

Lines changed: 50 additions & 4 deletions

File tree

verible/verilog/preprocessor/verilog-preprocess.cc

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,15 @@ absl::Status VerilogPreprocess::ConsumeAndParseMacroCall(
245245
if ((*token_iter)->text() == "(") {
246246
token_iter = GenerateBypassWhiteSpaces(generator); // skip the "("
247247
} else {
248+
preprocess_data_.errors.emplace_back(
249+
**token_iter,
250+
"Error it is illegal to call a callable macro without ().");
248251
return absl::InvalidArgumentError(
249252
"Error it is illegal to call a callable macro without ().");
250253
}
251254

252255
while (parameters_size > 0) {
256+
if ((*token_iter)->isEOF()) break; // truncated call; stop scanning args
253257
if ((*token_iter)->token_enum() == MacroArg) {
254258
macro_call->positional_arguments.emplace_back(**token_iter);
255259
token_iter = GenerateBypassWhiteSpaces(generator);
@@ -342,6 +346,10 @@ absl::Status VerilogPreprocess::ExpandText(
342346
lexer.DoNextToken()) {
343347
lexed_sequence.push_back(lexer.GetLastToken());
344348
}
349+
// Retain the EOF token as an end sentinel so a truncated callable-macro
350+
// invocation stops at EOF in GenerateBypassWhiteSpaces instead of
351+
// dereferencing past the end of the stream view.
352+
lexed_sequence.push_back(lexer.GetLastToken());
345353
verible::TokenStreamView lexed_streamview;
346354
// Initializing the lexed token stream view.
347355
InitTokenStreamView(lexed_sequence, &lexed_streamview);
@@ -352,6 +360,7 @@ absl::Status VerilogPreprocess::ExpandText(
352360
// Token-pulling loop.
353361
for (auto iter = iter_generator(); iter != end; iter = iter_generator()) {
354362
auto &last_token = **iter;
363+
if (last_token.isEOF()) break; // end sentinel; nothing to forward
355364
// TODO: handle lexical error
356365
if (lexer.GetLastToken().token_enum() == TK_SPACE) {
357366
continue; // don't forward spaces
@@ -396,6 +405,8 @@ absl::Status VerilogPreprocess::ExpandMacro(
396405
lexer.DoNextToken()) {
397406
lexed_sequence.push_back(lexer.GetLastToken());
398407
}
408+
// Retain EOF end sentinel (see ExpandText).
409+
lexed_sequence.push_back(lexer.GetLastToken());
399410
verible::TokenStreamView lexed_streamview;
400411
// Initializing the lexed token stream view.
401412
InitTokenStreamView(lexed_sequence, &lexed_streamview);
@@ -407,6 +418,7 @@ absl::Status VerilogPreprocess::ExpandMacro(
407418
for (auto iter = iter_generator(); iter != end; iter = iter_generator()) {
408419
// TODO: handle lexical error
409420
auto &last_token = **iter;
421+
if (last_token.isEOF()) break; // end sentinel; nothing to forward
410422
if (last_token.token_enum() == TK_SPACE) continue; // don't forward spaces
411423
// If the expanded token is another macro identifier that needs to be
412424
// expanded.
@@ -635,6 +647,9 @@ absl::Status VerilogPreprocess::HandleInclude(
635647
lexer.DoNextToken()) {
636648
included_sequence.push_back(lexer.GetLastToken());
637649
}
650+
// Retain EOF end sentinel; the child ScanStream expects an EOF-terminated
651+
// stream.
652+
included_sequence.push_back(lexer.GetLastToken());
638653

639654
// Preprocessing the included file tokens.
640655
verible::TokenStreamView lexed_streamview;
@@ -657,8 +672,11 @@ absl::Status VerilogPreprocess::HandleInclude(
657672
preprocess_data_.included_text_structure.push_back(std::move(u));
658673
}
659674

660-
// Forwarding the included preprocessed view.
675+
// Forwarding the included preprocessed view. The EOF end sentinel appended
676+
// above is consumed by the child ScanStream and must not be spliced into the
677+
// middle of the parent's token stream.
661678
for (const auto &u : child_preprocessed_data.preprocessed_token_stream) {
679+
if (u->isEOF()) continue;
662680
preprocess_data_.preprocessed_token_stream.push_back(u);
663681
}
664682

verible/verilog/preprocessor/verilog-preprocess.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,11 @@ class VerilogPreprocess {
164164
absl::Status HandleElse(TokenStreamView::const_iterator else_pos);
165165
absl::Status HandleEndif(TokenStreamView::const_iterator endif_pos);
166166

167-
static absl::Status ConsumeAndParseMacroCall(
168-
TokenStreamView::const_iterator, const StreamIteratorGenerator &,
169-
verible::MacroCall *, const verible::MacroDefinition &);
167+
// Non-static so it can record diagnostics into preprocess_data_.errors.
168+
absl::Status ConsumeAndParseMacroCall(TokenStreamView::const_iterator,
169+
const StreamIteratorGenerator &,
170+
verible::MacroCall *,
171+
const verible::MacroDefinition &);
170172

171173
// The following functions return nullptr when there is no error:
172174
absl::Status ConsumeMacroDefinition(const StreamIteratorGenerator &,

verible/verilog/preprocessor/verilog-preprocess_test.cc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,5 +1033,30 @@ TEST(VerilogPreprocessTest,
10331033
<< error.error_message;
10341034
}
10351035

1036+
// Regression: a callable-macro invocation truncated at end-of-stream (no '(',
1037+
// or '(' with no matching ')') must not crash or hang the preprocessor. Before
1038+
// the fix these inputs dereferenced past the end of the token stream view
1039+
// (SIGSEGV) or spun forever scanning arguments. With error-surfacing enabled
1040+
// the no-'(' cases also report a preprocessor diagnostic.
1041+
TEST(VerilogPreprocessTest, TruncatedCallableMacroDoesNotCrash) {
1042+
constexpr std::string_view kNoParenInputs[] = {
1043+
"`define A(x) hello `A\n`A(1)\n", // truncated callable ref in macro body
1044+
"`define A(x) x\n`A\n", // truncated callable ref at top level
1045+
};
1046+
for (std::string_view input : kNoParenInputs) {
1047+
PreprocessorTester tester(
1048+
input, VerilogPreprocess::Config({.expand_macros = true}));
1049+
EXPECT_FALSE(tester.Status().ok()) << input;
1050+
EXPECT_GE(tester.PreprocessorData().errors.size(), 1) << input;
1051+
}
1052+
1053+
// '(' with no matching ')': must terminate (was an infinite loop). The
1054+
// residue is rejected downstream, so only assert non-OK here.
1055+
PreprocessorTester open_paren(
1056+
"`define C(z) z\n`define A(x) hello `C(\n`A(1)\n",
1057+
VerilogPreprocess::Config({.expand_macros = true}));
1058+
EXPECT_FALSE(open_paren.Status().ok());
1059+
}
1060+
10361061
} // namespace
10371062
} // namespace verilog

verible/verilog/tools/preprocessor/verilog-preprocessor.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ static absl::Status PreprocessSingleFile(
122122
// source code just like it was, but with conditionals filtered.
123123
lexed_sequence.push_back(lexer.GetLastToken());
124124
}
125+
lexed_sequence.push_back(lexer.GetLastToken()); // EOF end sentinel
125126
verible::TokenStreamView lexed_streamview;
126127
// Initializing the lexed token stream view.
127128
InitTokenStreamView(lexed_sequence, &lexed_streamview);

0 commit comments

Comments
 (0)