Skip to content

Commit 44b6be4

Browse files
committed
Auto merge of #12442 - pietroalbini:pa-cve-2023-38497-beta, r=weihanglo
Fix CVE-2023-38497 for beta 1.72.0 Changes have been made by `@weihanglo` and reviewed by `@ehuss` in a private repository.
2 parents 11ffe0e + 6aa9859 commit 44b6be4

File tree

9 files changed

+216
-28
lines changed

9 files changed

+216
-28
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ shell-escape = "0.1.4"
8080
snapbox = { version = "0.4.0", features = ["diff", "path"] }
8181
strip-ansi-escapes = "0.1.0"
8282
syn = { version = "2.0.14", features = ["extra-traits", "full"] }
83-
tar = { version = "0.4.38", default-features = false }
83+
tar = { version = "0.4.39", default-features = false }
8484
tempfile = "3.1.0"
8585
termcolor = "1.1.2"
8686
time = { version = "0.3", features = ["parsing", "formatting"] }

src/cargo/sources/registry/mod.rs

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,11 @@
184184
//! [`IndexPackage`]: index::IndexPackage
185185
186186
use std::collections::HashSet;
187+
use std::fs;
187188
use std::fs::{File, OpenOptions};
188-
use std::io::{self, Write};
189+
use std::io;
190+
use std::io::Read;
191+
use std::io::Write;
189192
use std::path::{Path, PathBuf};
190193
use std::task::{ready, Poll};
191194

@@ -194,6 +197,7 @@ use cargo_util::paths::{self, exclude_from_backups_and_indexing};
194197
use flate2::read::GzDecoder;
195198
use log::debug;
196199
use serde::Deserialize;
200+
use serde::Serialize;
197201
use tar::Archive;
198202

199203
use crate::core::dependency::Dependency;
@@ -217,6 +221,14 @@ pub const CRATES_IO_HTTP_INDEX: &str = "sparse+https://index.crates.io/";
217221
pub const CRATES_IO_REGISTRY: &str = "crates-io";
218222
pub const CRATES_IO_DOMAIN: &str = "crates.io";
219223

