Skip to content

Commit a0003ce

Browse files
authored
Add blob size limits. (#2705)
* Add a blob size limit. * Add a bytecode size limit. * Add unit tests for limits. * Don't enforce the limit for already published bytecode. * Simplify LimitedWriter; add unit test. * Add decompressed_size_at_most. * Update and copy comment about #2710.
1 parent 529e108 commit a0003ce

File tree

16 files changed

+257
-33
lines changed

16 files changed

+257
-33
lines changed

CLI.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,9 @@ View or update the resource control policy
411411
* `--message <MESSAGE>` — Set the base price of sending a message from a block..
412412
* `--message-byte <MESSAGE_BYTE>` — Set the additional price for each byte in the argument of a user message
413413
* `--maximum-fuel-per-block <MAXIMUM_FUEL_PER_BLOCK>` — Set the maximum amount of fuel per block
414-
* `--maximum-executed-block-size <MAXIMUM_EXECUTED_BLOCK_SIZE>` — Set the maximum size of an executed block
414+
* `--maximum-executed-block-size <MAXIMUM_EXECUTED_BLOCK_SIZE>` — Set the maximum size of an executed block, in bytes
415+
* `--maximum-blob-size <MAXIMUM_BLOB_SIZE>` — Set the maximum size of data blobs, compressed bytecode and other binary blobs, in bytes
416+
* `--maximum-bytecode-size <MAXIMUM_BYTECODE_SIZE>` — Set the maximum size of decompressed contract or service bytecode, in bytes
415417
* `--maximum-bytes-read-per-block <MAXIMUM_BYTES_READ_PER_BLOCK>` — Set the maximum read data per block
416418
* `--maximum-bytes-written-per-block <MAXIMUM_BYTES_WRITTEN_PER_BLOCK>` — Set the maximum write data per block
417419

@@ -473,6 +475,8 @@ Create genesis configuration for a Linera deployment. Create initial user chains
473475
Default value: `0`
474476
* `--maximum-fuel-per-block <MAXIMUM_FUEL_PER_BLOCK>` — Set the maximum amount of fuel per block
475477
* `--maximum-executed-block-size <MAXIMUM_EXECUTED_BLOCK_SIZE>` — Set the maximum size of an executed block
478+
* `--maximum-bytecode-size <MAXIMUM_BYTECODE_SIZE>` — Set the maximum size of decompressed contract or service bytecode, in bytes
479+
* `--maximum-blob-size <MAXIMUM_BLOB_SIZE>` — Set the maximum size of data blobs, compressed bytecode and other binary blobs, in bytes
476480
* `--maximum-bytes-read-per-block <MAXIMUM_BYTES_READ_PER_BLOCK>` — Set the maximum read data per block
477481
* `--maximum-bytes-written-per-block <MAXIMUM_BYTES_WRITTEN_PER_BLOCK>` — Set the maximum write data per block
478482
* `--testing-prng-seed <TESTING_PRNG_SEED>` — Force this wallet to generate keys using a PRNG and a given seed. USE FOR TESTING ONLY

linera-base/src/data_types.rs

+42-15
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use crate::{
3737
ApplicationId, BlobId, BlobType, BytecodeId, Destination, GenericApplicationId, MessageId,
3838
UserApplicationId,
3939
},
40+
limited_writer::{LimitedWriter, LimitedWriterError},
4041
time::{Duration, SystemTime},
4142
};
4243

