Skip to content

Commit aa09a98

Browse files
authored
Merge pull request #6595 from nerdroychan/install_copy_permission
install: create destination file with safer modes before copy
2 parents d6d6d02 + a5867bd commit aa09a98

File tree

2 files changed

+80
-14
lines changed

2 files changed

+80
-14
lines changed

src/uu/install/src/install.rs

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ use filetime::{set_file_times, FileTime};
1313
use std::error::Error;
1414
use std::fmt::{Debug, Display};
1515
use std::fs;
16+
#[cfg(not(unix))]
1617
use std::fs::File;
1718
use std::os::unix::fs::MetadataExt;
1819
#[cfg(unix)]
20+
use std::os::unix::fs::OpenOptionsExt;
21+
#[cfg(unix)]
1922
use std::os::unix::prelude::OsStrExt;
2023
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
2124
use std::process;
@@ -750,27 +753,52 @@ fn perform_backup(to: &Path, b: &Behavior) -> UResult<Option<PathBuf>> {
750753
fn copy_file(from: &Path, to: &Path) -> UResult<()> {
751754
// fs::copy fails if destination is a invalid symlink.
752755
// so lets just remove all existing files at destination before copy.
753-
if let Err(e) = fs::remove_file(to) {
754-
if e.kind() != std::io::ErrorKind::NotFound {
755-
show_error!(
756-
"Failed to remove existing file {}. Error: {:?}",
757-
to.display(),
758-
e
759-
);
756+
let remove_destination = || {
757+
if let Err(e) = fs::remove_file(to) {
758+
if e.kind() != std::io::ErrorKind::NotFound {
759+
show_error!(
760+
"Failed to remove existing file {}. Error: {:?}",
761+
to.display(),
762+
e
763+
);
764+
}
760765
}
766+
};
767+
remove_destination();
768+
769+
// create the destination file first. Using safer mode on unix to avoid
770+
// potential unsafe mode between time-of-creation and time-of-chmod.
771+
#[cfg(unix)]
772+
let creation = fs::OpenOptions::new()
773+
.write(true)
774+
.create_new(true)
775+
.mode(0o600)
776+
.open(to);
777+
#[cfg(not(unix))]
778+
let creation = File::create(to);
779+
780+
if let Err(e) = creation {
781+
show_error!(
782+
"Failed to create destination file {}. Error: {:?}",
783+
to.display(),
784+
e
785+
);
786+
return Err(InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), e).into());
761787
}
762788

763-
if from.as_os_str() == "/dev/null" {
764-
/* workaround a limitation of fs::copy
765-
* https://github.com/rust-lang/rust/issues/79390
766-
*/
767-
if let Err(err) = File::create(to) {
789+
// drop the file to close the fd of creation
790+
drop(creation);
791+
792+
/* workaround a limitation of fs::copy: skip copy if source is /dev/null
793+
* https://github.com/rust-lang/rust/issues/79390
794+
*/
795+
if from.as_os_str() != "/dev/null" {
796+
if let Err(err) = fs::copy(from, to) {
797+
remove_destination();
768798
return Err(
769799
InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into(),
770800
);
771801
}
772-
} else if let Err(err) = fs::copy(from, to) {
773-
return Err(InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into());
774802
}
775803
Ok(())
776804
}

tests/by-util/test_install.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1717,3 +1717,41 @@ fn test_install_root_combined() {
17171717
run_and_check(&["-Cv", "c", "d"], "d", 0, 0);
17181718
run_and_check(&["-Cv", "c", "d"], "d", 0, 0);
17191719
}
1720+
1721+
#[cfg(all(unix, feature = "chmod"))]
1722+
#[test]
1723+
fn test_install_copy_failures() {
1724+
let scene = TestScenario::new(util_name!());
1725+
1726+
let at = &scene.fixtures;
1727+
1728+
let file1 = "source_file";
1729+
let file2 = "target_file";
1730+
1731+
at.touch(file1);
1732+
scene.ccmd("chmod").arg("000").arg(file1).succeeds();
1733+
1734+
// if source file is not readable, it will raise a permission error.
1735+
// since we create the file with mode 0600 before `fs::copy`, if the
1736+
// copy failed, the file should be removed.
1737+
scene
1738+
.ucmd()
1739+
.arg(file1)
1740+
.arg(file2)
1741+
.arg("--mode=400")
1742+
.fails()
1743+
.stderr_contains("permission denied");
1744+
assert!(!at.file_exists(file2));
1745+
1746+
// if source file is good to copy, it should succeed and set the
1747+
// destination file permissions accordingly
1748+
scene.ccmd("chmod").arg("644").arg(file1).succeeds();
1749+
scene
1750+
.ucmd()
1751+
.arg(file1)
1752+
.arg(file2)
1753+
.arg("--mode=400")
1754+
.succeeds();
1755+
assert!(at.file_exists(file2));
1756+
assert_eq!(0o100_400_u32, at.metadata(file2).permissions().mode());
1757+
}

0 commit comments

Comments
 (0)