The wave-3 gate read the landed server mappings against mapping.md/ADR-006/ADR-003/auth.md line by line: session/pty/shell/pipe (translation bridge + the derivation contract rules 1-6), git exec (full grammar, no-oracle validation, collapsed denial byte order), route-1 bridge direct rows, the full -R listen flow + forwarded-tcpip data leg, per-build decline matrices, identity observed at every seam, bounded bookkeeping, and the uniform safe tracing vocabulary. All four decomposition-time interpretations confirmed. Fixed inline (this gate): - install_forwarded_surface pre-checks the tunnel/forwarded op — alkcall's register silently replaces an existing entry, so a second adapter over the same shared seam registry silently stole the first adapter's arrival routes; now a failed construction (AdapterError::ListenRegistration), pinned by a new wire row - git take drops a stored pty entry (combined-build stale state) - doc hygiene: channel_cap release ordering, the session-row scoping of the handler Drop, the cancel-order recording, the post-cancel arrival caveat, the exit-chunk ADR lineage cross-ref, the stale lib.rs wave-state doc, the superseded dispatch test name, and mapping.md's listen-resource wording (address verbatim + tcp port) Booked for wave 4: the git row's status-before-close honored by scheduling not construction (needs a seam/shape change), the channel-not-held wire test rows, the tty coverage minors (cancel path, Closed/Break shape, stdout write deadline), and a defensive ListenRouter token-collision pre-check. Verified: cargo test (default) + --all-features (173 rows, 13 binaries), clippy --all-targets -D warnings ×2 (default + --all-features), fmt --check, cargo doc --no-deps (0 warnings), taskgraph validate (26 tasks).
198 lines
8.3 KiB
Rust
198 lines
8.3 KiB
Rust
//! The in-crate concurrent-channel cap (`w2-bounded-resources`) — the
|
|
//! bound itself, carried by the per-connection handler
|
|
//! ([`crate::handler`]) and enforced at every `channel_open_*` accept
|
|
//! path (`overview.md` §Bounded-resources policy: "concurrent channels
|
|
//! per session — ours (in-crate) + per-identity policy when bridged";
|
|
//! AGENTS.md convention 9 — SSH servers are internet-facing; unbounded
|
|
//! loops and unbounded buffers are bugs).
|
|
//!
|
|
//! ## Shape
|
|
//!
|
|
//! [`ChannelCap`] is a per-session count of active channels. Admission
|
|
//! ([`ChannelCap::admit`]) is the check-and-increment the accept paths
|
|
//! call: room under the cap admits (the count rises), otherwise the
|
|
//! open is refused — a clean decline, **never queued, never half-open**
|
|
//! (the fail-closed posture; the refusal reason is
|
|
//! [`crate::dispatch::cap_exceeded_open_decline`]). Release
|
|
//! ([`ChannelCap::release`]) fires from the `channel_close` hook for
|
|
//! established channels — and, since `w3-tunnel-direct`, from a
|
|
//! spawned route-1 bridge task on a fabric-open failure — and, since
|
|
//! `w3-tunnel-listen`, from the spawned `-R` arrival pumps on the
|
|
//! never-established paths (client-rejected open, dead session): see
|
|
//! the release-sites section.
|
|
//!
|
|
//! ## Release sites (the recorded decision, three sites since
|
|
//! `w3-tunnel-listen`)
|
|
//!
|
|
//! Every admission has exactly one release, and the paths are
|
|
//! disjoint by the wire shape:
|
|
//!
|
|
//! 1. **The established-channel close** — the `channel_close` hook.
|
|
//! russh routes `CHANNEL_CLOSE` only for channels it considers
|
|
//! established; a declined open never established one, and a close
|
|
//! is consumed at most once (russh removes the channel before the
|
|
//! hook).
|
|
//! 2. **The admitted-but-rejected bridge** — the route-1 bridge's
|
|
//! failure path (`w3-tunnel-direct`). The open hook admits and
|
|
//! holds the count across the fabric open; on fabric failure the
|
|
//! open is rejected with the explicit `ConnectFailed`
|
|
//! (ADR-006 decision 1: accept only after the fabric open
|
|
//! succeeds), the channel never establishes, and *no
|
|
//! `channel_close` can ever fire* — so the count must be released
|
|
//! from where the failure is known: inside the spawned bridge
|
|
//! task, **before** the reject is answered (the very next open
|
|
//! then observes the recovered budget — pinned by wire test). A
|
|
//! success never releases there (its close rides path 1).
|
|
//! 3. **The `-R` arrival pumps** (`w3-tunnel-listen`): a fabric
|
|
//! arrival is admitted into the owning route's count and opens the
|
|
//! SSH `forwarded-tcpip`/`forwarded-streamlocal` channel toward
|
|
//! the vanilla client through the session handle. A rejected open —
|
|
//! the client's own refusal, or a dead session's send failure —
|
|
//! never establishes the channel, so the count releases inside the
|
|
//! arrival task exactly once (an established channel's release is
|
|
//! path 1's). These are the mirror of site 2 on the server-initiated
|
|
//! open direction — the acceptor vocabulary's cap rows.
|
|
//!
|
|
//! ## Why the mutex (the single-writer reasoning, updated twice)
|
|
//!
|
|
//! The wave-2 shape was a bare `usize` field written only by the
|
|
//! session's packet loop — sound because every hook runs inline on
|
|
//! that loop with exclusive `&mut self` access. The bridge release
|
|
//! (`w3-tunnel-direct`) and the arrival admission (`w3-tunnel-listen`)
|
|
//! both break the single-writer property: those tasks are alkcall
|
|
//! serving tasks, unreachable from the packet loop. The recorded
|
|
//! decision is a `parking_lot::Mutex<usize>`: **packet-loop hooks and
|
|
//! spawned tasks both touch the count only through the mutex**, their
|
|
//! critical sections are single integer ops (no await inside the
|
|
//! lock), and all sites saturating. Channels never closed before
|
|
//! session teardown stay counted until the handler dies with the
|
|
//! session — per-session state, gone with the connection.
|
|
|
|
use parking_lot::Mutex;
|
|
|
|
/// The per-session active-channel count for the in-crate
|
|
/// concurrent-channel cap — see the module docs for the shape, the
|
|
/// release sites, and the writer/soundness reasoning. Shared with the
|
|
/// spawned route-1 bridge tasks ([`Arc<ChannelCap>`]) exactly so the
|
|
/// fabric-failure release can fire from the task that knows it.
|
|
#[derive(Debug, Default)]
|
|
pub(crate) struct ChannelCap {
|
|
active: Mutex<usize>,
|
|
}
|
|
|
|
impl ChannelCap {
|
|
/// The accept-path admission decision: room under `cap` increments
|
|
/// the count and admits; otherwise refuses (fail-closed — the
|
|
/// caller answers the open with a clean decline). `cap == 0`
|
|
/// refuses every open. Call sites: the packet loop's open hooks
|
|
/// `w2`-style, and — since `w3-tunnel-listen` — the `-R` arrival
|
|
/// tasks (the door's forwarded-arrival admission: an arrival is
|
|
/// admitted on the owning route's count inside the spawned
|
|
/// arrival pump — see the module doc's updated single-writer
|
|
/// reasoning). The mutex makes both sound.
|
|
pub(crate) fn admit(&self, cap: usize) -> bool {
|
|
let mut active = self.active.lock();
|
|
if *active < cap {
|
|
*active += 1;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// The release: the established channel's close (the hook) or the
|
|
/// admitted-but-rejected bridge's failure answer (the spawned
|
|
/// task). Disjoint per admission by the wire shape (see the
|
|
/// release-sites section). Saturating: no path manufactures
|
|
/// capacity.
|
|
pub(crate) fn release(&self) {
|
|
let mut active = self.active.lock();
|
|
*active = active.saturating_sub(1);
|
|
}
|
|
|
|
/// The active count — in-crate inspection for the unit rows.
|
|
#[cfg(test)]
|
|
pub(crate) fn active(&self) -> usize {
|
|
*self.active.lock()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::ChannelCap;
|
|
|
|
const CAP: usize = 2;
|
|
|
|
/// - *Contract stamp:* AGENTS.md convention 9 / `overview.md`
|
|
/// §Bounded-resources policy (the in-crate cap admits opens under
|
|
/// the configured ceiling and refuses beyond it).
|
|
#[test]
|
|
fn admits_up_to_the_cap_then_refuses() {
|
|
let cap = ChannelCap::default();
|
|
assert!(cap.admit(CAP));
|
|
assert!(cap.admit(CAP));
|
|
assert!(!cap.admit(CAP), "beyond the cap: refused");
|
|
assert_eq!(cap.active(), CAP);
|
|
}
|
|
|
|
/// - *Contract stamp:* the task's cap mechanics (a close recovers
|
|
/// the cap — the count decrements on close).
|
|
#[test]
|
|
fn release_recovers_the_cap() {
|
|
let cap = ChannelCap::default();
|
|
assert!(cap.admit(CAP));
|
|
assert!(cap.admit(CAP));
|
|
assert!(!cap.admit(CAP));
|
|
|
|
cap.release();
|
|
assert!(cap.admit(CAP), "an open succeeds again after a close");
|
|
assert!(!cap.admit(CAP));
|
|
}
|
|
|
|
/// - *Contract stamp:* the fail-closed cap posture (`cap == 0` is
|
|
/// a valid, assembly-tunable refusal of every open — never a
|
|
/// bypass).
|
|
#[test]
|
|
fn zero_cap_refuses_every_open() {
|
|
let cap = ChannelCap::default();
|
|
assert!(!cap.admit(0));
|
|
cap.release();
|
|
assert!(
|
|
!cap.admit(0),
|
|
"release never manufactures capacity below zero"
|
|
);
|
|
}
|
|
|
|
/// Release saturates: a decrement for a channel never counted can
|
|
/// never manufacture capacity (the soundness half of the
|
|
/// decrement invariant this crate's own gates make unreachable).
|
|
#[test]
|
|
fn release_saturates_at_zero() {
|
|
let cap = ChannelCap::default();
|
|
cap.release();
|
|
cap.release();
|
|
assert_eq!(cap.active(), 0);
|
|
assert!(cap.admit(CAP));
|
|
}
|
|
|
|
/// - *Contract stamp:* `w3-tunnel-direct`'s recorded release
|
|
/// mechanism (the admitted-but-rejected bridge releases inside
|
|
/// the spawned task): the cap is shared safely — admission from
|
|
/// the packet-loop site, release from anywhere that holds the
|
|
/// `Arc` — and disjoint releases never double-count.
|
|
#[test]
|
|
fn the_cap_is_shareable_between_the_loop_and_bridge_tasks() {
|
|
let cap = std::sync::Arc::new(ChannelCap::default());
|
|
let shared = std::sync::Arc::clone(&cap);
|
|
|
|
std::thread::spawn(move || {
|
|
shared.release();
|
|
})
|
|
.join()
|
|
.expect("the bridge-site release thread runs");
|
|
|
|
assert!(cap.admit(CAP), "a foreign-thread release stays sound");
|
|
assert_eq!(cap.active(), 1);
|
|
}
|
|
}
|