Skip to content

Commit cf8347b

Browse files
committed
fs::copy() set file mode early
A convenience method like fs::copy() should try to prevent pitfalls a normal user doesn't think about. In case of an empty umask, setting the file mode early prevents temporarily world readable or even writeable files, because the default mode is 0o666. In case the target is a named pipe or special device node, setting the file mode can lead to unwanted side effects, like setting permissons on `/dev/stdout` or for root setting permissions on `/dev/null`. copy_file_range() returns EINVAL, if the destination is a FIFO/pipe or a device like "/dev/null", so fallback to io::copy, too. Use `fcopyfile` on MacOS instead of `copyfile`. Fixes: rust-lang#26933 Fixed: rust-lang#37885
1 parent 7cf074a commit cf8347b

File tree

2 files changed

+83
-59
lines changed

2 files changed

+83
-59
lines changed

src/libstd/sys/unix/fs.rs

+74-55
Original file line numberDiff line numberDiff line change
@@ -827,30 +827,54 @@ pub fn canonicalize(p: &Path) -> io::Result<PathBuf> {
827827
Ok(PathBuf::from(OsString::from_vec(buf)))
828828
}
829829

830+
fn open_and_set_permissions(
831+
from: &Path,
832+
to: &Path,
833+
) -> io::Result<(crate::fs::File, crate::fs::File, u64, crate::fs::Metadata)> {
834+
use crate::fs::{File, OpenOptions};
835+
use crate::os::unix::fs::{OpenOptionsExt, PermissionsExt};
836+
837+
let reader = File::open(from)?;
838+
let (perm, len) = {
839+
let metadata = reader.metadata()?;
840+
if !metadata.is_file() {
841+
return Err(Error::new(
842+
ErrorKind::InvalidInput,
843+
"the source path is not an existing regular file",
844+
));
845+
}
846+
(metadata.permissions(), metadata.len())
847+
};
848+
let writer = OpenOptions::new()
849+
// create the file with the correct mode right away
850+
.mode(perm.mode())
851+
.write(true)
852+
.create(true)
853+
.truncate(true)
854+
.open(to)?;
855+
let writer_metadata = writer.metadata()?;
856+
if writer_metadata.is_file() {
857+
// Set the correct file permissions, in case the file already existed.
858+
// Don't set the permissions on already existing non-files like
859+
// pipes/FIFOs or device nodes.
860+
writer.set_permissions(perm)?;
861+
}
862+
Ok((reader, writer, len, writer_metadata))
863+
}
864+
830865
#[cfg(not(any(target_os = "linux",
831866
target_os = "android",
832867
target_os = "macos",
833868
target_os = "ios")))]
834869
pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
835-
use crate::fs::File;
836-
if !from.is_file() {
837-
return Err(Error::new(ErrorKind::InvalidInput,
838-
"the source path is not an existing regular file"))
839-
}
870+
let (mut reader, mut writer, _, _) = open_and_set_permissions(from, to)?;
840871

841-
let mut reader = File::open(from)?;
842-
let mut writer = File::create(to)?;
843-
let perm = reader.metadata()?.permissions();
844-
845-
let ret = io::copy(&mut reader, &mut writer)?;
846-
writer.set_permissions(perm)?;
847-
Ok(ret)
872+
io::copy(&mut reader, &mut writer)
848873
}
849874

850875
#[cfg(any(target_os = "linux", target_os = "android"))]
851876
pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
852877
use crate::cmp;
853-
use crate::fs::File;
854878
use crate::sync::atomic::{AtomicBool, Ordering};
855879

856880
// Kernel prior to 4.5 don't have copy_file_range
@@ -876,17 +900,7 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
876900
)
877901
}
878902

879-
if !from.is_file() {
880-
return Err(Error::new(ErrorKind::InvalidInput,
881-
"the source path is not an existing regular file"))
882-
}
883-
884-
let mut reader = File::open(from)?;
885-
let mut writer = File::create(to)?;
886-
let (perm, len) = {
887-
let metadata = reader.metadata()?;
888-
(metadata.permissions(), metadata.size())
889-
};
903+
let (mut reader, mut writer, len, _) = open_and_set_permissions(from, to)?;
890904