@@ -861,14 +862,8 @@ impl fmt::Debug for Bytecode {
861862
#[derive(Error, Debug)]
862863
pub enum DecompressionError {
863864
/// Compressed bytecode is invalid, and could not be decompressed.
864-
#[cfg(not(target_arch = "wasm32"))]
865-
#[error("Bytecode could not be decompressed")]
866-
InvalidCompressedBytecode(#[source] io::Error),
867-
868-
/// Compressed bytecode is invalid, and could not be decompressed.
869-
#[cfg(target_arch = "wasm32")]
870-
#[error("Bytecode could not be decompressed")]
871-
InvalidCompressedBytecode(#[from] ruzstd::frame_decoder::FrameDecoderError),
865+
#[error("Bytecode could not be decompressed: {0}")]
866+
InvalidCompressedBytecode(#[from] io::Error),
872867
}
873868

874869
/// A compressed WebAssembly module's bytecode.
@@ -880,20 +875,53 @@ pub struct CompressedBytecode {
880875
pub compressed_bytes: Vec<u8>,
881876
}
882877

878+
#[cfg(not(target_arch = "wasm32"))]
883879
impl CompressedBytecode {
880+
/// Returns `true` if the decompressed size does not exceed the limit.
881+
pub fn decompressed_size_at_most(&self, limit: u64) -> Result<bool, DecompressionError> {
882+
let mut decoder = zstd::stream::Decoder::new(&*self.compressed_bytes)?;
883+
let limit = usize::try_from(limit).unwrap_or(usize::MAX);
884+
let mut writer = LimitedWriter::new(io::sink(), limit);
885+
match io::copy(&mut decoder, &mut writer) {
886+
Ok(_) => Ok(true),
887+
Err(error) => {
888+
error.downcast::<LimitedWriterError>()?;
889+
Ok(false)
890+
}
891+
}
892+
}
893+
884894
/// Decompresses a [`CompressedBytecode`] into a [`Bytecode`].
885-
#[cfg(not(target_arch = "wasm32"))]
886895
pub fn decompress(&self) -> Result<Bytecode, DecompressionError> {
887896
#[cfg(with_metrics)]
888897
let _decompression_latency = BYTECODE_DECOMPRESSION_LATENCY.measure_latency();
889-
let bytes = zstd::stream::decode_all(&*self.compressed_bytes)
890-
.map_err(DecompressionError::InvalidCompressedBytecode)?;
898+
let bytes = zstd::stream::decode_all(&*self.compressed_bytes)?;
891899

892900
Ok(Bytecode { bytes })
893901
}
902+
}
903+
904+
#[cfg(target_arch = "wasm32")]
905+
impl CompressedBytecode {
906+
/// Returns `true` if the decompressed size does not exceed the limit.
907+
pub fn decompressed_size_at_most(&self, limit: u64) -> Result<bool, DecompressionError> {
908+
let compressed_bytes = &*self.compressed_bytes;
909+
let limit = usize::try_from(limit).unwrap_or(usize::MAX);
910+
let mut writer = LimitedWriter::new(io::sink(), limit);
911+
let mut decoder = ruzstd::streaming_decoder::StreamingDecoder::new(compressed_bytes)
912+
.map_err(io::Error::other)?;
913+
914+
// TODO(#2710): Decode multiple frames, if present
915+
match io::copy(&mut decoder, &mut writer) {
916+
Ok(_) => Ok(true),
917+
Err(error) => {
918+
error.downcast::<LimitedWriterError>()?;
919+
Ok(false)
920+
}
921+
}
922+
}
894923

895924
/// Decompresses a [`CompressedBytecode`] into a [`Bytecode`].
896-
#[cfg(target_arch = "wasm32")]
897925
pub fn decompress(&self) -> Result<Bytecode, DecompressionError> {
898926
use ruzstd::{io::Read, streaming_decoder::StreamingDecoder};
899927

@@ -902,10 +930,9 @@ impl CompressedBytecode {
902930

903931
let compressed_bytes = &*self.compressed_bytes;
904932
let mut bytes = Vec::new();
905-
let mut decoder = StreamingDecoder::new(compressed_bytes)?;
933+
let mut decoder = StreamingDecoder::new(compressed_bytes).map_err(io::Error::other)?;
906934

907-
// Decode multiple frames, if present
908-
// (https://github.com/KillingSpark/zstd-rs/issues/57)
935+
// TODO(#2710): Decode multiple frames, if present
909936
while !decoder.get_ref().is_empty() {
910937
decoder
911938
.read_to_end(&mut bytes)

linera-base/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub mod crypto;
1818
pub mod data_types;
1919
mod graphql;
2020
pub mod identifiers;
21+
mod limited_writer;
2122
pub mod ownership;
2223
#[cfg(not(target_arch = "wasm32"))]
2324
pub mod port;

linera-base/src/limited_writer.rs

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) Zefchain Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use std::io::{self, Write};
5+
6+
use thiserror::Error;
7+
8+
use crate::ensure;
9+
10+
#[derive(Error, Debug)]
11+
#[error("Writer limit exceeded")]
12+
pub struct LimitedWriterError;
13+
14+
/// Custom writer that enforces a byte limit.
15+
pub struct LimitedWriter<W: Write> {
16+
inner: W,
17+
limit: usize,
18+
written: usize,
19+
}
20+
21+
impl<W: Write> LimitedWriter<W> {
22+
pub fn new(inner: W, limit: usize) -> Self {
23+
Self {
24+
inner,
25+
limit,
26+
written: 0,
27+
}
28+
}
29+
}
30+
31+
impl<W: Write> Write for LimitedWriter<W> {
32+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
33+
// Calculate the number of bytes we can write without exceeding the limit.
34+
// Fail if the buffer doesn't fit.
35+
ensure!(
36+
self.limit
37+
.checked_sub(self.written)
38+
.is_some_and(|remaining| buf.len() <= remaining),
39+
io::Error::other(LimitedWriterError)
40+
);
41+
// Forward to the inner writer.
42+
let n = self.inner.write(buf)?;
43+
self.written += n;
44+
Ok(n)
45+
}
46+
47+
fn flush(&mut self) -> io::Result<()> {
48+
self.inner.flush()
49+
}
50+
}
51+
52+
#[cfg(test)]
53+
mod tests {
54+
use super::*;
55+
56+
#[test]
57+
fn test_limited_writer() {
58+
let mut out_buffer = Vec::new();
59+
let mut writer = LimitedWriter::new(&mut out_buffer, 5);
60+
assert_eq!(writer.write(b"foo").unwrap(), 3);
61+
assert_eq!(writer.write(b"ba").unwrap(), 2);
62+
assert!(writer
63+
.write(b"r")
64+
.unwrap_err()
65+
.downcast::<LimitedWriterError>()
66+
.is_ok());
67+
}
68+
}

linera-chain/src/unit_tests/chain_tests.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ async fn test_block_size_limit() {
110110
let mut chain = ChainStateView::new(chain_id).await;
111111

112112
// The size of the executed valid block below.
113-
let maximum_executed_block_size = 675;
113+
let maximum_executed_block_size = 691;
114114

115115
// Initialize the chain.
116116
let mut config = make_open_chain_config();

linera-client/src/client_options.rs

+19-1
Original file line numberDiff line numberDiff line change
@@ -517,10 +517,19 @@ pub enum ClientCommand {
517517
#[arg(long)]
518518
maximum_fuel_per_block: Option<u64>,
519519

520-
/// Set the maximum size of an executed block.
520+
/// Set the maximum size of an executed block, in bytes.
521521
#[arg(long)]
522522
maximum_executed_block_size: Option<u64>,
523523

524+
/// Set the maximum size of data blobs, compressed bytecode and other binary blobs,
525+
/// in bytes.
526+
#[arg(long)]
527+
maximum_blob_size: Option<u64>,
528+
529+
/// Set the maximum size of decompressed contract or service bytecode, in bytes.
530+
#[arg(long)]
531+
maximum_bytecode_size: Option<u64>,
532+
524533
/// Set the maximum read data per block.
525534
#[arg(long)]
526535
maximum_bytes_read_per_block: Option<u64>,
@@ -635,6 +644,15 @@ pub enum ClientCommand {
635644
#[arg(long)]
636645
maximum_executed_block_size: Option<u64>,
637646

647+
/// Set the maximum size of decompressed contract or service bytecode, in bytes.
648+
#[arg(long)]
649+
maximum_bytecode_size: Option<u64>,
650+
651+
/// Set the maximum size of data blobs, compressed bytecode and other binary blobs,
652+
/// in bytes.
653+
#[arg(long)]
654+
maximum_blob_size: Option<u64>,
655+
638656
/// Set the maximum read data per block.
639657
#[arg(long)]
640658
maximum_bytes_read_per_block: Option<u64>,

linera-core/src/chain_worker/state/temporary_changes.rs

+22-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use std::borrow::Cow;
77

88
use linera_base::{
9-
data_types::{ArithmeticError, Timestamp, UserApplicationDescription},
9+
data_types::{ArithmeticError, BlobContent, Timestamp, UserApplicationDescription},
1010
ensure,
1111
identifiers::{GenericApplicationId, UserApplicationId},
1212
};
@@ -179,6 +179,8 @@ where
179179
.current_committee()
180180
.expect("chain is active");
181181
check_block_epoch(epoch, block)?;
182+
let maximum_blob_size = committee.policy().maximum_blob_size;
183+
let maximum_bytecode_size = committee.policy().maximum_bytecode_size;
182184
// Check the authentication of the block.
183185
let public_key = self
184186
.0
@@ -213,6 +215,25 @@ where
213215
for blob in blobs {
214216
self.0.cache_recent_blob(Cow::Borrowed(blob)).await;
215217
}
218+
for blob in self.0.get_blobs(block.published_blob_ids()).await? {
219+
let blob_size = match blob.content() {
220+
BlobContent::Data(bytes) => bytes.len(),
221+
BlobContent::ContractBytecode(compressed_bytecode)
222+
| BlobContent::ServiceBytecode(compressed_bytecode) => {
223+
ensure!(
224+
compressed_bytecode.decompressed_size_at_most(maximum_bytecode_size)?,
225+
WorkerError::BytecodeTooLarge
226+
);
227+
compressed_bytecode.compressed_bytes.len()
228+
}
229+
};
230+
ensure!(
231+
u64::try_from(blob_size)
232+
.ok()
233+
.is_some_and(|size| size <= maximum_blob_size),
234+
WorkerError::BlobTooLarge
235+
)
236+
}
216237

217238
let local_time = self.0.storage.clock().current_time();
218239
ensure!(

linera-core/src/unit_tests/client_tests.rs

+12-2
Original file line numberDiff line numberDiff line change
@@ -2413,7 +2413,15 @@ async fn test_propose_block_with_messages_and_blobs<B>(storage_builder: B) -> an
24132413
where
24142414
B: StorageBuilder,
24152415
{
2416-
let mut builder = TestBuilder::new(storage_builder, 4, 0).await?;
2416+
let blob_bytes = b"blob".to_vec();
2417+
let large_blob_bytes = b"blob+".to_vec();
2418+
let policy = ResourceControlPolicy {
2419+
maximum_blob_size: blob_bytes.len() as u64,
2420+
..ResourceControlPolicy::default()
2421+
};
2422+
let mut builder = TestBuilder::new(storage_builder, 4, 0)
2423+
.await?
2424+
.with_policy(policy.clone());
24172425
let description1 = ChainDescription::Root(1);
24182426
let description2 = ChainDescription::Root(2);
24192427
let description3 = ChainDescription::Root(3);
@@ -2426,7 +2434,6 @@ where
24262434
builder.set_fault_type([3], FaultType::Offline).await;
24272435

24282436
// Publish a blob on chain 1.
2429-
let blob_bytes = b"blob".to_vec();
24302437
let blob_id = BlobId::new(
24312438
CryptoHash::new(&BlobBytes(blob_bytes.clone())),
24322439
BlobType::Data,
@@ -2458,5 +2465,8 @@ where
24582465
assert_eq!(executed_block.block.incoming_bundles.len(), 1);
24592466
assert_eq!(executed_block.required_blob_ids().len(), 1);
24602467

2468+
let result = client1.publish_data_blob(large_blob_bytes).await;
2469+
assert_matches!(result, Err(ChainClientError::LocalNodeError(_)));
2470+
24612471
Ok(())
24622472
}

linera-core/src/unit_tests/wasm_client_tests.rs

+31-9
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ use crate::client::client_tests::RocksDbStorageBuilder;
4141
use crate::client::client_tests::ScyllaDbStorageBuilder;
4242
#[cfg(feature = "storage-service")]
4343
use crate::client::client_tests::ServiceStorageBuilder;
44-
use crate::client::client_tests::{MemoryStorageBuilder, StorageBuilder, TestBuilder};
44+
use crate::client::{
45+
client_tests::{MemoryStorageBuilder, StorageBuilder, TestBuilder},
46+
ChainClientError,
47+
};
4548

4649
#[cfg_attr(feature = "wasmer", test_case(WasmRuntime::Wasmer ; "wasmer"))]
4750
#[cfg_attr(feature = "wasmtime", test_case(WasmRuntime::Wasmtime ; "wasmtime"))]
@@ -90,24 +93,31 @@ async fn run_test_create_application<B>(storage_builder: B) -> anyhow::Result<()
9093
where
9194
B: StorageBuilder,
9295
{
96+
let (contract_path, service_path) =
97+
linera_execution::wasm_test::get_example_bytecode_paths("counter")?;
98+
let contract_bytecode = Bytecode::load_from_file(contract_path).await?;
99+
let service_bytecode = Bytecode::load_from_file(service_path).await?;
100+
let contract_compressed_len = contract_bytecode.compress().compressed_bytes.len();
101+
let service_compressed_len = service_bytecode.compress().compressed_bytes.len();
102+
103+
let mut policy = ResourceControlPolicy::all_categories();
104+
policy.maximum_bytecode_size = contract_bytecode
105+
.bytes
106+
.len()
107+
.max(service_bytecode.bytes.len()) as u64;
108+
policy.maximum_blob_size = contract_compressed_len.max(service_compressed_len) as u64;
93109
let mut builder = TestBuilder::new(storage_builder, 4, 1)
94110
.await?
95-
.with_policy(ResourceControlPolicy::all_categories());
111+
.with_policy(policy.clone());
96112
let publisher = builder
97113
.add_initial_chain(ChainDescription::Root(0), Amount::from_tokens(3))
98114
.await?;
99115
let creator = builder
100116
.add_initial_chain(ChainDescription::Root(1), Amount::ONE)
101117
.await?;
102118

103-
let (contract_path, service_path) =
104-
linera_execution::wasm_test::get_example_bytecode_paths("counter")?;
105-
106119
let (bytecode_id, _cert) = publisher
107-
.publish_bytecode(
108-
Bytecode::load_from_file(contract_path).await?,
109-
Bytecode::load_from_file(service_path).await?,
110-
)
120+
.publish_bytecode(contract_bytecode, service_bytecode)
111121
.await
112122
.unwrap()
113123
.unwrap();
@@ -148,6 +158,18 @@ where
148158
let balance_after_init = creator.local_balance().await?;
149159
assert!(balance_after_init < balance_after_messaging);
150160

161+
let large_bytecode = Bytecode::new(vec![0; policy.maximum_bytecode_size as usize + 1]);
162+
let small_bytecode = Bytecode::new(vec![]);
163+
// Publishing bytecode that exceeds the limit fails.
164+
let result = publisher
165+
.publish_bytecode(large_bytecode.clone(), small_bytecode.clone())
166+
.await;
167+
assert_matches!(result, Err(ChainClientError::LocalNodeError(_)));
168+
let result = publisher
169+
.publish_bytecode(small_bytecode, large_bytecode)
170+
.await;
171+
assert_matches!(result, Err(ChainClientError::LocalNodeError(_)));
172+
151173
Ok(())
152174
}
153175

0 commit comments

Comments
 (0)