Files
alkssh/src/channel_cap.rs
T
glm-5.3-flash 930d9c1996 docs(review): close the wave-3 review gate — verdict green (review-wave-3)
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).
2026-10-29 05:22:14 +00:00

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);
}
}