From 4772235952b87190a274713df8e6d0edd8093357 Mon Sep 17 00:00:00 2001 From: Anatol Belski Date: Thu, 16 Apr 2026 18:01:34 +0200 Subject: [PATCH] block: qcow: Fix O_DIRECT EINVAL in async io_uring path Override AsyncIo::alignment() to report the actual device sector size so that execute_async() correctly bounces misaligned guest memory pointers. Guard the io_uring fast path in resolve_read() with an alignment check. When O_DIRECT is active, guest requests can have I/O sizes smaller than the device sector size (e.g. 512 byte UEFI reads on a 4096 byte sector device). The kernel rejects these with EINVAL. Route such reads through scatter_read_sync() which uses AlignedBuf and aligned_pread to satisfy O_DIRECT size and offset requirements. Signed-off-by: Anatol Belski --- block/src/qcow_async.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/block/src/qcow_async.rs b/block/src/qcow_async.rs index 2794b4235..308c2e27d 100644 --- a/block/src/qcow_async.rs +++ b/block/src/qcow_async.rs @@ -6,7 +6,7 @@ //! QCOW2 async disk backend. -use std::cmp::min; +use std::cmp::{max, min}; use std::collections::VecDeque; use std::fs::File; use std::io::Error; @@ -30,7 +30,7 @@ use crate::qcow_common::{ AlignedBuf, aligned_pread, aligned_pwrite, gather_from_iovecs_into, pread_exact, pwrite_all, scatter_to_iovecs, zero_fill_iovecs, }; -use crate::{BatchRequest, RequestType, disk_file}; +use crate::{BatchRequest, RequestType, SECTOR_SIZE, disk_file}; /// Device level handle for a QCOW2 image. /// @@ -175,6 +175,8 @@ pub struct QcowAsync { sparse: bool, /// O_DIRECT alignment requirement (0 = no alignment needed). alignment: usize, + /// I/O alignment for the AsyncIo trait (at least SECTOR_SIZE). + io_alignment: u64, io_uring: IoUring, eventfd: EventFd, completion_list: VecDeque<(u64, i32)>, @@ -189,6 +191,7 @@ impl QcowAsync { ring_depth: u32, ) -> io::Result { let alignment = data_file.file().alignment(); + let io_alignment = max(alignment as u64, SECTOR_SIZE); let io_uring = IoUring::new(ring_depth)?; let eventfd = EventFd::new(libc::EFD_NONBLOCK)?; io_uring.submitter().register_eventfd(eventfd.as_raw_fd())?; @@ -199,6 +202,7 @@ impl QcowAsync { backing_file, sparse, alignment, + io_alignment, io_uring, eventfd, completion_list: VecDeque::new(), @@ -367,6 +371,10 @@ impl AsyncIo for QcowAsync { true } + fn alignment(&self) -> u64 { + self.io_alignment + } + fn submit_batch_requests(&mut self, batch_request: &[BatchRequest]) -> AsyncIoResult<()> { let (submitter, mut sq, _) = self.io_uring.split(); let mut needs_submit = false; @@ -464,7 +472,15 @@ impl QcowAsync { .map_clusters_for_read(address, total_len, has_backing) .map_err(AsyncIoError::ReadVectored)?; - if mappings.len() == 1 + // The fast path returns a host offset so the caller can submit a + // single io_uring readv with the original iovecs. This only works + // without O_DIRECT because it requires I/O + // size and file offset to be multiples of the device sector size. + // Guest requests can be smaller (e.g. 512 byte UEFI reads on a + // 4096 byte sector device), so O_DIRECT reads fall through to the + // alignment aware synchronous path instead. + if alignment == 0 + && mappings.len() == 1 && let ClusterReadMapping::Allocated { offset: host_offset, length,