From 1391b3960169b17b9d7f0c7f9718b479013a2cdf Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 16 Mar 2026 13:48:35 +0000 Subject: [PATCH] fix(remoteingress-core): guard zero-window reads to avoid false EOF handling on stalled streams --- changelog.md | 6 ++++++ rust/crates/remoteingress-core/src/edge.rs | 9 ++++++++- rust/crates/remoteingress-core/src/hub.rs | 9 ++++++++- ts/00_commitinfo_data.ts | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 66abf18..339054f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,11 @@ # Changelog +## 2026-03-16 - 4.5.10 - fix(remoteingress-core) +guard zero-window reads to avoid false EOF handling on stalled streams + +- Prevent upload and download loops from calling read on an empty buffer when flow-control window remains at 0 after stall timeout +- Log a warning and close the affected stream instead of misinterpreting Ok(0) as end-of-file + ## 2026-03-16 - 4.5.9 - fix(remoteingress-core) delay stream close until downstream response draining finishes to prevent truncated transfers diff --git a/rust/crates/remoteingress-core/src/edge.rs b/rust/crates/remoteingress-core/src/edge.rs index 39fc78a..ec7402d 100644 --- a/rust/crates/remoteingress-core/src/edge.rs +++ b/rust/crates/remoteingress-core/src/edge.rs @@ -726,8 +726,15 @@ async fn handle_client_connection( } if client_token.is_cancelled() { break; } - // Limit read size to available window + // Limit read size to available window. + // IMPORTANT: if window is 0 (stall timeout fired), we must NOT + // read into an empty buffer — read(&mut buf[..0]) returns Ok(0) + // which would be falsely interpreted as EOF. let w = send_window.load(Ordering::Acquire) as usize; + if w == 0 { + log::warn!("Stream {} upload: window still 0 after stall timeout, closing", stream_id); + break; + } let max_read = w.min(buf.len()); tokio::select! { diff --git a/rust/crates/remoteingress-core/src/hub.rs b/rust/crates/remoteingress-core/src/hub.rs index d07d1ae..3ed7b03 100644 --- a/rust/crates/remoteingress-core/src/hub.rs +++ b/rust/crates/remoteingress-core/src/hub.rs @@ -601,8 +601,15 @@ async fn handle_edge_connection( } if stream_token.is_cancelled() { break; } - // Limit read size to available window + // Limit read size to available window. + // IMPORTANT: if window is 0 (stall timeout fired), we must NOT + // read into an empty buffer — read(&mut buf[..0]) returns Ok(0) + // which would be falsely interpreted as EOF. let w = send_window.load(Ordering::Acquire) as usize; + if w == 0 { + log::warn!("Stream {} download: window still 0 after stall timeout, closing", stream_id); + break; + } let max_read = w.min(buf.len()); tokio::select! { diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 9b34648..dc0f79e 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/remoteingress', - version: '4.5.9', + version: '4.5.10', description: 'Edge ingress tunnel for DcRouter - accepts incoming TCP connections at network edge and tunnels them to DcRouter SmartProxy preserving client IP via PROXY protocol v1.' }