diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 412b6668fe9..ae05858a684 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -65,6 +65,39 @@ use uucore::{format_usage, show_error}; const BUF_INIT_BYTE: u8 = 0xDD; +/// Helper function to allocate a page-aligned buffer on Linux/Android. +/// +/// O_DIRECT requires buffers to be aligned to page boundaries (typically 4096 bytes). +/// This function allocates a `Vec` with proper alignment to support O_DIRECT +/// without triggering EINVAL errors. +#[cfg(any(target_os = "linux", target_os = "android"))] +fn allocate_aligned_buffer(size: usize) -> Vec { + let alignment = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }; + // cspell:disable-next-line + let ptr = unsafe { libc::memalign(alignment, size) as *mut u8 }; + + assert!( + !ptr.is_null(), + "Failed to allocate aligned buffer of size {size}" + ); + + // Initialize with BUF_INIT_BYTE + unsafe { + std::ptr::write_bytes(ptr, BUF_INIT_BYTE, size); + } + + // Convert raw pointer to Vec + // cspell:disable-next-line + // SAFETY: We just allocated this memory with memalign, so it's valid + unsafe { Vec::from_raw_parts(ptr, 0, size) } +} + +/// Fallback for non-Linux platforms - use regular Vec allocation +#[cfg(not(any(target_os = "linux", target_os = "android")))] +fn allocate_aligned_buffer(size: usize) -> Vec { + vec![BUF_INIT_BYTE; size] +} + /// Final settings after parsing #[derive(Default)] struct Settings { @@ -693,8 +726,17 @@ fn is_sparse(buf: &[u8]) -> bool { /// Handle O_DIRECT write errors by temporarily removing the flag and retrying. /// This follows GNU dd behavior for partial block writes with O_DIRECT. +/// +/// With proper buffer alignment (page-aligned), O_DIRECT should only fail for +/// partial blocks (size < output_blocksize). This function only removes O_DIRECT +/// when necessary, matching GNU dd behavior and minimizing system call overhead. #[cfg(any(target_os = "linux", target_os = "android"))] -fn handle_o_direct_write(f: &mut File, buf: &[u8], original_error: io::Error) -> io::Result { +fn handle_o_direct_write( + f: &mut File, + buf: &[u8], + output_blocksize: usize, + original_error: io::Error, +) -> io::Result { use nix::fcntl::{FcntlArg, OFlag, fcntl}; // Get current flags using nix @@ -703,8 +745,10 @@ fn handle_o_direct_write(f: &mut File, buf: &[u8], original_error: io::Error) -> Err(_) => return Err(original_error), }; - // If O_DIRECT is set, try removing it temporarily - if oflags.contains(OFlag::O_DIRECT) { + // If O_DIRECT is set, only remove it for partial blocks (size < output_blocksize) + // This matches GNU dd behavior and minimizes system call overhead. + // With proper buffer alignment, full blocks should not fail with EINVAL. + if oflags.contains(OFlag::O_DIRECT) && buf.len() < output_blocksize { let flags_without_direct = oflags - OFlag::O_DIRECT; // Remove O_DIRECT flag using nix @@ -715,7 +759,7 @@ fn handle_o_direct_write(f: &mut File, buf: &[u8], original_error: io::Error) -> // Retry the write without O_DIRECT let write_result = f.write(buf); - // Restore O_DIRECT flag using nix (GNU doesn't restore it, but we'll be safer) + // Restore O_DIRECT flag using nix // Log any restoration errors without failing the operation if let Err(os_err) = fcntl(&mut *f, FcntlArg::F_SETFL(oflags)) { // Just log the error, don't fail the whole operation @@ -724,16 +768,18 @@ fn handle_o_direct_write(f: &mut File, buf: &[u8], original_error: io::Error) -> write_result } else { - // O_DIRECT wasn't set, return original error + // O_DIRECT wasn't set or this is a full block, return original error Err(original_error) } } /// Stub for non-Linux platforms - just return the original error. #[cfg(not(any(target_os = "linux", target_os = "android")))] +#[allow(dead_code)] fn handle_o_direct_write( _f: &mut File, _buf: &[u8], + _output_blocksize: usize, original_error: io::Error, ) -> io::Result { Err(original_error) @@ -750,21 +796,7 @@ impl Write for Dest { f.seek(SeekFrom::Current(seek_amt))?; Ok(buf.len()) } - Self::File(f, _) => { - // Try the write first - match f.write(buf) { - Ok(len) => Ok(len), - Err(e) - if e.kind() == io::ErrorKind::InvalidInput - && e.raw_os_error() == Some(libc::EINVAL) => - { - // This might be an O_DIRECT alignment issue. - // Try removing O_DIRECT temporarily and retry. - handle_o_direct_write(f, buf, e) - } - Err(e) => Err(e), - } - } + Self::File(f, _) => f.write(buf), Self::Stdout(stdout) => stdout.write(buf), #[cfg(unix)] Self::Fifo(f) => f.write(buf), @@ -932,6 +964,36 @@ impl<'a> Output<'a> { } } + /// Write to the destination with O_DIRECT awareness. + /// + /// This method handles O_DIRECT write errors by temporarily removing the flag + /// for partial blocks, matching GNU dd behavior. + #[cfg(any(target_os = "linux", target_os = "android"))] + fn write_with_o_direct_handling(&mut self, buf: &[u8]) -> io::Result { + match self.dst.write(buf) { + Ok(len) => Ok(len), + Err(e) + if e.kind() == io::ErrorKind::InvalidInput + && e.raw_os_error() == Some(libc::EINVAL) => + { + // This might be an O_DIRECT alignment issue. + // Try removing O_DIRECT temporarily and retry (only for partial blocks). + if let Dest::File(f, _) = &mut self.dst { + handle_o_direct_write(f, buf, self.settings.obs, e) + } else { + Err(e) + } + } + Err(e) => Err(e), + } + } + + /// Fallback for non-Linux platforms - use regular write + #[cfg(not(any(target_os = "linux", target_os = "android")))] + fn write_with_o_direct_handling(&mut self, buf: &[u8]) -> io::Result { + self.dst.write(buf) + } + /// writes a block of data. optionally retries when first try didn't complete /// /// this is needed by gnu-test: tests/dd/stats.s @@ -942,7 +1004,7 @@ impl<'a> Output<'a> { let full_len = chunk.len(); let mut base_idx = 0; loop { - match self.dst.write(&chunk[base_idx..]) { + match self.write_with_o_direct_handling(&chunk[base_idx..]) { Ok(wlen) => { base_idx += wlen; // take iflags.fullblock as oflags shall not have this option @@ -1156,7 +1218,11 @@ fn dd_copy(mut i: Input, o: Output) -> io::Result<()> { // Create a common buffer with a capacity of the block size. // This is the max size needed. - let mut buf = vec![BUF_INIT_BYTE; bsize]; + // + // On Linux/Android, use an aligned buffer for O_DIRECT support. + // O_DIRECT requires buffers to be aligned to page boundaries (typically 4096 bytes). + // This prevents EINVAL errors when writing with oflag=direct. + let mut buf = allocate_aligned_buffer(bsize); // Spawn a timer thread to provide a scheduled signal indicating when we // should send an update of our progress to the reporting thread. @@ -1606,4 +1672,102 @@ mod tests { Output::new_file(Path::new(settings.outfile.as_ref().unwrap()), &settings).is_err() ); } + + // ===== O_DIRECT Buffer Alignment Tests ===== + + #[test] + #[cfg(any(target_os = "linux", target_os = "android"))] + fn test_aligned_buffer_allocation() { + // Test that allocate_aligned_buffer creates page-aligned buffers + let buf = super::allocate_aligned_buffer(4096); + + // Verify buffer is created + assert_eq!(buf.capacity(), 4096); + + // Verify buffer pointer is page-aligned + let ptr = buf.as_ptr() as usize; + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }; + assert_eq!(ptr % page_size, 0, "Buffer should be page-aligned"); + } + + #[test] + #[cfg(any(target_os = "linux", target_os = "android"))] + fn test_aligned_buffer_various_sizes() { + // Test alignment for various buffer sizes + let sizes = vec![512, 1024, 2048, 4096, 8192, 16384]; + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }; + + for size in sizes { + let buf = super::allocate_aligned_buffer(size); + let ptr = buf.as_ptr() as usize; + assert_eq!( + ptr % page_size, + 0, + "Buffer of size {size} should be page-aligned" + ); + } + } + + #[test] + #[cfg(any(target_os = "linux", target_os = "android"))] + fn test_aligned_buffer_initialization() { + // Test that buffer is initialized with BUF_INIT_BYTE + let buf = super::allocate_aligned_buffer(1024); + + // Check that buffer is initialized (not all zeros) + let init_byte = super::BUF_INIT_BYTE; + for &byte in &buf { + assert_eq!( + byte, init_byte, + "Buffer should be initialized with BUF_INIT_BYTE" + ); + } + } + + #[test] + #[cfg(not(any(target_os = "linux", target_os = "android")))] + fn test_aligned_buffer_fallback() { + // Test that non-Linux platforms use regular Vec allocation + let buf = super::allocate_aligned_buffer(4096); + + // Should still create a valid buffer + assert_eq!(buf.capacity(), 4096); + assert_eq!(buf.len(), 4096); + } + + #[test] + fn test_calc_bsize_alignment() { + // Test that calculated buffer size is reasonable for O_DIRECT + let ibs = 4096; + let obs = 4096; + let bsize = calc_bsize(ibs, obs); + + // Should be a multiple of both ibs and obs + assert_eq!(bsize % ibs, 0); + assert_eq!(bsize % obs, 0); + + // Should be at least as large as both + assert!(bsize >= ibs); + assert!(bsize >= obs); + } + + #[test] + fn test_calc_bsize_lcm() { + // Test LCM calculation for various block sizes + let test_cases = vec![ + (512, 512, 512), + (512, 1024, 1024), + (1024, 2048, 2048), + (4096, 4096, 4096), + (512, 4096, 4096), + ]; + + for (ibs, obs, expected) in test_cases { + let bsize = calc_bsize(ibs, obs); + assert_eq!( + bsize, expected, + "calc_bsize({ibs}, {obs}) should be {expected}" + ); + } + } } diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index a6a52e66fb5..6f8419363e2 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -1831,6 +1831,117 @@ fn test_oflag_direct_partial_block() { at.remove(output_file); } +// ===== O_DIRECT Buffer Alignment Integration Tests ===== + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_o_direct_with_aligned_buffer_full_blocks() { + // Test O_DIRECT with full blocks (should not trigger O_DIRECT removal) + let (at, mut ucmd) = at_and_ucmd!(); + let input_file = "test_input_full_blocks.bin"; + let output_file = "test_output_full_blocks.bin"; + + // Create input file with multiple full blocks (16 * 4096 = 65536 bytes) + let block_size = 4096; + let num_blocks = 16; + let input_size = block_size * num_blocks; + let input_data: Vec = (0..input_size).map(|i| (i % 256) as u8).collect(); + at.write_bytes(input_file, &input_data); + + // Run dd with O_DIRECT + ucmd.args(&[ + format!("if={}", at.plus(input_file).display()), + format!("of={}", at.plus(output_file).display()), + format!("bs={block_size}"), + "oflag=direct".to_string(), + "status=none".to_string(), + ]) + .succeeds(); + + // Verify output matches input + let output_data = at.read_bytes(output_file); + assert_eq!(output_data.len(), input_size); + assert_eq!(output_data, input_data); + + // Clean up + at.remove(input_file); + at.remove(output_file); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_o_direct_with_partial_final_block() { + // Test O_DIRECT with partial final block (should trigger O_DIRECT removal only for final block) + let (at, mut ucmd) = at_and_ucmd!(); + let input_file = "test_input_partial.bin"; + let output_file = "test_output_partial.bin"; + + // Create input file with partial final block + let block_size = 4096; + let num_full_blocks = 8; + let partial_size = 2048; // Partial block + let input_size = (block_size * num_full_blocks) + partial_size; + let input_data: Vec = (0..input_size).map(|i| (i % 256) as u8).collect(); + at.write_bytes(input_file, &input_data); + + // Run dd with O_DIRECT + ucmd.args(&[ + format!("if={}", at.plus(input_file).display()), + format!("of={}", at.plus(output_file).display()), + format!("bs={block_size}"), + "oflag=direct".to_string(), + "status=none".to_string(), + ]) + .succeeds(); + + // Verify output matches input + let output_data = at.read_bytes(output_file); + assert_eq!(output_data.len(), input_size); + assert_eq!(output_data, input_data); + + // Clean up + at.remove(input_file); + at.remove(output_file); +} + +#[test] +#[cfg(any(target_os = "linux", target_os = "android"))] +fn test_o_direct_various_block_sizes() { + // Test O_DIRECT with various block sizes + let ts = TestScenario::new(util_name!()); + let block_sizes = vec![512, 1024, 2048, 4096, 8192]; + + for block_size in block_sizes { + let input_file = format!("test_input_bs_{block_size}.bin"); + let output_file = format!("test_output_bs_{block_size}.bin"); + + // Create input file + let input_size = block_size * 4; // 4 full blocks + let input_data: Vec = (0..input_size).map(|i| (i % 256) as u8).collect(); + ts.fixtures.write_bytes(&input_file, &input_data); + + // Run dd with O_DIRECT + ts.ucmd() + .args(&[ + format!("if={}", ts.fixtures.plus(&input_file).display()), + format!("of={}", ts.fixtures.plus(&output_file).display()), + format!("bs={block_size}"), + "oflag=direct".to_string(), + "status=none".to_string(), + ]) + .succeeds(); + + // Verify output matches input + let output_data = ts.fixtures.read_bytes(&output_file); + assert_eq!(output_data.len(), input_size); + assert_eq!(output_data, input_data); + + // Clean up + ts.fixtures.remove(&input_file); + ts.fixtures.remove(&output_file); + } +} + #[test] fn test_skip_overflow() { new_ucmd!()