Skip to content

Commit c85d8b5

Browse files
authored
Merge pull request #9016 from naoNao89/fix/dd-oflag-direct-isolated
fix(dd): handle O_DIRECT partial block writes
2 parents 09a8514 + bcba024 commit c85d8b5

File tree

2 files changed

+113
-1
lines changed

2 files changed

+113
-1
lines changed

src/uu/dd/src/dd.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,54 @@ fn is_sparse(buf: &[u8]) -> bool {
686686
buf.iter().all(|&e| e == 0u8)
687687
}
688688

689+
/// Handle O_DIRECT write errors by temporarily removing the flag and retrying.
690+
/// This follows GNU dd behavior for partial block writes with O_DIRECT.
691+
#[cfg(any(target_os = "linux", target_os = "android"))]
692+
fn handle_o_direct_write(f: &mut File, buf: &[u8], original_error: io::Error) -> io::Result<usize> {
693+
use nix::fcntl::{FcntlArg, OFlag, fcntl};
694+
695+
// Get current flags using nix
696+
let oflags = match fcntl(&mut *f, FcntlArg::F_GETFL) {
697+
Ok(flags) => OFlag::from_bits_retain(flags),
698+
Err(_) => return Err(original_error),
699+
};
700+
701+
// If O_DIRECT is set, try removing it temporarily
702+
if oflags.contains(OFlag::O_DIRECT) {
703+
let flags_without_direct = oflags - OFlag::O_DIRECT;
704+
705+
// Remove O_DIRECT flag using nix
706+
if fcntl(&mut *f, FcntlArg::F_SETFL(flags_without_direct)).is_err() {
707+
return Err(original_error);
708+
}
709+
710+
// Retry the write without O_DIRECT
711+
let write_result = f.write(buf);
712+
713+
// Restore O_DIRECT flag using nix (GNU doesn't restore it, but we'll be safer)
714+
// Log any restoration errors without failing the operation
715+
if let Err(os_err) = fcntl(&mut *f, FcntlArg::F_SETFL(oflags)) {
716+
// Just log the error, don't fail the whole operation
717+
show_error!("Failed to restore O_DIRECT flag: {}", os_err);
718+
}
719+
720+
write_result
721+
} else {
722+
// O_DIRECT wasn't set, return original error
723+
Err(original_error)
724+
}
725+
}
726+
727+
/// Stub for non-Linux platforms - just return the original error.
728+
#[cfg(not(any(target_os = "linux", target_os = "android")))]
729+
fn handle_o_direct_write(
730+
_f: &mut File,
731+
_buf: &[u8],
732+
original_error: io::Error,
733+
) -> io::Result<usize> {
734+
Err(original_error)
735+
}
736+
689737
impl Write for Dest {
690738
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
691739
match self {
@@ -697,7 +745,21 @@ impl Write for Dest {
697745
f.seek(SeekFrom::Current(seek_amt))?;
698746
Ok(buf.len())
699747
}
700-
Self::File(f, _) => f.write(buf),
748+
Self::File(f, _) => {
749+
// Try the write first
750+
match f.write(buf) {
751+
Ok(len) => Ok(len),
752+
Err(e)
753+
if e.kind() == io::ErrorKind::InvalidInput
754+
&& e.raw_os_error() == Some(libc::EINVAL) =>
755+
{
756+
// This might be an O_DIRECT alignment issue.
757+
// Try removing O_DIRECT temporarily and retry.
758+
handle_o_direct_write(f, buf, e)
759+
}
760+
Err(e) => Err(e),
761+
}
762+
}
701763
Self::Stdout(stdout) => stdout.write(buf),
702764
#[cfg(unix)]
703765
Self::Fifo(f) => f.write(buf),

tests/by-util/test_dd.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1780,3 +1780,53 @@ fn test_wrong_number_err_msg() {
17801780
.fails()
17811781
.stderr_contains("dd: invalid number: '1kBb555'\n");
17821782
}
1783+
1784+
#[test]
1785+
#[cfg(any(target_os = "linux", target_os = "android"))]
1786+
fn test_oflag_direct_partial_block() {
1787+
// Test for issue #9003: dd should handle partial blocks with oflag=direct
1788+
// This reproduces the scenario where writing a partial block with O_DIRECT fails
1789+
1790+
let ts = TestScenario::new(util_name!());
1791+
let at = &ts.fixtures;
1792+
1793+
// Create input file with size that's not a multiple of block size
1794+
// This will trigger the partial block write issue
1795+
let input_file = "test_direct_input.iso";
1796+
let output_file = "test_direct_output.img";
1797+
let block_size = 8192; // 8K blocks
1798+
let input_size = block_size * 3 + 511; // 3 full blocks + 511 byte partial block
1799+
1800+
// Create test input file with known pattern
1801+
let input_data = vec![0x42; input_size]; // Use non-zero pattern for better verification
1802+
at.write_bytes(input_file, &input_data);
1803+
1804+
// Get full paths for the dd command
1805+
let input_path = at.plus(input_file);
1806+
let output_path = at.plus(output_file);
1807+
1808+
// Test with oflag=direct - should succeed with the fix
1809+
new_ucmd!()
1810+
.args(&[
1811+
format!("if={}", input_path.display()),
1812+
format!("of={}", output_path.display()),
1813+
"oflag=direct".to_string(),
1814+
format!("bs={block_size}"),
1815+
"status=none".to_string(),
1816+
])
1817+
.succeeds()
1818+
.stdout_is("")
1819+
.stderr_is("");
1820+
assert!(output_path.exists());
1821+
let output_size = output_path.metadata().unwrap().len() as usize;
1822+
assert_eq!(output_size, input_size);
1823+
1824+
// Verify content matches input
1825+
let output_content = std::fs::read(&output_path).unwrap();
1826+
assert_eq!(output_content.len(), input_size);
1827+
assert_eq!(output_content, input_data);
1828+
1829+
// Clean up
1830+
at.remove(input_file);
1831+
at.remove(output_file);
1832+
}

0 commit comments

Comments
 (0)