66
77use libc:: { SEEK_DATA , SEEK_HOLE } ;
88use std:: fs:: { File , OpenOptions } ;
9- use std:: io:: Read ;
9+ use std:: io:: { self , Read } ;
1010use std:: os:: unix:: fs:: FileExt ;
1111use std:: os:: unix:: fs:: MetadataExt ;
1212use std:: os:: unix:: fs:: { FileTypeExt , OpenOptionsExt } ;
1313use std:: os:: unix:: io:: AsRawFd ;
1414use std:: path:: Path ;
1515use uucore:: buf_copy;
16- use uucore:: mode:: get_umask;
1716use uucore:: translate;
1817
1918use crate :: {
@@ -50,25 +49,64 @@ enum CopyMethod {
5049 SparseCopyWithoutHole ,
5150}
5251
52+ /// Copy file contents with restrictive permissions initially to prevent race conditions.
53+ /// Creates the destination file with mode 0600 & !umask, copies the content,
54+ /// and lets the caller set the final permissions.
55+ fn copy_file_with_secure_permissions < P > ( source : P , dest : P ) -> io:: Result < u64 >
56+ where
57+ P : AsRef < Path > ,
58+ {
59+ let mut src_file = File :: open ( & source) ?;
60+ // Create destination with restrictive permissions initially (to prevent race conditions)
61+ // Mode 0o600 means read/write for owner only
62+ let mut dst_file = OpenOptions :: new ( )
63+ . create ( true )
64+ . write ( true )
65+ . truncate ( true )
66+ . mode ( 0o600 )
67+ . open ( & dest) ?;
68+ io:: copy ( & mut src_file, & mut dst_file)
69+ }
70+
5371/// Use the Linux `ioctl_ficlone` API to do a copy-on-write clone.
5472///
5573/// `fallback` controls what to do if the system call fails.
5674#[ cfg( any( target_os = "linux" , target_os = "android" ) ) ]
57- fn clone < P > ( source : P , dest : P , fallback : CloneFallback ) -> std :: io:: Result < ( ) >
75+ fn clone < P > ( source : P , dest : P , fallback : CloneFallback ) -> io:: Result < ( ) >
5876where
5977 P : AsRef < Path > ,
6078{
6179 let src_file = File :: open ( & source) ?;
62- let dst_file = File :: create ( & dest) ?;
80+ // Create destination with restrictive permissions initially (to prevent race conditions)
81+ // Mode 0o600 means read/write for owner only
82+ let dst_file = OpenOptions :: new ( )
83+ . create ( true )
84+ . write ( true )
85+ . truncate ( false )
86+ . mode ( 0o600 )
87+ . open ( & dest) ?;
6388 let src_fd = src_file. as_raw_fd ( ) ;
6489 let dst_fd = dst_file. as_raw_fd ( ) ;
6590 let result = unsafe { libc:: ioctl ( dst_fd, libc:: FICLONE , src_fd) } ;
6691 if result == 0 {
6792 return Ok ( ( ) ) ;
6893 }
94+ let err = io:: Error :: last_os_error ( ) ;
6995 match fallback {
70- CloneFallback :: Error => Err ( std:: io:: Error :: last_os_error ( ) ) ,
71- CloneFallback :: FSCopy => std:: fs:: copy ( source, dest) . map ( |_| ( ) ) ,
96+ CloneFallback :: Error => {
97+ // FICLONE fails with EINVAL if the destination is not empty.
98+ // Truncate and retry once before giving up.
99+ if err. raw_os_error ( ) == Some ( libc:: EINVAL ) {
100+ dst_file. set_len ( 0 ) ?;
101+ let retry = unsafe { libc:: ioctl ( dst_fd, libc:: FICLONE , src_fd) } ;
102+ if retry == 0 {
103+ return Ok ( ( ) ) ;
104+ }
105+ return Err ( io:: Error :: last_os_error ( ) ) ;
106+ }
107+ Err ( err)
108+ }
109+ CloneFallback :: FSCopy => copy_file_with_secure_permissions ( source, dest) . map ( |_| ( ) ) ,
72110 CloneFallback :: SparseCopy => sparse_copy ( source, dest) ,
73111 CloneFallback :: SparseCopyWithoutHole => sparse_copy_without_hole ( source, dest) ,
74112 }
78116/// This function returns a tuple of (bool, u64, u64) signifying a tuple of (whether a file has
79117/// data, its size, no of blocks it has allocated in disk)
80118#[ cfg( any( target_os = "linux" , target_os = "android" ) ) ]
81- fn check_for_data ( source : & Path ) -> Result < ( bool , u64 , u64 ) , std :: io:: Error > {
119+ fn check_for_data ( source : & Path ) -> Result < ( bool , u64 , u64 ) , io:: Error > {
82120 let mut src_file = File :: open ( source) ?;
83121 let metadata = src_file. metadata ( ) ?;
84122
@@ -98,14 +136,14 @@ fn check_for_data(source: &Path) -> Result<(bool, u64, u64), std::io::Error> {
98136 match result {
99137 -1 => Ok ( ( false , size, blocks) ) , // No data found or end of file
100138 _ if result >= 0 => Ok ( ( true , size, blocks) ) , // Data found
101- _ => Err ( std :: io:: Error :: last_os_error ( ) ) ,
139+ _ => Err ( io:: Error :: last_os_error ( ) ) ,
102140 }
103141}
104142
105143#[ cfg( any( target_os = "linux" , target_os = "android" ) ) ]
106144/// Checks whether a file is sparse i.e. it contains holes, uses the crude heuristic blocks < size / 512
107145/// Reference:`<https://doc.rust-lang.org/std/os/unix/fs/trait.MetadataExt.html#tymethod.blocks>`
108- fn check_sparse_detection ( source : & Path ) -> Result < bool , std :: io:: Error > {
146+ fn check_sparse_detection ( source : & Path ) -> Result < bool , io:: Error > {
109147 let src_file = File :: open ( source) ?;
110148 let metadata = src_file. metadata ( ) ?;
111149 let size = metadata. size ( ) ;
@@ -120,17 +158,24 @@ fn check_sparse_detection(source: &Path) -> Result<bool, std::io::Error> {
120158/// Optimized [`sparse_copy`] doesn't create holes for large sequences of zeros in non `sparse_files`
121159/// Used when `--sparse=auto`
122160#[ cfg( any( target_os = "linux" , target_os = "android" ) ) ]
123- fn sparse_copy_without_hole < P > ( source : P , dest : P ) -> std :: io:: Result < ( ) >
161+ fn sparse_copy_without_hole < P > ( source : P , dest : P ) -> io:: Result < ( ) >
124162where
125163 P : AsRef < Path > ,
126164{
127165 let src_file = File :: open ( source) ?;
128- let dst_file = File :: create ( dest) ?;
166+ // Create destination with restrictive permissions initially (to prevent race conditions)
167+ // Mode 0o600 means read/write for owner only
168+ let dst_file = OpenOptions :: new ( )
169+ . create ( true )
170+ . write ( true )
171+ . truncate ( true )
172+ . mode ( 0o600 )
173+ . open ( dest) ?;
129174 let dst_fd = dst_file. as_raw_fd ( ) ;
130175
131176 let size = src_file. metadata ( ) ?. size ( ) ;
132177 if unsafe { libc:: ftruncate ( dst_fd, size. try_into ( ) . unwrap ( ) ) } < 0 {
133- return Err ( std :: io:: Error :: last_os_error ( ) ) ;
178+ return Err ( io:: Error :: last_os_error ( ) ) ;
134179 }
135180 let src_fd = src_file. as_raw_fd ( ) ;
136181 let mut current_offset: isize = 0 ;
@@ -152,7 +197,7 @@ where
152197 break ;
153198 }
154199 if result <= -2 || hole <= -2 {
155- return Err ( std :: io:: Error :: last_os_error ( ) ) ;
200+ return Err ( io:: Error :: last_os_error ( ) ) ;
156201 }
157202 let len: isize = hole - current_offset;
158203 // Read and write data in chunks of `step` while reusing the same buffer
@@ -170,17 +215,24 @@ where
170215/// Perform a sparse copy from one file to another.
171216/// Creates a holes for large sequences of zeros in `non_sparse_files`, used for `--sparse=always`
172217#[ cfg( any( target_os = "linux" , target_os = "android" ) ) ]
173- fn sparse_copy < P > ( source : P , dest : P ) -> std :: io:: Result < ( ) >
218+ fn sparse_copy < P > ( source : P , dest : P ) -> io:: Result < ( ) >
174219where
175220 P : AsRef < Path > ,
176221{
177222 let mut src_file = File :: open ( source) ?;
178- let dst_file = File :: create ( dest) ?;
223+ // Create destination with restrictive permissions initially (to prevent race conditions)
224+ // Mode 0o600 means read/write for owner only
225+ let dst_file = OpenOptions :: new ( )
226+ . create ( true )
227+ . write ( true )
228+ . truncate ( true )
229+ . mode ( 0o600 )
230+ . open ( dest) ?;
179231 let dst_fd = dst_file. as_raw_fd ( ) ;
180232
181233 let size: usize = src_file. metadata ( ) ?. size ( ) . try_into ( ) . unwrap ( ) ;
182234 if unsafe { libc:: ftruncate ( dst_fd, size. try_into ( ) . unwrap ( ) ) } < 0 {
183- return Err ( std :: io:: Error :: last_os_error ( ) ) ;
235+ return Err ( io:: Error :: last_os_error ( ) ) ;
184236 }
185237
186238 let blksize = dst_file. metadata ( ) ?. blksize ( ) ;
@@ -214,7 +266,7 @@ fn check_dest_is_fifo(dest: &Path) -> bool {
214266}
215267
216268/// Copy the contents of a stream from `source` to `dest`.
217- fn copy_stream < P > ( source : P , dest : P ) -> std :: io:: Result < u64 >
269+ fn copy_stream < P > ( source : P , dest : P ) -> io:: Result < u64 >
218270where
219271 P : AsRef < Path > ,
220272{
@@ -237,21 +289,21 @@ where
237289 // TODO Update the code below to respect the case where
238290 // `--preserve=ownership` is not true.
239291 let mut src_file = File :: open ( & source) ?;
240- let mode = 0o622 & !get_umask ( ) ;
292+ // Create with restrictive permissions initially to prevent race conditions
293+ // Mode 0o600 means read/write for owner only
294+ // Use truncate(true) to ensure we overwrite existing content
241295 let mut dst_file = OpenOptions :: new ( )
242296 . create ( true )
243297 . write ( true )
244- . mode ( mode)
298+ . truncate ( true )
299+ . mode ( 0o600 )
245300 . open ( & dest) ?;
246301
247- let dest_is_stream = is_stream ( & dst_file. metadata ( ) ?) ;
248- if !dest_is_stream {
249- // `copy_stream` doesn't clear the dest file, if dest is not a stream, we should clear it manually.
250- dst_file. set_len ( 0 ) ?;
251- }
302+ let _dest_is_stream = is_stream ( & dst_file. metadata ( ) ?) ;
303+ // No need to set_len(0) since we're already using truncate(true)
252304
253305 let num_bytes_copied = buf_copy:: copy_stream ( & mut src_file, & mut dst_file)
254- . map_err ( |e| std :: io:: Error :: other ( format ! ( "{e}" ) ) ) ?;
306+ . map_err ( |e| io:: Error :: other ( format ! ( "{e}" ) ) ) ?;
255307
256308 Ok ( num_bytes_copied)
257309}
@@ -287,7 +339,9 @@ pub(crate) fn copy_on_write(
287339 }
288340
289341 match copy_method {
290- CopyMethod :: FSCopy => std:: fs:: copy ( source, dest) . map ( |_| ( ) ) ,
342+ CopyMethod :: FSCopy => {
343+ copy_file_with_secure_permissions ( source, dest) . map ( |_| ( ) )
344+ }
291345 _ => sparse_copy ( source, dest) ,
292346 }
293347 }
@@ -303,7 +357,7 @@ pub(crate) fn copy_on_write(
303357 if let Ok ( debug) = result {
304358 copy_debug = debug;
305359 }
306- std :: fs :: copy ( source, dest) . map ( |_| ( ) )
360+ copy_file_with_secure_permissions ( source, dest) . map ( |_| ( ) )
307361 }
308362 }
309363 ( ReflinkMode :: Never , SparseMode :: Auto ) => {
@@ -322,7 +376,7 @@ pub(crate) fn copy_on_write(
322376
323377 match copy_method {
324378 CopyMethod :: SparseCopyWithoutHole => sparse_copy_without_hole ( source, dest) ,
325- _ => std :: fs :: copy ( source, dest) . map ( |_| ( ) ) ,
379+ _ => copy_file_with_secure_permissions ( source, dest) . map ( |_| ( ) ) ,
326380 }
327381 }
328382 }
@@ -401,7 +455,7 @@ pub(crate) fn copy_on_write(
401455fn handle_reflink_auto_sparse_always (
402456 source : & Path ,
403457 dest : & Path ,
404- ) -> Result < ( CopyDebug , CopyMethod ) , std :: io:: Error > {
458+ ) -> Result < ( CopyDebug , CopyMethod ) , io:: Error > {
405459 let mut copy_debug = CopyDebug {
406460 offload : OffloadReflinkDebug :: Unknown ,
407461 reflink : OffloadReflinkDebug :: Unsupported ,
@@ -438,7 +492,7 @@ fn handle_reflink_auto_sparse_always(
438492
439493/// Handles debug results when flags are "--reflink=auto" and "--sparse=auto" and specifies what
440494/// type of copy should be used
441- fn handle_reflink_never_sparse_never ( source : & Path ) -> Result < CopyDebug , std :: io:: Error > {
495+ fn handle_reflink_never_sparse_never ( source : & Path ) -> Result < CopyDebug , io:: Error > {
442496 let mut copy_debug = CopyDebug {
443497 offload : OffloadReflinkDebug :: Unknown ,
444498 reflink : OffloadReflinkDebug :: No ,
@@ -459,7 +513,7 @@ fn handle_reflink_never_sparse_never(source: &Path) -> Result<CopyDebug, std::io
459513
460514/// Handles debug results when flags are "--reflink=auto" and "--sparse=never", files will be copied
461515/// through cloning them with fallback switching to [`std::fs::copy`]
462- fn handle_reflink_auto_sparse_never ( source : & Path ) -> Result < CopyDebug , std :: io:: Error > {
516+ fn handle_reflink_auto_sparse_never ( source : & Path ) -> Result < CopyDebug , io:: Error > {
463517 let mut copy_debug = CopyDebug {
464518 offload : OffloadReflinkDebug :: Unknown ,
465519 reflink : OffloadReflinkDebug :: No ,
@@ -484,7 +538,7 @@ fn handle_reflink_auto_sparse_never(source: &Path) -> Result<CopyDebug, std::io:
484538fn handle_reflink_auto_sparse_auto (
485539 source : & Path ,
486540 dest : & Path ,
487- ) -> Result < ( CopyDebug , CopyMethod ) , std :: io:: Error > {
541+ ) -> Result < ( CopyDebug , CopyMethod ) , io:: Error > {
488542 let mut copy_debug = CopyDebug {
489543 offload : OffloadReflinkDebug :: Unknown ,
490544 reflink : OffloadReflinkDebug :: Unsupported ,
@@ -527,7 +581,7 @@ fn handle_reflink_auto_sparse_auto(
527581fn handle_reflink_never_sparse_auto (
528582 source : & Path ,
529583 dest : & Path ,
530- ) -> Result < ( CopyDebug , CopyMethod ) , std :: io:: Error > {
584+ ) -> Result < ( CopyDebug , CopyMethod ) , io:: Error > {
531585 let mut copy_debug = CopyDebug {
532586 offload : OffloadReflinkDebug :: Unknown ,
533587 reflink : OffloadReflinkDebug :: No ,
@@ -563,7 +617,7 @@ fn handle_reflink_never_sparse_auto(
563617fn handle_reflink_never_sparse_always (
564618 source : & Path ,
565619 dest : & Path ,
566- ) -> Result < ( CopyDebug , CopyMethod ) , std :: io:: Error > {
620+ ) -> Result < ( CopyDebug , CopyMethod ) , io:: Error > {
567621 let mut copy_debug = CopyDebug {
568622 offload : OffloadReflinkDebug :: Unknown ,
569623 reflink : OffloadReflinkDebug :: No ,
0 commit comments