Skip to content

Commit 7f1d04c

Browse files
committed
fix: clear cache for old .cargo-ok format
In 1.71, `.cargo-ok` changed to contain a JSON `{ v: 1 }` to indicate the version of it. A failure of parsing will result in a heavy-hammer approach that unpacks the `.crate` file again. This is in response to a security issue that the unpacking didn't respect umask on Unix systems.
1 parent f315a70 commit 7f1d04c

File tree

2 files changed

+147
-52
lines changed

2 files changed

+147
-52
lines changed

src/cargo/sources/registry/mod.rs

Lines changed: 73 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@
161161
use std::borrow::Cow;
162162
use std::collections::BTreeMap;
163163
use std::collections::HashSet;
164+
use std::fs;
164165
use std::fs::{File, OpenOptions};
165166
use std::io;
166167
use std::io::Read;
@@ -174,6 +175,7 @@ use flate2::read::GzDecoder;
174175
use log::debug;
175176
use semver::Version;
176177
use serde::Deserialize;
178+
use serde::Serialize;
177179
use tar::Archive;
178180

179181
use crate::core::dependency::{DepKind, Dependency};
@@ -201,6 +203,14 @@ const CHECKSUM_TEMPLATE: &str = "{sha256-checksum}";
201203
const MAX_UNPACK_SIZE: u64 = 512 * 1024 * 1024;
202204
const MAX_COMPRESSION_RATIO: usize = 20; // 20:1
203205

