feat(iso-on-tcp): add S7comm ISO-on-TCP carry-buffer reassembly (STORY-186) - #470
Merged
Merged
Conversation
Add compilable S7commAnalyzer/S7commFlowState skeleton in src/analyzer/s7comm.rs (SS-21) that consumes STORY-184/185's SS-20 iso_on_tcp::parse_tpkt_header / parse_cotp_header. MAX_S7_ISO_ON_TCP_CARRY_BYTES const, minimal S7commFlowState (carry fields only), on_data/on_flow_close/resync_one_byte signatures are complete; bodies are todo!() per Red Gate discipline (BC-5.38.001) -- no business logic copied from sibling analyzers. Registers pub mod s7comm; in src/analyzer/mod.rs (not yet wired to the dispatcher -- STORY-193). No tests added yet; that is the next step. Claude-Session: https://claude.ai/code/session_01EjfRzG4sTxXaPAUsFsUgt4
… walk-first frame extraction, resync, and flow-close Implements S7commAnalyzer::on_data (BC-2.20.013/014/015) and on_flow_close (BC-2.21.003) against the STORY-186 failing test suite: - Frame-walk loop runs unconditionally on carry[direction] ++ incoming_data (walk-first, no aggregate carry+delivery pre-check; BC-2.20.013). - Carry-overflow check on the directional carry alone at call entry, strict `>` MAX_S7_ISO_ON_TCP_CARRY_BYTES (65,535); clears carry and emits exactly one T0814 (Anomaly/Possible/Medium) per direction, guarded by carry_overflow_reported_c2s/_s2c (BC-2.20.014). - Shared resync_one_byte sub-routine advances exactly 1 byte per iteration, reused verbatim for both bad-version-byte and post-overflow resync (BC-2.20.015). - on_flow_close removes S7commFlowState, discarding carry bytes with no finding; no-op for an unknown flow_key (BC-2.21.003). All 16 tests in tests/s7comm_analyzer_tests.rs pass (14 behavioral + 2 static regression guards for the frozen SS-20 module boundary). Claude-Session: https://claude.ai/code/session_01EjfRzG4sTxXaPAUsFsUgt4
…er BC-2.20.014 v1.1 (STORY-186 F-02) Claude-Session: https://claude.ai/code/session_01EjfRzG4sTxXaPAUsFsUgt4
…data-unreachability and EC-002 double-close tests (adversarial F-02/F-06) Claude-Session: https://claude.ai/code/session_01EjfRzG4sTxXaPAUsFsUgt4
…LOG entry, defer timestamp compute (adversarial F-01/F-02/F-03/F-07) Claude-Session: https://claude.ai/code/session_01EjfRzG4sTxXaPAUsFsUgt4
… D5 T0814 defense-in-depth (adversarial P2 MINOR/NIT) Claude-Session: https://claude.ai/code/session_01EjfRzG4sTxXaPAUsFsUgt4
…versarial P5/P3 NITs) Claude-Session: https://claude.ai/code/session_01EjfRzG4sTxXaPAUsFsUgt4
…defense-in-depth guard, flow-close) Claude-Session: https://claude.ai/code/session_01EjfRzG4sTxXaPAUsFsUgt4
Zious11
added a commit
that referenced
this pull request
Sep 8, 2026
…CP carry-buffer reassembly) PR #470 squash-merged to develop as 294174f (2026-09-07T23:49Z); develop e0ea30c->294174f5. STORY-186 status ready->delivered (frontmatter + STORY-INDEX.md v4.27->v4.28 index row + wave-89 Wave Delivery Progress row). stories_delivered 122->123. Pipeline PAUSED->IN-PROGRESS (resumed from SESSION-WRAP-PAUSE D-564); F4 delta-implementation now 3/11 delivered; NEXT STORY-187 (wave 90). Per-story adversarial CONVERGED 3/3 (BC-5.39.001) across 5 passes (P1/P1b/P2/P3/P5): Pass 1 found 2 MAJOR (CHANGELOG gap; dead-code T0814 overflow guard exposing a BC-2.20.013-vs-BC-2.20.014 spec contradiction) + 2 MINOR + 3 NIT; human ruling Option B (defense-in-depth) drove a same-burst spec reconciliation (BC-2.20.013/014 v1.0->v1.1, ADR-0014 note, STORY-186 v1.0->v1.1). Red Gate PASSED (14 tests red pre-impl, 18/18 green post-impl). pr-reviewer APPROVE; security CLEAN x2 reviews; CI 13/13; demo evidence 12/12 ACs. Artifacts written: cycles/feature-s7comm/STORY-186/{convergence-report.md, implementation/red-gate-log.md, demo-evidence-pointer.md}. Three accepted-residual NITs + 4 process observations (3 [infra], 1 [process-gap]) logged to lessons.md; the [process-gap] (F2/F3 cross-BC consistency gap) recorded as a new Drift Item (DRIFT-F2-CROSS-BC-CONSISTENCY-CHECK) in drift-items-and-carry-forwards.md. PG-CANONICAL-HOLDOUT-NOT-AC-ENFORCED did NOT recur a 3rd time (watch updated, not escalated). New STORY-193 security forward-look carried forward (confirm evicted flows reach S7commAnalyzer::on_flow_close once dispatcher-wired). SESSION-WRAP-PAUSE (D-564) checkpoint archived to cycles/feature-s7comm/session-checkpoints.md; new Session Resume Checkpoint written for D-565. Current Phase Steps: D-560 row evicted (full text preserved verbatim in Decisions Log D-560 row). Pre-existing worktree drift (regression-state.json + sidecar-learning.md hook-churn, and the already-written code-delivery/STORY-186/pr-description.md from earlier in the delivery pipeline) folded into this same commit. Claude-Session: https://claude.ai/code/session_01EjfRzG4sTxXaPAUsFsUgt4
Zious11
added a commit
that referenced
this pull request
Sep 8, 2026
…1 delivered, next STORY-187 wave 90 Session-wrap-pause checkpoint (D-566). Pipeline IN-PROGRESS→PAUSED; version 3.3→3.4. F4 delta-implementation (feature-s7comm, epic E-23) now has STORY-184/185/186 DELIVERED (3/11) — STORY-186 (PR #470, develop 294174f) fully merged, its worktree and feature branch already cleaned up, no work in-flight. NEXT: STORY-187 (wave 90). D-565 Session Resume Checkpoint archived to cycles/feature-s7comm/session-checkpoints.md; new checkpoint written with all six required fields. Current Phase Steps: new row added, D-561 row evicted (preserved verbatim in Decisions Log D-561). Added the mandatory `## Convergence Status` section (D-434(e) sub-check 1), previously absent from this STATE.md and blocking the state-structure validator — kept to a one-line status pointer, no new per-pass trajectory file, per the trivial-only scope for this pause burst. Size-budget banner reconciled to the post-edit wc -l (232 lines). sidecar-learning.md session-end marker (pre-existing, left by a prior/concurrent session) folded into this same commit. Pre-existing note: validate-closes-completeness flags 5 bare-umbrella Decisions Log citations (D-394..D-561 x2, D-001..D-458, D-516..D-551, D-552..D-556) in content this burst did not touch (frontmatter story_index_note, Project Metadata Mode row, Historical Content table) — out of scope for this bookkeeping-only pause, left for a future maintenance/compact-state pass. No story/spec/code content changed — bookkeeping-only wrap-pause burst, the only STATE.md mutation performed during wrap (BC-6.28.001 INV-1), single-commit burst (TD-VSDD-053). Claude-Session: https://claude.ai/code/session_01EjfRzG4sTxXaPAUsFsUgt4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[STORY-186] S7comm ISO-on-TCP Carry-Buffer Reassembly, Walk-First Frame Extraction, Resync, and the Frozen SS-20/SS-21 Module Boundary
Epic: S7comm over ISO-on-TCP (TPKT/COTP) stream dispatch and parser design (ADR-0014)
Mode: feature (brownfield, wave 89)
Convergence: CONVERGED after 5 adversarial passes (P1, P1b, P2, P3, P5 — see Adversarial Review below)
This PR adds
S7commAnalyzer(src/analyzer/s7comm.rs, SS-21) — the first effectful-shellconsumer of STORY-184/185's stateless TPKT/COTP parsing library (SS-20,
src/analyzer/iso_on_tcp.rs). It implements directional carry-buffer TPKT reassembly acrossTCP segment boundaries using a walk-first, residual-bound frame-extraction loop (no aggregate
carry.len() + data.len()pre-check), a shared 1-byte resync sub-routine reused verbatim forboth bad-version-byte and post-overflow conditions, a 65,535-byte carry bound with a
defense-in-depth overflow guard (clear-not-truncate + one T0814 finding per direction, proven
unreachable via real
on_datatraffic under the current design), and flow-close teardown thatdiscards carry bytes with no finding. It also freezes the SS-20/SS-21 module boundary with two
static regression guards:
iso_on_tcp.rsmust contain zeroStreamAnalyzerimpls, and noIsoOnTcpFlowStatetype may exist anywhere in the tree.Architecture Changes
graph TD IsoOnTcp["iso_on_tcp.rs (SS-20)<br/>parse_tpkt_header / parse_cotp_header<br/>stateless, pure-core"] -->|calls| S7comm["s7comm.rs (SS-21)<br/>S7commAnalyzer, new"] S7comm -.->|new dependency| FlowState["S7commFlowState<br/>carry_c2s / carry_s2c<br/>per-direction overflow latches"] style S7comm fill:#90EE90 style FlowState fill:#90EE90Architecture Decision Record
ADR: Carry-overflow T0814 guard reclassified as defense-in-depth (ADR-0014 v1.1)
Context: BC-2.20.014 originally specified an overflow guard for
residual.len() > 65,535. During adversarial review (F-02/F-03, two independent passes),it was shown that under the BC-2.20.013 walk-first + BC-2.20.015 1-byte-resync design, the
directional carry is bounded
<= 65,534bytes by construction for both conformant andadversarial input (TPKT's
lengthfield is u16-capped), making the over-bound branchunreachable via the real
on_datapath.Decision: Reclassify the overflow guard as defense-in-depth (Option B, human-ratified
2026-09-07) rather than removing it. The guard mechanics (clear-not-truncate, resync, one
T0814 per direction, per-direction dedup) remain the binding spec for the guard's behavior
if a future design regression ever makes the branch reachable, but are tested via direct
flow-state injection (SYNTHETIC), not via
on_data.Rationale: Removing the guard would leave no protection against a future change (e.g. a
resync or walk-first regression) that reintroduces reachability. Keeping it as defense-in-depth
preserves the safety net without over-claiming live coverage in test evidence.
Alternatives Considered:
regressions in the walk-first/resync invariants.
evidence; the adversarial pass found this to be a false claim of on-data reachability.
Consequences:
AC-186-005/006 SYNTHETIC) plus a positive unreachability proof
(
test_BC_2_20_014_overflow_unreachable_via_on_data, 200,000-byte garbage flood emits noT0814).
593c28ef).Story Dependencies
graph LR S184[STORY-184<br/>merged] --> S185[STORY-185<br/>merged] S185 --> S186[STORY-186<br/>this PR] S186 --> S187[STORY-187<br/>protocol-id dispatch, not started] S186 --> S193[STORY-193<br/>CLI wiring, not started] style S186 fill:#FFD700STORY-186
depends_onSTORY-185 (COTP TPDU header parser, merged todevelopate0ea30ce,PR #467). No other open dependency PRs block this one.
Spec Traceability
flowchart LR BC1[BC-2.20.013<br/>walk-first extraction] --> AC1["AC-186-001..003"] BC2[BC-2.20.014<br/>carry bound + guard] --> AC2["AC-186-004..006"] BC3[BC-2.20.015<br/>1-byte resync] --> AC3["AC-186-007..009"] BC4[BC-2.20.016<br/>frozen module boundary] --> AC4["AC-186-010..011"] BC5[BC-2.21.003<br/>flow-close teardown] --> AC5["AC-186-012"] AC1 --> T1[s7comm_analyzer_tests.rs] AC2 --> T1 AC3 --> T1 AC4 --> T1 AC5 --> T1 T1 --> S1[src/analyzer/s7comm.rs]Test Evidence
Coverage Summary
s7comm.rsmodule fully exercised by 18 tests + 3 proptest harnessesTest Flow
graph LR Unit["15 Unit/Regression Tests"] Proptest["3 Proptest Harnesses (VP-050)"] Wave["Wave Gate"] Unit -->|100% of new module| Pass1["PASS"] Proptest -->|default case count| Pass2["PASS"] Wave -->|deferred| Pass3["N/A yet"] style Pass1 fill:#90EE90 style Pass2 fill:#90EE90 style Pass3 fill:#87CEEBsrc/analyzer/s7comm.rs, 321 lines) — n/a baseline, fully covered by new testscargo mutantsrun recorded for this storycargo fmt --checkandcargo clippy --all-targets -- -D warningsboth clean locally at PR-manager verification timeDetailed Test Results (row-verified against evidence-report.md and local re-run, PG-W74-PRDESC-ROW-VERIFY)
New Tests (This PR) —
tests/s7comm_analyzer_tests.rstest_BC_2_20_013_walk_first_no_aggregate_prechecktest_BC_2_20_013_adversarial_burst_head_frame_not_droppedtest_BC_2_20_013_split_frame_across_two_callscargo testrun and evidence-report.md)test_BC_2_20_014_at_bound_residual_no_overflowcargo testrun and evidence-report.md)test_BC_2_20_014_overflow_clear_resync_one_t0814_per_directiontest_BC_2_20_014_repeated_overflow_dedup_same_directiontest_BC_2_20_014_overflow_unreachable_via_on_datatest_BC_2_20_014_overflow_dedup_independent_per_directiontest_BC_2_20_015_resync_advances_exactly_one_bytetest_BC_2_20_015_single_resync_implementation_sharedtest_BC_2_20_015_resync_terminates_no_valid_anchortest_BC_2_20_016_iso_on_tcp_has_no_stream_analyzer_implcargo testrun and evidence-report.md)test_BC_2_20_016_no_iso_on_tcp_flow_state_type_existstest_s7comm_on_flow_close_removes_state_discards_carrytest_BC_2_21_003_double_close_same_flow_key_is_idempotent_no_opstory_186::vp050::proptest_vp050_walk_first_residual_boundstory_186::vp050::proptest_vp050_direction_isolationstory_186::vp050::proptest_vp050_resync_one_byte_advanceAggregate-count cross-check (PG-W74-PRDESC-ROW-VERIFY): evidence-report.md claims
"18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out" — local re-run by pr-manager
(
cargo test --test s7comm_analyzer_tests) confirms 18 passed; 0 failed with the identical18 test names listed above. Aggregate count matches exactly; no discrepancy found.
Coverage Analysis
src/analyzer/s7comm.rs) + 1108 (test file)on_datawalk-first loop, resync, overflow guard (synthetic), andon_flow_closehas a dedicated testMutation Testing
Not run for this story delivery cycle — deferred to the wave's formal-hardening phase per
this wave's schedule. No mutation kill-rate claim is made in this PR.
Demo Evidence
Committed at
5e25d2baunderdocs/demo-evidence/STORY-186/: 7 recordings (.tapeVHSsource +
.gif+.webmeach) plusevidence-report.md(index). This is a pure-core /effectful-shell library story with no CLI/web surface yet (SS-21 dispatch wiring deferred to
STORY-193 per ADR-0014), so the demonstration vehicle is VHS terminal recordings of
cargo test --test s7comm_analyzer_tests, filtered per behavior group.AC-001-003-carry-reassembly.gif/.webmAC-004-006-defense-in-depth.gif/.webmAC-007-009-resync.gif/.webmAC-010-011-module-boundary.gif/.webmAC-012-flow-close.gif/.webmVP-050-proptests.gif/.webmAC-ALL-18-green.gif/.webmAll 12 acceptance criteria (AC-186-001..012) are covered by at least one recorded artifact.
Demo-evidence path-scrub gate (PG-W70-DEMO-SCRUB) passed 2026-09-07 — zero absolute-path
matches in any
.tape/.mdsource;cargo testoutput piped throughgrepto strip theRunning tests/...line that would otherwise leak a worktree filesystem path.Holdout Evaluation
N/A — evaluated at wave gate (per PR template convention for feature-mode stories; wave 89
gate has not yet run).
Adversarial Review
2078236d)98b28f9c)98b28f9c)Convergence: CONVERGED — 5 review passes across this story's adversarial cycle (P1,
P1b, P2, P3, P5), all findings resolved; no residual MINOR or blocking findings at PR-open
time. (Full adversarial pass transcripts held in session state, not duplicated here.)
Key Findings & Resolutions
Finding: Carry-overflow guard branch is unreachable via real on_data traffic (F-02/F-03)
src/analyzer/s7comm.rs(overflow guard inon_data)residual.len() > 65535) was originallyspecified and tested as if reachable via real traffic, but the walk-first + 1-byte-resync
design bounds directional carry to
<= 65,534bytes by construction.2026-09-07). Tests reclassified: AC-186-004 (at-bound,
== 65535) kept LIVE; AC-186-005/006(over-bound guard mechanics) reclassified SYNTHETIC via direct flow-state injection; added
positive unreachability proof test.
test_BC_2_20_014_overflow_unreachable_via_on_data()Finding: Test provenance / doc wording NITs (P2, P3, P5)
the repo's past-tense convention; EC-004 citation was mis-numbered; at-bound test doc
wording was ambiguous about LIVE vs. SYNTHETIC status.
2078236dand98b28f9c.Security Review
graph LR Critical["Critical: 0"] High["High: 0"] Medium["Medium: 0"] Low["Low: 0"] style Critical fill:#90EE90 style High fill:#90EE90 style Medium fill:#90EE90 style Low fill:#90EE90Security Scan Details
IMPORTANT PROVENANCE NOTE: Two
vsdd-factory:security-reviewersub-agents(
sec-review-186,sec-review-186b) were dispatched against this PR's diff. Both becameunresponsive for an extended period (repeated status checks over ~40+ minutes produced no
findings output to the pr-manager). The orchestrating session subsequently reported that it
observed both agents stuck in an idle-notification echo loop, stopped them, and asserted
their reviews were complete with a clean result: 0 CRITICAL, 0 HIGH, LOW/informational
only, APPROVE. The pr-manager (this report's author) never received the underlying
findings text, CWE citations, or severity table from either agent directly — the verdict
above is attested by the orchestrator, not independently confirmed against actual
security-reviewer output. Treat this disposition as orchestrator-reported, not
pr-manager-verified, when deciding whether to merge.
Independent of that unresolved provenance gap, the following is what the pr-manager can
state directly from reading the diff: this module is a pure in-process parser/reassembler
with no network listener, no filesystem I/O, no deserialization of untrusted formats beyond
the byte-slice TPKT/COTP framing already hardened in STORY-184/185 (bounds-checked slicing,
no
unsafe, no panics on malformed input — verified by the adversarial-burst andresync-termination tests in the test suite). Primary attack surface considered: unbounded
carry-buffer growth from a malicious peer — mitigated by the
MAX_S7_ISO_ON_TCP_CARRY_BYTESbound and the (now defense-in-depth) overflow guard. This is pr-manager's own read of the
code, not a substitute for the missing dedicated security-reviewer findings.
Risk Assessment & Deployment
Blast Radius
src/analyzer/s7comm.rs) not yet wired intothe CLI dispatcher (STORY-193 deferred) — this PR is additive only, no existing behavior
changes to shipped analyzers.
until STORY-193 lands.
Performance Impact
Not applicable — new module not yet wired into any executable code path (dispatch deferred
to STORY-193); no existing benchmark baseline to compare against.
Rollback Instructions
Immediate rollback:
Verification after rollback:
cargo test --all-targetsgreen ondevelopS7commAnalyzerremain reachable from the CLI dispatcher (there werenone before this PR either)
Traceability
test_BC_2_20_013_*(3 tests)test_BC_2_20_014_*(5 tests)test_BC_2_20_015_*(3 tests)test_BC_2_20_016_*(2 tests)test_s7comm_on_flow_close_*,test_BC_2_21_003_double_close_*(2 tests)story_186::vp050::proptest_*(3 harnesses)Full VSDD Contract Chain
AI Pipeline Metadata
Pipeline Details
Pre-Merge Checklist
git revert, additive-only change)[Unreleased]entry present (required — this PR touchessrc/)5e25d2ba, covering all 12 ACs + VP-050develop(e0ea30ce, PR feat(iso-on-tcp): add COTP TPDU parser (STORY-185) #467) before this PR openshttps://claude.ai/code/session_01EjfRzG4sTxXaPAUsFsUgt4