Skip to main content

soil_client/maybe_compressed_blob/
mod.rs

1// This file is part of Soil.
2
3// Copyright (C) Soil contributors.
4// Copyright (C) Parity Technologies (UK) Ltd.
5// SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later WITH Classpath-exception-2.0
6
7//! Handling of blobs that may be compressed, based on an 8-byte magic identifier
8//! at the head.
9
10use std::{
11	borrow::Cow,
12	io::{Read, Write},
13};
14
15// An arbitrary prefix, that indicates a blob beginning with should be decompressed with
16// Zstd compression.
17//
18// This differs from the WASM magic bytes, so real WASM blobs will not have this prefix.
19const ZSTD_PREFIX: [u8; 8] = [82, 188, 83, 118, 70, 219, 142, 5];
20
21/// A recommendation for the bomb limit for code blobs.
22///
23/// This may be adjusted upwards in the future, but is set much higher than the
24/// expected maximum code size. When adjusting upwards, nodes should be updated
25/// before performing a runtime upgrade to a blob with larger compressed size.
26pub const CODE_BLOB_BOMB_LIMIT: usize = 50 * 1024 * 1024;
27
28/// A possible bomb was encountered.
29#[derive(Debug, Clone, PartialEq, thiserror::Error)]
30pub enum Error {
31	/// Decoded size was too large, and the code payload may be a bomb.
32	#[error("Possible compression bomb encountered")]
33	PossibleBomb,
34	/// The compressed value had an invalid format.
35	#[error("Blob had invalid format")]
36	Invalid,
37}
38
39fn read_from_decoder(
40	decoder: impl Read,
41	blob_len: usize,
42	bomb_limit: usize,
43) -> Result<Vec<u8>, Error> {
44	let mut decoder = decoder.take((bomb_limit + 1) as u64);
45
46	let mut buf = Vec::with_capacity(blob_len);
47	decoder.read_to_end(&mut buf).map_err(|_| Error::Invalid)?;
48
49	if buf.len() <= bomb_limit {
50		Ok(buf)
51	} else {
52		Err(Error::PossibleBomb)
53	}
54}
55
56fn decompress_zstd(blob: &[u8], bomb_limit: usize) -> Result<Vec<u8>, Error> {
57	let decoder = zstd::Decoder::new(blob).map_err(|_| Error::Invalid)?;
58
59	read_from_decoder(decoder, blob.len(), bomb_limit)
60}
61
62/// Decode a blob, if it indicates that it is compressed. Provide a `bomb_limit`, which
63/// is the limit of bytes which should be decompressed from the blob.
64pub fn decompress(blob: &[u8], bomb_limit: usize) -> Result<Cow<'_, [u8]>, Error> {
65	if blob.starts_with(&ZSTD_PREFIX) {
66		decompress_zstd(&blob[ZSTD_PREFIX.len()..], bomb_limit).map(Into::into)
67	} else {
68		Ok(blob.into())
69	}
70}
71
72/// Weakly compress a blob who's size is limited by `bomb_limit`.
73///
74/// If the blob's size is over the bomb limit, this will not compress the blob,
75/// as the decoder will not be able to be able to differentiate it from a compression bomb.
76pub fn compress_weakly(blob: &[u8], bomb_limit: usize) -> Option<Vec<u8>> {
77	compress_with_level(blob, bomb_limit, 3)
78}
79
80/// Strongly compress a blob who's size is limited by `bomb_limit`.
81///
82/// If the blob's size is over the bomb limit, this will not compress the blob, as the decoder will
83/// not be able to be able to differentiate it from a compression bomb.
84pub fn compress_strongly(blob: &[u8], bomb_limit: usize) -> Option<Vec<u8>> {
85	compress_with_level(blob, bomb_limit, 22)
86}
87
88/// Compress a blob who's size is limited by `bomb_limit`.
89///
90/// If the blob's size is over the bomb limit, this will not compress the blob, as the decoder will
91/// not be able to be able to differentiate it from a compression bomb.
92#[deprecated(
93	note = "Will be removed after June 2026. Use compress_strongly, compress_weakly or compress_with_level instead"
94)]
95pub fn compress(blob: &[u8], bomb_limit: usize) -> Option<Vec<u8>> {
96	compress_with_level(blob, bomb_limit, 3)
97}
98
99/// Compress a blob who's size is limited by `bomb_limit` with adjustable compression level.
100///
101/// The levels are passed through to `zstd` and can be in range [1, 22] (weakest to strongest).
102///
103/// If the blob's size is over the bomb limit, this will not compress the blob, as the decoder will
104/// not be able to be able to differentiate it from a compression bomb.
105fn compress_with_level(blob: &[u8], bomb_limit: usize, level: i32) -> Option<Vec<u8>> {
106	if blob.len() > bomb_limit {
107		return None;
108	}
109
110	let mut buf = ZSTD_PREFIX.to_vec();
111
112	{
113		let mut v = zstd::Encoder::new(&mut buf, level).ok()?.auto_finish();
114		v.write_all(blob).ok()?;
115	}
116
117	Some(buf)
118}
119
120#[cfg(test)]
121mod tests {
122	use super::*;
123
124	const BOMB_LIMIT: usize = 10;
125
126	#[test]
127	fn refuse_to_encode_over_limit() {
128		let mut v = vec![0; BOMB_LIMIT + 1];
129		assert!(compress_weakly(&v, BOMB_LIMIT).is_none());
130		assert!(compress_strongly(&v, BOMB_LIMIT).is_none());
131
132		let _ = v.pop();
133		assert!(compress_weakly(&v, BOMB_LIMIT).is_some());
134		assert!(compress_strongly(&v, BOMB_LIMIT).is_some());
135	}
136
137	#[test]
138	fn compress_and_decompress() {
139		let v = vec![0; BOMB_LIMIT];
140
141		let compressed_weakly = compress_weakly(&v, BOMB_LIMIT).unwrap();
142		let compressed_strongly = compress_strongly(&v, BOMB_LIMIT).unwrap();
143
144		assert!(compressed_weakly.starts_with(&ZSTD_PREFIX));
145		assert!(compressed_strongly.starts_with(&ZSTD_PREFIX));
146
147		assert_eq!(&decompress(&compressed_weakly, BOMB_LIMIT).unwrap()[..], &v[..]);
148		assert_eq!(&decompress(&compressed_strongly, BOMB_LIMIT).unwrap()[..], &v[..]);
149	}
150
151	#[test]
152	fn decompresses_only_when_magic() {
153		let v = vec![0; BOMB_LIMIT + 1];
154
155		assert_eq!(&decompress(&v, BOMB_LIMIT).unwrap()[..], &v[..]);
156	}
157
158	#[test]
159	fn possible_bomb_fails() {
160		let encoded_bigger_than_bomb = vec![0; BOMB_LIMIT + 1];
161		let mut buf = ZSTD_PREFIX.to_vec();
162
163		{
164			let mut v = zstd::Encoder::new(&mut buf, 3).unwrap().auto_finish();
165			v.write_all(&encoded_bigger_than_bomb[..]).unwrap();
166		}
167
168		assert_eq!(decompress(&buf[..], BOMB_LIMIT).err(), Some(Error::PossibleBomb));
169	}
170}