Skip to main content

netget/server/mysql/
packet_limit.rs

1//! A bound on the largest MySQL packet a stranger can make this server buffer.
2//!
3//! # The defect
4//!
5//! `opensrv-mysql` 0.7.0 has no maximum-packet check anywhere. Its `PacketReader::next_async`
6//! grows `self.bytes` by `max(1 MiB, len * 2)` until a whole *logical* packet has been
7//! buffered, and `packet()` assembles that logical packet with
8//! `nom::multi::fold_many0(fullpacket, …)` — an unbounded fold over 16 MiB continuation
9//! packets, each of which extends a `Vec`. So the peer decides how much this process
10//! allocates, and it decides before authentication and before any model call: the very first
11//! thing a client sends is its handshake response, read through exactly this path.
12//!
13//! Worse, the server *publishes* a bound. `opensrv-mysql` answers
14//! `SELECT @@max_allowed_packet` itself, with `67108864`, without ever consulting the shim —
15//! and never compares an incoming packet against it. A published bound that is not enforced is
16//! worse than no bound: a client sizes its writes by the number it was given.
17//!
18//! # The bound
19//!
20//! [`MAX_PACKET_BYTES`] is that same 67108864, so the number the server publishes and the
21//! number it enforces are one number. The refusal is MySQL's own: error **1153
22//! `ER_NET_PACKET_TOO_LARGE`**, SQLSTATE `08S01`, which is precisely what a real MySQL server
23//! sends when `max_allowed_packet` is exceeded, carrying the message a real server carries.
24//!
25//! # Where it is enforced
26//!
27//! Here, in a reader that sits *beneath* `opensrv-mysql` and decides each packet from the
28//! length the peer **declared** in its 4-byte header, before that packet's payload is read and
29//! therefore before anything is allocated for it. Bounding what has already arrived would be
30//! the NATS `HPUB` mistake — there the limit was applied to `total − header`, so `header ==
31//! total` passed every check and thirty bytes on the wire bought a 4 GB buffer.
32//!
33//! For a chained packet the accumulation is what is bounded, not each fragment: a fragment
34//! whose declared length is exactly [`FULL_PACKET_PAYLOAD`] means "more follows", so the
35//! logical packet's declared size is the running sum, and it is that sum which is compared
36//! against the bound at every header.
37
38use std::pin::Pin;
39use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering};
40use std::sync::Arc;
41use std::task::{Context, Poll};
42
43use tokio::io::{AsyncRead, ReadBuf};
44
45/// Largest logical MySQL packet this server will read, in payload bytes.
46///
47/// Exactly the value `opensrv-mysql` answers `SELECT @@max_allowed_packet` with, and it is
48/// chosen for that reason rather than for being a round number: the point of this bound is
49/// that the server stops publishing a ceiling it does not apply. Enforcing anything *smaller*
50/// than the published value would leave the same mismatch pointing the other way — a client
51/// that sized a write by `@@max_allowed_packet` would be refused at a length the server had
52/// told it was acceptable.
53pub const MAX_PACKET_BYTES: usize = 67_108_864;
54
55/// Payload length that means "this packet is a fragment; the next one continues it".
56///
57/// 2^24 − 1, the largest value the 3-byte little-endian length header can carry. A packet
58/// declaring exactly this is never the end of a logical packet — that is the protocol's own
59/// framing rule, and it is the construct that makes `fold_many0` unbounded.
60pub const FULL_PACKET_PAYLOAD: usize = 0x00FF_FFFF;
61
62/// What the limiting reader tells the connection task after it has refused.
63///
64/// A reader can only return an `io::Error`, and an `io::Error` is indistinguishable from a
65/// read timeout or a reset by the time `run_on` has turned it into its own error type. The
66/// connection task needs three things to answer in MySQL's vocabulary — that the bound was the
67/// reason, the sequence id the refusal must carry, and the size to log — so they are recorded
68/// here rather than parsed back out of an error string.
69#[derive(Debug, Default)]
70pub struct PacketLimitTrip {
71    tripped: AtomicBool,
72    /// Sequence id of the header that crossed the bound. The ERR packet answering it must
73    /// carry this plus one, or a conforming client discards the reply as out of order.
74    sequence: AtomicU8,
75    /// Declared size of the logical packet at the moment it crossed, for the log line.
76    declared: AtomicUsize,
77}
78
79impl PacketLimitTrip {
80    /// Whether the bound refused a packet on this connection.
81    pub fn tripped(&self) -> bool {
82        self.tripped.load(Ordering::SeqCst)
83    }
84
85    /// Sequence id the refusal must carry: one past the header that crossed the bound.
86    pub fn reply_sequence(&self) -> u8 {
87        self.sequence.load(Ordering::SeqCst).wrapping_add(1)
88    }
89
90    /// Declared size of the logical packet that crossed the bound.
91    pub fn declared_bytes(&self) -> usize {
92        self.declared.load(Ordering::SeqCst)
93    }
94
95    fn record(&self, sequence: u8, declared: usize) {
96        self.sequence.store(sequence, Ordering::SeqCst);
97        self.declared.store(declared, Ordering::SeqCst);
98        self.tripped.store(true, Ordering::SeqCst);
99    }
100}
101
102/// An `AsyncRead` that refuses a MySQL packet larger than its bound, before the payload is read.
103///
104/// It buffers nothing of its own beyond a four-byte header: bytes are handed straight through
105/// to the caller and the framing is tracked alongside them. That is what makes it safe to put
106/// beneath a crate that does its own buffering — the decision is taken on the header, so the
107/// crate underneath never sees the payload it would have allocated for.
108pub struct PacketLimitReader<R> {
109    inner: R,
110    limit: usize,
111    trip: Arc<PacketLimitTrip>,
112    /// Bytes of the current 4-byte packet header seen so far.
113    header: [u8; 4],
114    header_len: usize,
115    /// Payload bytes still to pass through before the next header begins.
116    payload_remaining: usize,
117    /// Declared payload bytes accumulated so far for the logical packet being assembled.
118    /// Non-zero only while a chain of [`FULL_PACKET_PAYLOAD`] fragments is in progress.
119    logical_declared: usize,
120}
121
122impl<R> PacketLimitReader<R> {
123    /// Wrap `inner` with the production bound.
124    pub fn new(inner: R, trip: Arc<PacketLimitTrip>) -> Self {
125        Self::with_limit(inner, MAX_PACKET_BYTES, trip)
126    }
127
128    /// Wrap `inner` with an arbitrary bound.
129    ///
130    /// Exists so a test can drive the framing decision at a size that fits in a test rather
131    /// than having to move 64 MiB to reach the production ceiling. It is **not** a knob:
132    /// nothing in `src/` calls this with anything but [`MAX_PACKET_BYTES`], because a bound
133    /// decided by configuration is a bound decided by whoever writes the configuration.
134    pub fn with_limit(inner: R, limit: usize, trip: Arc<PacketLimitTrip>) -> Self {
135        Self {
136            inner,
137            limit,
138            trip,
139            header: [0u8; 4],
140            header_len: 0,
141            payload_remaining: 0,
142            logical_declared: 0,
143        }
144    }
145
146    /// Give the wrapped reader back, framing checks and all state dropped.
147    ///
148    /// Used for the lingering drain after a refusal: draining through the limiter would trip
149    /// it again on the very bytes it is discarding.
150    pub fn into_inner(self) -> R {
151        self.inner
152    }
153
154    /// Track `bytes` through the framing, refusing at the first header that crosses the bound.
155    ///
156    /// Returns `Err(())` on refusal, having recorded the sequence id and the declared size.
157    fn observe(&mut self, bytes: &[u8]) -> Result<(), ()> {
158        let mut i = 0usize;
159        while i < bytes.len() {
160            if self.payload_remaining > 0 {
161                let take = self.payload_remaining.min(bytes.len() - i);
162                self.payload_remaining -= take;
163                i += take;
164                continue;
165            }
166
167            let need = 4 - self.header_len;
168            let take = need.min(bytes.len() - i);
169            self.header[self.header_len..self.header_len + take]
170                .copy_from_slice(&bytes[i..i + take]);
171            self.header_len += take;
172            i += take;
173            if self.header_len < 4 {
174                // A header split across two reads. Nothing is decided until all four bytes
175                // are in hand — deciding on a partial length is how a bound comes to be
176                // applied to the wrong number.
177                break;
178            }
179            self.header_len = 0;
180
181            let declared =
182                u32::from_le_bytes([self.header[0], self.header[1], self.header[2], 0]) as usize;
183            let sequence = self.header[3];
184            let logical = self.logical_declared.saturating_add(declared);
185
186            if logical > self.limit {
187                self.trip.record(sequence, logical);
188                return Err(());
189            }
190
191            self.payload_remaining = declared;
192            // A fragment of exactly FULL_PACKET_PAYLOAD continues into the next packet, so the
193            // logical size carries over. Anything shorter ends the logical packet.
194            self.logical_declared = if declared == FULL_PACKET_PAYLOAD {
195                logical
196            } else {
197                0
198            };
199        }
200        Ok(())
201    }
202}
203
204impl<R: AsyncRead + Unpin> AsyncRead for PacketLimitReader<R> {
205    fn poll_read(
206        self: Pin<&mut Self>,
207        cx: &mut Context<'_>,
208        buf: &mut ReadBuf<'_>,
209    ) -> Poll<std::io::Result<()>> {
210        let this = self.get_mut();
211        let before = buf.filled().len();
212        match Pin::new(&mut this.inner).poll_read(cx, buf) {
213            Poll::Ready(Ok(())) => {
214                let filled = buf.filled().len();
215                if filled > before {
216                    // Copied rather than borrowed because `observe` takes `&mut self` while
217                    // `buf` borrows from the caller; the slice is one read's worth, never a
218                    // packet's worth, so this is bounded by the caller's buffer.
219                    let fresh = buf.filled()[before..filled].to_vec();
220                    if this.observe(&fresh).is_err() {
221                        return Poll::Ready(Err(std::io::Error::new(
222                            std::io::ErrorKind::InvalidData,
223                            "MySQL packet exceeds max_allowed_packet",
224                        )));
225                    }
226                }
227                Poll::Ready(Ok(()))
228            }
229            other => other,
230        }
231    }
232}
233
234/// The ERR packet a peer gets when its packet crosses [`MAX_PACKET_BYTES`].
235///
236/// Laid out by hand because it is a wire packet, and matching a real MySQL server byte for
237/// byte is the whole point: 3-byte little-endian payload length, sequence id, `0xFF` marking
238/// an ERR packet, error number 1153 little-endian, the `#` SQL-state marker, the five
239/// characters of SQLSTATE `08S01`, and the message text MySQL itself uses. A client that
240/// reads only the error number sees 1153; one that classifies by SQLSTATE sees the
241/// communication-link-failure class; one that shows the operator a string sees the same
242/// sentence it would see from mysqld.
243///
244/// `sequence` must be one past the header that was refused. MySQL numbers packets within a
245/// command, and a reply whose sequence does not follow the request is discarded by a
246/// conforming client as "packet out of order" — so getting this wrong turns a clear refusal
247/// back into the silence this bound exists to replace.
248pub fn packet_too_large_err(sequence: u8) -> Vec<u8> {
249    const MESSAGE: &[u8] = b"Got a packet bigger than 'max_allowed_packet' bytes";
250    const ER_NET_PACKET_TOO_LARGE: u16 = 1153;
251
252    let mut payload = Vec::with_capacity(9 + MESSAGE.len());
253    payload.push(0xFF);
254    payload.extend_from_slice(&ER_NET_PACKET_TOO_LARGE.to_le_bytes());
255    payload.push(b'#');
256    payload.extend_from_slice(b"08S01");
257    payload.extend_from_slice(MESSAGE);
258
259    let len = payload.len();
260    let mut packet = Vec::with_capacity(4 + len);
261    packet.push((len & 0xFF) as u8);
262    packet.push(((len >> 8) & 0xFF) as u8);
263    packet.push(((len >> 16) & 0xFF) as u8);
264    packet.push(sequence);
265    packet.extend_from_slice(&payload);
266    packet
267}