Skip to content

Commit 9ef926d

Browse files
committed
Auto merge of #11089 - pietroalbini:pa-cves-nightly, r=weihanglo
[master] Fix for CVE-2022-36113 and CVE-2022-36114 This PR includes the fixes for CVE-2022-36113 and CVE-2022-36114 targeting the master branch. See [the advisory](https://blog.rust-lang.org/2022/09/14/cargo-cves.html) for more information about the vulnerabilities.
2 parents 22dd4bf + 0bf436d commit 9ef926d

File tree

5 files changed

+214
-13
lines changed

5 files changed

+214
-13
lines changed

crates/cargo-test-support/src/registry.rs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -403,10 +403,17 @@ pub struct Dependency {
403403
optional: bool,
404404
}
405405

406+
/// Entry with data that corresponds to [`tar::EntryType`].
407+
#[non_exhaustive]
408+
enum EntryData {
409+
Regular(String),
410+
Symlink(PathBuf),
411+
}
412+
406413
/// A file to be created in a package.
407414
struct PackageFile {
408415
path: String,
409-
contents: String,
416+
contents: EntryData,
410417
/// The Unix mode for the file. Note that when extracted on Windows, this
411418
/// is mostly ignored since it doesn't have the same style of permissions.
412419
mode: u32,
@@ -780,13 +787,24 @@ impl Package {
780787
pub fn file_with_mode(&mut self, path: &str, mode: u32, contents: &str) -> &mut Package {
781788
self.files.push(PackageFile {
782789
path: path.to_string(),
783-
contents: contents.to_string(),
790+
contents: EntryData::Regular(contents.into()),
784791
mode,
785792
extra: false,
786793
});
787794
self
788795
}
789796

797+
/// Adds a symlink to a path to the package.
798+
pub fn symlink(&mut self, dst: &str, src: &str) -> &mut Package {
799+
self.files.push(PackageFile {
800+
path: dst.to_string(),
801+
contents: EntryData::Symlink(src.into()),
802+
mode: DEFAULT_MODE,
803+
extra: false,
804+
});
805+
self
806+
}
807+
790808
/// Adds an "extra" file that is not rooted within the package.
791809
///
792810
/// Normal files are automatically placed within a directory named
@@ -795,7 +813,7 @@ impl Package {
795813
pub fn extra_file(&mut self, path: &str, contents: &str) -> &mut Package {
796814
self.files.push(PackageFile {
797815
path: path.to_string(),
798-
contents: contents.to_string(),
816+
contents: EntryData::Regular(contents.to_string()),
799817
mode: DEFAULT_MODE,
800818
extra: true,
801819
});
@@ -1033,7 +1051,12 @@ impl Package {
10331051
self.append_manifest(&mut a);
10341052
}
10351053
if self.files.is_empty() {
1036-
self.append(&mut a, "src/lib.rs", DEFAULT_MODE, "");
1054+
self.append(
1055+
&mut a,
1056+
"src/lib.rs",
1057+
DEFAULT_MODE,
1058+
&EntryData::Regular("".into()),
1059+
);
10371060
} else {
10381061
for PackageFile {
10391062
path,
@@ -1107,10 +1130,15 @@ impl Package {
11071130
manifest.push_str("[lib]\nproc-macro = true\n");
11081131
}
11091132

1110-
self.append(ar, "Cargo.toml", DEFAULT_MODE, &manifest);
1133+
self.append(
1134+
ar,
1135+
"Cargo.toml",
1136+
DEFAULT_MODE,
1137+
&EntryData::Regular(manifest.into()),
1138+
);
11111139
}
11121140

1113-
fn append<W: Write>(&self, ar: &mut Builder<W>, file: &str, mode: u32, contents: &str) {
1141+
fn append<W: Write>(&self, ar: &mut Builder<W>, file: &str, mode: u32, contents: &EntryData) {
11141142
self.append_raw(
11151143
ar,
11161144
&format!("{}-{}/{}", self.name, self.vers, file),
@@ -1119,8 +1147,22 @@ impl Package {
11191147
);
11201148
}
11211149

1122-
fn append_raw<W: Write>(&self, ar: &mut Builder<W>, path: &str, mode: u32, contents: &str) {
1150+
fn append_raw<W: Write>(
1151+
&self,
1152+
ar: &mut Builder<W>,
1153+
path: &str,
1154+
mode: u32,
1155+
contents: &EntryData,
1156+
) {
11231157
let mut header = Header::new_ustar();
1158+
let contents = match contents {
1159+
EntryData::Regular(contents) => contents.as_str(),
1160+
EntryData::Symlink(src) => {
1161+
header.set_entry_type(tar::EntryType::Symlink);
1162+
t!(header.set_link_name(src));
1163+
"" // Symlink has no contents.
1164+
}
1165+
};
11241166
header.set_size(contents.len() as u64);
11251167
t!(header.set_path(path));
11261168
header.set_mode(mode);

src/cargo/sources/registry/mod.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,9 @@ use crate::util::hex;
182182
use crate::util::interning::InternedString;
183183
use crate::util::into_url::IntoUrl;
184184
use crate::util::network::PollExt;
185-
use crate::util::{restricted_names, CargoResult, Config, Filesystem, OptVersionReq};
185+
use crate::util::{
186+
restricted_names, CargoResult, Config, Filesystem, LimitErrorReader, OptVersionReq,
187+
};
186188

187189
const PACKAGE_SOURCE_LOCK: &str = ".cargo-ok";
188190
pub const CRATES_IO_INDEX: &str = "https://github.com/rust-lang/crates.io-index";
@@ -194,6 +196,7 @@ const VERSION_TEMPLATE: &str = "{version}";
194196
const PREFIX_TEMPLATE: &str = "{prefix}";
195197
const LOWER_PREFIX_TEMPLATE: &str = "{lowerprefix}";
196198
const CHECKSUM_TEMPLATE: &str = "{sha256-checksum}";
199+
const MAX_UNPACK_SIZE: u64 = 512 * 1024 * 1024;
197200

198201
/// A "source" for a local (see `local::LocalRegistry`) or remote (see
199202
/// `remote::RemoteRegistry`) registry.
@@ -615,6 +618,7 @@ impl<'cfg> RegistrySource<'cfg> {
615618
}
616619
}
617620
let gz = GzDecoder::new(tarball);
621+
let gz = LimitErrorReader::new(gz, max_unpack_size());
618622
let mut tar = Archive::new(gz);
619623
let prefix = unpack_dir.file_name().unwrap();
620624
let parent = unpack_dir.parent().unwrap();
@@ -639,6 +643,13 @@ impl<'cfg> RegistrySource<'cfg> {
639643
prefix
640644
)
641645
}
646+
// Prevent unpacking the lockfile from the crate itself.
647+
if entry_path
648+
.file_name()
649+
.map_or(false, |p| p == PACKAGE_SOURCE_LOCK)
650+
{
651+
continue;
652+
}
642653
// Unpacking failed
643654
let mut result = entry.unpack_in(parent).map_err(anyhow::Error::from);
644655
if cfg!(windows) && restricted_names::is_windows_reserved_path(&entry_path) {
@@ -654,16 +665,14 @@ impl<'cfg> RegistrySource<'cfg> {
654665
.with_context(|| format!("failed to unpack entry at `{}`", entry_path.display()))?;
655666
}
656667

657-
// The lock file is created after unpacking so we overwrite a lock file
658-
// which may have been extracted from the package.
668+
// Now that we've finished unpacking, create and write to the lock file to indicate that
669+
// unpacking was successful.
659670
let mut ok = OpenOptions::new()
660-
.create(true)
671+
.create_new(true)
661672
.read(true)
662673
.write(true)
663674
.open(&path)
664675
.with_context(|| format!("failed to open `{}`", path.display()))?;
665-
666-
// Write to the lock file to indicate that unpacking was successful.
667676
write!(ok, "ok")?;
668677

669678
Ok(unpack_dir.to_path_buf())
@@ -826,6 +835,20 @@ impl<'cfg> Source for RegistrySource<'cfg> {
826835
}
827836
}
828837

838+
/// For integration test only.
839+
#[inline]
840+
fn max_unpack_size() -> u64 {
841+
const VAR: &str = "__CARGO_TEST_MAX_UNPACK_SIZE";
842+
if cfg!(debug_assertions) && std::env::var(VAR).is_ok() {
843+
std::env::var(VAR)
844+
.unwrap()
845+
.parse()
846+
.expect("a max unpack size in bytes")
847+
} else {
848+
MAX_UNPACK_SIZE
849+
}
850+
}
851+
829852
fn make_dep_prefix(name: &str) -> String {
830853
match name.len() {
831854
1 => String::from("1"),

src/cargo/util/io.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use std::io::{self, Read, Take};
2+
3+
#[derive(Debug)]
4+
pub struct LimitErrorReader<R> {
5+
inner: Take<R>,
6+
}
7+
8+
impl<R: Read> LimitErrorReader<R> {
9+
pub fn new(r: R, limit: u64) -> LimitErrorReader<R> {
10+
LimitErrorReader {
11+
inner: r.take(limit),
12+
}
13+
}
14+
}
15+
16+
impl<R: Read> Read for LimitErrorReader<R> {
17+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
18+
match self.inner.read(buf) {
19+
Ok(0) if self.inner.limit() == 0 => Err(io::Error::new(
20+
io::ErrorKind::Other,
21+
"maximum limit reached when reading",
22+
)),
23+
e => e,
24+
}
25+
}
26+
}
27+
28+
#[cfg(test)]
29+
mod tests {
30+
use super::LimitErrorReader;
31+
32+
use std::io::Read;
33+
34+
#[test]
35+
fn under_the_limit() {
36+
let buf = &[1; 7][..];
37+
let mut r = LimitErrorReader::new(buf, 8);
38+
let mut out = Vec::new();
39+
assert!(matches!(r.read_to_end(&mut out), Ok(7)));
40+
assert_eq!(buf, out.as_slice());
41+
}
42+
43+
#[test]
44+
#[should_panic = "maximum limit reached when reading"]
45+
fn over_the_limit() {
46+
let buf = &[1; 8][..];
47+
let mut r = LimitErrorReader::new(buf, 8);
48+
let mut out = Vec::new();
49+
r.read_to_end(&mut out).unwrap();
50+
}
51+
}

src/cargo/util/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub use self::hasher::StableHasher;
1414
pub use self::hex::{hash_u64, short_hash, to_hex};
1515
pub use self::into_url::IntoUrl;
1616
pub use self::into_url_with_base::IntoUrlWithBase;
17+
pub(crate) use self::io::LimitErrorReader;
1718
pub use self::lev_distance::{closest, closest_msg, lev_distance};
1819
pub use self::lockserver::{LockServer, LockServerClient, LockServerStarted};
1920
pub use self::progress::{Progress, ProgressStyle};
@@ -44,6 +45,7 @@ pub mod important_paths;
4445
pub mod interning;
4546
pub mod into_url;
4647
mod into_url_with_base;
48+
mod io;
4749
pub mod job;
4850
pub mod lev_distance;
4951
mod lockserver;

tests/testsuite/registry.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2583,6 +2583,47 @@ fn package_lock_inside_package_is_overwritten() {
25832583
assert_eq!(ok.metadata().unwrap().len(), 2);
25842584
}
25852585

2586+
#[cargo_test]
2587+
fn package_lock_as_a_symlink_inside_package_is_overwritten() {
2588+
let registry = registry::init();
2589+
let p = project()
2590+
.file(
2591+
"Cargo.toml",
2592+
r#"
2593+
[project]
2594+
name = "foo"
2595+
version = "0.0.1"
2596+
authors = []
2597+
2598+
[dependencies]
2599+
bar = ">= 0.0.0"
2600+
"#,
2601+
)
2602+
.file("src/main.rs", "fn main() {}")
2603+
.build();
2604+
2605+
Package::new("bar", "0.0.1")
2606+
.file("src/lib.rs", "pub fn f() {}")
2607+
.symlink(".cargo-ok", "src/lib.rs")
2608+
.publish();
2609+
2610+
p.cargo("build").run();
2611+
2612+
let id = SourceId::for_registry(registry.index_url()).unwrap();
2613+
let hash = cargo::util::hex::short_hash(&id);
2614+
let pkg_root = cargo_home()
2615+
.join("registry")
2616+
.join("src")
2617+
.join(format!("-{}", hash))
2618+
.join("bar-0.0.1");
2619+
let ok = pkg_root.join(".cargo-ok");
2620+
let librs = pkg_root.join("src/lib.rs");
2621+
2622+
// Is correctly overwritten and doesn't affect the file linked to
2623+
assert_eq!(ok.metadata().unwrap().len(), 2);
2624+
assert_eq!(fs::read_to_string(librs).unwrap(), "pub fn f() {}");
2625+
}
2626+
25862627
#[cargo_test]
25872628
fn ignores_unknown_index_version_http() {
25882629
let _server = setup_http();
@@ -2656,3 +2697,45 @@ fn http_requires_trailing_slash() {
26562697
.with_stderr("[ERROR] registry url must end in a slash `/`: sparse+https://index.crates.io")
26572698
.run()
26582699
}
2700+
2701+
#[cargo_test]
2702+
fn reach_max_unpack_size() {
2703+
let p = project()
2704+
.file(
2705+
"Cargo.toml",
2706+
r#"
2707+
[project]
2708+
name = "foo"
2709+
version = "0.0.1"
2710+
2711+
[dependencies]
2712+
bar = ">= 0.0.0"
2713+
"#,
2714+
)
2715+
.file("src/main.rs", "fn main() {}")
2716+
.build();
2717+
2718+
Package::new("bar", "0.0.1").publish();
2719+
2720+
p.cargo("build")
2721+
.env("__CARGO_TEST_MAX_UNPACK_SIZE", "8") // hit 8 bytes limit and boom!
2722+
.with_status(101)
2723+
.with_stderr(
2724+
"\
2725+
[UPDATING] `dummy-registry` index
2726+
[DOWNLOADING] crates ...
2727+
[DOWNLOADED] bar v0.0.1 (registry `dummy-registry`)
2728+
[ERROR] failed to download replaced source registry `crates-io`
2729+
2730+
Caused by:
2731+
failed to unpack package `bar v0.0.1 (registry `dummy-registry`)`
2732+
2733+
Caused by:
2734+
failed to iterate over archive
2735+
2736+
Caused by:
2737+
maximum limit reached when reading
2738+
",
2739+
)
2740+
.run();
2741+
}

0 commit comments

Comments
 (0)