review 002: the protocol core reviewed — readiness-contract hardening + two convention fixes

The batch-1 gate review (tasks/implementation/review-protocol-core.md):
all eight criteria examined (seam contracts, wire fidelity, access
control, teardown/lifecycle, conventions, wasm, POC fidelity, docs
sync); the gate to batch 2 opens. Findings R-1/R-2 were corrected in
the review change; R-3..R-5 are doc residues fixed in the same change;
R-6 is accepted with rationale.

- R-1 (major): the datagram stage's Ok(None) re-park is unsafe against
  a zero-yield egress — a next_reply that is a pure instantly-ready
  Ok(None) with no await wins every select! poll and livelocks the
  stage on a current-thread runtime (empirically reproduced; the
  landed unit test's mutex masked the hazard at its lock's yield
  boundary; multi-thread survives). Fix: the Ok(None) handler yields
  before re-parking; a pinning unit test drives the strictest
  zero-yield violator.
- R-2: the two into_inner expect()s (consumer sessions) become typed
  Result<_, SpentSessionError> — convention 2's no-panic rule; spent-
  session unit tests pin the shape; test call sites updated.
- R-3/R-4/R-5: doc residues — stale task-time phrasings in
  egress.rs/producer.rs, producer.md's register() sketch reconciled
  to the landed signature, the establishment-bound test doc reworded
  to what the substrate-free probe actually pins.
- R-6 (accepted): the wasm gate rides a transitive getrandom/wasm_js
  feature unification (alkcall -> jsonschema/referencing -> ahash) —
  verified compiling clean today; noted for OQ-SK2-06.

Verified: cargo test 348 green; --all-features green; clippy
--all-targets -D warnings clean; fmt --check clean; both wasm checks
clean (from a clean wasm target dir); cargo doc --no-deps clean.
The bind-only-identity reconciliation from the integration session
was independently verified as correct (ADR-006 as landed; the suite
pins it). Batch boundary holds: no local/composed code exists.
This commit is contained in:
glm-5.3-flash committed 2026-09-24 09:13:08 +00:00
1 parent 1ad8d9d489
commit 16b21e3447
7 files changed
+466 -46

No files matched your search

