Re-enable compatibility with readonly CARGO_HOME

Previously Cargo would attempt to work as much as possible with a
previously filled out CARGO_HOME, even if it was mounted as read-only.
In #6880 this was regressed as a few global locks and files were always
attempted to be opened in writable mode.

This commit fixes these issues by correcting two locations:

* First the global package cache lock has error handling to allow
  acquiring the lock in read-only mode inaddition to read/write mode. If
  the read/write mode failed due to an error that looks like a readonly
  filesystem then we assume everything in the package cache is readonly
  and we switch to just acquiring any lock, this time a shared readonly
  one. We in theory aren't actually doing any synchronization at that
  point since it's all readonly anyway.

* Next when unpacking package we're careful to issue a `stat` call
  before opening a file in writable mode. This way our preexisting guard
  to return early if a package is unpacked will succeed before we open
  anything in writable mode.

Closes #6928
This commit is contained in:
Alex Crichton 2019-05-14 07:30:10 -07:00
parent fd3d06b3c7
commit 5d9383ed76
4 changed files with 117 additions and 12 deletions

View File

@ -449,15 +449,16 @@ impl<'cfg> RegistrySource<'cfg> {
let path = dst.join(PACKAGE_SOURCE_LOCK); let path = dst.join(PACKAGE_SOURCE_LOCK);
let path = self.config.assert_package_cache_locked(&path); let path = self.config.assert_package_cache_locked(&path);
let unpack_dir = path.parent().unwrap(); let unpack_dir = path.parent().unwrap();
if let Ok(meta) = path.metadata() {
if meta.len() > 0 {
return Ok(unpack_dir.to_path_buf());
}
}
let mut ok = OpenOptions::new() let mut ok = OpenOptions::new()
.create(true) .create(true)
.read(true) .read(true)
.write(true) .write(true)
.open(&path)?; .open(&path)?;
let meta = ok.metadata()?;
if meta.len() > 0 {
return Ok(unpack_dir.to_path_buf());
}
let gz = GzDecoder::new(tarball); let gz = GzDecoder::new(tarball);
let mut tar = Archive::new(gz); let mut tar = Archive::new(gz);

View File

@ -6,7 +6,7 @@ use std::env;
use std::fmt; use std::fmt;
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::prelude::*; use std::io::prelude::*;
use std::io::SeekFrom; use std::io::{self, SeekFrom};
use std::mem; use std::mem;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
@ -860,21 +860,71 @@ impl Config {
return ret; return ret;
} }
/// Acquires an exclusive lock on the global "package cache"
///
/// This lock is global per-process and can be acquired recursively. An RAII
/// structure is returned to release the lock, and if this process
/// abnormally terminates the lock is also released.
pub fn acquire_package_cache_lock<'a>(&'a self) -> CargoResult<PackageCacheLock<'a>> { pub fn acquire_package_cache_lock<'a>(&'a self) -> CargoResult<PackageCacheLock<'a>> {
let mut slot = self.package_cache_lock.borrow_mut(); let mut slot = self.package_cache_lock.borrow_mut();
match *slot { match *slot {
// We've already acquired the lock in this process, so simply bump
// the count and continue.
Some((_, ref mut cnt)) => { Some((_, ref mut cnt)) => {
*cnt += 1; *cnt += 1;
} }
None => { None => {
let lock = self let path = ".package-cache";
.home_path let desc = "package cache lock";
.open_rw(".package-cache", self, "package cache lock")
.chain_err(|| "failed to acquire package cache lock")?; // First, attempt to open an exclusive lock which is in general
*slot = Some((lock, 1)); // the purpose of this lock!
//
// If that fails because of a readonly filesystem, though, then
// we don't want to fail because it's a readonly filesystem. In
// some situations Cargo is prepared to have a readonly
// filesystem yet still work since it's all been pre-downloaded
// and/or pre-unpacked. In these situations we want to keep
// Cargo running if possible, so if it's a readonly filesystem
// switch to a shared lock which should hopefully succeed so we
// can continue.
//
// Note that the package cache lock protects files in the same
// directory, so if it's a readonly filesystem we assume that
// the entire package cache is readonly, so we're just acquiring
// something to prove it works, we're not actually doing any
// synchronization at that point.
match self.home_path.open_rw(path, self, desc) {
Ok(lock) => *slot = Some((lock, 1)),
Err(e) => {
if maybe_readonly(&e) {
if let Ok(lock) = self.home_path.open_ro(path, self, desc) {
*slot = Some((lock, 1));
return Ok(PackageCacheLock(self));
}
}
Err(e).chain_err(|| "failed to acquire package cache lock")?;
}
}
} }
} }
Ok(PackageCacheLock(self)) return Ok(PackageCacheLock(self));
fn maybe_readonly(err: &failure::Error) -> bool {
err.iter_chain().any(|err| {
if let Some(io) = err.downcast_ref::<io::Error>() {
if io.kind() == io::ErrorKind::PermissionDenied {
return true;
}
#[cfg(unix)]
return io.raw_os_error() == Some(libc::EROFS);
}
false
})
}
} }
pub fn release_package_cache_lock(&self) {} pub fn release_package_cache_lock(&self) {}

View File

@ -1,5 +1,6 @@
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::prelude::*; use std::io::prelude::*;
use std::path::Path;
use crate::support::cargo_process; use crate::support::cargo_process;
use crate::support::git; use crate::support::git;
@ -1979,3 +1980,48 @@ fn ignore_invalid_json_lines() {
p.cargo("build").run(); p.cargo("build").run();
} }
#[test]
fn readonly_registry_still_works() {
Package::new("foo", "0.1.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
[project]
name = "a"
version = "0.5.0"
authors = []
[dependencies]
foo = '0.1.0'
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("generate-lockfile").run();
p.cargo("fetch --locked").run();
chmod_readonly(&paths::home());
p.cargo("build").run();
fn chmod_readonly(path: &Path) {
for entry in t!(path.read_dir()) {
let entry = t!(entry);
let path = entry.path();
if t!(entry.file_type()).is_dir() {
chmod_readonly(&path);
} else {
set_readonly(&path);
}
}
set_readonly(path);
}
fn set_readonly(path: &Path) {
let mut perms = t!(path.metadata()).permissions();
perms.set_readonly(true);
t!(fs::set_permissions(path, perms));
}
}

View File

@ -158,10 +158,18 @@ where
{ {
match f(path) { match f(path) {
Ok(()) => {} Ok(()) => {}
Err(ref e) if cfg!(windows) && e.kind() == ErrorKind::PermissionDenied => { Err(ref e) if e.kind() == ErrorKind::PermissionDenied => {
let mut p = t!(path.metadata()).permissions(); let mut p = t!(path.metadata()).permissions();
p.set_readonly(false); p.set_readonly(false);
t!(fs::set_permissions(path, p)); t!(fs::set_permissions(path, p));
// Unix also requires the parent to not be readonly for example when
// removing files
let parent = path.parent().unwrap();
let mut p = t!(parent.metadata()).permissions();
p.set_readonly(false);
t!(fs::set_permissions(parent, p));
f(path).unwrap_or_else(|e| { f(path).unwrap_or_else(|e| {
panic!("failed to {} {}: {}", desc, path.display(), e); panic!("failed to {} {}: {}", desc, path.display(), e);
}) })