Skip to main content

disk_file/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4#![expect(missing_docs)]
5#![forbid(unsafe_code)]
6
7mod readwriteat;
8
9use self::readwriteat::ReadWriteAt;
10use blocking::unblock;
11use disk_backend::DiskError;
12use disk_backend::DiskIo;
13use disk_backend::resolve::ResolveDiskParameters;
14use disk_backend::resolve::ResolvedDisk;
15use disk_backend_resources::FileDiskHandle;
16use guestmem::MemoryRead;
17use guestmem::MemoryWrite;
18use inspect::Inspect;
19use scsi_buffers::RequestBuffers;
20use std::fs;
21use std::sync::Arc;
22use thiserror::Error;
23use vm_resource::ResolveResource;
24use vm_resource::declare_static_resolver;
25use vm_resource::kind::DiskHandleKind;
26
27pub struct FileDiskResolver;
28declare_static_resolver!(FileDiskResolver, (DiskHandleKind, FileDiskHandle));
29
30#[derive(Debug, Error)]
31pub enum ResolveFileDiskError {
32    #[error("i/o error")]
33    Io(#[source] std::io::Error),
34    #[error("invalid disk")]
35    InvalidDisk(#[source] disk_backend::InvalidDisk),
36}
37
38impl ResolveResource<DiskHandleKind, FileDiskHandle> for FileDiskResolver {
39    type Output = ResolvedDisk;
40    type Error = ResolveFileDiskError;
41
42    fn resolve(
43        &self,
44        rsrc: FileDiskHandle,
45        input: ResolveDiskParameters<'_>,
46    ) -> Result<Self::Output, Self::Error> {
47        ResolvedDisk::new(
48            FileDisk::open(rsrc.0, input.read_only).map_err(ResolveFileDiskError::Io)?,
49        )
50        .map_err(ResolveFileDiskError::InvalidDisk)
51    }
52}
53
54#[derive(Debug, Inspect)]
55pub struct FileDisk {
56    file: Arc<fs::File>,
57    metadata: Metadata,
58    sector_shift: u32,
59}
60
61#[derive(Debug, Inspect)]
62pub struct Metadata {
63    pub disk_size: u64,
64    pub sector_size: u32,
65    pub physical_sector_size: u32,
66    pub read_only: bool,
67}
68
69#[cfg(windows)]
70impl std::os::windows::io::AsHandle for FileDisk {
71    fn as_handle(&self) -> std::os::windows::io::BorrowedHandle<'_> {
72        self.file.as_handle()
73    }
74}
75
76impl FileDisk {
77    pub fn open(file: fs::File, read_only: bool) -> Result<Self, std::io::Error> {
78        let metadata = Metadata {
79            disk_size: file.metadata()?.len(),
80            sector_size: 512,
81            physical_sector_size: 4096,
82            read_only,
83        };
84        Ok(Self::with_metadata(file, metadata))
85    }
86
87    /// Opens the disk using the specified metadata.
88    ///
89    /// This ensures that no metadata queries are made to the file, which may be
90    /// appropriate if this is wrapped in another disk implementation that
91    /// retrieves metadata in another way.
92    pub fn with_metadata(file: fs::File, metadata: Metadata) -> Self {
93        assert!(metadata.sector_size.is_power_of_two());
94        assert!(metadata.sector_size >= 512);
95        let sector_shift = metadata.sector_size.trailing_zeros();
96        FileDisk {
97            file: Arc::new(file),
98            metadata,
99            sector_shift,
100        }
101    }
102
103    pub fn into_inner(self) -> fs::File {
104        Arc::try_unwrap(self.file).expect("no outstanding IOs")
105    }
106}
107
108impl FileDisk {
109    pub async fn read(&self, buffers: &RequestBuffers<'_>, sector: u64) -> Result<(), DiskError> {
110        if ((sector << self.sector_shift) + buffers.len() as u64) > self.metadata.disk_size {
111            return Err(DiskError::IllegalBlock);
112        }
113        let mut buffer = vec![0; buffers.len()];
114        let file = self.file.clone();
115        let offset = sector << self.sector_shift;
116        let buffer = unblock(move || -> Result<_, std::io::Error> {
117            let count = file.read_at(&mut buffer, offset)?;
118            if count != buffer.len() {
119                Err(std::io::Error::new(
120                    std::io::ErrorKind::UnexpectedEof,
121                    "short disk read",
122                ))
123            } else {
124                Ok(buffer)
125            }
126        })
127        .await
128        .map_err(DiskError::Io)?;
129        buffers.writer().write(&buffer)?;
130        Ok(())
131    }
132
133    pub async fn write(
134        &self,
135        buffers: &RequestBuffers<'_>,
136        sector: u64,
137        _fua: bool,
138    ) -> Result<(), DiskError> {
139        if ((sector << self.sector_shift) + buffers.len() as u64) > self.metadata.disk_size {
140            return Err(DiskError::IllegalBlock);
141        }
142        let mut buffer = vec![0; buffers.len()];
143        let file = self.file.clone();
144        buffers.reader().read(&mut buffer)?;
145        let offset = sector << self.sector_shift;
146        unblock(move || -> Result<(), std::io::Error> {
147            let count = file.write_at(&buffer, offset)?;
148            if count != buffer.len() {
149                Err(std::io::Error::other("short disk write"))
150            } else {
151                Ok(())
152            }
153        })
154        .await
155        .map_err(DiskError::Io)?;
156        Ok(())
157    }
158
159    pub async fn flush(&self) -> Result<(), DiskError> {
160        let file = self.file.clone();
161        unblock(move || file.sync_all())
162            .await
163            .map_err(DiskError::Io)?;
164        Ok(())
165    }
166}
167
168impl DiskIo for FileDisk {
169    fn disk_type(&self) -> &str {
170        "file"
171    }
172
173    fn sector_count(&self) -> u64 {
174        self.metadata.disk_size >> self.sector_shift
175    }
176
177    fn sector_size(&self) -> u32 {
178        self.metadata.sector_size
179    }
180
181    fn is_read_only(&self) -> bool {
182        self.metadata.read_only
183    }
184
185    fn disk_id(&self) -> Option<[u8; 16]> {
186        None
187    }
188
189    fn physical_sector_size(&self) -> u32 {
190        self.metadata.physical_sector_size
191    }
192
193    fn is_fua_respected(&self) -> bool {
194        false
195    }
196
197    async fn read_vectored(
198        &self,
199        buffers: &RequestBuffers<'_>,
200        sector: u64,
201    ) -> Result<(), DiskError> {
202        self.read(buffers, sector).await
203    }
204
205    async fn write_vectored(
206        &self,
207        buffers: &RequestBuffers<'_>,
208        sector: u64,
209        fua: bool,
210    ) -> Result<(), DiskError> {
211        self.write(buffers, sector, fua).await
212    }
213
214    async fn sync_cache(&self) -> Result<(), DiskError> {
215        self.flush().await
216    }
217
218    async fn unmap(
219        &self,
220        _sector: u64,
221        _count: u64,
222        _block_level_only: bool,
223    ) -> Result<(), DiskError> {
224        Ok(())
225    }
226
227    fn unmap_behavior(&self) -> disk_backend::UnmapBehavior {
228        disk_backend::UnmapBehavior::Ignored
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::FileDisk;
235    use disk_backend::Disk;
236    use disk_backend::DiskError;
237    use guestmem::GuestMemory;
238    use pal_async::async_test;
239    use scsi_buffers::OwnedRequestBuffers;
240
241    const SECTOR_SIZE: usize = 512;
242    const DISK_SIZE: u64 = 1024 * 1024;
243
244    fn file_disk() -> Disk {
245        let file = tempfile::tempfile().unwrap();
246        file.set_len(DISK_SIZE).unwrap();
247        Disk::new(FileDisk::open(file, false).unwrap()).unwrap()
248    }
249
250    #[async_test]
251    async fn short_read_returns_error() {
252        let file = tempfile::tempfile().unwrap();
253        file.set_len(DISK_SIZE).unwrap();
254        let disk = FileDisk::open(file.try_clone().unwrap(), false).unwrap();
255        file.set_len((SECTOR_SIZE + SECTOR_SIZE / 2) as u64)
256            .unwrap();
257
258        let mem = GuestMemory::allocate(SECTOR_SIZE);
259        mem.write_at(0, &[0xcd; SECTOR_SIZE]).unwrap();
260        let buffers = OwnedRequestBuffers::linear(0, SECTOR_SIZE, true);
261        let result = disk.read(&buffers.buffer(&mem), 1).await;
262
263        assert!(
264            matches!(result, Err(DiskError::Io(ref error)) if error.kind() == std::io::ErrorKind::UnexpectedEof),
265            "{result:?}"
266        );
267        let mut contents = [0; SECTOR_SIZE];
268        mem.read_at(0, &mut contents).unwrap();
269        assert_eq!(contents, [0xcd; SECTOR_SIZE]);
270    }
271
272    #[async_test]
273    async fn sector_range_conformance() {
274        storage_tests::sector_range::test_disk_sector_range_conformance(&file_disk()).await;
275    }
276
277    /// The range check here is in byte units, computing `sector << sector_shift`.
278    /// A left shift discards high bits without panicking, so a large enough
279    /// sector wraps to a small byte offset and passes the check.
280    ///
281    /// This is the one defect in this area whose symptom was silent wrong data
282    /// rather than a panic, so the test asserts both that the request is
283    /// rejected and that it did not return the contents of sector 0.
284    #[async_test]
285    async fn sector_does_not_wrap_when_shifted() {
286        let disk = file_disk();
287        let mem = GuestMemory::allocate(SECTOR_SIZE);
288
289        mem.write_at(0, &[0xcd; SECTOR_SIZE]).unwrap();
290        disk.write_vectored(
291            &OwnedRequestBuffers::linear(0, SECTOR_SIZE, false).buffer(&mem),
292            0,
293            false,
294        )
295        .await
296        .unwrap();
297        mem.write_at(0, &[0; SECTOR_SIZE]).unwrap();
298
299        // `1 << 55` shifted left by 9 (512-byte sectors) is `1 << 64`, which
300        // truncates to a byte offset of zero.
301        let r = disk
302            .read_vectored(
303                &OwnedRequestBuffers::linear(0, SECTOR_SIZE, true).buffer(&mem),
304                1 << 55,
305            )
306            .await;
307
308        let mut buf = [0; SECTOR_SIZE];
309        mem.read_at(0, &mut buf).unwrap();
310        assert_ne!(buf, [0xcd; SECTOR_SIZE], "read returned sector 0");
311        assert!(matches!(r, Err(DiskError::IllegalBlock)), "{r:?}");
312    }
313}