+3 -1
View File
@@ -140,7 +140,9 @@ let producer = Socks5Producer::builder()
.egress(egress_factory) // EgressFactory — required for ASSOCIATE .egress(egress_factory) // EgressFactory — required for ASSOCIATE
.accept(bind_accept_factory) // BindAcceptFactory — required for BIND .accept(bind_accept_factory) // BindAcceptFactory — required for BIND
.build(); .build();
producer.register(&mut channel_core)?; // register_openable_with_establisher // register(&core, &registry, auth, witness) — the establisher rides
// the registration (the optional witness is the CF-006 test probe).
producer.register(&core, &registry, auth, None)?;
``` ```
Missing callbacks = those commands refuse RFC-faithfully Missing callbacks = those commands refuse RFC-faithfully
+70 -10
View File
@@ -67,6 +67,15 @@ use crate::rfc1928::util::target_addr::{read_address, TargetAddr, ToTargetAddr};
use crate::rfc1928::{ReplyError, Socks5Command, SocksError, UdpHeaderError}; use crate::rfc1928::{ReplyError, Socks5Command, SocksError, UdpHeaderError};
use crate::wire::{frame, DatagramReader, WireError}; use crate::wire::{frame, DatagramReader, WireError};
/// The spent-session marker (review 002 R-2): [`Socks5ChannelSession
/// ::into_inner`] / [`AssociateSession::into_inner`] return it when
/// the pipe/stream was already taken — reachable only by calling
/// twice on a session whose inner is gone. Carries nothing: a spent
/// session's teardown is already done (the reap ran with the take).
#[derive(Debug, thiserror::Error)]
#[error("the session's pipe/stream was already taken (spent)")]
pub struct SpentSessionError;
/// A completed SOCKS5 CONNECT session on the consumer side: the /// A completed SOCKS5 CONNECT session on the consumer side: the
/// adopted channel + the post-reply byte pipe to the target. /// adopted channel + the post-reply byte pipe to the target.
/// ///
@@ -159,11 +168,18 @@ impl Socks5ChannelSession {
/// The session rides along still owning the channel's teardown: /// The session rides along still owning the channel's teardown:
/// drop the pair (or `close` the session) when done — dropping /// drop the pair (or `close` the session) when done — dropping
/// `inner` EOFs the data plane structurally. /// `inner` EOFs the data plane structurally.
pub fn into_inner(mut self) -> TakenInner { ///
let inner = self.inner.take(); /// [`SpentSessionError`] on a spent session (the pipe already
TakenInner { /// taken): a spent session's manager entry is always reaped
session: self, /// already (`close`/`shutdown`/`Drop` all take it), so dropping
inner: inner.expect("into_inner on a spent session"), /// it loses nothing — no panic path (AGENTS.md convention 2).
pub fn into_inner(mut self) -> Result<TakenInner, SpentSessionError> {
match self.inner.take() {
Some(inner) => Ok(TakenInner {
session: self,
inner,
}),
None => Err(SpentSessionError),
} }
} }
@@ -426,11 +442,19 @@ impl AssociateSession {
/// the POC's `raw_mut` seam, widened). The session rides along /// the POC's `raw_mut` seam, widened). The session rides along
/// still owning the channel's teardown: drop the pair (or /// still owning the channel's teardown: drop the pair (or
/// `shutdown`/`close` the session) when done. /// `shutdown`/`close` the session) when done.
pub fn into_inner(mut self) -> TakenAssociateInner { ///
let inner = self.inner.take(); /// [`SpentSessionError`] on a spent session (the stream already
TakenAssociateInner { /// taken): a spent session's manager entry is always reaped
session: self, /// already (`shutdown`/`close`/`Drop` all take it), so dropping it
inner: inner.expect("into_inner on a spent session"), /// loses nothing — no panic path (AGENTS.md convention 2), and the
/// `Err` side stays small (a spent session is not carried).
pub fn into_inner(mut self) -> Result<TakenAssociateInner, SpentSessionError> {
match self.inner.take() {
Some(inner) => Ok(TakenAssociateInner {
session: self,
inner,
}),
None => Err(SpentSessionError),
} }
} }
@@ -864,6 +888,22 @@ mod tests {
assert!(session.split().is_none(), "no pipe after teardown"); assert!(session.split().is_none(), "no pipe after teardown");
} }
#[test]
fn spent_session_into_inner_is_an_err_not_a_panic() {
// The `expect` the review removed (R-2): the spent-session
// path is reachable (a session whose inner is gone), so it
// must be a typed `Err` — no panic path.
let session = Socks5ChannelSession {
channel_id: 9,
manager: None,
inner: None,
};
assert!(
session.into_inner().is_err(),
"a spent session must return Err, not Ok"
);
}
#[test] #[test]
fn open_errors_carry_the_typed_surface_through_connects_mapping() { fn open_errors_carry_the_typed_surface_through_connects_mapping() {
// The mapping `connect` applies to an open failure: the typed // The mapping `connect` applies to an open failure: the typed
@@ -909,6 +949,26 @@ mod tests {
assert!(session.relay_addr().is_none()); assert!(session.relay_addr().is_none());
} }
#[test]
fn spent_associate_into_inner_is_an_err_not_a_panic() {
// The `expect` the review removed (R-2): the spent-session
// path is reachable (`into_inner` twice), so it must be a
// typed `Err` — no panic path. The teardown owner rides back
// to the caller either way.
let session = AssociateSession {
channel_id: 9,
manager: None,
inner: None,
reader: DatagramReader::new(),
pending: Default::default(),
relay_addr: None,
};
assert!(
session.into_inner().is_err(),
"a spent session must return Err, not Ok"
);
}
#[tokio::test] #[tokio::test]
async fn recv_datagram_persists_reader_state_across_calls() { async fn recv_datagram_persists_reader_state_across_calls() {
use crate::wire::frame as wire_frame; use crate::wire::frame as wire_frame;
+5 -4
View File
@@ -39,10 +39,11 @@
//! session-level refusal stays channels-level, target-level refusal //! session-level refusal stays channels-level, target-level refusal
//! is in-stream). //! is in-stream).
//! //!
//! The readiness-contract behavioral test (an instant-`Ok(None)` //! The readiness-contract behavioral tests (an instant-`Ok(None)`
//! egress must not starve the datagram stage) lands with the //! egress must not starve the datagram stage — including the
//! producer-associate task — it needs the stage. This module carries //! zero-yield shape, review 002 R-1) live in the producer module's
//! the error→reply-code mapping tables, the sync-lift factory //! test suite, next to the stage they pin. This module carries the
//! error→reply-code mapping tables, the sync-lift factory
//! constructor, and the compile-shape assertions. //! constructor, and the compile-shape assertions.
use std::future::Future; use std::future::Future;
+1 -1
View File
@@ -66,7 +66,7 @@ pub mod wire;
pub use consumer::{ pub use consumer::{
AssociateSession, BindDataPlane, BindReply1, BindSession, Socks5ChannelSession, AssociateSession, BindDataPlane, BindReply1, BindSession, Socks5ChannelSession,
TakenAssociateInner, SpentSessionError, TakenAssociateInner,
}; };
pub use egress::{ pub use egress::{
egress_factory, BindAccept, BindAcceptError, BindAcceptFactory, BindAcceptFactoryError, egress_factory, BindAccept, BindAcceptError, BindAcceptFactory, BindAcceptFactoryError,
+121 -12
View File
@@ -24,7 +24,8 @@
//! A channels-level refusal is the registry ACL gate's job (it runs //! A channels-level refusal is the registry ACL gate's job (it runs
//! before the establisher — `socks5:open`, ADR-006; the scope-less //! before the establisher — `socks5:open`, ADR-006; the scope-less
//! identity fails closed with `FORBIDDEN`, no phantom channel). The //! identity fails closed with `FORBIDDEN`, no phantom channel). The
//! establisher itself never refuses in this task's scope. //! establisher itself never refuses — it is the identity-recording
//! session gate (ADR-005).
//! //!
//! ## The pump handler + dispatch //! ## The pump handler + dispatch
//! //!
@@ -320,8 +321,11 @@ where
/// - **The egress readiness contract** (`Egress::next_reply`): it /// - **The egress readiness contract** (`Egress::next_reply`): it
/// must be `Pending` when no reply is ready; an instantly-ready /// must be `Pending` when no reply is ready; an instantly-ready
/// `Ok(None)` starves a biased select into livelock. The stage /// `Ok(None)` starves a biased select into livelock. The stage
/// tolerates a spurious `Ok(None)` by re-parking (never a hot /// tolerates a spurious `Ok(None)` by yielding once and re-parking
/// loop); an `Err` ends the association (the egress drops with it). /// (never a hot loop — and never a zero-yield hot spin on a
/// current-thread scheduler, the livelock a pure `async { Ok(None)
/// }` egress reproduces); an `Err` ends the association (the egress
/// drops with it).
/// - **Framing applied exactly once, by the wire layer** — the stage /// - **Framing applied exactly once, by the wire layer** — the stage
/// reads frames via `DatagramReader` (channel → egress `send` gets /// reads frames via `DatagramReader` (channel → egress `send` gets
/// the payload) and writes frames via `frame_inner` (egress /// the payload) and writes frames via `frame_inner` (egress
@@ -353,7 +357,14 @@ where
loop { loop {
tokio::select! { tokio::select! {
// The egress's reply stream (its own pump; `None` = a // The egress's reply stream (its own pump; `None` = a
// spurious wakeup, the stage re-parks — never a hot loop). // spurious wakeup — the stage re-parks, never a hot
// loop). The yield makes the tolerance safe against a
// contract-violating zero-yield egress too: without it a
// pure `async { Ok(None) }` starves the channel branch
// and the runtime's other tasks on a current-thread
// scheduler (an instantly-ready branch that yields
// nothing wins every poll — the readiness contract's POC
// #2 finding, hardened by review 002 R-1).
reply = egress.next_reply() => { reply = egress.next_reply() => {
match reply { match reply {
Ok(Some(frame)) => { Ok(Some(frame)) => {
@@ -369,7 +380,7 @@ where
} }
} }
} }
Ok(None) => {} Ok(None) => tokio::task::yield_now().await,
Err(err) => { Err(err) => {
tracing::debug!("udp egress relay ended: {err:#}"); tracing::debug!("udp egress relay ended: {err:#}");
return StageExit::EgressEnded; return StageExit::EgressEnded;
@@ -1299,6 +1310,103 @@ mod tests {
.expect("clean"); .expect("clean");
} }
/// The readiness contract's zero-yield bound (review 002 R-1): an
/// egress whose `next_reply` is a pure `async { Ok(None) }` —
/// instantly ready AND yielding no poll boundary — must still not
/// starve the stage's channel branch. The stage's `Ok(None)`
/// handler yields before re-parking; this test hangs (livelock)
/// without that yield, on any scheduler.
#[tokio::test]
async fn udp_zero_yield_ok_none_egress_does_not_starve_the_stage() {
struct PureHotNoneEgress {
inbound: tokio::sync::mpsc::UnboundedSender<Bytes>,
outbound: tokio::sync::mpsc::UnboundedReceiver<Bytes>,
}
impl crate::egress::Egress for PureHotNoneEgress {
fn send(
&mut self,
addr: TargetAddr,
payload: Bytes,
) -> futures::future::BoxFuture<'_, Result<(), EgressError>> {
Box::pin(async move {
let mut inner = addr
.to_be_bytes()
.map_err(|e| EgressError::General(format!("addr encode: {e}")))?;
inner.extend_from_slice(&payload);
self.inbound
.send(Bytes::from(inner))
.map_err(|_| EgressError::General("egress closed".into()))
})
}
fn next_reply(
&mut self,
) -> futures::future::BoxFuture<'_, Result<Option<Bytes>, EgressError>> {
Box::pin(async {
// Zero awaits: instantly ready, no yield point at
// all — the strictest readiness-contract
// violation (the mutex-guarded hot egress in
// `udp_instant_ok_none_egress_does_not_starve_
// the_stage` accidentally yields at its lock;
// this one must be survived structurally).
match self.outbound.try_recv() {
Ok(inner) => Ok(Some(inner)),
Err(_) => Ok(None),
}
})
}
}
let factory: EgressFactory = Arc::new(|| {
Box::pin(async {
let (inbound, outbound) = tokio::sync::mpsc::unbounded_channel();
Ok(Box::new(PureHotNoneEgress { inbound, outbound }) as Box<dyn Egress + Send>)
})
});
let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let handle = tokio::spawn(serve_socks5_over(
server_end,
None,
Some(factory),
None,
false,
));
let mut client = client_end;
greet(&mut client).await;
write_request(
&mut client,
&Socks5Command::UDPAssociate,
&TargetAddr::Domain("pure.hot".into(), 1080),
)
.await;
let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00);
let target = TargetAddr::Ip(SocketAddr::from(([10, 0, 0, 6], 53)));
client
.write_all(&datagram_frame(&target, b"pure"))
.await
.unwrap();
let mut buf = vec![0u8; 2 + 7 + 4];
tokio::time::timeout(std::time::Duration::from_secs(5), async {
client.read_exact(&mut buf).await
})
.await
.expect("the channel branch is not starved — the echo arrived")
.expect("read the echo");
let (_, echoed) = decode_datagram(&buf);
assert_eq!(&echoed[..], b"pure");
drop(client);
tokio::time::timeout(std::time::Duration::from_secs(5), handle)
.await
.expect("session ends")
.expect("join")
.expect("clean");
}
/// A missing egress callback refuses ASSOCIATE RFC-faithfully /// A missing egress callback refuses ASSOCIATE RFC-faithfully
/// (`CommandNotSupported` 0x07 — ADR-008's missing-callback class, /// (`CommandNotSupported` 0x07 — ADR-008's missing-callback class,
/// never a panic), and CONNECT still serves the same registration /// never a panic), and CONNECT still serves the same registration
@@ -1973,7 +2081,8 @@ mod tests {
/// The establishment bound covers only the open-op round trip /// The establishment bound covers only the open-op round trip
/// (ADR-007): the quiet wait outlives any establishment-length /// (ADR-007): the quiet wait outlives any establishment-length
/// timeout — a client that BINDs and stays silent (no dial) keeps /// timeout — a client that BINDs and stays silent (no dial) keeps
/// the session alive well past the alkcall 10s default bound. /// the session alive (the wait is teardown-governed; the
/// over-channels shape is the harness's pin).
#[tokio::test] #[tokio::test]
async fn bind_quiet_wait_is_not_under_the_establishment_bound() { async fn bind_quiet_wait_is_not_under_the_establishment_bound() {
let accept = parked_accept_factory(); let accept = parked_accept_factory();
@@ -1988,12 +2097,12 @@ mod tests {
let (code, _addr) = read_reply(&mut client).await; let (code, _addr) = read_reply(&mut client).await;
assert_eq!(code, 0x00, "reply#1 succeeded"); assert_eq!(code, 0x00, "reply#1 succeeded");
// Stay silent past the 10s establishment bound — the wait is // Stay silent — the wait is teardown-governed, not
// teardown-governed, not establishment-governed. (The // establishment-governed. (The substrate-free shape has no
// substrate-free shape has no alkcall bound; the harness // alkcall bound; the harness integration test pins the
// integration test pins the over-channels shape. Here the // over-channels shape. Here the parked accept still answers
// parked accept still answers after a sleep — the wait is // after a sleep — the wait is alive, not under any in-stream
// alive, not under any in-stream timeout.) // timeout.)
tokio::time::sleep(std::time::Duration::from_millis(500)).await; tokio::time::sleep(std::time::Duration::from_millis(500)).await;
client.write_all(b"x").await.unwrap(); client.write_all(b"x").await.unwrap();
let mut buf = [0u8; 1]; let mut buf = [0u8; 1];
+263 -15
View File
@@ -1,7 +1,7 @@
--- ---
id: review-protocol-core id: review-protocol-core
name: Review — the protocol core before any assembly-layer work name: Review — the protocol core before any assembly-layer work
status: pending status: completed
depends_on: [implementation/core-integration-suite] depends_on: [implementation/core-integration-suite]
scope: narrow scope: narrow
risk: low risk: low
@@ -43,26 +43,274 @@ Review against the architecture, in this order:
teardown matrix tests actually pin the semantics; no teardown matrix tests actually pin the semantics; no
teardown-at-birth paths (the third-witness pattern held). teardown-at-birth paths (the third-witness pattern held).
5. **Convention compliance** (AGENTS.md): no `unwrap`/`expect`/ 5. **Convention compliance** (AGENTS.md): no `unwrap`/`expect`/
panics outside tests; `thiserror` surfaces; no comments in code panics outside tests; `thiserror` surfaces; no comments in code
except doc comments + pinned non-obvious-correctness notes; except doc comments + pinned non-obvious-correctness notes;
naming; module structure (one module per file, `lib.rs` naming; module structure (one module per file, `lib.rs`
re-exports); producer/consumer vocabulary (no server/client re-exports); producer/consumer vocabulary (no server/client
framing outside RFC vocabulary). framing outside RFC vocabulary).
6. **Wasm-clean invariant** (ADR-009): both wasm checks green; no 6. **Wasm-clean invariant** (ADR-009): both wasm checks green; no
`net`-shaped feature; tokio on the wasm-clean subset; the harness `net`-shaped feature; tokio on the wasm-clean subset; the harness
and tests stay out of the wasm build. and tests stay out of the wasm build.
7. **POC-fidelity spot-check**: for two or three of the trickiest 7. **POC-fidelity spot-check**: for two or three of the trickiest
behaviors (the readiness contract, the quiet-wait two-exit behaviors (the readiness contract, the quiet-wait two-exit
semantics, the split-datagram reader persistence), diff the semantics, the split-datagram reader persistence), diff the
crate's behavior against the POC crates' findings — the POCs are crate's behavior against the POC crates' findings — the POCs are
the empirical record; drift is a finding. the empirical record; drift is a finding.
8. **Docs sync**: the specs match what landed (the integration 8. **Docs sync**: the specs match what landed (the integration
task's reconciliation verified; any residue is a finding). task's reconciliation verified; any residue is a finding).
## Notes ## Notes
> To be filled by implementation agent > Filled by the review agent — see the Summary and the Findings.
## Summary ## Summary
> To be filled on completion Review performed 2026-09-24 against the eight-criteria order; the
gate opens. Two findings were corrected in the review change itself
(R-1 readiness-contract hardening; R-2 a non-test `expect`), plus
three minor residues (R-3..R-5, fixed in the same change); no
seam-contract, wire-fidelity, ACL, teardown, wasm, or POC-fidelity
drift was found. One prior-session reconciliation (the bind-only
identity row) was independently verified as correct — see the Notes.
**1. Seam contracts (ADR-004, producer.md §The Three Seams) — clean,
one finding corrected (R-1).** The readiness contract is documented
on the trait (`egress.rs:174-185`), table-tested, and pinned
behaviorally twice (the producer unit test's mutex-guarded hot
egress + the integration suite's hot-`Ok(None)` under concurrent
load). Ownership rides the handle structurally (`TargetHandle`/
`BindTargetHandle` payloads own their halves; no `_` bindings of
owning values anywhere in the crate — swept); the accept future owns
its resources end-to-end (`BindAccept::new` boxes it; the quiet
wait's `select!`-pinned drop is the client-gone teardown, pinned by
the drop-flag tests in `src/producer.rs` and `tests/core-
integration.rs`); framing-once holds (`Egress::send` takes payload
only, `next_reply` returns inner bytes, `frame_inner`/`frame` are
the only prefixers; `frame == frame_inner` byte-identity is
table-tested). R-1 is the readiness contract's own tolerance: see
the Findings.
**2. Wire fidelity — clean.** The datagram frame matches wire.md §The
Data Plane byte-for-byte: `[len: u16 BE][addr + data]`, the joint
bound (`frame`/`frame_inner` reject > 65535 with the exact per-form
maxima pinned: 65528 v4 / 65516 v6 / 65276 domain), F-2 at payload
level (empty datagram round-trips end to end, never EOF; `len == 0`
inner is `EmptyFrame` — unreachable from a compliant peer), RSV/FRAG
absent from the in-band codec. The phase model holds: no mode byte;
post-reply garbage is refused structurally in all three commands
(CONNECT = pure pipe, ASSOCIATE = garbage ATYP fails loud, BIND =
the quiet wait's one-byte read ends the session with no reply#2) —
each pinned by test. Sentinels verified: ASSOCIATE's `127.0.0.1:0`
(asserted in the integration suite + the raw probes), BIND's
variant-3 zeros (`0.0.0.0:0` reply#1 + reply#2, pinned);
association lifetime = channel lifetime (EOF rows in the teardown
matrix). The BAST document's joint-constraint note matches the
codec.
**3. Access control (ADR-006) — clean.** The open gate is
`required_scopes = ["socks5:open"]` on the single op spec
(`socks5_open_spec`); the command gate resolves `socks5:bind` from
the per-call opener identity at BIND dispatch and precedes the
accept seam (a scope-less BIND never mints a listener — pinned).
Both refusals are RFC-faithful (0x02 at the command gate; the
registry's `FORBIDDEN` at the open gate); the mapping tables
(dial/egress/accept/factory errors → reply codes) are table-tested
and route through the forked `ReplyError` — nothing collapsed into
generic errors. The integration suite's identity matrix pins all
three rows end to end with the CF-006 witness asserted.
**4. Teardown/lifecycle — clean.** `pump_bidi` is awaited inline in
all three data planes (CONNECT, BIND post-reply#2) inside the
handler task; the `JoinHandle` tracks the data-plane lifetime (R-02
held — no spawn-and-forget). The teardown matrix tests pin the real
semantics (half-close / EOF / client-gone-with-parked-accept, each
followed by fresh sessions of every command). No teardown-at-birth
paths: the egress-drop witness test flips the flag on stage exit,
the accept-drop witness flips on the client-gone branch; the
consumer sessions reap on `close`/`shutdown`/`Drop` with the leak
probes asserting no phantom channels on either side.
**5. Convention compliance — one finding corrected (R-2).** No
`unwrap`/`expect`/`panic!`/`unreachable!` in non-test library code
after R-2 (swept: `src/` non-test regions are clean; the forked
core's panic translations were re-verified). `thiserror` surfaces
throughout; naming/module structure (one module per file, `lib.rs`
re-exports) hold; producer/consumer vocabulary holds (RFC-facing
"server"/"client" only inside `src/rfc1928/`, the RFC vocabulary's
home). Cargo.toml's two inline policy comments are the
extraction-review F-3-accepted posture (carried, no fix demanded).
**6. Wasm-clean invariant — green, with one accepted note (R-5).**
Both wasm checks clean (`cargo check`/`clippy --target
wasm32-unknown-unknown -- -D warnings`, from scratch); no
`net`-shaped feature (the two declared features are empty markers);
tokio on the wasm-clean subset with `default-features = false` (no
`full` in `[dependencies]`); the harness/tests ride dev-deps only.
R-5: see the Findings (a transitive `getrandom` carrier outside
this crate's control).
**7. POC-fidelity spot-check — clean, one finding corrected (R-1).**
- *Readiness contract*: the crate's stage matches the POC's
`select!`-driven shape (channel reads ∥ reply drains, `Ok(None)`
tolerated as a spurious wakeup); the POC's LocalEgress/
ComposedEgress both park properly (`readable()` / queue
`recv().await`) and the ComposedEgress's closed-queue arm returns
`Err` rather than a hot `None` — exactly the posture the crate
inherited. The drift the spot-check surfaced is the *zero-yield*
bound of the contract (R-1) — a pure `async { Ok(None) }` egress
livelocked the landed stage on a current-thread runtime
(empirically reproduced; the POC's findings named the biased-select
and hot-None hazards, the no-yield variant is the same class one
step further). Fixed in the review change.
- *Quiet-wait two-exit semantics*: the landed `select!` is
single-shot with exactly the two exits (accept / EOF-garbage-error),
not a loop — matches the POC's `never_loop` finding; the parked-
accept probes make both exits deterministic; reply#1/reply#2 carry
the factory's `bind_addr`/peer byte-exactly on the provider
variant and the sentinels on variant 3.
- *Split-datagram reader persistence*: `AssociateSession::recv_
datagram`'s reader + pending queue persist across calls (pinned by
the 2-byte-prefix chunk test and the consumer-associate suite) —
matches ADR-003's re-read of the POC finding.
**8. Docs sync — clean, three residues fixed in the review change.**
The integration task's reconciliation was verified against ADR-006
§Consequences, producer.md §Scope Gating, and the landed spec/tests
(see the Notes). Residues found and fixed: `src/egress.rs`'s module
doc still said the readiness tests "land with the producer-associate
task" (that task is landed — reworded to point at where they live);
`src/producer.rs`'s establisher doc said "in this task's scope"
(same class — reworded); producer.md §Wiring's sketch showed
`producer.register(&mut channel_core)` — the landed signature takes
`(&ChannelCore, &OperationRegistry, AuthContext, Option<IdentityWitness>)`
and also carries the establisher (corrected). The rest of the specs
(wire.md, consumer.md §Wiring, ADR-006, rfc1928.md, README Current
State, lib.rs crate docs) match the tree.
## Acceptance Criteria
- [x] Seam contracts hold (readiness / ownership / accept-future /
framing-once) — with R-1 corrected
- [x] Wire fidelity: the frame, phase model, sentinels, lifetimes
byte-checked against wire.md/bast.md
- [x] Access control: both gates + never-implied grants pinned, the
prior session's reconciliation verified
- [x] Teardown/lifecycle: pump_bidi inline, the matrix pins the
semantics, no teardown-at-birth paths
- [x] Convention compliance: no unwrap/expect/panics outside tests
after R-2; thiserror; naming; module structure; vocabulary
- [x] Both wasm checks green (from clean), no net-shaped feature,
tokio on the wasm-clean subset
- [x] POC-fidelity spot-check of the three trickiest behaviors —
the zero-yield drift (R-1) found and fixed
- [x] Docs sync: specs match the tree; residues fixed in the same
change
- [x] Findings recorded; the gate to batch 2 opens
### Findings
- **R-1 (major → fixed in the review change)**: the datagram stage's
`Ok(None)` tolerance (re-park) is safe against a hot egress only
while its futures yield at least one poll boundary. A *zero-yield*
egress — `next_reply` = a pure `async { Ok(None) }`, no await at
all — wins every `select!` poll forever: the channel branch and
every other task on a current-thread scheduler starve (empirically
reproduced: the echo never arrives; the multi-thread runtime
survives, so the landed mutex-guarded unit test masked the hazard
at its lock's yield boundary). The POC's contract wording ("must
be `Pending` when nothing is ready") is right; the landed stage's
handling had one gap. Fix: the stage's `Ok(None)` handler now
`yield_now()`s before re-parking (`src/producer.rs`
`run_datagram_stage`), plus a pinning unit test with a pure
zero-yield egress (the strictest violator — hangs without the
yield) alongside the existing hot-None test. ADR-003's contract
prose and `egress.rs`'s trait doc stay as written (the contract
is on the egress; the stage now additionally defends itself).
- **R-2 (convention, fixed in the review change)**: `Socks5Channel
Session::into_inner` / `AssociateSession::into_inner` ended in
`inner.expect("into_inner on a spent session")` — the only
non-test `expect` in `src/`. Reachability is narrow (a spent
session's inner is gone only after close/shutdown/Drop, and no
public API constructs a spent session), but convention 2's rule is
the rule: the shape is now `Result<TakenInner, SpentSessionError>`
/ `Result<TakenAssociateInner, SpentSessionError>` with a
zero-carrier error type (`SpentSessionError` — a spent session's
manager entry is always already reaped, so dropping it loses
nothing; the large-Err/unit-Err lints shaped the choice), pinned
by two spent-session unit tests. Test call sites updated
(`.expect("the session is live")` — test-side, allowed).
- **R-3 (doc residue, fixed)**: `src/egress.rs` module docs + the
`src/producer.rs` establisher doc carried task-time phrasings
("lands with the producer-associate task" / "this task's scope")
that are stale now the tasks are landed. Reworded to point at the
landed reality.
- **R-4 (doc residue, fixed)**: producer.md §Wiring's assembly sketch
showed a one-argument `producer.register(...)`; the landed
signature takes the core/registry/auth/witness quadruple. Reconciled
(the same sync discipline the integration task applied to
consumer.md).
- **R-5 (doc residue, fixed)**: the
`bind_quiet_wait_is_not_under_the_establishment_bound` test doc
claimed the substrate-free probe exercises "the alkcall 10s
default bound" — it cannot (the substrate-free shape has no
alkcall bound; the over-channels wait-outlives-establishment
behavior is the harness's pin by construction). Reworded to say
what the test actually pins (the wait is teardown-governed, no
in-stream timeout).
- **R-6 (accepted, no fix demanded)**: the wasm-clean invariant rides
a transitive feature unification outside this crate: `alkcall →
jsonschema/referencing → ahash (runtime-rng) → getrandom 0.3.4`,
and `wasm_js` (required to compile `getrandom` on
`wasm32-unknown-unknown`) is enabled only because jsonschema/
referencing declare it for the wasm32 target. The default-crate
check passes today (verified from a clean target dir), and the
alkty sibling shows the identical posture; but a future
alkcall/jsonschema feature change could silently break the wasm
gate with no in-tree cause. Not actionable in this crate (the dep
graph is alkcall's); noted for the wasm-adapter story's
eventual OQ-SK2-06 work and for the batch-2 wasm checks.
## References
- docs/architecture/wire.md, producer.md, consumer.md, rfc1928.md,
bast.md, README.md (the specs under review)
- docs/architecture/decisions/001/003/004/006/007/009/013
- docs/research/poc-udp-associate-findings.md,
poc-bind-findings.md, poc-connect-wasm-findings.md (the POC
fidelity sources)
- /workspace/alksocks-udp-poc (the POC reference for the readiness
contract's empirical record)
- AGENTS.md (conventions 1–17; the verification commands)
## Notes
- The prior integration session's Notes/Summary were read and
cross-checked against the specs and the tree. The one
reconciliation it flagged — the task file's identity-matrix row 2
expecting a bind-only identity to serve BIND, vs ADR-006's landed
`required_scopes = ["socks5:open"]` unconditionally — is **correct
as landed**: the op spec (the single op, all three commands, ADR-
001) carries the open gate unconditionally; a bind-only identity
therefore fails closed for every command (FORBIDDEN at the
registry, the establisher never runs, no phantom channel — pinned
producer- and consumer-side in
`identity_matrix_bind_only_fails_closed_at_the_open_gate`), and a
BIND-capable deployment grants both scopes explicitly. The suite
pins this; ADR-006 §Consequences states the asymmetry explicitly
("never implied, in either direction"); the task file's Notes
record the reasoning. No residue: the drift was a Phase-0-era task
phrasing, not a spec-vs-code contradiction. The user's flag was
well-placed — this review treats the reconciliation as verified
rather than open.
- The self-reported verification claims reproduce (the landed suite
was green at review start; this review's fixes re-ran the full
sweep — see below).
- Post-review verification (the review change included): `cargo
test` 348 green (121 lib + 38 connect + 39 associate + 40 bind +
40 integration + 30 differential + 37 rfc1928 + 3 doctests — the
two R-1/R-2 additions and the four R-2 call-site updates
included), `cargo test --all-features` green, `cargo clippy
--all-targets -- -D warnings` clean, `cargo fmt --check` clean,
both wasm checks clean (re-run from a clean wasm target dir),
`cargo doc --no-deps` clean. The batch boundary holds: no
`local`/`composed` code exists.
+3 -3
View File
@@ -302,7 +302,7 @@ async fn half_close_write_eof_ends_the_producer_pump() {
let TakenInner { let TakenInner {
session: _owner, session: _owner,
inner, inner,
} = session.into_inner(); } = session.into_inner().expect("the session is live");
let (mut read, mut write) = tokio::io::split(inner); let (mut read, mut write) = tokio::io::split(inner);
write.write_all(b"pre").await.expect("write"); write.write_all(b"pre").await.expect("write");
let mut buf = [0u8; 3]; let mut buf = [0u8; 3];
@@ -344,7 +344,7 @@ async fn into_inner_pairs_the_pipe_with_the_teardown_owner() {
.await .await
.expect("the session connects"); .expect("the session connects");
let channel_id = session.channel_id(); let channel_id = session.channel_id();
let TakenInner { session, inner } = session.into_inner(); let TakenInner { session, inner } = session.into_inner().expect("the session is live");
assert!(topo.consumer.manager().has_channel(channel_id)); assert!(topo.consumer.manager().has_channel(channel_id));
let (mut read, write) = tokio::io::split(inner); let (mut read, write) = tokio::io::split(inner);
@@ -424,7 +424,7 @@ async fn close_after_into_inner_reaps() {
.await .await
.expect("the session connects"); .expect("the session connects");
let channel_id = session.channel_id(); let channel_id = session.channel_id();
let TakenInner { session, .. } = session.into_inner(); let TakenInner { session, .. } = session.into_inner().expect("the session is live");
// The inner pipe was taken; `close` still reaps the entry. // The inner pipe was taken; `close` still reaps the entry.
assert!(session.close().await, "the entry existed before the close"); assert!(session.close().await, "the entry existed before the close");
no_leaked_channels(&topo).await; no_leaked_channels(&topo).await;