224+
/// The content inside `.cargo-ok`.
225+
/// See [`RegistrySource::unpack_package`] for more.
226+
#[derive(Deserialize, Serialize)]
227+
struct LockMetadata {
228+
/// The version of `.cargo-ok` file
229+
v: u32,
230+
}
231+
220232
/// A [`Source`] implementation for a local or a remote registry.
221233
///
222234
/// This contains common functionality that is shared between each registry
@@ -544,6 +556,11 @@ impl<'cfg> RegistrySource<'cfg> {
544556
/// `.crate` files making `.cargo-ok` a symlink causing cargo to write "ok"
545557
/// to any arbitrary file on the filesystem it has permission to.
546558
///
559+
/// In 1.71, `.cargo-ok` changed to contain a JSON `{ v: 1 }` to indicate
560+
/// the version of it. A failure of parsing will result in a heavy-hammer
561+
/// approach that unpacks the `.crate` file again. This is in response to a
562+
/// security issue that the unpacking didn't respect umask on Unix systems.
563+
///
547564
/// This is all a long-winded way of explaining the circumstances that might
548565
/// cause a directory to contain a `.cargo-ok` file that is empty or
549566
/// otherwise corrupted. Either this was extracted by a version of Rust
@@ -565,22 +582,32 @@ impl<'cfg> RegistrySource<'cfg> {
565582
let path = dst.join(PACKAGE_SOURCE_LOCK);
566583
let path = self.config.assert_package_cache_locked(&path);
567584
let unpack_dir = path.parent().unwrap();
568-
match path.metadata() {
569-
Ok(meta) if meta.len() > 0 => return Ok(unpack_dir.to_path_buf()),
570-
Ok(_meta) => {
571-
// See comment of `unpack_package` about why removing all stuff.
572-
log::warn!("unexpected length of {path:?}, clearing cache");
573-
paths::remove_dir_all(dst.as_path_unlocked())?;
574-
}
585+
match fs::read_to_string(path) {
586+
Ok(ok) => match serde_json::from_str::<LockMetadata>(&ok) {
587+
Ok(lock_meta) if lock_meta.v == 1 => {
588+
return Ok(unpack_dir.to_path_buf());
589+
}
590+
_ => {
591+
if ok == "ok" {
592+
log::debug!("old `ok` content found, clearing cache");
593+
} else {
594+
log::warn!("unrecognized .cargo-ok content, clearing cache: {ok}");
595+
}
596+
// See comment of `unpack_package` about why removing all stuff.
597+
paths::remove_dir_all(dst.as_path_unlocked())?;
598+
}
599+
},
575600
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
576-
Err(e) => anyhow::bail!("failed to access package completion {path:?}: {e}"),
601+
Err(e) => anyhow::bail!("unable to read .cargo-ok file at {path:?}: {e}"),
577602
}
578603
dst.create_dir()?;
579604
let mut tar = {
580605
let size_limit = max_unpack_size(self.config, tarball.metadata()?.len());
581606
let gz = GzDecoder::new(tarball);
582607
let gz = LimitErrorReader::new(gz, size_limit);
583-
Archive::new(gz)
608+
let mut tar = Archive::new(gz);
609+
set_mask(&mut tar);
610+
tar
584611
};
585612
let prefix = unpack_dir.file_name().unwrap();
586613
let parent = unpack_dir.parent().unwrap();
@@ -635,7 +662,9 @@ impl<'cfg> RegistrySource<'cfg> {
635662
.write(true)
636663
.open(&path)
637664
.with_context(|| format!("failed to open `{}`", path.display()))?;
638-
write!(ok, "ok")?;
665+
666+
let lock_meta = LockMetadata { v: 1 };
667+
write!(ok, "{}", serde_json::to_string(&lock_meta).unwrap())?;
639668

640669
Ok(unpack_dir.to_path_buf())
641670
}
@@ -908,3 +937,16 @@ fn max_unpack_size(config: &Config, size: u64) -> u64 {
908937

909938
u64::max(max_unpack_size, size * max_compression_ratio as u64)
910939
}
940+
941+
/// Set the current [`umask`] value for the given tarball. No-op on non-Unix
942+
/// platforms.
943+
///
944+
/// On Windows, tar only looks at user permissions and tries to set the "read
945+
/// only" attribute, so no-op as well.
946+
///
947+
/// [`umask`]: https://man7.org/linux/man-pages/man2/umask.2.html
948+
#[allow(unused_variables)]
949+
fn set_mask<R: Read>(tar: &mut Archive<R>) {
950+
#[cfg(unix)]
951+
tar.set_mask(crate::util::get_umask());
952+
}

src/cargo/util/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,24 @@ pub fn try_canonicalize<P: AsRef<Path>>(path: P) -> std::io::Result<PathBuf> {
207207
})
208208
}
209209

210+
/// Get the current [`umask`] value.
211+
///
212+
/// [`umask`]: https://man7.org/linux/man-pages/man2/umask.2.html
213+
#[cfg(unix)]
214+
pub fn get_umask() -> u32 {
215+
use std::sync::OnceLock;
216+
static UMASK: OnceLock<libc::mode_t> = OnceLock::new();
217+
// SAFETY: Syscalls are unsafe. Calling `umask` twice is even unsafer for
218+
// multithreading program, since it doesn't provide a way to retrive the
219+
// value without modifications. We use a static `OnceLock` here to ensure
220+
// it only gets call once during the entire program lifetime.
221+
*UMASK.get_or_init(|| unsafe {
222+
let umask = libc::umask(0o022);
223+
libc::umask(umask);
224+
umask
225+
}) as u32 // it is u16 on macos
226+
}
227+
210228
#[cfg(test)]
211229
mod test {
212230
use super::*;

tests/testsuite/bench.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,9 +313,8 @@ fn cargo_bench_failing_test() {
313313
[FINISHED] bench [optimized] target(s) in [..]
314314
[RUNNING] [..] (target/release/deps/foo-[..][EXE])",
315315
)
316-
.with_stdout_contains(
317-
"[..]thread '[..]' panicked at 'assertion failed: `(left == right)`[..]",
318-
)
316+
.with_stdout_contains("[..]thread '[..]' panicked at[..]")
317+
.with_stdout_contains("[..]assertion failed[..]")
319318
.with_stdout_contains("[..]left: `\"hello\"`[..]")
320319
.with_stdout_contains("[..]right: `\"nope\"`[..]")
321320
.with_stdout_contains("[..]src/main.rs:15[..]")

tests/testsuite/install.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1284,7 +1284,8 @@ fn reports_unsuccessful_subcommand_result() {
12841284
.run();
12851285
cargo_process("fail")
12861286
.with_status(101)
1287-
.with_stderr_contains("thread '[..]' panicked at 'explicit panic', [..]")
1287+
.with_stderr_contains("thread '[..]' panicked at [..]src/main.rs:1:[..]")
1288+
.with_stderr_contains("[..]explicit panic[..]")
12881289
.run();
12891290
}
12901291

tests/testsuite/registry.rs

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2546,7 +2546,7 @@ fn package_lock_inside_package_is_overwritten() {
25462546
.join("bar-0.0.1")
25472547
.join(".cargo-ok");
25482548

2549-
assert_eq!(ok.metadata().unwrap().len(), 2);
2549+
assert_eq!(ok.metadata().unwrap().len(), 7);
25502550
}
25512551

25522552
#[cargo_test]
@@ -2586,7 +2586,7 @@ fn package_lock_as_a_symlink_inside_package_is_overwritten() {
25862586
let librs = pkg_root.join("src/lib.rs");
25872587

25882588
// Is correctly overwritten and doesn't affect the file linked to
2589-
assert_eq!(ok.metadata().unwrap().len(), 2);
2589+
assert_eq!(ok.metadata().unwrap().len(), 7);
25902590
assert_eq!(fs::read_to_string(librs).unwrap(), "pub fn f() {}");
25912591
}
25922592

@@ -3135,7 +3135,7 @@ fn corrupted_ok_overwritten() {
31353135
fs::write(&ok, "").unwrap();
31363136
assert_eq!(fs::read_to_string(&ok).unwrap(), "");
31373137
p.cargo("fetch").with_stderr("").run();
3138-
assert_eq!(fs::read_to_string(&ok).unwrap(), "ok");
3138+
assert_eq!(fs::read_to_string(&ok).unwrap(), r#"{"v":1}"#);
31393139
}
31403140

31413141
#[cargo_test]
@@ -3403,3 +3403,129 @@ Caused by:
34033403
Please slow down
34043404
").run();
34053405
}
3406+
3407+
#[cfg(unix)]
3408+
#[cargo_test]
3409+
fn set_mask_during_unpacking() {
3410+
use std::os::unix::fs::MetadataExt;
3411+
3412+
Package::new("bar", "1.0.0")
3413+
.file_with_mode("example.sh", 0o777, "#!/bin/sh")
3414+
.file_with_mode("src/lib.rs", 0o666, "")
3415+
.publish();
3416+
3417+
let p = project()
3418+
.file(
3419+
"Cargo.toml",
3420+
r#"
3421+
[package]
3422+
name = "foo"
3423+
version = "0.1.0"
3424+
3425+
[dependencies]
3426+
bar = "1.0"
3427+
"#,
3428+
)
3429+
.file("src/lib.rs", "")
3430+
.build();
3431+
3432+
p.cargo("fetch")
3433+
.with_stderr(
3434+
"\
3435+
[UPDATING] `dummy-registry` index
3436+
[DOWNLOADING] crates ...
3437+
[DOWNLOADED] bar v1.0.0 (registry `dummy-registry`)
3438+
",
3439+
)
3440+
.run();
3441+
let src_file_path = |path: &str| {
3442+
glob::glob(
3443+
paths::home()
3444+
.join(".cargo/registry/src/*/bar-1.0.0/")
3445+
.join(path)
3446+
.to_str()
3447+
.unwrap(),
3448+
)
3449+
.unwrap()
3450+
.next()
3451+
.unwrap()
3452+
.unwrap()
3453+
};
3454+
3455+
let umask = cargo::util::get_umask();
3456+
let metadata = fs::metadata(src_file_path("src/lib.rs")).unwrap();
3457+
assert_eq!(metadata.mode() & 0o777, 0o666 & !umask);
3458+
let metadata = fs::metadata(src_file_path("example.sh")).unwrap();
3459+
assert_eq!(metadata.mode() & 0o777, 0o777 & !umask);
3460+
}
3461+
3462+
#[cargo_test]
3463+
fn unpack_again_when_cargo_ok_is_unrecognized() {
3464+
Package::new("bar", "1.0.0").publish();
3465+
3466+
let p = project()
3467+
.file(
3468+
"Cargo.toml",
3469+
r#"
3470+
[package]
3471+
name = "foo"
3472+
version = "0.1.0"
3473+
3474+
[dependencies]
3475+
bar = "1.0"
3476+
"#,
3477+
)
3478+
.file("src/lib.rs", "")
3479+
.build();
3480+
3481+
p.cargo("fetch")
3482+
.with_stderr(
3483+
"\
3484+
[UPDATING] `dummy-registry` index
3485+
[DOWNLOADING] crates ...
3486+
[DOWNLOADED] bar v1.0.0 (registry `dummy-registry`)
3487+
",
3488+
)
3489+
.run();
3490+
3491+
let src_file_path = |path: &str| {
3492+
glob::glob(
3493+
paths::home()
3494+
.join(".cargo/registry/src/*/bar-1.0.0/")
3495+
.join(path)
3496+
.to_str()
3497+
.unwrap(),
3498+
)
3499+
.unwrap()
3500+
.next()
3501+
.unwrap()
3502+
.unwrap()
3503+
};
3504+
3505+
// Change permissions to simulate the old behavior not respecting umask.
3506+
let lib_rs = src_file_path("src/lib.rs");
3507+
let cargo_ok = src_file_path(".cargo-ok");
3508+
let mut perms = fs::metadata(&lib_rs).unwrap().permissions();
3509+
assert!(!perms.readonly());
3510+
perms.set_readonly(true);
3511+
fs::set_permissions(&lib_rs, perms).unwrap();
3512+
let ok = fs::read_to_string(&cargo_ok).unwrap();
3513+
assert_eq!(&ok, r#"{"v":1}"#);
3514+
3515+
p.cargo("fetch").with_stderr("").run();
3516+
3517+
// Without changing `.cargo-ok`, a unpack won't be triggered.
3518+
let perms = fs::metadata(&lib_rs).unwrap().permissions();
3519+
assert!(perms.readonly());
3520+
3521+
// Write "ok" to simulate the old behavior and trigger the unpack again.
3522+
fs::write(&cargo_ok, "ok").unwrap();
3523+
3524+
p.cargo("fetch").with_stderr("").run();
3525+
3526+
// Permission has been restored and `.cargo-ok` is in the new format.
3527+
let perms = fs::metadata(lib_rs).unwrap().permissions();
3528+
assert!(!perms.readonly());
3529+
let ok = fs::read_to_string(&cargo_ok).unwrap();
3530+
assert_eq!(&ok, r#"{"v":1}"#);
3531+
}

tests/testsuite/test.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -387,8 +387,9 @@ test test_hello ... FAILED
387387
failures:
388388
389389
---- test_hello stdout ----
390-
[..]thread '[..]' panicked at 'assertion failed:[..]",
390+
[..]thread '[..]' panicked at [..]",
391391
)
392+
.with_stdout_contains("[..]assertion failed[..]")
392393
.with_stdout_contains("[..]`(left == right)`[..]")
393394
.with_stdout_contains("[..]left: `\"hello\"`,[..]")
394395
.with_stdout_contains("[..]right: `\"nope\"`[..]")
@@ -437,10 +438,10 @@ test test_hello ... FAILED
437438
failures:
438439
439440
---- test_hello stdout ----
440-
[..]thread '[..]' panicked at 'assertion failed: false', \
441-
tests/footest.rs:1[..]
441+
[..]thread '[..]' panicked at [..]tests/footest.rs:1:[..]
442442
",
443443
)
444+
.with_stdout_contains("[..]assertion failed[..]")
444445
.with_stdout_contains(
445446
"\
446447
failures:
@@ -473,10 +474,10 @@ test test_hello ... FAILED
473474
failures:
474475
475476
---- test_hello stdout ----
476-
[..]thread '[..]' panicked at 'assertion failed: false', \
477-
src/lib.rs:1[..]
477+
[..]thread '[..]' panicked at [..]src/lib.rs:1:[..]
478478
",
479479
)
480+
.with_stdout_contains("[..]assertion failed[..]")
480481
.with_stdout_contains(
481482
"\
482483
failures:

tests/testsuite/vendor.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,10 +1051,11 @@ fn vendor_preserves_permissions() {
10511051

10521052
p.cargo("vendor --respect-source-config").run();
10531053

1054+
let umask = cargo::util::get_umask();
10541055
let metadata = fs::metadata(p.root().join("vendor/bar/src/lib.rs")).unwrap();
1055-
assert_eq!(metadata.mode() & 0o777, 0o644);
1056+
assert_eq!(metadata.mode() & 0o777, 0o644 & !umask);
10561057
let metadata = fs::metadata(p.root().join("vendor/bar/example.sh")).unwrap();
1057-
assert_eq!(metadata.mode() & 0o777, 0o755);
1058+
assert_eq!(metadata.mode() & 0o777, 0o755 & !umask);
10581059
}
10591060

10601061
#[cargo_test]

0 commit comments

Comments
 (0)