Skip to content

Commit d0c2366

Browse files
authored
fix(s2n-quic-dc): clamp send quantum to max syscall size (#2549)
1 parent 4b3bcef commit d0c2366

File tree

4 files changed

+92
-13
lines changed

4 files changed

+92
-13
lines changed

dc/s2n-quic-dc/src/msg/segment.rs

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,75 @@ use crate::stream::TransportFeatures;
55
use arrayvec::ArrayVec;
66
use core::ops::Deref;
77
use s2n_quic_core::{ensure, inet::ExplicitCongestionNotification};
8+
use s2n_quic_platform::features;
89
use std::io::IoSlice;
910

10-
/// The maximum number of segments in sendmsg calls
11-
///
12-
/// From <https://elixir.bootlin.com/linux/v6.8.7/source/include/uapi/linux/uio.h#L27>
13-
/// > #define UIO_FASTIOV 8
14-
pub const MAX_COUNT: usize = if cfg!(target_os = "linux") { 8 } else { 1 };
11+
/// The maximum size of a IP+UDP header
12+
const IPV4_HEADER_LEN: u16 = 20;
13+
const IPV6_HEADER_LEN: u16 = 40;
14+
const UDP_HEADER_LEN: u16 = 8;
15+
16+
const fn min_u16(a: u16, b: u16) -> u16 {
17+
if a < b {
18+
a
19+
} else {
20+
b
21+
}
22+
}
1523

16-
/// The maximum payload allowed in sendmsg calls using UDP
24+
/// The maximum number of segments in sendmsg calls
1725
///
18-
/// From <https://github.com/torvalds/linux/blob/8cd26fd90c1ad7acdcfb9f69ca99d13aa7b24561/net/ipv4/ip_output.c#L987-L995>
19-
/// > Linux enforces a u16::MAX - IP_HEADER_LEN - UDP_HEADER_LEN
20-
pub const MAX_TOTAL: u16 = u16::MAX - 50;
26+
/// From <https://elixir.bootlin.com/linux/v6.8.7/source/include/uapi/linux/uio.h#L28>
27+
/// > #define UIO_MAXIOV 1024
28+
pub const MAX_COUNT: usize = if features::gso::IS_SUPPORTED {
29+
// base the max segments on the max datagram size for the default ethernet mtu
30+
let max_datagram_size = 1500 - min_u16(IPV4_HEADER_LEN, IPV6_HEADER_LEN) - UDP_HEADER_LEN;
31+
32+
(MAX_TOTAL / max_datagram_size) as _
33+
} else {
34+
// only a single segment can be sent per syscall
35+
1
36+
};
37+
38+
/// The maximum payload allowed in sendmsg calls using IPv4+UDP
39+
const MAX_TOTAL_IPV4: u16 = if cfg!(target_os = "linux") {
40+
// From <https://github.com/torvalds/linux/blob/8cd26fd90c1ad7acdcfb9f69ca99d13aa7b24561/net/ipv4/ip_output.c#L987-L995>
41+
// > Linux enforces a u16::MAX - IP_HEADER_LEN - UDP_HEADER_LEN
42+
u16::MAX - IPV4_HEADER_LEN - UDP_HEADER_LEN
43+
} else {
44+
9001 - IPV4_HEADER_LEN - UDP_HEADER_LEN
45+
};
46+
47+
/// The maximum payload allowed in sendmsg calls using IPv6+UDP
48+
const MAX_TOTAL_IPV6: u16 = if cfg!(target_os = "linux") {
49+
// IPv6 doesn't include the IP header size in the calculation
50+
u16::MAX - UDP_HEADER_LEN
51+
} else {
52+
9001 - IPV6_HEADER_LEN - UDP_HEADER_LEN
53+
};
54+
55+
/// The minimum payload size between the IPv4 and IPv6 sizes
56+
pub const MAX_TOTAL: u16 = min_u16(MAX_TOTAL_IPV4, MAX_TOTAL_IPV6);
57+
58+
#[test]
59+
fn max_total_test() {
60+
let tests = [("127.0.0.1:0", MAX_TOTAL_IPV4), ("[::1]:0", MAX_TOTAL_IPV6)];
61+
62+
for (addr, total) in tests {
63+
let socket = std::net::UdpSocket::bind(addr).unwrap();
64+
let addr = socket.local_addr().unwrap();
65+
66+
let mut buffer = vec![0u8; total as usize + 1];
67+
68+
// This behavior may not be consistent across kernel versions so the check is disabled by default
69+
let _ = socket.send_to(&buffer, addr);
70+
71+
buffer.pop().unwrap();
72+
socket
73+
.send_to(&buffer, addr)
74+
.expect("send should succeed when limited to MAX_TOTAL");
75+
}
76+
}
2177

2278
type Segments<'a> = ArrayVec<IoSlice<'a>, MAX_COUNT>;
2379

dc/s2n-quic-dc/src/stream/environment/tokio.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ where
6565
#[inline]
6666
pub fn build(self) -> io::Result<Environment<Sub>> {
6767
let clock = self.clock.unwrap_or_default();
68-
let gso = self.gso.unwrap_or_default();
68+
let gso = self.gso.unwrap_or_else(|| {
69+
// rather than clamping it to the max burst size, let the CCA be the only
70+
// component that controls send quantums
71+
features::gso::MAX_SEGMENTS.into()
72+
});
6973
let socket_options = self.socket_options.unwrap_or_default();
7074

7175
let thread_count = self.threads.unwrap_or_else(|| {

dc/s2n-quic-dc/src/stream/send/flow.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use s2n_quic_core::varint::VarInt;
55

66
pub mod blocking;
77
pub mod non_blocking;
8-
pub use crate::msg::segment::MAX_TOTAL;
98

109
/// Flow credits acquired by an application request
1110
#[derive(Debug)]
@@ -35,7 +34,7 @@ impl Request {
3534
/// Clamps the request with the given number of credits
3635
#[inline]
3736
pub fn clamp(&mut self, credits: u64) {
38-
let len = self.len.min(credits.min(MAX_TOTAL as _) as usize);
37+
let len = self.len.min(credits as usize);
3938

4039
// if we didn't acquire the entire len, then clear the `is_fin` flag
4140
if self.len != len {

dc/s2n-quic-dc/src/stream/send/path.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
use crate::msg::segment::MAX_TOTAL;
45
use core::{
56
fmt,
67
sync::atomic::{AtomicU64, Ordering},
78
};
8-
use s2n_quic_core::{inet::ExplicitCongestionNotification, varint::VarInt};
9+
use s2n_quic_core::{
10+
inet::ExplicitCongestionNotification, path::MINIMUM_MAX_DATAGRAM_SIZE, varint::VarInt,
11+
};
912

1013
/// Contains the current state of a transmission path
1114
pub struct State {
@@ -95,6 +98,15 @@ impl State {
9598
data |= ecn as u8 as u64;
9699
data <<= 8;
97100

101+
let max_datagram_size = max_datagram_size.max(MINIMUM_MAX_DATAGRAM_SIZE);
102+
103+
// clamp the number of segments so it's under the max payload we can write in
104+
// a single syscall
105+
let max_segments = MAX_TOTAL / max_datagram_size;
106+
let send_quantum = send_quantum.min(max_segments as u8);
107+
// we need this to be at least 1
108+
let send_quantum = send_quantum.max(1);
109+
98110
data |= send_quantum as u64;
99111
data <<= 16;
100112

@@ -149,6 +161,14 @@ mod tests {
149161
let actual =
150162
State::decode_info(State::encode_info(ecn, send_quantum, max_datagram_size));
151163

164+
// this needs to be at least MINIMUM_MAX_DATAGRAM_SIZE
165+
let max_datagram_size = max_datagram_size.max(MINIMUM_MAX_DATAGRAM_SIZE);
166+
167+
// this needs to be at least 1 but less than what is allowed in a single syscall
168+
let send_quantum = send_quantum
169+
.min((MAX_TOTAL / max_datagram_size) as u8)
170+
.max(1);
171+
152172
assert_eq!((ecn, send_quantum, max_datagram_size), actual);
153173
})
154174
}

0 commit comments

Comments
 (0)