Skip to content

Commit 81118c3

Browse files
committed
fs::copy() linux: handle sparse files and 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`. Not handling sparse files could fill up the users disk very quickly. Fixes: #26933 #37885 #58635
1 parent 0e25a68 commit 81118c3

File tree

1 file changed

+183
-53
lines changed

1 file changed

+183
-53
lines changed

src/libstd/sys/unix/fs.rs

+183-53
Original file line numberDiff line numberDiff line change
@@ -843,12 +843,16 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
843843
#[cfg(any(target_os = "linux", target_os = "android"))]
844844
pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
845845
use cmp;
846-
use fs::File;
846+
use io::{Read, Write};
847+
use os::unix::fs::{OpenOptionsExt, PermissionsExt};
848+
use fs::{File, OpenOptions};
847849
use sync::atomic::{AtomicBool, Ordering};
848-
849850
// Kernel prior to 4.5 don't have copy_file_range
850851
// We store the availability in a global to avoid unnecessary syscalls
851852
static HAS_COPY_FILE_RANGE: AtomicBool = AtomicBool::new(true);
853+
// Kernel prior to 2.2 don't have sendfile
854+
// We store the availability in a global to avoid unnecessary syscalls
855+
static HAS_SENDFILE: AtomicBool = AtomicBool::new(true);
852856

853857
unsafe fn copy_file_range(
854858
fd_in: libc::c_int,
@@ -869,67 +873,193 @@ pub fn copy(from: &Path, to: &Path) -> io::Result<u64> {
869873
)
870874
}
871875

872-
if !from.is_file() {
873-
return Err(Error::new(ErrorKind::InvalidInput,
874-
"the source path is not an existing regular file"))
875-
}
876-
877876
let mut reader = File::open(from)?;
878-
let mut writer = File::create(to)?;
879-
let (perm, len) = {
877+
878+
let (mode, len) = {
880879
let metadata = reader.metadata()?;
881-
(metadata.permissions(), metadata.size())
880+
if !metadata.is_file() {
881+
return Err(Error::new(
882+
ErrorKind::InvalidInput,
883+
"the source path is not an existing regular file",
884+
));
885+
}
886+
(metadata.permissions().mode(), metadata.len())
882887
};
888+
let bytes_to_copy: i64 = len as i64;
889+
890+
let mut writer = OpenOptions::new()
891+
// prevent world readable/writeable file in case of empty umask
892+
.mode(0o000)
893+
.write(true)
894+
.create(true)
895+
.truncate(true)
896+
.open(to)?;
897+
898+
let mut can_handle_sparse = true;
899+
900+
let fd_in = reader.as_raw_fd();
901+
let fd_out = writer.as_raw_fd();
902+
903+
let writer_metadata = writer.metadata()?;
904+
// prevent root from setting permissions on e.g. `/dev/null`
905+
// prevent users from setting permissions on e.g. `/dev/stdout` or a named pipe
906+
if writer_metadata.is_file() {
907+
// set the correct file mode
908+
cvt_r(|| unsafe { libc::fchmod(fd_out, mode) })?;
909+
match cvt_r(|| unsafe { ftruncate64(fd_out, bytes_to_copy) }) {
910+
Ok(_) => {}
911+
Err(err) => match err.raw_os_error() {
912+
Some(libc::EINVAL) => {
913+
can_handle_sparse = false;
914+
}
915+
_ => {
916+
return Err(err);
917+
}
918+
},
919+
}
920+
} else {
921+
can_handle_sparse = false;
922+
}
883923

884-
let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed);
885-
let mut written = 0u64;
886-
while written < len {
887-
let copy_result = if has_copy_file_range {
888-
let bytes_to_copy = cmp::min(len - written, usize::max_value() as u64) as usize;
889-
let copy_result = unsafe {
890-
// We actually don't have to adjust the offsets,
891-
// because copy_file_range adjusts the file offset automatically
892-
cvt(copy_file_range(reader.as_raw_fd(),
893-
ptr::null_mut(),
894-
writer.as_raw_fd(),
895-
ptr::null_mut(),
896-
bytes_to_copy,
897-
0)
898-
)
899-
};
900-
if let Err(ref copy_err) = copy_result {
901-
match copy_err.raw_os_error() {
924+
let mut use_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed);
925+
let mut use_sendfile = HAS_SENDFILE.load(Ordering::Relaxed);
926+
927+
let mut srcpos: i64 = 0;
928+
929+
let mut next_beg: libc::loff_t = if can_handle_sparse {
930+
let ret = unsafe { lseek64(fd_in, srcpos, libc::SEEK_DATA) };
931+
if ret == -1 {
932+
can_handle_sparse = false;
933+
0
934+
} else {
935+
ret
936+
}
937+
} else {
938+
0
939+
};
940+
941+
let mut next_end: libc::loff_t = if can_handle_sparse {
942+
let ret = unsafe { lseek64(fd_in, next_beg, libc::SEEK_HOLE) };
943+
if ret == -1 {
944+
can_handle_sparse = false;
945+
bytes_to_copy
946+
} else {
947+
ret
948+
}
949+
} else {
950+
bytes_to_copy
951+
};
952+
953+
let mut next_len = next_end - next_beg;
954+
955+
while srcpos < bytes_to_copy {
956+
if srcpos != 0 {
957+
if can_handle_sparse {
958+
next_beg = cvt(unsafe { lseek64(fd_in, srcpos, libc::SEEK_DATA) })?;
959+
next_end = cvt(unsafe { lseek64(fd_in, next_beg, libc::SEEK_HOLE) })?;
960+
961+
next_len = next_end - next_beg;
962+
} else {
963+
next_beg = srcpos;
964+
next_end = bytes_to_copy - srcpos;
965+
}
966+
}
967+
968+
if next_len <= 0 {
969+
srcpos = next_end;
970+
continue;
971+
}
972+
973+
let num = if use_copy_file_range {
974+
match cvt(unsafe {
975+
copy_file_range(
976+
fd_in,
977+
&mut next_beg,
978+
fd_out,
979+
&mut next_beg,
980+
next_len as usize,
981+
0,
982+
)
983+
}) {
984+
Ok(n) => n as isize,
985+
Err(err) => match err.raw_os_error() {
986+
// Try fallback if either:
987+
// - Kernel version is < 4.5 (ENOSYS)
988+
// - Files are mounted on different fs (EXDEV)
989+
// - copy_file_range is disallowed, for example by seccomp (EPERM)
902990
Some(libc::ENOSYS) | Some(libc::EPERM) => {
903991
HAS_COPY_FILE_RANGE.store(false, Ordering::Relaxed);
992+
use_copy_file_range = false;
993+
continue;
904994
}
905-
_ => {}
906-
}
995+
Some(libc::EXDEV) | Some(libc::EINVAL) => {
996+
use_copy_file_range = false;
997+
continue;
998+
}
999+
_ => {
1000+
return Err(err);
1001+
}
1002+
},
1003+
}
1004+
} else if use_sendfile {
1005+
if can_handle_sparse && next_beg != 0 {
1006+
cvt(unsafe { lseek64(fd_out, next_beg, libc::SEEK_SET) })?;
1007+
}
1008+
match cvt(unsafe { libc::sendfile(fd_out, fd_in, &mut next_beg, next_len as usize) }) {
1009+
Ok(n) => n,
1010+
Err(err) => match err.raw_os_error() {
1011+
// Try fallback if either:
1012+
// - Kernel version is < 2.2 (ENOSYS)
1013+
// - sendfile is disallowed, for example by seccomp (EPERM)
1014+
// - can't use sendfile on source or destination (EINVAL)
1015+
Some(libc::ENOSYS) | Some(libc::EPERM) => {
1016+
HAS_SENDFILE.store(false, Ordering::Relaxed);
1017+
use_sendfile = false;
1018+
continue;
1019+
}
1020+
Some(libc::EINVAL) => {
1021+
use_sendfile = false;
1022+
continue;
1023+
}
1024+
_ => {
1025+
return Err(err);
1026+
}
1027+
},
9071028
}
908-
copy_result
9091029
} else {
910-
Err(io::Error::from_raw_os_error(libc::ENOSYS))
911-
};
912-
match copy_result {
913-
Ok(ret) => written += ret as u64,
914-
Err(err) => {
915-
match err.raw_os_error() {
916-
Some(os_err) if os_err == libc::ENOSYS
917-
|| os_err == libc::EXDEV
918-
|| os_err == libc::EPERM => {
919-
// Try fallback io::copy if either:
920-
// - Kernel version is < 4.5 (ENOSYS)
921-
// - Files are mounted on different fs (EXDEV)
922-
// - copy_file_range is disallowed, for example by seccomp (EPERM)
923-
assert_eq!(written, 0);
924-
let ret = io::copy(&mut reader, &mut writer)?;
925-
writer.set_permissions(perm)?;
926-
return Ok(ret)
927-
},
928-
_ => return Err(err),
1030+
if can_handle_sparse {
1031+
cvt(unsafe { lseek64(fd_in, next_beg, libc::SEEK_SET) })?;
1032+
if next_beg != 0 {
1033+
cvt(unsafe { lseek64(fd_out, next_beg, libc::SEEK_SET) })?;
9291034
}
9301035
}
931-
}
1036+
const DEFAULT_BUF_SIZE: usize = ::sys_common::io::DEFAULT_BUF_SIZE;
1037+
let mut buf = unsafe {
1038+
let buf: [u8; DEFAULT_BUF_SIZE] = mem::uninitialized();
1039+
buf
1040+
};
1041+
1042+
let mut written = 0;
1043+
while next_len > 0 {
1044+
let slice_len = cmp::min(next_len as usize, DEFAULT_BUF_SIZE);
1045+
let len = match reader.read(&mut buf[..slice_len]) {
1046+
Ok(0) => {
1047+
// break early out of copy loop, because nothing is to be read anymore
1048+
srcpos += written;
1049+
break;
1050+
}
1051+
Ok(len) => len,
1052+
Err(ref err) if err.kind() == io::ErrorKind::Interrupted => continue,
1053+
Err(err) => return Err(err),
1054+
};
1055+
writer.write_all(&buf[..len])?;
1056+
written += len as i64;
1057+
next_len -= len as i64;
1058+
}
1059+
written as isize
1060+
};
1061+
srcpos += num as i64;
9321062
}
933-
writer.set_permissions(perm)?;
934-
Ok(written)
1063+
1064+
Ok(srcpos as u64)
9351065
}

0 commit comments

Comments
 (0)