891905
let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed);
892906
let mut written = 0u64;
@@ -896,13 +910,14 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
896910
let copy_result = unsafe {
897911
// We actually don't have to adjust the offsets,
898912
// because copy_file_range adjusts the file offset automatically
899-
cvt(copy_file_range(reader.as_raw_fd(),
900-
ptr::null_mut(),
901-
writer.as_raw_fd(),
902-
ptr::null_mut(),
903-
bytes_to_copy,
904-
0)
905-
)
913+
cvt(copy_file_range(
914+
reader.as_raw_fd(),
915+
ptr::null_mut(),
916+
writer.as_raw_fd(),
917+
ptr::null_mut(),
918+
bytes_to_copy,
919+
0,
920+
))
906921
};
907922
if let Err(ref copy_err) = copy_result {
908923
match copy_err.raw_os_error() {
@@ -920,24 +935,25 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
920935
Ok(ret) => written += ret as u64,
921936
Err(err) => {
922937
match err.raw_os_error() {
923-
Some(os_err) if os_err == libc::ENOSYS
924-
|| os_err == libc::EXDEV
925-
|| os_err == libc::EPERM => {
926-
// Try fallback io::copy if either:
927-
// - Kernel version is < 4.5 (ENOSYS)
928-
// - Files are mounted on different fs (EXDEV)
929-
// - copy_file_range is disallowed, for example by seccomp (EPERM)
930-
assert_eq!(written, 0);
931-
let ret = io::copy(&mut reader, &mut writer)?;
932-
writer.set_permissions(perm)?;
933-
return Ok(ret)
934-
},
938+
Some(os_err)
939+
if os_err == libc::ENOSYS
940+
|| os_err == libc::EXDEV
941+
|| os_err == libc::EINVAL
942+
|| os_err == libc::EPERM =>
943+
{
944+
// Try fallback io::copy if either:
945+
// - Kernel version is < 4.5 (ENOSYS)
946+
// - Files are mounted on different fs (EXDEV)
947+
// - copy_file_range is disallowed, for example by seccomp (EPERM)
948+
// - copy_file_range cannot be used with pipes or device nodes (EINVAL)
949+
assert_eq!(written, 0);
950+
return io::copy(&mut reader, &mut writer);
951+
}
935952
_ => return Err(err),
936953
}
937954
}
938955
}
939956
}
940-
writer.set_permissions(perm)?;
941957
Ok(written)
942958
}
943959

@@ -960,9 +976,9 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
960976
type copyfile_flags_t = u32;
961977

962978
extern "C" {
963-
fn copyfile(
964-
from: *const libc::c_char,
965-
to: *const libc::c_char,
979+
fn fcopyfile(
980+
from: libc::c_int,
981+
to: libc::c_int,
966982
state: copyfile_state_t,
967983
flags: copyfile_flags_t,
968984
) -> libc::c_int;
@@ -988,10 +1004,7 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
9881004
}
9891005
}
9901006

991-
if !from.is_file() {
992-
return Err(Error::new(ErrorKind::InvalidInput,
993-
"the source path is not an existing regular file"))
994-
}
1007+
let (reader, writer, _, writer_metadata) = open_and_set_permissions(from, to)?;
9951008

9961009
// We ensure that `FreeOnDrop` never contains a null pointer so it is
9971010
// always safe to call `copyfile_state_free`
@@ -1003,12 +1016,18 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
10031016
FreeOnDrop(state)
10041017
};
10051018

1019+
let flags = if writer_metadata.is_file() {
1020+
COPYFILE_ALL
1021+
} else {
1022+
COPYFILE_DATA
1023+
};
1024+
10061025
cvt(unsafe {
1007-
copyfile(
1008-
cstr(from)?.as_ptr(),
1009-
cstr(to)?.as_ptr(),
1026+
fcopyfile(
1027+
reader.as_raw_fd(),
1028+
writer.as_raw_fd(),
10101029
state.0,
1011-
COPYFILE_ALL,
1030+
flags,
10121031
)
10131032
})?;
10141033

src/test/run-pass/paths-containing-nul.rs

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![allow(deprecated)]
22
// ignore-cloudabi no files or I/O
33
// ignore-wasm32-bare no files or I/O
4+
// ignore-emscripten no files
45

56
use std::fs;
67
use std::io;
@@ -22,14 +23,18 @@ fn main() {
2223
assert_invalid_input("remove_file", fs::remove_file("\0"));
2324
assert_invalid_input("metadata", fs::metadata("\0"));
2425
assert_invalid_input("symlink_metadata", fs::symlink_metadata("\0"));
26+
27+
// If `dummy_file` does not exist, then we might get another unrelated error
28+
let dummy_file = std::env::current_exe().unwrap();
29+
2530
assert_invalid_input("rename1", fs::rename("\0", "a"));
26-
assert_invalid_input("rename2", fs::rename("a", "\0"));
31+
assert_invalid_input("rename2", fs::rename(&dummy_file, "\0"));
2732
assert_invalid_input("copy1", fs::copy("\0", "a"));
28-
assert_invalid_input("copy2", fs::copy("a", "\0"));
33+
assert_invalid_input("copy2", fs::copy(&dummy_file, "\0"));
2934
assert_invalid_input("hard_link1", fs::hard_link("\0", "a"));
30-
assert_invalid_input("hard_link2", fs::hard_link("a", "\0"));
35+
assert_invalid_input("hard_link2", fs::hard_link(&dummy_file, "\0"));
3136
assert_invalid_input("soft_link1", fs::soft_link("\0", "a"));
32-
assert_invalid_input("soft_link2", fs::soft_link("a", "\0"));
37+
assert_invalid_input("soft_link2", fs::soft_link(&dummy_file, "\0"));
3338
assert_invalid_input("read_link", fs::read_link("\0"));
3439
assert_invalid_input("canonicalize", fs::canonicalize("\0"));
3540
assert_invalid_input("create_dir", fs::create_dir("\0"));

0 commit comments

Comments
 (0)