use crate::{
error::{Error, ErrorExt, ErrorImpl, ErrorKind},
utils::{kernel_version, FdExt},
};
use std::{
io::{BufRead, BufReader, Read, Seek, SeekFrom},
os::unix::{
fs::MetadataExt,
io::{AsFd, AsRawFd},
},
str::FromStr,
};
fn parse_and_find_fdinfo_field<T>(
rdr: &mut impl Read,
want_field_name: &str,
) -> Result<Option<T>, Error>
where
T: FromStr,
T::Err: Into<Error>,
{
let rdr = BufReader::new(rdr);
let want_prefix = format!("{want_field_name}:");
for line in rdr.lines() {
let line = line.map_err(|err| ErrorImpl::OsError {
operation: "read line from fdinfo".into(),
source: err,
})?;
if let Some(value) = line.strip_prefix(&want_prefix) {
return value.trim().parse().map(Some).map_err(Into::into);
}
}
Ok(None)
}
pub(crate) fn fd_get_verify_fdinfo<T>(
rdr: &mut (impl Read + Seek),
fd: impl AsFd,
want_field_name: &str,
) -> Result<Option<T>, Error>
where
T: FromStr,
T::Err: Into<Error>,
{
let fd = fd.as_fd();
let actual_ino: u64 = fd.metadata().wrap("get inode number of fd")?.ino();
let fdinfo_ino: Option<u64> =
match parse_and_find_fdinfo_field(rdr, "ino").map_err(|err| (err.kind(), err)) {
Ok(Some(ino)) => Ok(Some(ino)),
Ok(None) => {
if kernel_version::is_gte!(5, 14) {
Err(ErrorImpl::SafetyViolation {
description: format!(
r#"fd {:?} has a fake fdinfo: missing "ino" field"#,
fd.as_raw_fd(),
)
.into(),
})?;
}
Ok(None)
}
Err((ErrorKind::InternalError, _)) => Err(ErrorImpl::SafetyViolation {
description: format!(
r#"fd {:?} has a fake fdinfo: invalid "ino" field"#,
fd.as_raw_fd(),
)
.into(),
}
.into()),
Err((_, err)) => Err(err),
}?;
if let Some(fdinfo_ino) = fdinfo_ino {
if actual_ino != fdinfo_ino {
Err(ErrorImpl::SafetyViolation {
description: format!(
"fd {:?} has a fake fdinfo: wrong inode number (ino is {fdinfo_ino:X} not {actual_ino:X})",
fd.as_raw_fd()
)
.into(),
})?;
}
}
rdr.seek(SeekFrom::Start(0))
.map_err(|err| ErrorImpl::OsError {
operation: format!("seek to start of fd {:?} fdinfo", fd.as_raw_fd()).into(),
source: err,
})?;
parse_and_find_fdinfo_field(rdr, want_field_name)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{error::ErrorKind, syscalls};
use std::{
fmt::Debug,
fs::File,
io::Cursor,
net::{AddrParseError, Ipv4Addr, SocketAddrV4},
};
use anyhow::{bail, Context, Error};
use indoc::{formatdoc, indoc};
use pretty_assertions::{assert_matches, Comparison};
impl From<AddrParseError> for ErrorImpl {
fn from(err: AddrParseError) -> Self {
unimplemented!("this test-only impl is only needed for type reasons -- {err:?}")
}
}
fn check_parse_and_find_fdinfo_field<T>(
rdr: &mut impl Read,
want_field_name: &str,
expected: Result<Option<T>, ErrorKind>,
) -> Result<(), Error>
where
T: FromStr + PartialEq + Debug,
T::Err: Into<Error> + Into<ErrorImpl>,
{
let got = match parse_and_find_fdinfo_field(rdr, want_field_name) {
Ok(res) => Ok(res),
Err(err) => {
if expected.is_ok() {
eprintln!("unexpected error: {err:?}");
}
Err(err.kind())
}
};
if got != expected {
eprintln!("{}", Comparison::new(&got, &expected));
bail!(
"unexpected result when parsing {want_field_name:?} field (as {:?}) from fdinfo (should be {expected:?})",
std::any::type_name::<T>()
);
}
Ok(())
}
#[test]
fn parse_and_find_fdinfo_field_basic() {
const FAKE_FDINFO: &[u8] = indoc! {b"
foo:\t123456
bar_baz:\t1
invalid line that should be skipped
lorem ipsum: dolor sit amet
: leading colon with no tab
multiple: colons: in: one: line:
repeated colons:: are not:: deduped
repeated:\t1
repeated:\t2
repeated:\t3
last:\t \t127.0.0.1:8080\t\t
"};
check_parse_and_find_fdinfo_field(&mut &FAKE_FDINFO[..], "foo", Ok(Some(123456u64)))
.expect(r#"parse "foo: 123456" line"#);
check_parse_and_find_fdinfo_field(&mut &FAKE_FDINFO[..], "bar_baz", Ok(Some(1u8)))
.expect(r#"parse "bar_baz: 1" line"#);
check_parse_and_find_fdinfo_field(
&mut &FAKE_FDINFO[..],
"",
Ok(Some("leading colon with no tab".to_string())),
)
.expect(r#"parse ": leading colon with no tab" line"#);
check_parse_and_find_fdinfo_field(&mut &FAKE_FDINFO[..], "repeated", Ok(Some(1i16)))
.expect(r#"first matching entry should be returned"#);
check_parse_and_find_fdinfo_field(
&mut &FAKE_FDINFO[..],
"last",
Ok(Some(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8080))),
)
.expect(r#"first matching entry should be returned"#);
check_parse_and_find_fdinfo_field::<u32>(&mut &FAKE_FDINFO[..], "does_not_exist", Ok(None))
.expect(r#"non-existent field"#);
check_parse_and_find_fdinfo_field::<String>(
&mut &FAKE_FDINFO[..],
"lorem ipsum",
Ok(Some("dolor sit amet".to_string())),
)
.expect(r#"parse "lorem ipsum: dolor sit amet" line"#);
check_parse_and_find_fdinfo_field::<String>(&mut &FAKE_FDINFO[..], "lorem ipsu", Ok(None))
.expect(r#"parse "lorem ipsum: dolor sit amet" line"#);
check_parse_and_find_fdinfo_field::<String>(
&mut &FAKE_FDINFO[..],
"lorem ipsum:",
Ok(None),
)
.expect(r#"parse "lorem ipsum: dolor sit amet" line"#);
check_parse_and_find_fdinfo_field::<String>(
&mut &FAKE_FDINFO[..],
"lorem ipsum: dolor sit amet",
Ok(None),
)
.expect(r#"parse "lorem ipsum: dolor sit amet" line"#);
check_parse_and_find_fdinfo_field::<String>(
&mut &FAKE_FDINFO[..],
"lorem ipsum: dolor sit amet",
Ok(None),
)
.expect(r#"parse "lorem ipsum: dolor sit amet" line"#);
check_parse_and_find_fdinfo_field::<String>(
&mut &FAKE_FDINFO[..],
"multiple: colons: in: one: line:",
Ok(None),
)
.expect(r#"parse "multiple: colons: in: one: line:" line"#);
check_parse_and_find_fdinfo_field::<String>(
&mut &FAKE_FDINFO[..],
"multiple: colons: in: one: line",
Ok(Some("".to_string())),
)
.expect(r#"parse "multiple: colons: in: one: line:" line"#);
check_parse_and_find_fdinfo_field::<String>(
&mut &FAKE_FDINFO[..],
"multiple: colons: in: one",
Ok(Some("line:".to_string())),
)
.expect(r#"parse "multiple: colons: in: one: line:" line"#);
check_parse_and_find_fdinfo_field::<String>(
&mut &FAKE_FDINFO[..],
"multiple: colons: in",
Ok(Some("one: line:".to_string())),
)
.expect(r#"parse "multiple: colons: in: one: line:" line"#);
check_parse_and_find_fdinfo_field::<String>(
&mut &FAKE_FDINFO[..],
"multiple: colons",
Ok(Some("in: one: line:".to_string())),
)
.expect(r#"parse "multiple: colons: in: one: line:" line"#);
check_parse_and_find_fdinfo_field::<String>(
&mut &FAKE_FDINFO[..],
"multiple",
Ok(Some("colons: in: one: line:".to_string())),
)
.expect(r#"parse "multiple: colons: in: one: line:" line"#);
check_parse_and_find_fdinfo_field::<String>(
&mut &FAKE_FDINFO[..],
"repeated colons:: are not:",
Ok(Some("deduped".to_string())),
)
.expect(r#"parse "repeated colons:: are not:: deduped" line"#);
check_parse_and_find_fdinfo_field::<String>(
&mut &FAKE_FDINFO[..],
"repeated colons:: are not",
Ok(Some(": deduped".to_string())),
)
.expect(r#"parse "repeated colons:: are not:: deduped" line"#);
check_parse_and_find_fdinfo_field::<String>(
&mut &FAKE_FDINFO[..],
"repeated colons:: are not::",
Ok(None),
)
.expect(r#"parse "repeated colons:: are not:: deduped" line"#);
}
#[test]
fn parse_and_find_fdinfo_field_parse_error() {
const FAKE_FDINFO: &[u8] = indoc! {b"
nonint:\tnonint
nonint_leading:\ta123
nonint_trailing:\t456a
nonuint: -15
"};
check_parse_and_find_fdinfo_field::<isize>(
&mut &FAKE_FDINFO[..],
"nonint",
Err(ErrorKind::InternalError),
)
.expect(r#"parse "nonint: nonint" line"#);
assert_matches!(
parse_and_find_fdinfo_field::<isize>(&mut &FAKE_FDINFO[..], "nonint")
.expect_err("should not be able to parse fdinfo for 'nonint'")
.into_inner(),
ErrorImpl::ParseIntError(_),
"non-integer 'nonint' field should fail with ParseIntError"
);
check_parse_and_find_fdinfo_field::<i32>(
&mut &FAKE_FDINFO[..],
"nonint_leading",
Err(ErrorKind::InternalError),
)
.expect(r#"parse "nonint_leading: a123" line"#);
assert_matches!(
parse_and_find_fdinfo_field::<i32>(&mut &FAKE_FDINFO[..], "nonint_leading")
.expect_err("should not be able to parse fdinfo for 'nonint_leading'")
.into_inner(),
ErrorImpl::ParseIntError(_),
"non-integer 'nonint_leading' field should fail with ParseIntError"
);
check_parse_and_find_fdinfo_field::<i64>(
&mut &FAKE_FDINFO[..],
"nonint_trailing",
Err(ErrorKind::InternalError),
)
.expect(r#"parse "nonint_trailing: 456a" line"#);
assert_matches!(
parse_and_find_fdinfo_field::<i64>(&mut &FAKE_FDINFO[..], "nonint_trailing")
.expect_err("should not be able to parse fdinfo for 'nonint_trailing'")
.into_inner(),
ErrorImpl::ParseIntError(_),
"non-integer 'nonint_trailing' field should fail with ParseIntError"
);
check_parse_and_find_fdinfo_field::<isize>(&mut &FAKE_FDINFO[..], "nonuint", Ok(Some(-15)))
.expect(r#"parse "nonuint: -15" line"#);
check_parse_and_find_fdinfo_field::<usize>(
&mut &FAKE_FDINFO[..],
"nonuint",
Err(ErrorKind::InternalError),
)
.expect(r#"parse "nonuint: -15" line"#);
assert_matches!(
parse_and_find_fdinfo_field::<usize>(&mut &FAKE_FDINFO[..], "nonuint")
.expect_err("should not be able to parse fdinfo for 'nonuint'")
.into_inner(),
ErrorImpl::ParseIntError(_),
"signed integer 'nonuint' field parsing as unsigned should fail with ParseIntError"
);
}
fn check_fd_get_verify_fdinfo<T>(
rdr: &mut (impl Read + Seek),
fd: impl AsFd,
want_field_name: &str,
expected: Result<Option<T>, ErrorKind>,
) -> Result<(), Error>
where
T: FromStr + PartialEq + Debug,
T::Err: Into<Error> + Into<ErrorImpl>,
{
let got = match fd_get_verify_fdinfo(rdr, fd, want_field_name) {
Ok(res) => Ok(res),
Err(err) => {
if expected.is_ok() {
eprintln!("unexpected error: {err:?}");
}
Err(err.kind())
}
};
if got != expected {
eprintln!("{}", Comparison::new(&got, &expected));
bail!(
"unexpected result when parsing {want_field_name:?} field (as {:?}) from fdinfo (should be {expected:?})",
std::any::type_name::<T>()
);
}
Ok(())
}
#[test]
fn fd_get_verify_fdinfo_real_ino() -> Result<(), Error> {
let file = File::open("/").context("open dummy file")?;
let real_ino = file.metadata().context("get dummy file metadata")?.ino();
let fake_fdinfo = formatdoc! {"
ino:\t{real_ino}
mnt_id: 12345
"};
check_fd_get_verify_fdinfo(
&mut Cursor::new(&fake_fdinfo),
&file,
"mnt_id",
Ok(Some(12345)),
)
.expect(r#"get "mnt_id" from fdinfo with correct ino"#);
check_fd_get_verify_fdinfo(
&mut Cursor::new(&fake_fdinfo),
&file,
"ino",
Ok(Some(real_ino)),
)
.expect(r#"get "ino" from fdinfo with correct ino"#);
check_fd_get_verify_fdinfo::<String>(
&mut Cursor::new(&fake_fdinfo),
&file,
"non_exist",
Ok(None),
)
.expect(r#"get "non_exist" from fdinfo with correct ino"#);
Ok(())
}
#[test]
fn fd_get_verify_fdinfo_bad_ino() -> Result<(), Error> {
let file = File::open(".").context("open dummy file")?;
let fake_ino = file.metadata().context("get dummy file metadata")?.ino() + 32;
let fake_fdinfo = formatdoc! {"
ino:\t{fake_ino}
mnt_id: 12345
"};
check_fd_get_verify_fdinfo::<u64>(
&mut Cursor::new(&fake_fdinfo),
&file,
"mnt_id",
Err(ErrorKind::SafetyViolation),
)
.expect(r#"get "mnt_id" from fdinfo with incorrect ino"#);
check_fd_get_verify_fdinfo::<u64>(
&mut Cursor::new(&fake_fdinfo),
&file,
"ino",
Err(ErrorKind::SafetyViolation),
)
.expect(r#"get "ino" from fdinfo with incorrect ino"#);
check_fd_get_verify_fdinfo::<String>(
&mut Cursor::new(&fake_fdinfo),
&file,
"non_exist",
Err(ErrorKind::SafetyViolation),
)
.expect(r#"get "non_exist" from fdinfo with incorrect ino"#);
Ok(())
}
#[test]
fn fd_get_verify_fdinfo_no_ino_kernel514() -> Result<(), Error> {
if !kernel_version::is_gte!(5, 14) {
return Ok(());
}
const FAKE_FDINFO: &[u8] = indoc! {b"
foo: abcdef
mnt_id: 12345
"};
let file = File::open(".").context("open dummy file")?;
check_fd_get_verify_fdinfo::<u64>(
&mut Cursor::new(&FAKE_FDINFO),
&file,
"mnt_id",
Err(ErrorKind::SafetyViolation),
)
.expect(r#"get "mnt_id" from fdinfo with missing ino"#);
check_fd_get_verify_fdinfo::<u64>(
&mut Cursor::new(&FAKE_FDINFO),
&file,
"ino",
Err(ErrorKind::SafetyViolation),
)
.expect(r#"get "ino" from fdinfo with missing ino"#);
check_fd_get_verify_fdinfo::<String>(
&mut Cursor::new(&FAKE_FDINFO),
&file,
"non_exist",
Err(ErrorKind::SafetyViolation),
)
.expect(r#"get "non_exist" from fdinfo with missing ino"#);
Ok(())
}
#[test]
fn fd_get_verify_fdinfo_no_ino_oldkernel() -> Result<(), Error> {
let _persona_guard = syscalls::scoped_personality(syscalls::PER_UNAME26);
const FAKE_FDINFO: &[u8] = indoc! {b"
foo: abcdef
mnt_id: 12345
"};
let file = File::open(".").context("open dummy file")?;
check_fd_get_verify_fdinfo::<u64>(
&mut Cursor::new(&FAKE_FDINFO),
&file,
"mnt_id",
Ok(Some(12345)),
)
.expect(r#"get "mnt_id" from fdinfo with missing ino (pre-5.14)"#);
check_fd_get_verify_fdinfo::<u64>(&mut Cursor::new(&FAKE_FDINFO), &file, "ino", Ok(None))
.expect(r#"get "ino" from fdinfo with missing ino (pre-5.14)"#);
check_fd_get_verify_fdinfo::<String>(
&mut Cursor::new(&FAKE_FDINFO),
&file,
"non_exist",
Ok(None),
)
.expect(r#"get "non_exist" from fdinfo with missing ino (pre-5.14)"#);
Ok(())
}
#[test]
fn fd_get_verify_fdinfo_wrongtype_ino() -> Result<(), Error> {
const FAKE_FDINFO_I64: &[u8] = indoc! {b"
ino: -1234
mnt_id: 12345
"};
const FAKE_FDINFO_STR: &[u8] = indoc! {b"
ino: foobar
mnt_id: 12345
"};
let file = File::open(".").context("open dummy file")?;
for fake_fdinfo in [&FAKE_FDINFO_I64, &FAKE_FDINFO_STR] {
check_fd_get_verify_fdinfo::<u64>(
&mut Cursor::new(fake_fdinfo),
&file,
"mnt_id",
Err(ErrorKind::SafetyViolation),
)
.expect(r#"get "mnt_id" from fdinfo with non-u64 ino"#);
check_fd_get_verify_fdinfo::<u64>(
&mut Cursor::new(fake_fdinfo),
&file,
"ino",
Err(ErrorKind::SafetyViolation),
)
.expect(r#"get "ino" from fdinfo with non-u64 ino"#);
check_fd_get_verify_fdinfo::<String>(
&mut Cursor::new(fake_fdinfo),
&file,
"non_exist",
Err(ErrorKind::SafetyViolation),
)
.expect(r#"get "non_exist" from fdinfo with non-u64 ino"#);
}
Ok(())
}
}