Skip to content

yaml/compiler: cycle detection misses a genuine cycle when an adjacent bidirectional-node cycle is found first #533

@staging-devin-ai-integration

Description

Severity

P2 / Major — a validation bypass: a pipeline containing a genuine (non-bidirectional) circular dependency can pass compile() and reach the engine, where Reliable backpressure on the cycle can deadlock the pipeline instead of producing a clear authoring error. Trigger is topology/ordering-dependent (lower confidence on severity — could be argued P3), but it is a real correctness hole.

Environment

  • Build: main @ 2f77388, Linux x86_64
  • Component: crates/api/src/yaml/compiler.rs (detect_cycles)

Summary

Cycle detection deliberately exempts cycles that involve a bidirectional node (transport::moq::peer). But the DFS returns on the first cycle it finds, and the top-level loop merely continues past an exempt cycle without re-exploring the edges it abandoned. Nodes on the exempt path are already in visited, so a second, genuine cycle reachable only via those unexplored edges is treated as a cross-edge and never detected. Net: a real circular dependency slips through whenever a bidirectional node is discovered adjacent to it first.

Steps to reproduce

Compile this DAG via streamkit_api::yaml::{parse_yaml, compile} (names chosen so the bidirectional branch is visited first):

mode: dynamic
nodes:
  aaa:
    kind: test_node
    needs: [bbb, ccc]
  bbb:
    kind: transport::moq::peer
    needs: aaa
  ccc:
    kind: test_node
    needs: aaa

Here aaa <-> ccc (both non-bidirectional) is a genuine cycle; bbb <-> aaa is the exempt bidirectional cycle.

Expected

compile() returns Err("Circular dependency detected: ... aaa ... ccc ..."). (Control: remove bbb and the bare aaa <-> ccc cycle is correctly rejected.)

Actual

compile() returns Ok(...); the resulting Pipeline contains both ccc -> aaa and aaa -> ccc, i.e. a real cycle, with no error.

User/business impact

An invalid pipeline with an accidental circular dependency passes YAML/API validation whenever a transport::moq::peer (heavily used in this repo's stream samples) sits adjacent to the cycle, defeating the purpose of cycle validation and risking a runtime deadlock/hang rather than a clear compile-time error.

Evidence

  • crates/api/src/yaml/compiler.rs:65-148 (detect_cycles): inner dfs returns Some on the first cycle (:82-86); the outer loop applies the bidirectional exemption and continues (:137-145) without re-exploring abandoned sibling edges, while nodes on the exempt path remain in visited, so the second cycle's back-edge fails the rec_stack check (:87).
  • Verified via an executed throwaway integration test against the public yaml::compile API (since removed; nothing committed): the masked-cycle case returned Ok with both aaa->ccc and ccc->aaa present; the control (no peer) returned Err("Circular dependency detected: ...").

Suspected fix

Don't abandon exploration on the first cycle: either enumerate all cycles and apply the bidirectional exemption per-cycle without leaving sibling edges unexplored, or exclude only bidirectional-node edges from the graph before running standard cycle detection on the remainder.

Dedupe notes

No existing issue. The lower-confidence param-validation gap found alongside this (non-object params only validated for audio::mixer) is already tracked by open #524, so it is not filed here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions