Skip to main content

disktest_rawio/
linux.rs

1// -*- coding: utf-8 -*-
2//
3// disktest - Storage tester
4//
5// Copyright 2020-2026 Michael Büsch <m@bues.ch>
6//
7// Licensed under the Apache License version 2.0
8// or the MIT license, at your option.
9// SPDX-License-Identifier: Apache-2.0 OR MIT
10//
11
12use super::{RawIoOsIntf, RawIoResult};
13use anyhow::{self as ah, Context as _};
14use libc::{POSIX_FADV_DONTNEED, c_int};
15use std::{
16    fs::{File, OpenOptions, metadata},
17    io::{Read as _, Seek as _, SeekFrom, Write as _},
18    os::unix::{fs::MetadataExt as _, io::AsRawFd as _},
19    path::{Path, PathBuf},
20};
21
22#[allow(clippy::unnecessary_cast)]
23const S_IFBLK: u32 = libc::S_IFBLK as u32;
24#[allow(clippy::unnecessary_cast)]
25const S_IFCHR: u32 = libc::S_IFCHR as u32;
26#[allow(clippy::unnecessary_cast)]
27const S_IFMT: u32 = libc::S_IFMT as u32;
28
29/// Raw device I/O for Linux OS.
30pub struct RawIoLinux {
31    path: PathBuf,
32    file: Option<File>,
33    read_mode: bool,
34    write_mode: bool,
35    is_blk: bool,
36    is_chr: bool,
37    sector_size: Option<u32>,
38}
39
40impl RawIoLinux {
41    fn read_disk_geometry(&mut self) -> ah::Result<()> {
42        if let Ok(meta) = metadata(&self.path) {
43            let mode_ifmt = meta.mode() & S_IFMT;
44            if mode_ifmt == S_IFBLK {
45                self.is_blk = true;
46            }
47            if mode_ifmt == S_IFCHR {
48                self.is_chr = true;
49            }
50        }
51
52        if self.is_blk {
53            let Some(file) = self.file.as_ref() else {
54                return Err(ah::format_err!("No file object"));
55            };
56
57            let mut sector_size: c_int = 0;
58            // SAFETY: The ioctl call is safe, because:
59            // - The raw file descriptor is valid. (Closing sets self.file to None).
60            // - sector_size points to a valid and initialized c_int.
61            // - The ioctl only fetches the sector size and has no other side effects.
62            let res = unsafe {
63                libc::ioctl(
64                    file.as_raw_fd(),
65                    libc::BLKPBSZGET, // get physical sector size.
66                    (&raw mut sector_size).cast::<c_int>(),
67                )
68            };
69            if res < 0 {
70                return Err(ah::format_err!(
71                    "Get device block size: ioctl(BLKPBSZGET) failed."
72                ));
73            }
74            if sector_size <= 0 {
75                return Err(ah::format_err!(
76                    "Get device block size: ioctl(BLKPBSZGET) invalid size."
77                ));
78            }
79
80            self.sector_size = Some(sector_size.try_into().unwrap_or(0));
81        } else {
82            self.sector_size = None;
83        }
84        Ok(())
85    }
86}
87
88impl RawIoOsIntf for RawIoLinux {
89    fn new(path: &Path, mut create: bool, read: bool, write: bool) -> ah::Result<Self> {
90        if path.starts_with("/dev/") {
91            // Do not create dev nodes by accident.
92            // This check is not meant to catch all possible cases,
93            // but only the common ones.
94            create = false;
95        }
96
97        let file = match OpenOptions::new()
98            .create(create)
99            .read(read)
100            .write(write)
101            .open(path)
102        {
103            Ok(f) => f,
104            Err(e) => {
105                return Err(ah::format_err!(
106                    "Failed to open file {}: {e}",
107                    path.display()
108                ));
109            }
110        };
111
112        let mut self_ = Self {
113            path: path.into(),
114            file: Some(file),
115            read_mode: read,
116            write_mode: write,
117            is_blk: false,
118            is_chr: false,
119            sector_size: None,
120        };
121
122        if let Err(e) = self_.read_disk_geometry() {
123            let _ = self_.close();
124            return Err(e);
125        }
126
127        Ok(self_)
128    }
129
130    fn get_sector_size(&self) -> Option<u32> {
131        self.sector_size
132    }
133
134    fn drop_file_caches(&mut self, offset: u64, size: u64) -> ah::Result<()> {
135        let Some(file) = self.file.take() else {
136            return Ok(());
137        };
138
139        if self.is_chr {
140            // This is a character device.
141            // We're done. Don't flush.
142            return Ok(());
143        }
144
145        if self.write_mode {
146            // fsync()
147            if let Err(e) = file.sync_all() {
148                return Err(ah::format_err!("Failed to flush: {e}"));
149            }
150        }
151
152        // Try FADV_DONTNEED to drop caches.
153        //
154        // SAFETY: The ioctl call is safe, because:
155        // - The raw file descriptor is valid. (Closing sets self.file to None).
156        // - fadvise DONTNEED has no safety relevant side effects.
157        let ret = unsafe {
158            libc::posix_fadvise(
159                file.as_raw_fd(),
160                offset.try_into().context("File offset overflows off_t")?,
161                size.try_into().context("File size overflows off_t")?,
162                POSIX_FADV_DONTNEED,
163            )
164        };
165
166        if ret == 0 {
167            // fadvise success.
168            Ok(())
169        } else {
170            // Try global drop_caches.
171
172            drop(file);
173
174            let proc_file = "/proc/sys/vm/drop_caches";
175            let proc_value = b"3\n";
176
177            match OpenOptions::new().write(true).open(proc_file) {
178                Ok(mut file) => match file.write_all(proc_value) {
179                    Ok(()) => Ok(()),
180                    Err(e) => Err(ah::format_err!("{e}")),
181                },
182                Err(e) => Err(ah::format_err!("{e}")),
183            }
184        }
185    }
186
187    fn close(&mut self) -> ah::Result<()> {
188        let Some(file) = self.file.take() else {
189            return Ok(());
190        };
191        if self.write_mode && !self.is_chr {
192            if let Err(e) = file.sync_all() {
193                return Err(ah::format_err!("Failed to flush: {e}"));
194            }
195        }
196        Ok(())
197    }
198
199    fn sync(&mut self) -> ah::Result<()> {
200        if self.write_mode && !self.is_chr {
201            let Some(file) = self.file.as_mut() else {
202                return Err(ah::format_err!("No file object"));
203            };
204            file.sync_all()?;
205        }
206        Ok(())
207    }
208
209    fn set_len(&mut self, size: u64) -> ah::Result<()> {
210        if !self.write_mode {
211            return Err(ah::format_err!("File is opened without write permission."));
212        }
213        if self.is_chr || self.is_blk {
214            return Err(ah::format_err!("Cannot set length of raw device."));
215        }
216        let Some(file) = self.file.as_mut() else {
217            return Err(ah::format_err!("No file object"));
218        };
219        Ok(file.set_len(size)?)
220    }
221
222    fn seek(&mut self, offset: u64) -> ah::Result<u64> {
223        let Some(file) = self.file.as_mut() else {
224            return Err(ah::format_err!("No file object"));
225        };
226        Ok(file.seek(SeekFrom::Start(offset))?)
227    }
228
229    fn read(&mut self, buffer: &mut [u8]) -> ah::Result<RawIoResult> {
230        if !self.read_mode {
231            return Err(ah::format_err!("File is opened without read permission."));
232        }
233        let Some(file) = self.file.as_mut() else {
234            return Err(ah::format_err!("No file object"));
235        };
236        match file.read(buffer) {
237            Ok(count) => Ok(RawIoResult::Ok(count)),
238            Err(e) => Err(ah::format_err!("Read error: {e}")),
239        }
240    }
241
242    fn write(&mut self, buffer: &[u8]) -> ah::Result<RawIoResult> {
243        if !self.write_mode {
244            return Err(ah::format_err!("File is opened without write permission."));
245        }
246        let Some(file) = self.file.as_mut() else {
247            return Err(ah::format_err!("No file object"));
248        };
249        if let Err(e) = file.write_all(buffer) {
250            if let Some(err_code) = e.raw_os_error() {
251                if err_code == libc::ENOSPC {
252                    return Ok(RawIoResult::Enospc);
253                }
254            }
255            return Err(ah::format_err!("Write error: {e}"));
256        }
257        Ok(RawIoResult::Ok(buffer.len()))
258    }
259}
260
261impl Drop for RawIoLinux {
262    fn drop(&mut self) {
263        if let Err(e) = self.close() {
264            eprintln!("Warning: Failed to close device: {e}");
265        }
266    }
267}
268
269// vim: ts=4 sw=4 expandtab