Skip to content

Commit

Permalink
fix(devinfo): use a singleton to store if kernel bug state
Browse files Browse the repository at this point in the history
Signed-off-by: Niladri Halder <[email protected]>
  • Loading branch information
niladrih committed Feb 12, 2024
1 parent f4e9125 commit eaba6ad
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 101 deletions.
41 changes: 41 additions & 0 deletions devinfo/src/mountinfo/bytebuf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::io::{IoSliceMut, Read};

/// This is io::Read capable Sized type. This can wrap around a Vec<u8>.
pub struct ByteBuf {
inner: Vec<u8>,
}

impl Read for ByteBuf {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.as_slice().read(buf)
}

fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> std::io::Result<usize> {
self.inner.as_slice().read_vectored(bufs)
}

fn read_to_end(&mut self, buf: &mut Vec<u8>) -> std::io::Result<usize> {
self.inner.as_slice().read_to_end(buf)
}

fn read_to_string(&mut self, buf: &mut String) -> std::io::Result<usize> {
self.inner.as_slice().read_to_string(buf)
}

fn read_exact(&mut self, buf: &mut [u8]) -> std::io::Result<()> {
self.inner.as_slice().read_exact(buf)
}

fn by_ref(&mut self) -> &mut Self
where
Self: Sized,
{
self
}
}

impl From<Vec<u8>> for ByteBuf {
fn from(inner: Vec<u8>) -> Self {
Self { inner }
}
}
66 changes: 66 additions & 0 deletions devinfo/src/mountinfo/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use std::{
error::Error,
ffi::OsString,
fmt::{Debug, Display, Formatter},
path::PathBuf,
};

pub type Result<T, E = MountInfoError> = std::result::Result<T, E>;

#[derive(Debug)]
pub enum MountInfoError {
Io(std::io::Error),
InconsistentRead { filepath: PathBuf, attempts: u32 },
Nix(nix::Error),
ConvertOsStrToStr { source: OsString },
Semver(semver::Error),
}

impl Display for MountInfoError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(ref err) => write!(f, "IO error: {err}"),
Self::InconsistentRead {
filepath: path,
attempts,
} => write!(
f,
"failed to get a consistent read output from file {} after {attempts} attempts",
path.display()
),
Self::Nix(ref err) => write!(f, "{err}"),
Self::ConvertOsStrToStr { source: src } => {
write!(f, "failed to convert {:?} to &str", src)
}
Self::Semver(ref err) => write!(f, "semver error: {err}"),
}
}
}
impl Error for MountInfoError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match *self {
Self::Io(ref err) => err.source(),
Self::Nix(ref err) => err.source(),
Self::Semver(ref err) => err.source(),
_ => None,
}
}
}

impl From<std::io::Error> for MountInfoError {
fn from(io_err: std::io::Error) -> Self {
Self::Io(io_err)
}
}

impl From<nix::Error> for MountInfoError {
fn from(nix_err: nix::Error) -> Self {
Self::Nix(nix_err)
}
}

impl From<semver::Error> for MountInfoError {
fn from(semver_err: semver::Error) -> Self {
Self::Semver(semver_err)
}
}
84 changes: 18 additions & 66 deletions devinfo/src/mountinfo/io_utils.rs
Original file line number Diff line number Diff line change
@@ -1,75 +1,27 @@
use std::{
fs::read,
io::{BufRead, Read},
path::Path,
};
use crate::mountinfo::error::{MountInfoError, Result};
use std::{fs::read, path::Path};

const DEFAULT_RETRY_COUNT: u32 = 2;

type InconsistentReadError = String;
/// This is a container which is a BufRead while carrying the entire read
/// payload in a buffer.
pub(crate) struct ConsistentBufReader {
buf: Vec<u8>,
}
pub(crate) fn consistent_read<P: AsRef<Path>>(
path: P,
retry_count: Option<u32>,
) -> Result<Vec<u8>> {
let mut current_content = read(path.as_ref())?;

impl Read for ConsistentBufReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.buf.as_slice().read(buf)
}
}

impl BufRead for ConsistentBufReader {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
Ok(self.buf.as_slice())
}

fn consume(&mut self, amt: usize) {
self.buf.as_slice().consume(amt)
}

fn read_until(&mut self, byte: u8, buf: &mut Vec<u8>) -> std::io::Result<usize> {
self.buf.as_slice().read_until(byte, buf)
}
let retries = retry_count.unwrap_or(DEFAULT_RETRY_COUNT);
for _ in 0 .. retries {
let new_content = read(path.as_ref())?;

fn read_line(&mut self, buf: &mut String) -> std::io::Result<usize> {
self.buf.as_slice().read_line(buf)
}
}

