Skip to content

Set recursion limit for QPY deserialization - #17415

Open
Matthew Treinish (mtreinish) wants to merge 8 commits into
Qiskit:mainfrom
mtreinish:fix-main-squashed
Open

Matthew Treinish (mtreinish) wants to merge 8 commits into
Qiskit:mainfrom
mtreinish:fix-main-squashed

Conversation

@mtreinish

Copy link
Copy Markdown
Member

Several parts of QPY deserialization are currently implemented recursively, primarily deserialization of control flow instruction bodies, Expr, and parameter expression substitutions. This could cause a segfault from a stack overflow if the recursion is too deep. To address this as a temporary solution this sets a recursion limit that returns a catchable error in the case the recursion is too deep. Longer term we can look at ways to either rewrite the deserialization to avoid recursion or come up with other mechanisms like using stacker, but these require more care or have larger issues associated with them then what we can fix in a security patch release.

An additional place where there is recursion triggered through QPY deserialization, but is more general than just QPY, occurs during building out ParameterExpressions. This is fixed in this commit because QPY can trigger it and it would have the same issue being addressed by the rest of the commit. Unlike the recursion limit set in the qpy crate, the stacker library is used to address this for the parameter expression. Using stacker there applied cleanly and enables the stack to spill onto the heap when needed avoiding a hard limit.

AI/LLM disclosure

  • No part of this submission is LLM generated.
  • Some written text was generated by:
  • Some submitted code was generated by:

Several parts of QPY deserialization are currently implemented
recursively, primarily deserialization of control flow instruction
bodies, Expr, and parameter expression substitutions. This could cause a
segfault from a stack overflow if the recursion is too deep. To address
this as a temporary solution this sets a recursion limit that returns
a catchable error in the case the recursion is too deep. Longer term
we can look at ways to either rewrite the deserialization to avoid
recursion or come up with other mechanisms like using stacker, but
these require more care or have larger issues associated with them
then what we can fix in a security patch release.

An additional place where there is recursion triggered through QPY
deserialization, but is more general than just QPY, occurs during
building out ParameterExpressions. This is fixed in this commit
because QPY can trigger it and it would have the same issue being
addressed by the rest of the commit. Unlike the recursion limit set in
the qpy crate, the stacker library is used to address this for the
parameter expression. Using stacker there applied cleanly and
enables the stack to spill onto the heap when needed avoiding a hard
limit.

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
Co-authored-by: David F. Cohen <me@davidfcohen.com>
@mtreinish Matthew Treinish (mtreinish) added this to the 2.7.0 milestone Nov 4, 2026
@mtreinish
Matthew Treinish (mtreinish) requested a review from a team as a code owner November 4, 2026 14:04
@mtreinish Matthew Treinish (mtreinish) added the Changelog: Fixed Add a "Fixed" entry in the GitHub Release changelog. label Nov 4, 2026
@mtreinish
Matthew Treinish (mtreinish) requested a review from a team as a code owner November 4, 2026 14:04
@github-project-automation github-project-automation Bot moved this to Ready in Qiskit 2.7 Nov 4, 2026
@qiskit-bot

Copy link
Copy Markdown
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core
  • @mtreinish

Turns out that we call` iter_symbols` indirectly to produce `Err`
variants in `try_into_value` in non-exceptional code and we then
immediately discard them...  Leaving that aside, the previous
implementation of `iter_symbols` required a (not lazy!) recursively
boxed structure on the heap, which is problematic for `Drop` and for the
iterator-combinators that end up recursive too.

This logic is order-identical (left-first DFS), but turns the iteration
logic into a flat structure with an iterative algorithm.  We only push
to the stack when we encounter `Binary` nodes; everything else is
traversed with no per-level memory overhead.
In debug builds the stack frame size is larger and can trigger stack
overflows while building the qpy. That's not part of this fix and to
avoid the testing failure this sets a small depth that won't trigger a
stack overflow and then using unittest mock to set the recursion limit
even lower.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest commits LGTM.

Comment on lines +3712 to +3713
BinaryOp::Mul => match crate::stack::grow(|| self.mul_opt(l, recursive)) {
Some(e) => match crate::stack::grow(|| e.mul_opt(r, recursive)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could have used the same mul_opt_inner form we did in other cases, but not big deal.

@mtreinish
Matthew Treinish (mtreinish) added this pull request to the merge queue Nov 4, 2026
@jakelishman
Jake Lishman (jakelishman) removed this pull request from the merge queue due to a manual request Nov 4, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Changelog: Fixed Add a "Fixed" entry in the GitHub Release changelog.

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

5 participants