fix: remediate consumer findings CF-001..004 (alkhttp ledger)
- CF-004: services_schema_handler now applies the same visibility + AccessControl gates as invoke() (identity resolution mirrors invoke: handler_identity under internal). Restricted ops return spec-404 NOT_FOUND — matches "restricted ops don't exist" and leaks nothing about the restricted surface. Closes the unauthenticated /call-path disclosure. - CF-003: publish_schema compiled at registration time (both OperationRegistry::register and OperationRegistryBuilder::store); un-compilable schemas are a registration error — an unvalidated ingest path can no longer be constructed. Compiled validator cached per-op (publish_validator) and consumed by dispatch; per-request compile gone. BEHAVIOR CHANGE: register/builder reject un-compilable publish_schema. - CF-002: demux TooLarge skip streams through a fixed 64 KiB buffer instead of allocating the peer-declared length (u32, up to ~4 GiB); cumulative 256 MiB skipped-bytes budget tears down dribbling peers. Existing resync test passes unchanged. - CF-001: new retryable CallError::connection_closed (CONNECTION_CLOSED) applied only where the call is provably undelivered — request-frame write failures on all consumer paths (call/subscribe/publish, both stream modes; publish pump tags write stages). Mid-publish failures and producer-side fail_all stay non-retryable INTERNAL (delivery ambiguous). New code string is additive; retryable flag is the machine-readable signal. Verification: cargo test (558 pass, 15 new), clippy --all-targets -D warnings, fmt --check, wasm32-unknown-unknown check.
This commit is contained in:
1 parent
a2d72f9737
commit
8cb2a6eb6d
8 files changed
+613
-167
No files matched your search
+27
-4
@@ -128,6 +128,15 @@ impl ChannelsAdapter {
|
||||
reader: Box<dyn tokio::io::AsyncRead + Send + Unpin>,
|
||||
policy: Option<&Arc<dyn ChannelLifecyclePolicy>>,
|
||||
) {
|
||||
// Stack buffer for skipping oversized payloads — sized once, never
|
||||
// from peer-declared lengths (a peer-declared `length` is untrusted
|
||||
// up to u32::MAX ≈ 4 GiB; pre-sizing off it is an allocation-shape
|
||||
// DoS). A cumulative cap tears down dribbling peers that loop
|
||||
// TooLarge headers to burn bandwidth indefinitely.
|
||||
const SKIP_BUF_LEN: usize = 64 * 1024;
|
||||
const MAX_CONSECUTIVE_SKIPPED_BYTES: u64 = 256 * 1024 * 1024;
|
||||
let mut skip_buf = vec![0u8; SKIP_BUF_LEN];
|
||||
let mut skipped_since_last_valid: u64 = 0;
|
||||
let mut reader = reader;
|
||||
let mut header_buf = [0u8; CHUNK_HEADER_LEN];
|
||||
loop {
|
||||
@@ -141,10 +150,23 @@ impl ChannelsAdapter {
|
||||
max = super::wire::MAX_CHUNK_LEN,
|
||||
"demux: chunk too large, skipping payload bytes"
|
||||
);
|
||||
let mut discard = vec![0u8; length as usize];
|
||||
if let Err(e) = reader.read_exact(&mut discard).await {
|
||||
warn!(error = %e, "demux: failed to skip oversized payload");
|
||||
break;
|
||||
let mut remaining = length as u64;
|
||||
while remaining > 0 {
|
||||
let take = remaining.min(SKIP_BUF_LEN as u64) as usize;
|
||||
if let Err(e) = reader.read_exact(&mut skip_buf[..take]).await {
|
||||
warn!(error = %e, "demux: failed to skip oversized payload");
|
||||
return;
|
||||
}
|
||||
remaining -= take as u64;
|
||||
}
|
||||
skipped_since_last_valid += length as u64;
|
||||
if skipped_since_last_valid > MAX_CONSECUTIVE_SKIPPED_BYTES {
|
||||
warn!(
|
||||
skipped_since_last_valid,
|
||||
max = MAX_CONSECUTIVE_SKIPPED_BYTES,
|
||||
"demux: skipped-bytes budget exceeded; tearing down connection"
|
||||
);
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -170,6 +192,7 @@ impl ChannelsAdapter {
|
||||
}
|
||||
};
|
||||
manager.route_payload(header.channel_id, payload).await;
|
||||
skipped_since_last_valid = 0;
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::UnexpectedEof {
|
||||
|
||||
Reference in new issue
Block a user