206+
/// The content inside `.cargo-ok`.
207+
/// See [`RegistrySource::unpack_package`] for more.
208+
#[derive(Deserialize, Serialize)]
209+
struct LockMetadata {
210+
/// The version of `.cargo-ok` file
211+
v: u32,
212+
}
213+
204214
/// A "source" for a local (see `local::LocalRegistry`) or remote (see
205215
/// `remote::RemoteRegistry`) registry.
206216
///
@@ -637,6 +647,50 @@ impl<'cfg> RegistrySource<'cfg> {
637647
/// compiled.
638648
///
639649
/// No action is taken if the source looks like it's already unpacked.
650+
///
651+
/// # History of interruption detection with `.cargo-ok` file
652+
///
653+
/// Cargo has always included a `.cargo-ok` file ([`PACKAGE_SOURCE_LOCK`])
654+
/// to detect if extraction was interrupted, but it was originally empty.
655+
///
656+
/// In 1.34, Cargo was changed to create the `.cargo-ok` file before it
657+
/// started extraction to implement fine-grained locking. After it was
658+
/// finished extracting, it wrote two bytes to indicate it was complete.
659+
/// It would use the length check to detect if it was possibly interrupted.
660+
///
661+
/// In 1.36, Cargo changed to not use fine-grained locking, and instead used
662+
/// a global lock. The use of `.cargo-ok` was no longer needed for locking
663+
/// purposes, but was kept to detect when extraction was interrupted.
664+
///
665+
/// In 1.49, Cargo changed to not create the `.cargo-ok` file before it
666+
/// started extraction to deal with `.crate` files that inexplicably had
667+
/// a `.cargo-ok` file in them.
668+
///
669+
/// In 1.64, Cargo changed to detect `.crate` files with `.cargo-ok` files
670+
/// in them in response to [CVE-2022-36113], which dealt with malicious
671+
/// `.crate` files making `.cargo-ok` a symlink causing cargo to write "ok"
672+
/// to any arbitrary file on the filesystem it has permission to.
673+
///
674+
/// In 1.71, `.cargo-ok` changed to contain a JSON `{ v: 1 }` to indicate
675+
/// the version of it. A failure of parsing will result in a heavy-hammer
676+
/// approach that unpacks the `.crate` file again. This is in response to a
677+
/// security issue that the unpacking didn't respect umask on Unix systems.
678+
///
679+
/// This is all a long-winded way of explaining the circumstances that might
680+
/// cause a directory to contain a `.cargo-ok` file that is empty or
681+
/// otherwise corrupted. Either this was extracted by a version of Rust
682+
/// before 1.34, in which case everything should be fine. However, an empty
683+
/// file created by versions 1.36 to 1.49 indicates that the extraction was
684+
/// interrupted and that we need to start again.
685+
///
686+
/// Another possibility is that the filesystem is simply corrupted, in
687+
/// which case deleting the directory might be the safe thing to do. That
688+
/// is probably unlikely, though.
689+
///
690+
/// To be safe, we deletes the directory and starts over again if an empty
691+
/// `.cargo-ok` file is found.
692+
///
693+
/// [CVE-2022-36113]: https://blog.rust-lang.org/2022/09/14/cargo-cves.html#arbitrary-file-corruption-cve-2022-36113
640694
fn unpack_package(&self, pkg: PackageId, tarball: &File) -> CargoResult<PathBuf> {
641695
// The `.cargo-ok` file is used to track if the source is already
642696
// unpacked.
@@ -645,55 +699,23 @@ impl<'cfg> RegistrySource<'cfg> {
645699
let path = dst.join(PACKAGE_SOURCE_LOCK);
646700
let path = self.config.assert_package_cache_locked(&path);
647701
let unpack_dir = path.parent().unwrap();
648-
match path.metadata() {
649-
Ok(meta) if meta.len() > 0 => return Ok(unpack_dir.to_path_buf()),
650-
Ok(_meta) => {
651-
// The `.cargo-ok` file is not in a state we expect it to be
652-
// (with two bytes containing "ok").
653-
//
654-
// Cargo has always included a `.cargo-ok` file to detect if
655-
// extraction was interrupted, but it was originally empty.
656-
//
657-
// In 1.34, Cargo was changed to create the `.cargo-ok` file
658-
// before it started extraction to implement fine-grained
659-
// locking. After it was finished extracting, it wrote two
660-
// bytes to indicate it was complete. It would use the length
661-
// check to detect if it was possibly interrupted.
662-
//
663-
// In 1.36, Cargo changed to not use fine-grained locking, and
664-
// instead used a global lock. The use of `.cargo-ok` was no
665-
// longer needed for locking purposes, but was kept to detect
666-
// when extraction was interrupted.
667-
//
668-
// In 1.49, Cargo changed to not create the `.cargo-ok` file
669-
// before it started extraction to deal with `.crate` files
670-
// that inexplicably had a `.cargo-ok` file in them.
671-
//
672-
// In 1.64, Cargo changed to detect `.crate` files with
673-
// `.cargo-ok` files in them in response to CVE-2022-36113,
674-
// which dealt with malicious `.crate` files making
675-
// `.cargo-ok` a symlink causing cargo to write "ok" to any
676-
// arbitrary file on the filesystem it has permission to.
677-
//
678-
// This is all a long-winded way of explaining the
679-
// circumstances that might cause a directory to contain a
680-
// `.cargo-ok` file that is empty or otherwise corrupted.
681-
// Either this was extracted by a version of Rust before 1.34,
682-
// in which case everything should be fine. However, an empty
683-
// file created by versions 1.36 to 1.49 indicates that the
684-
// extraction was interrupted and that we need to start again.
685-
//
686-
// Another possibility is that the filesystem is simply
687-
// corrupted, in which case deleting the directory might be
688-
// the safe thing to do. That is probably unlikely, though.
689-
//
690-
// To be safe, this deletes the directory and starts over
691-
// again.
692-
log::warn!("unexpected length of {path:?}, clearing cache");
693-
paths::remove_dir_all(dst.as_path_unlocked())?;
694-
}
702+
match fs::read_to_string(path) {
703+
Ok(ok) => match serde_json::from_str::<LockMetadata>(&ok) {
704+
Ok(lock_meta) if lock_meta.v == 1 => {
705+
return Ok(unpack_dir.to_path_buf());
706+
}
707+
_ => {
708+
if ok == "ok" {
709+
log::debug!("old `ok` content found, clearing cache");
710+
} else {
711+
log::warn!("unrecognized .cargo-ok content, clearing cache: {ok}");
712+
}
713+
// See comment of `unpack_package` about why removing all stuff.
714+
paths::remove_dir_all(dst.as_path_unlocked())?;
715+
}
716+
},
695717
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
696-
Err(e) => anyhow::bail!("failed to access package completion {path:?}: {e}"),
718+
Err(e) => anyhow::bail!("unable to read .cargo-ok file at {path:?}: {e}"),
697719
}
698720
dst.create_dir()?;
699721
let mut tar = {
@@ -757,7 +779,9 @@ impl<'cfg> RegistrySource<'cfg> {
757779
.write(true)
758780
.open(&path)
759781
.with_context(|| format!("failed to open `{}`", path.display()))?;
760-
write!(ok, "ok")?;
782+
783+
let lock_meta = LockMetadata { v: 1 };
784+
write!(ok, "{}", serde_json::to_string(&lock_meta).unwrap())?;
761785

762786
Ok(unpack_dir.to_path_buf())
763787
}

tests/testsuite/registry.rs

Lines changed: 74 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]
@@ -3458,3 +3458,74 @@ fn set_mask_during_unpacking() {
34583458
let metadata = fs::metadata(src_file_path("example.sh")).unwrap();
34593459
assert_eq!(metadata.mode() & 0o777, 0o777 & !umask);
34603460
}
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+
}

0 commit comments

Comments
 (0)