Skip to content

Non-consuming multi-context set grows the context stack without making progress #712

Open

Description

@thorseraq

Summary

ParseState::parse_line does not return within a 2-second deadline when a zero-width match repeatedly executes set: [main, main]. Each transition replaces one context with two at the same input position, so the context stack keeps growing.

Minimal reproducer

Create a Cargo project with these two files.

Cargo.toml:

[package]
name = "syntect-set-repro"
version = "0.1.0"
edition = "2021"

[dependencies]
syntect = { version = "=5.3.0", default-features = false }

[features]
default = ["onig"]
onig = ["syntect/default-onig"]
fancy = ["syntect/default-fancy"]

src/main.rs:

use syntect::parsing::{ParseState, SyntaxDefinition, SyntaxSetBuilder};

fn main() {
    let syntax = r#"
name: Synthetic
scope: source.synthetic
contexts:
  main:
    - match: '(?=x)'
      set: [main, main]
"#;
    let definition = SyntaxDefinition::load_from_str(syntax, true, None).unwrap();
    let mut builder = SyntaxSetBuilder::new();
    builder.add(definition);
    let syntaxes = builder.build();
    let mut parser = ParseState::new(&syntaxes.syntaxes()[0]);
    let input = std::env::args().nth(1).unwrap_or_else(|| "x".to_owned());

    eprintln!("Starting parse_line({input:?})");
    let result = parser.parse_line(&input, &syntaxes);
    eprintln!("parse_line returned: {result:?}");
}

Build first, then run with a deadline so compilation time is excluded:

cargo build --release
python3 - <<'PY'
import subprocess

for value in ("y", "x"):
    print(f"Testing input {value!r}", flush=True)
    try:
        subprocess.run(
            ["target/release/syntect-set-repro", value],
            check=True,
            timeout=2,
        )
    except subprocess.TimeoutExpired:
        print("Timed out after 2 seconds; child killed and reaped.", flush=True)
PY

The Python timeout kills and reaps the child process. To test the other regex engine, rebuild with the following command and run the same Python snippet:

cargo build --release --no-default-features --features fancy

Expected and actual behavior

Expected: parsing terminates with a result or a bounded error, even if this cyclic grammar cannot produce useful highlighting.

Actual: y prints parse_line returned: Ok(...) and exits successfully. For x, the last parser message is Starting parse_line("x"); it never prints a return value before the 2-second deadline kills the process. The timeout is an observation window, not a measurement of eventual completion.

Versions tested

Verified on 2026-10-21 using release builds on macOS/aarch64, with Rust 1.99.0:

Syntect Oniguruma fancy-regex
crates.io 5.3.0 x: timed out; y: returned x: timed out; y: returned
master 4aa78031e93ebd3e0be7278120d0bd9d2508b1a3 x: timed out; y: returned x: timed out; y: returned

Resolved regex versions: Oniguruma wrapper onig 6.5.3 for both; fancy-regex 0.16.2 for 5.3.0 and 0.18.0 for master.

Possible cause and related work

In src/parsing/parser.rs, push_too_deep covers Push (and Branch/Embed on master), but does not cover Set. Here, (?=x) consumes no input and each set increases the stack depth by one, bypassing that depth guard.

This differs from the flat-stack set cycle described in #706: the full stack changes on every iteration. #708 proposes optional parsing budgets; this reproduction uses the existing parse_line API. Both PRs were open when checked on 2026-10-21.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions