Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aware of compression ratio for unpack size limit #11337

Merged
merged 1 commit into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions src/cargo/sources/registry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ const PREFIX_TEMPLATE: &str = "{prefix}";
const LOWER_PREFIX_TEMPLATE: &str = "{lowerprefix}";
const CHECKSUM_TEMPLATE: &str = "{sha256-checksum}";
const MAX_UNPACK_SIZE: u64 = 512 * 1024 * 1024;
const MAX_COMPRESSION_RATIO: usize = 20; // 20:1

/// A "source" for a local (see `local::LocalRegistry`) or remote (see
/// `remote::RemoteRegistry`) registry.
Expand Down Expand Up @@ -617,9 +618,12 @@ impl<'cfg> RegistrySource<'cfg> {
return Ok(unpack_dir.to_path_buf());
}
}
let gz = GzDecoder::new(tarball);
let gz = LimitErrorReader::new(gz, max_unpack_size());
let mut tar = Archive::new(gz);
let mut tar = {
let size_limit = max_unpack_size(tarball.metadata()?.len());
let gz = GzDecoder::new(tarball);
let gz = LimitErrorReader::new(gz, size_limit);
Archive::new(gz)
};
let prefix = unpack_dir.file_name().unwrap();
let parent = unpack_dir.parent().unwrap();
for entry in tar.entries()? {
Expand Down Expand Up @@ -835,18 +839,47 @@ impl<'cfg> Source for RegistrySource<'cfg> {
}
}

/// For integration test only.
#[inline]
fn max_unpack_size() -> u64 {
const VAR: &str = "__CARGO_TEST_MAX_UNPACK_SIZE";
if cfg!(debug_assertions) && std::env::var(VAR).is_ok() {
std::env::var(VAR)
/// Get the maximum upack size that Cargo permits
/// based on a given `size of your compressed file.
///
/// Returns the larger one between `size * max compression ratio`
/// and a fixed max unpacked size.
///
/// In reality, the compression ratio usually falls in the range of 2:1 to 10:1.
/// We choose 20:1 to cover almost all possible cases hopefully.
/// Any ratio higher than this is considered as a zip bomb.
///
/// In the future we might want to introduce a configurable size.
///
/// Some of the real world data from common compression algorithms:
///
/// * <https://www.zlib.net/zlib_tech.html>
/// * <https://cran.r-project.org/web/packages/brotli/vignettes/brotli-2015-09-22.pdf>
/// * <https://blog.cloudflare.com/results-experimenting-brotli/>
/// * <https://tukaani.org/lzma/benchmarks.html>
fn max_unpack_size(size: u64) -> u64 {
const SIZE_VAR: &str = "__CARGO_TEST_MAX_UNPACK_SIZE";
const RATIO_VAR: &str = "__CARGO_TEST_MAX_UNPACK_RATIO";
let max_unpack_size = if cfg!(debug_assertions) && std::env::var(SIZE_VAR).is_ok() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the CFG mean that the test suite will only pass in debug mode?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, unfortunately.
If SIZE_VAR were exposed, this PR would become unnecessary 😆.

// Limit the test to debug builds so that `__CARGO_TEST_MAX_UNPACK_SIZE` will take affect.
#[cfg(debug_assertions)]
#[cargo_test]
fn reach_max_unpack_size() {

// For integration test only.
std::env::var(SIZE_VAR)
.unwrap()
.parse()
.expect("a max unpack size in bytes")
} else {
MAX_UNPACK_SIZE
}
};
let max_compression_ratio = if cfg!(debug_assertions) && std::env::var(RATIO_VAR).is_ok() {
// For integration test only.
std::env::var(RATIO_VAR)
.unwrap()
.parse()
.expect("a max compresssion ratio in bytes")
} else {
MAX_COMPRESSION_RATIO
};

u64::max(max_unpack_size, size * max_compression_ratio as u64)
}

fn make_dep_prefix(name: &str) -> String {
Expand Down
14 changes: 14 additions & 0 deletions tests/testsuite/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2750,10 +2750,12 @@ fn reach_max_unpack_size() {
.file("src/main.rs", "fn main() {}")
.build();

// Size of bar.crate is around 180 bytes.
Package::new("bar", "0.0.1").publish();

p.cargo("build")
.env("__CARGO_TEST_MAX_UNPACK_SIZE", "8") // hit 8 bytes limit and boom!
.env("__CARGO_TEST_MAX_UNPACK_RATIO", "0")
.with_status(101)
.with_stderr(
"\
Expand All @@ -2770,6 +2772,18 @@ Caused by:

Caused by:
maximum limit reached when reading
",
)
.run();

// Restore to the default ratio and it should compile.
p.cargo("build")
.env("__CARGO_TEST_MAX_UNPACK_SIZE", "8")
.with_stderr(
"\
[COMPILING] bar v0.0.1
[COMPILING] foo v0.0.1 ([..])
[FINISHED] dev [..]
",
)
.run();
Expand Down