161
161
use std:: borrow:: Cow ;
162
162
use std:: collections:: BTreeMap ;
163
163
use std:: collections:: HashSet ;
164
+ use std:: fs;
164
165
use std:: fs:: { File , OpenOptions } ;
165
166
use std:: io;
166
167
use std:: io:: Read ;
@@ -174,6 +175,7 @@ use flate2::read::GzDecoder;
174
175
use log:: debug;
175
176
use semver:: Version ;
176
177
use serde:: Deserialize ;
178
+ use serde:: Serialize ;
177
179
use tar:: Archive ;
178
180
179
181
use crate :: core:: dependency:: { DepKind , Dependency } ;
@@ -201,6 +203,14 @@ const CHECKSUM_TEMPLATE: &str = "{sha256-checksum}";
201
203
const MAX_UNPACK_SIZE : u64 = 512 * 1024 * 1024 ;
202
204
const MAX_COMPRESSION_RATIO : usize = 20 ; // 20:1
203
205
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
+
204
214
/// A "source" for a local (see `local::LocalRegistry`) or remote (see
205
215
/// `remote::RemoteRegistry`) registry.
206
216
///
@@ -637,6 +647,50 @@ impl<'cfg> RegistrySource<'cfg> {
637
647
/// compiled.
638
648
///
639
649
/// 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
640
694
fn unpack_package ( & self , pkg : PackageId , tarball : & File ) -> CargoResult < PathBuf > {
641
695
// The `.cargo-ok` file is used to track if the source is already
642
696
// unpacked.
@@ -645,55 +699,23 @@ impl<'cfg> RegistrySource<'cfg> {
645
699
let path = dst. join ( PACKAGE_SOURCE_LOCK ) ;
646
700
let path = self . config . assert_package_cache_locked ( & path) ;
647
701
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
+ } ,
695
717
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}" ) ,
697
719
}
698
720
dst. create_dir ( ) ?;
699
721
let mut tar = {
@@ -757,7 +779,9 @@ impl<'cfg> RegistrySource<'cfg> {
757
779
. write ( true )
758
780
. open ( & path)
759
781
. 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( ) ) ?;
761
785
762
786
Ok ( unpack_dir. to_path_buf ( ) )
763
787
}
0 commit comments