protocol: refuse a mismatched correlation where the frame is encoded #218

Merged
natsukium merged 1 commit from fix/correlation-encode-gate-196 into main 2026-09-08 03:11:06 +09:00
Owner

Fixes #196.

A correlation violation was only caught at the receiver, where the driver
refuses the frame and drops the connection: the sender saw a bare EOF and the
diagnosis stayed in the daemon's log. Worse, the reachable spelling of a
subscribe — send(&msg) with no envelope — was exactly the wrong one, so a
satellite client's standing subscription died silently under a redial loop.

CheckedFrame now applies the class half of the same rule at encode time: it
bounds both paths on Directed and refuses a correlated arm handed no
envelope, an uncorrelated arm handed one, and an id of the wrong kind in either
direction. Every FrameWriter::send*, the client's outgoing queue and the
daemon's fan-out inherit the check. It reads the arm's CorrelationClass,
never whether a message happens to carry an id (Conn::Cancel/End/Error
name a stream in an ordinary field and stay uncorrelated). Identity — the next
unissued id, an outstanding request, a live stream — remains the driver's, so
the two ends keep one ledger. TransportError::Correlation mirrors
DriverError::Correlation field for field, so one pairing renders one sentence
on both ends.

Doc cascade: docs/reference/ipc.md states the encoder-side refusal beside the
receiver-side rule, docs/explanation/architecture/ipc.md carries why the
class check is duplicated at the encoder while identity is not, and
docs/reference/testing.md tracks the moved coverage.

Verified: just check green (fmt, clippy, nextest, deny, proto-compat); reviewed
by pi luna + pi sol; docs proofread by Gemini gemini-3.8-flash-high

Deferred

  • No proptest or Kani coverage of the full class-by-envelope matrix: five
    classes by three envelope states; the four refusal shapes and the honest
    pairings are each covered by a focused test, and a generator would only
    restate the same matches! arm.
Fixes #196. A correlation violation was only caught at the receiver, where the driver refuses the frame and drops the connection: the sender saw a bare EOF and the diagnosis stayed in the daemon's log. Worse, the reachable spelling of a subscribe — `send(&msg)` with no envelope — was exactly the wrong one, so a satellite client's standing subscription died silently under a redial loop. `CheckedFrame` now applies the class half of the same rule at encode time: it bounds both paths on `Directed` and refuses a correlated arm handed no envelope, an uncorrelated arm handed one, and an id of the wrong kind in either direction. Every `FrameWriter::send*`, the client's outgoing queue and the daemon's fan-out inherit the check. It reads the arm's `CorrelationClass`, never whether a message happens to carry an id (`Conn::Cancel`/`End`/`Error` name a stream in an ordinary field and stay uncorrelated). Identity — the next unissued id, an outstanding request, a live stream — remains the driver's, so the two ends keep one ledger. `TransportError::Correlation` mirrors `DriverError::Correlation` field for field, so one pairing renders one sentence on both ends. Doc cascade: `docs/reference/ipc.md` states the encoder-side refusal beside the receiver-side rule, `docs/explanation/architecture/ipc.md` carries why the class check is duplicated at the encoder while identity is not, and `docs/reference/testing.md` tracks the moved coverage. Verified: just check green (fmt, clippy, nextest, deny, proto-compat); reviewed by pi luna + pi sol; docs proofread by Gemini gemini-3.8-flash-high ## Deferred - No proptest or Kani coverage of the full class-by-envelope matrix: five classes by three envelope states; the four refusal shapes and the honest pairings are each covered by a focused test, and a generator would only restate the same `matches!` arm.
protocol: refuse a mismatched correlation where the frame is encoded
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 50s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m10s
windows / cargo clippy (Windows cross) (pull_request) Failing after 14s
bench / Criterion regression gate (pull_request) Successful in 2m13s
darwin / detect relevant changes (pull_request) Successful in 3s
darwin / darwin docs-only no-op (pull_request) Has been skipped
pr / nix flake check (pull_request) Successful in 30s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m43s
pr / wire schema is compatible with the base (pull_request) Successful in 11s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m11s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 3s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / frontend smoke (Windows) (pull_request) Has been cancelled
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been cancelled
windows / cargo nextest (Windows) (pull_request) Has been cancelled
fe96e99b28
A correlation violation is fatal at the receiver: the driver refuses the
frame and ends the connection, so a caller that sent one reads a bare EOF
and the diagnosis lives only in the daemon's log. Nothing on the sending
side said so, and the reachable spelling of a subscribe -- `send(&msg)`
without an envelope -- was exactly the wrong one, leaving a satellite
client's standing subscription silently dead under a redial loop.

The encoder now checks the class half of the same rule: `CheckedFrame`
bounds both paths on `Directed` and refuses a correlated arm handed no
envelope, an uncorrelated arm handed one, and an id of the wrong kind in
either direction, so every `FrameWriter::send*`, the client's outgoing
queue and the daemon's fan-out inherit it. The check reads the arm's
class, never whether a message carries an id: `Conn::Cancel`/`End`/`Error`
name a stream in an ordinary field and are uncorrelated. Identity -- the
next unissued id, an outstanding request, a live stream -- stays the
driver's, since duplicating that ledger would give the two ends two
answers.

`TransportError::Correlation` mirrors `DriverError::Correlation` field for
field so one pairing renders one sentence on both ends: a caller reading
the refusal at the call site and a maintainer reading the daemon's log are
chasing the same text, not two spellings of it.

Refs #196
natsukium force-pushed fix/correlation-encode-gate-196 from fe96e99b28
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 50s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m10s
windows / cargo clippy (Windows cross) (pull_request) Failing after 14s
bench / Criterion regression gate (pull_request) Successful in 2m13s
darwin / detect relevant changes (pull_request) Successful in 3s
darwin / darwin docs-only no-op (pull_request) Has been skipped
pr / nix flake check (pull_request) Successful in 30s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m43s
pr / wire schema is compatible with the base (pull_request) Successful in 11s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 1m11s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 3s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / frontend smoke (Windows) (pull_request) Has been cancelled
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been cancelled
windows / cargo nextest (Windows) (pull_request) Has been cancelled
to b11050b6d5
All checks were successful
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m8s
pr / nix flake check (pull_request) Successful in 35s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m54s
pr / wire schema is compatible with the base (pull_request) Successful in 8s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 3s
bench / Criterion regression gate (pull_request) Successful in 2m7s
darwin / detect relevant changes (pull_request) Successful in 3s
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 52s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 52s
windows / cargo nextest (Windows) (pull_request) Successful in 4m25s
windows / windows docs-only no-op (pull_request) Has been skipped
windows / frontend smoke (Windows) (pull_request) Successful in 2m4s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
windows / cargo clippy (Windows cross) (pull_request) Successful in 16s
2026-09-08 02:34:25 +09:00
Compare
natsukium deleted branch fix/correlation-encode-gate-196 2026-09-08 03:11:06 +09:00
Sign in to join this conversation.
No description provided.