From d83213e553103a8e37a1b9b8a155f433b3f63878 Mon Sep 17 00:00:00 2001 From: NaokiMizuta5 Date: Mon, 7 Sep 2026 20:07:22 +0900 Subject: [PATCH] fix(qasmtools): harden OpenQASM parser against malformed input Five short malformed OpenQASM inputs reach the public entry point parse_string (and thus pystaq.parse_str, the staq CLI, and any embedder) and abort or hang the host process: - out-of-range integer/real literals: uncaught std::out_of_range from std::stoi/std::stof (SIGABRT) - deeply nested parenthesised expressions: unbounded recursion in parse_exp/parse_atom (stack overflow, SIGSEGV) - unterminated string literal at EOF: lex_string loop omits EOF (infinite loop, unbounded memory growth) - token kind/value mismatch (e.g. `qreg 5[2];`): as_string/as_int on a mismatched token throws std::bad_variant_access (SIGABRT) The exception cases bypass the parser's documented ParseError channel, so a caller catching only ParseError still aborts; the recursion and infinite loop bypass exception handling entirely, so a pystaq-based service crashes or hangs too. Wrap stoi/stof in try/catch and emit a Token::Kind::error; add an EOF guard to the lex_string loop; bound expression recursion with an RAII depth guard (MAX_EXPR_DEPTH); make as_int/as_real/as_string total via std::get_if. Each input now yields a parse error instead of aborting or hanging, and all 82 bundled sample .qasm files parse identically before and after. --- CHANGES.md | 4 ++++ qasmtools/include/qasmtools/parser/lexer.hpp | 18 ++++++++++++------ qasmtools/include/qasmtools/parser/parser.hpp | 18 ++++++++++++++++++ qasmtools/include/qasmtools/parser/token.hpp | 18 +++++++++++++++--- 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 72e185d8..1f7526cf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,10 @@ - Updated third party library [fmt](https://github.com/fmtlib/fmt) to version 11.1.2 - Fixed MSVC compiling issues on Windows +- Hardened the OpenQASM 2.0 parser against malformed input: out-of-range + numeric literals, deeply nested expressions, unterminated string literals at + end of file, and token kind/value mismatches now yield a parse error instead + of aborting or hanging the process # Version 3.5 - 8 March 2024 diff --git a/qasmtools/include/qasmtools/parser/lexer.hpp b/qasmtools/include/qasmtools/parser/lexer.hpp index 6918f201..6ce8096f 100644 --- a/qasmtools/include/qasmtools/parser/lexer.hpp +++ b/qasmtools/include/qasmtools/parser/lexer.hpp @@ -165,11 +165,17 @@ class Lexer { } } - if (integral) { - return Token(tok_start, Token::Kind::nninteger, str, - std::stoi(str)); - } else { - return Token(tok_start, Token::Kind::real, str, std::stof(str)); + try { + if (integral) { + return Token(tok_start, Token::Kind::nninteger, str, + std::stoi(str)); + } else { + return Token(tok_start, Token::Kind::real, str, std::stof(str)); + } + } catch (const std::out_of_range&) { + std::cerr << "Lexical error at " << tok_start + << ": numeric literal out of range\n"; + return Token(tok_start, Token::Kind::error, str); } } @@ -213,7 +219,7 @@ class Lexer { str.reserve(64); // Reserve space to avoid reallocation while (buf_->peek() != '"' && buf_->peek() != '\n' && - buf_->peek() != '\r') { + buf_->peek() != '\r' && buf_->peek() != EOF) { str.push_back(buf_->peek()); skip_char(); } diff --git a/qasmtools/include/qasmtools/parser/parser.hpp b/qasmtools/include/qasmtools/parser/parser.hpp index 5d88f85b..2231120d 100644 --- a/qasmtools/include/qasmtools/parser/parser.hpp +++ b/qasmtools/include/qasmtools/parser/parser.hpp @@ -70,6 +70,10 @@ class Parser { Token current_token_; ///< current token int bits_ = 0; ///< number of bits int qubits_ = 0; ///< number of qubits + int expr_depth_ = 0; ///< current expression-parsing recursion depth + /// Cap on expression nesting; bounds recursion to prevent stack overflow + /// on adversarial input (e.g. deeply nested parentheses). + static constexpr int MAX_EXPR_DEPTH = 1000; #ifdef EXPR_GMP bool use_gmp_ = false; ///< whether to use gmp to parse reals #endif /* EXPR_GMP */ @@ -650,6 +654,20 @@ class Parser { * \return Unique pointer to an expression object */ ast::ptr parse_exp(int min_precedence = 1) { + struct DepthGuard { + int& d; + explicit DepthGuard(int& d_) : d(d_) { ++d; } + ~DepthGuard() { --d; } + } depth_guard{expr_depth_}; + if (expr_depth_ > MAX_EXPR_DEPTH) { + error_ = true; + if (!supress_errors_) { + std::cerr << current_token_.position() + << ": expression nesting exceeds maximum depth\n"; + } + throw ParseError(); + } + auto pos = current_token_.position(); auto lexp = parse_atom(); diff --git a/qasmtools/include/qasmtools/parser/token.hpp b/qasmtools/include/qasmtools/parser/token.hpp index 446c124f..8fd0b58b 100644 --- a/qasmtools/include/qasmtools/parser/token.hpp +++ b/qasmtools/include/qasmtools/parser/token.hpp @@ -294,7 +294,13 @@ class Token { * * \return The value of the token as an integer */ - int as_int() const { return std::get(value_); } + int as_int() const { + // Total accessor: a kind/value mismatch (e.g. a token returned by a + // failed expect_and_consume_token) must not throw std::bad_variant_access + // and abort the host; the parser's error_ flag already records the error. + auto* p = std::get_if(&value_); + return p ? *p : 0; + } /** * \brief Get the floating point value @@ -303,7 +309,10 @@ class Token { * * \return The value of the token as a floating point number */ - double as_real() const { return std::get(value_); } + double as_real() const { + auto* p = std::get_if(&value_); + return p ? *p : 0.0; + } /** * \brief Get the string value @@ -312,7 +321,10 @@ class Token { * * \return The value of the token as a string */ - std::string as_string() const { return std::get(value_); } + std::string as_string() const { + auto* p = std::get_if(&value_); + return p ? *p : std::string(); + } /** * \brief Return the position of the token