Skip to content

Commit b3cc502

Browse files
committed
install: create destination file with safer modes before copy
1 parent 389ccbc commit b3cc502

File tree

2 files changed

+132
-16
lines changed

2 files changed

+132
-16
lines changed

src/uu/install/src/install.rs

Lines changed: 68 additions & 16 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;
@@ -748,29 +751,78 @@ fn perform_backup(to: &Path, b: &Behavior) -> UResult<Option<PathBuf>> {
748751
/// Returns an empty Result or an error in case of failure.
749752
///
750753
fn copy_file(from: &Path, to: &Path) -> UResult<()> {
751-
// fs::copy fails if destination is a invalid symlink.
752-
// 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-
);
754+
// Open the source file and get a file descriptor, making sure the source file exists.
755+
#[cfg(unix)]
756+
let src = fs::OpenOptions::new().read(true).open(from);
757+
#[cfg(not(unix))]
758+
let src = File::open(from);
759+
if let Err(e) = src {
760+
show_error!(
761+
"Failed to open source file {}. Error: {:?}",
762+
from.display(),
763+
e
764+
);
765+
return Err(InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), e).into());
766+
}
767+
let mut src = src.unwrap();
768+
769+
// If the source file is opened to be read, the copy should fail only when there is a problem
770+
// with the destination, so lets just remove all existing files at destination before copy.
771+
let remove_destination = || {
772+
if let Err(e) = fs::remove_file(to) {
773+
if e.kind() != std::io::ErrorKind::NotFound {
774+
show_error!(
775+
"Failed to remove existing file {}. Error: {:?}",
776+
to.display(),
777+
e
778+
);
779+
return Err(e);
780+
}
760781
}
782+
Ok(())
783+
};
784+
785+
// Errors out this case if the destination file cannot be created.
786+
if let Err(e) = remove_destination() {
787+
return Err(InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), e).into());
761788
}
762789

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) {
790+
// Create the destination file first. Using safer mode on unix to avoid
791+
// potential unsafe mode between time-of-creation and time-of-chmod.
792+
#[cfg(unix)]
793+
let dst = fs::OpenOptions::new()
794+
.write(true)
795+
.create_new(true)
796+
.mode(0o600)
797+
.open(to);
798+
#[cfg(not(unix))]
799+
let dst = File::create(to);
800+
801+
if let Err(e) = dst {
802+
show_error!(
803+
"Failed to create destination file {}. Error: {:?}",
804+
to.display(),
805+
e
806+
);
807+
return Err(InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), e).into());
808+
}
809+
let mut dst = dst.unwrap();
810+
811+
/* workaround a limitation of fs::copy: skip copy if source is /dev/null
812+
* https://github.com/rust-lang/rust/issues/79390
813+
*/
814+
if from.as_os_str() != "/dev/null" {
815+
// Use `std::io::copy` to avoid unsafe file modes here.
816+
if let Err(err) = std::io::copy(&mut src, &mut dst) {
817+
drop(src);
818+
drop(dst);
819+
// This removal should be successful unless changes happened after the
820+
// creation of the destination, so we just ignore it here.
821+
let _ = remove_destination();
768822
return Err(
769823
InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into(),
770824
);
771825
}
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());
774826
}
775827
Ok(())
776828
}

tests/by-util/test_install.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1717,3 +1717,67 @@ 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(unix)]
1722+
#[test]
1723+
fn test_install_only_when_source_exists() {
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(file2);
1732+
at.append(file2, "test");
1733+
1734+
// if source file does not exist, the install should fail and the
1735+
// destination file should not be removed, and content is not tampered
1736+
scene
1737+
.ucmd()
1738+
.arg(file1)
1739+
.arg(file2)
1740+
.arg("--mode=400")
1741+
.fails()
1742+
.stderr_contains("No such file or directory");
1743+
assert!(at.file_exists(file2));
1744+
assert_eq!(at.read(file2), "test");
1745+
}
1746+
1747+
#[cfg(all(unix, feature = "chmod"))]
1748+
#[test]
1749+
fn test_install_copy_failures() {
1750+
let scene = TestScenario::new(util_name!());
1751+
1752+
let at = &scene.fixtures;
1753+
1754+
let file1 = "source_file";
1755+
let file2 = "target_file";
1756+
1757+
at.touch(file1);
1758+
scene.ccmd("chmod").arg("000").arg(file1).succeeds();
1759+
1760+
// if source file is not readable, it will raise a permission error.
1761+
// since we create the file with mode 0600 before `fs::copy`, if the
1762+
// copy failed, the file should be removed.
1763+
scene
1764+
.ucmd()
1765+
.arg(file1)
1766+
.arg(file2)
1767+
.arg("--mode=400")
1768+
.fails()
1769+
.stderr_contains("permission denied");
1770+
assert!(!at.file_exists(file2));
1771+
1772+
// if source file is good to copy, it should succeed and set the
1773+
// destination file permissions accordingly
1774+
scene.ccmd("chmod").arg("644").arg(file1).succeeds();
1775+
scene
1776+
.ucmd()
1777+
.arg(file1)
1778+
.arg(file2)
1779+
.arg("--mode=400")
1780+
.succeeds();
1781+
assert!(at.file_exists(file2));
1782+
assert_eq!(0o100_400_u32, at.metadata(file2).permissions().mode());
1783+
}

0 commit comments

Comments
 (0)