bound the fragmented WebSocket message too, and say what lowering the cap does not promise

The two WebSocket tests both stopped at max_frame_size: a header over the cap
and a single large frame after the cap is lifted. Dropping max_message_size
from set_max_packet_length left both passing, and that line is what refuses
two frames each under the cap that reassemble to a message over it - the other
way a peer buys the allocation. A test now sends exactly that, 12 KiB and a
12 KiB continuation against a 16 KiB cap, and fails when the line is dropped.

The doc on Stream::set_max_packet_length claimed that lowering the bound never
discards a message already being reassembled. That is the TCP codec, which
checks only on the header; WebRTC and a fragmented WebSocket message re-check
the bound on every fragment and end the message once it passes. The doc now
promises only what holds everywhere: set it before the first read, and do not
lean on lowering it to constrain a message in progress.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab
This commit is contained in:
rustdeskandClaude Fable 5.1 committed 2026-09-17 17:25:10 +08:00
1 parent e999dcea24
commit f0f1548ba3
2 files changed
+32 -6

No files matched your search

+2 -2
View File
@@ -38,8 +38,8 @@ impl Stream {
/// legitimately sends is large, so an unauthenticated connection cannot make us hold more than
/// that. Each transport clamps `n` to its own ceiling, so `usize::MAX` restores the default.
///
/// Set it before the first read of untrusted data: a lower bound applies from the next frame
/// on and does not discard one whose decoding or reassembly has already begun.
/// Set it before the first read of untrusted data. Lowering it later is not a way to constrain
/// a message already in progress: what that does to one is up to the transport.
#[inline]
pub fn set_max_packet_length(&mut self, n: usize) {
match self {
+30 -4
View File
@@ -555,10 +555,11 @@ mod tests {
assert_eq!(check_ws("127.0.0.1:34567"), "ws://127.0.0.1:34569");
Config::set_options(options);
}
// A server-to-client binary frame is unmasked, so it can be put on the wire by hand: the header
// alone, which is all it takes to ask tungstenite for the allocation, or with its payload.
fn ws_binary_frame(len: usize, with_payload: bool) -> Vec<u8> {
let mut f = vec![0x82];
// A server-to-client frame is unmasked, so it can be put on the wire by hand: the header alone,
// which is all it takes to ask tungstenite for the allocation, or with its payload. `head` is
// the first byte, FIN and opcode.
fn ws_frame(head: u8, len: usize, with_payload: bool) -> Vec<u8> {
let mut f = vec![head];
if len < 126 {
f.push(len as u8);
} else if len <= u16::MAX as usize {
@@ -574,6 +575,10 @@ mod tests {
f
}
fn ws_binary_frame(len: usize, with_payload: bool) -> Vec<u8> {
ws_frame(0x82, len, with_payload)
}
async fn ws_loopback() -> (WsFramedStream, TcpStream) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
@@ -620,4 +625,25 @@ mod tests {
.unwrap();
assert_eq!(got.len(), 200_000, "lifting the cap lets a large message through again");
}
// Two frames each under the cap that reassemble to a message over it: the frame bound lets
// both through, so this is what the message bound alone refuses.
#[tokio::test]
async fn max_packet_length_bounds_fragmented_message() {
const CAP: usize = 16 * 1024;
let (mut ws, mut server) = ws_loopback().await;
ws.set_max_packet_length(CAP);
// Binary with FIN clear, then a continuation with FIN set: 12 KiB each, 24 KiB together.
server.write_all(&ws_frame(0x02, 12 * 1024, true)).await.unwrap();
server.write_all(&ws_frame(0x80, 12 * 1024, true)).await.unwrap();
match timeout(Duration::from_secs(5), ws.next()).await {
Ok(Some(Err(e))) => assert!(e.to_string().contains("Message too long"), "{}", e),
Ok(other) => panic!(
"expected a refusal, got {:?}",
other.map(|r| r.map(|b| b.len()))
),
Err(_) => panic!("next() hung on a fragmented message over the cap"),
}
}
}