Protocol: don't panic on malformed chunk sections, compressed frames or legacy pings

A server sending a palette wider than 64 bits divided by zero in storage_len (and 33-64 bits overflowed shifts); SimpleBitStorage allows at most 32. A non-canonical data-length VarInt in a compressed frame was misread. An empty first read in the legacy ping handler indexed past the end.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
sirrayiandDevin committed 2026-10-27 13:36:51 +00:00
1 parent effb29e571
commit 2812cf201d
3 files changed
+43 -5

No files matched your search

+1 -1
View File
@@ -381,7 +381,7 @@ fn serve_legacy(mut stream: TcpStream, shared: &Shared) -> Result<(), String> {
let body = if data.len() == 1 {
format!("{motd}\u{a7}{online}\u{a7}{max}")
} else {
if data[1] != 1 {
if data.get(1) != Some(&1) {
return Ok(());
}
if data.len() > 2 {
+23 -1
View File
@@ -2,7 +2,7 @@
//! serialized LevelChunkSections with their PalettedContainers, block
//! entities) and ClientboundLightUpdatePacketData (sky/block DataLayers).
use crate::{
Result,
Error, Result,
buf::{BitSet, Bytes, Codec, Longs, Reader, VarInt, Writer},
packet,
};
@@ -83,6 +83,11 @@ impl Paletted {
pub fn read(r: &mut Reader, strategy: Strategy, global_bits: u8) -> Result<Self> {
let wire = r.u8()?;
let (has_palette, bits) = strategy.configuration(wire, global_bits);
// SimpleBitStorage holds 1 to 32 bits; wider wire values would
// divide by zero or overflow a shift.
if bits > 32 {
return Err(Error::new(format!("{bits} bits per entry, more than 32")));
}
let palette = if !has_palette {
Vec::new()
} else if bits == 0 {
@@ -425,6 +430,23 @@ mod tests {
assert_eq!(g.get(4095), 4095 * 7);
}
#[test]
fn wire_bits_wider_than_storage_are_rejected() {
for wire in [33u8, 200] {
let data = [wire];
let mut r = Reader::new(&data);
assert!(Paletted::read(&mut r, Strategy::Blocks, 16).is_err());
}
// 15- and 16-bit global palettes still read (1024 longs of zeroes).
for (wire, global) in [(15u8, 15u8), (16, 16)] {
let mut data = vec![wire];
data.resize(1 + 1024 * 8, 0);
let mut r = Reader::new(&data);
let p = Paletted::read(&mut r, Strategy::Blocks, global).unwrap();
assert_eq!(p.bits, wire);
}
}
#[test]
fn chunk_data_round_trips() {
let mut states = vec![0; BLOCKS];
+19 -3
View File
@@ -2,7 +2,7 @@
//! zlib compression above a threshold (CompressionEncoder / CompressionDecoder).
use crate::{
Error, Result,
buf::{Reader, varint_size, write_varint},
buf::{Reader, write_varint},
};
use flate2::{Compression, read::ZlibDecoder, write::ZlibEncoder};
use std::io::{Read, Write};
@@ -85,7 +85,7 @@ pub fn decompress(frame: Vec<u8>, threshold: Option<usize>) -> Result<Vec<u8>> {
let mut r = Reader::new(&frame);
let size = r.varint()?;
if size == 0 {
return Ok(frame[1..].to_vec());
return Ok(frame[r.position()..].to_vec());
}
let size = usize::try_from(size).map_err(|_| Error::new("negative uncompressed length"))?;
if size < threshold {
@@ -98,7 +98,7 @@ pub fn decompress(frame: Vec<u8>, threshold: Option<usize>) -> Result<Vec<u8>> {
"Badly compressed packet - size of {size} is larger than protocol maximum"
)));
}
let start = varint_size(size as i32);
let start = r.position();
let mut out = Vec::with_capacity(size);
ZlibDecoder::new(&frame[start..])
.take(size as u64 + 1)
@@ -163,4 +163,20 @@ mod tests {
frame.extend(z.finish().unwrap());
assert!(decompress(frame, Some(256)).is_err());
}
#[test]
fn a_noncanonical_length_prefix_still_reads() {
// The data-length VarInt is read, not measured: zero as `0x80 0x00`.
assert_eq!(
decompress(vec![0x80, 0x00, 7, 1, 2], Some(256)).unwrap(),
[7, 1, 2]
);
// And a compressed packet whose size was written wide: 300.
let body = vec![9; 300];
let mut frame = vec![0xAC, 0x82, 0x00];
let mut z = ZlibEncoder::new(Vec::new(), Compression::default());
z.write_all(&body).unwrap();
frame.extend(z.finish().unwrap());
assert_eq!(decompress(frame, Some(256)).unwrap(), body);
}
}