impl ConsistentBufReader {
// This tries to perform a consistent read, i.e. if two consecutive reads return the same byte
// sequence, then the read is consistent, and we are not missing out on any entries due to a
// seq index on a /proc file.
pub(crate) fn new(
path: &Path,
retry_count: Option<u32>,
) -> Result<Self, InconsistentReadError> {
let read_error = |error: std::io::Error| -> InconsistentReadError {
format!(
"failed to read file at {}: {}",
path.to_string_lossy(),
error
)
};

let mut current_content = read(path).map_err(read_error)?;

let retries = retry_count.unwrap_or(DEFAULT_RETRY_COUNT);
for _ in 0 .. retries {
let new_content = read(path).map_err(read_error)?;

if new_content.eq(&current_content) {
return Ok(Self { buf: new_content });
}

current_content = new_content;
if new_content.eq(&current_content) {
return Ok(new_content);
}

Err(format!(
"failed to get a consistent read output from file {} after {} attempts",
path.to_string_lossy(),
retries
))
current_content = new_content;
}

Err(MountInfoError::InconsistentRead {
filepath: path.as_ref().to_path_buf(),
attempts: retries,
})
}
98 changes: 63 additions & 35 deletions devinfo/src/mountinfo/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
use crate::partition::PartitionID;
use io_utils::ConsistentBufReader;
use crate::{
mountinfo::{
bytebuf::ByteBuf,
error::{MountInfoError, Result},
},
partition::PartitionID,
};
use io_utils::consistent_read;
use std::{
ffi::OsString,
fmt::{self, Display, Formatter},
Expand All @@ -8,9 +14,11 @@ use std::{
os::unix::prelude::OsStringExt,
path::{Path, PathBuf},
str::FromStr,
sync::Once,
sync::OnceLock,
};

mod bytebuf;
mod error;
/// Contains tools to interact with files, etc.
mod io_utils;

Expand Down Expand Up @@ -143,14 +151,21 @@ pub struct MountIter<R> {
buffer: String,
}

impl MountIter<Box<dyn BufRead>> {
impl<R: io::Read> MountIter<BufReader<R>> {
/// Read mounts from any mount-tab-like file.
pub fn new_from_readable(readable: R) -> io::Result<Self> {
Ok(Self::new_from_reader(BufReader::new(readable)))
}
}

impl MountIter<BufReader<File>> {
pub fn new() -> io::Result<Self> {
Self::new_from_file("/proc/mounts")
}

/// Read mounts from any mount-tab-like file.
pub fn new_from_file<P: AsRef<Path>>(path: P) -> io::Result<Self> {
Ok(Self::new_from_reader(read_mount_info(path)?))
Ok(Self::new_from_reader(BufReader::new(File::open(path)?)))
}
}

Expand Down Expand Up @@ -210,49 +225,62 @@ impl<R: BufRead> Iterator for MountIter<R> {
}
}

/// This is the flag for the /proc/mounts linux bug. The bug has been fixed in
/// commit 9f6c61f96f2d97 (v5.8+). This borrows the consistent read solution from
/// k8s.io/mount-utils. Assumes bug exists by default, if version check fails.
static mut KERNEL_HAS_MOUNT_INFO_BUG: bool = true;
static INIT_KERNEL_HAS_MOUNT_INFO_BUG: Once = Once::new();
/// Parses kernel version and returns true if version is less than 5.8.
fn kernel_has_mount_info_bug() -> bool {
unsafe {
INIT_KERNEL_HAS_MOUNT_INFO_BUG.call_once(|| {
pub static SAFE_MOUNT_ITER: OnceLock<SafeMountIter> = OnceLock::new();

/// This returns a Result<Iterator> with reads /proc/mounts consistently.
pub struct SafeMountIter {
/// This is the flag for the /proc/mounts linux bug. The bug has been fixed in
/// commit 9f6c61f96f2d97 (v5.8+). This borrows the consistent read solution from
/// k8s.io/mount-utils. Assumes bug exists by default, if version check fails.
kernel_has_mount_info_bug: bool,
mounts_filepath: PathBuf,
}

impl SafeMountIter {
/// Initialize (if not done already) and get a Result<MountIter>.
pub fn get() -> Result<MountIter<BufReader<Box<dyn io::Read>>>> {
// Init.
let safe_mount_iter = SAFE_MOUNT_ITER.get_or_init(|| {
use nix::sys::utsname::uname;
use semver::Version;

// Bug was fixed in v5.8 with the commit 9f6c61f96f2d97.
const FIXED_VERSION: Version = Version::new(5, 8, 0);

let kernel_version_has_bug = || -> Result<bool, String> {
let uname = uname().map_err(|error| format!("failed uname: {}", error))?;
let check_uname = || -> Result<bool> {
let uname = uname()?;

let release = uname
.release()
.to_str()
.ok_or(format!("failed to convert {:?} to &str", uname.release()))?;
let version = Version::parse(release)
.map_err(|e| format!("failed to parse version from {}: {}", release, e))?;
let release =
uname
.release()
.to_str()
.ok_or(MountInfoError::ConvertOsStrToStr {
source: uname.release().to_os_string(),
})?;
let version = Version::parse(release)?;

Ok(version.lt(&FIXED_VERSION))
};

// Assume bug exists by default.
KERNEL_HAS_MOUNT_INFO_BUG = kernel_version_has_bug().unwrap_or(true);
let kernel_has_mount_info_bug = check_uname().unwrap_or(true);

Self {
kernel_has_mount_info_bug,
mounts_filepath: PathBuf::from("/proc/mounts"),
}
});
KERNEL_HAS_MOUNT_INFO_BUG
}
}

/// Returns a BufRead for the /proc/mounts file.
fn read_mount_info<P: AsRef<Path>>(path: P) -> Result<Box<dyn BufRead>, io::Error> {
if kernel_has_mount_info_bug() {
return Ok(Box::new(
ConsistentBufReader::new(path.as_ref(), Some(6))
.map_err(|_| io::Error::from(ErrorKind::UnexpectedEof))?,
));
}
// Decide if consistent read is required.
if safe_mount_iter.kernel_has_mount_info_bug {
let buf: ByteBuf =
consistent_read(safe_mount_iter.mounts_filepath.as_path(), Some(2))?.into();
let buf: Box<dyn io::Read> = Box::new(buf);
return Ok(MountIter::new_from_readable(buf)?);
}

Ok(Box::new(BufReader::new(File::open(path)?)))
let file = File::open(safe_mount_iter.mounts_filepath.as_path())?;
let file: Box<dyn io::Read> = Box::new(file);
Ok(MountIter::new_from_readable(file)?)
}
}

0 comments on commit eaba6ad

Please sign in to comment.