161161use std:: borrow:: Cow ;
162162use std:: collections:: BTreeMap ;
163163use std:: collections:: HashSet ;
164+ use std:: fs;
164165use std:: fs:: { File , OpenOptions } ;
165166use std:: io;
166167use std:: io:: Read ;
@@ -174,6 +175,7 @@ use flate2::read::GzDecoder;
174175use log:: debug;
175176use semver:: Version ;
176177use serde:: Deserialize ;
178+ use serde:: Serialize ;
177179use tar:: Archive ;
178180
179181use crate :: core:: dependency:: { DepKind , Dependency } ;
@@ -201,6 +203,14 @@ const CHECKSUM_TEMPLATE: &str = "{sha256-checksum}";
201203const MAX_UNPACK_SIZE : u64 = 512 * 1024 * 1024 ;
202204const 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 }
0 commit comments