Repository: aya-rs/aya (language: rust)

Issue:
Kernel version detection is broken on WSL
Using aya from with the yesterday's commit fails with
```
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ParseError(Parse("90.1", "patch"))', /home/debian/.cargo/git/checkouts/aya-c55fbc69175ac116/242d8c3/aya/src/maps/mod.rs:504:55
```

when running on Windows/WSL2 Debian 12.

`uname -r` for this system:
```
5.15.90.1-microsoft-standard-WSL2
```

Looks like the reworked kernel version detection from a couple of days ago doesn't work with this string.


Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 10a438d66..580e0a483 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,7 +24,6 @@ resolver = "2"
 
 default-members = [
     "aya",
-    "aya-bpf-macros",
     "aya-log",
     "aya-log-common",
     "aya-log-parser",
diff --git a/aya-obj/Cargo.toml b/aya-obj/Cargo.toml
index d2a5fb448..305e60083 100644
--- a/aya-obj/Cargo.toml
+++ b/aya-obj/Cargo.toml
@@ -2,7 +2,7 @@
 name = "aya-obj"
 version = "0.1.0"
 description = "An eBPF object file parsing library with BTF and relocation support."
-keywords = ["ebpf", "bpf", "btf", "elf", "object"]
+keywords = ["bpf", "btf", "ebpf", "elf", "object"]
 license = "MIT OR Apache-2.0"
 authors = ["The Aya Contributors"]
 repository = "https://github.com/aya-rs/aya"
@@ -13,13 +13,16 @@ edition = "2021"
 [dependencies]
 bytes = "1"
 log = "0.4"
-object = { version = "0.31", default-features = false, features = ["read_core", "elf"] }
+object = { version = "0.31", default-features = false, features = [
+    "elf",
+    "read_core",
+] }
 hashbrown = { version = "0.14" }
 thiserror = { version = "1", default-features = false }
 core-error = { version = "0.0.0" }
 
 [dev-dependencies]
-matches = "0.1.8"
+assert_matches = "1.5.0"
 rbpf = "0.2.0"
 
 [features]
diff --git a/aya-obj/src/obj.rs b/aya-obj/src/obj.rs
index d3ccb2651..3c5ff1267 100644
--- a/aya-obj/src/obj.rs
+++ b/aya-obj/src/obj.rs
@@ -1486,7 +1486,7 @@ pub fn copy_instructions(data: &[u8]) -> Result<Vec<bpf_insn>, ParseError> {
 #[cfg(test)]
 mod tests {
     use alloc::vec;
-    use matches::assert_matches;
+    use assert_matches::assert_matches;
     use object::Endianness;
 
     use super::*;
diff --git a/aya/Cargo.toml b/aya/Cargo.toml
index 28054e445..d2a39a770 100644
--- a/aya/Cargo.toml
+++ b/aya/Cargo.toml
@@ -24,13 +24,12 @@ object = { version = "0.31", default-features = false, features = [
     "std",
 ] }
 parking_lot = { version = "0.12.0", features = ["send_guard"] }
-text_io = "0.1.12"
 thiserror = "1"
 tokio = { version = "1.24.0", features = ["rt"], optional = true }
 
 [dev-dependencies]
+assert_matches = "1.5.0"
 futures = { version = "0.3.12", default-features = false, features = ["std"] }
-matches = "0.1.8"
 tempfile = "3"
 
 [features]
diff --git a/aya/src/maps/bloom_filter.rs b/aya/src/maps/bloom_filter.rs
index 315fe7d45..6135da5b1 100644
--- a/aya/src/maps/bloom_filter.rs
+++ b/aya/src/maps/bloom_filter.rs
@@ -88,8 +88,8 @@ mod tests {
         obj::{self, maps::LegacyMap, BpfSectionKind},
         sys::{override_syscall, SysResult, Syscall},
     };
+    use assert_matches::assert_matches;
     use libc::{EFAULT, ENOENT};
-    use matches::assert_matches;
     use std::io;
 
     fn new_obj_map() -> obj::Map {
diff --git a/aya/src/maps/hash_map/hash_map.rs b/aya/src/maps/hash_map/hash_map.rs
index b37e5c8f3..34f0b8516 100644
--- a/aya/src/maps/hash_map/hash_map.rs
+++ b/aya/src/maps/hash_map/hash_map.rs
@@ -107,8 +107,8 @@ impl<T: Borrow<MapData>, K: Pod, V: Pod> IterableMap<K, V> for HashMap<T, K, V>
 mod tests {
     use std::io;
 
+    use assert_matches::assert_matches;
     use libc::{EFAULT, ENOENT};
-    use matches::assert_matches;
 
     use crate::{
         bpf_map_def,
diff --git a/aya/src/maps/lpm_trie.rs b/aya/src/maps/lpm_trie.rs
index f2fb10e3a..b2e21e888 100644
--- a/aya/src/maps/lpm_trie.rs
+++ b/aya/src/maps/lpm_trie.rs
@@ -207,8 +207,8 @@ mod tests {
         obj::{self, maps::LegacyMap, BpfSectionKind},
         sys::{override_syscall, SysResult, Syscall},
     };
+    use assert_matches::assert_matches;
     use libc::{EFAULT, ENOENT};
-    use matches::assert_matches;
     use std::{io, mem, net::Ipv4Addr};
 
     fn new_obj_map() -> obj::Map {
diff --git a/aya/src/maps/mod.rs b/aya/src/maps/mod.rs
index 7a455861e..fca421104 100644
--- a/aya/src/maps/mod.rs
+++ b/aya/src/maps/mod.rs
@@ -842,8 +842,8 @@ impl<T: Pod> Deref for PerCpuValues<T> {
 
 #[cfg(test)]
 mod tests {
+    use assert_matches::assert_matches;
     use libc::EFAULT;
-    use matches::assert_matches;
 
     use crate::{
         bpf_map_def,
diff --git a/aya/src/maps/perf/perf_buffer.rs b/aya/src/maps/perf/perf_buffer.rs
index c5b47e3a3..e32be94d9 100644
--- a/aya/src/maps/perf/perf_buffer.rs
+++ b/aya/src/maps/perf/perf_buffer.rs
@@ -319,7 +319,7 @@ mod tests {
         generated::perf_event_mmap_page,
         sys::{override_syscall, Syscall, TEST_MMAP_RET},
     };
-    use matches::assert_matches;
+    use assert_matches::assert_matches;
     use std::{fmt::Debug, mem};
 
     const PAGE_SIZE: usize = 4096;
diff --git a/aya/src/programs/links.rs b/aya/src/programs/links.rs
index 1bfc6d0b5..0cd9420d2 100644
--- a/aya/src/programs/links.rs
+++ b/aya/src/programs/links.rs
@@ -353,7 +353,7 @@ pub enum LinkError {
 
 #[cfg(test)]
 mod tests {
-    use matches::assert_matches;
+    use assert_matches::assert_matches;
     use std::{cell::RefCell, fs::File, mem, os::unix::io::AsRawFd, rc::Rc};
     use tempfile::tempdir;
 
diff --git a/aya/src/util.rs b/aya/src/util.rs
index fb1026afd..2e6e01ecb 100644
--- a/aya/src/util.rs
+++ b/aya/src/util.rs
@@ -25,18 +25,14 @@ pub struct KernelVersion {
     pub(crate) patch: u16,
 }
 
-/// An error encountered while fetching the current kernel version.
 #[derive(thiserror::Error, Debug)]
-pub enum CurrentKernelVersionError {
-    /// The kernel version string could not be read.
+enum CurrentKernelVersionError {
     #[error("failed to read kernel version")]
-    IOError(#[from] io::Error),
-    /// The kernel version string could not be parsed.
+    IO(#[from] io::Error),
     #[error("failed to parse kernel version")]
-    ParseError(#[from] text_io::Error),
-    /// The kernel version string was not valid UTF-8.
+    ParseError(String),
     #[error("kernel version string is not valid UTF-8")]
-    Utf8Error(#[from] Utf8Error),
+    Utf8(#[from] Utf8Error),
 }
 
 impl KernelVersion {
@@ -74,7 +70,7 @@ impl KernelVersion {
 
     fn get_ubuntu_kernel_version() -> Result<Option<Self>, CurrentKernelVersionError> {
         const UBUNTU_KVER_FILE: &str = "/proc/version_signature";
-        let s = match fs::read(UBUNTU_KVER_FILE) {
+        let s = match fs::read_to_string(UBUNTU_KVER_FILE) {
             Ok(s) => s,
             Err(e) => {
                 if e.kind() == io::ErrorKind::NotFound {
@@ -83,13 +79,16 @@ impl KernelVersion {
                 return Err(e.into());
             }
         };
-        let ubuntu: String;
-        let ubuntu_version: String;
-        let major: u8;
-        let minor: u8;
-        let patch: u16;
-        text_io::try_scan!(s.iter().copied() => "{} {} {}.{}.{}\n", ubuntu, ubuntu_version, major, minor, patch);
-        Ok(Some(Self::new(major, minor, patch)))
+        let mut parts = s.split_terminator(char::is_whitespace);
+        let mut next = || {
+            parts
+                .next()
+                .ok_or_else(|| CurrentKernelVersionError::ParseError(s.to_string()))
+        };
+        let _ubuntu: &str = next()?;
+        let _ubuntu_version: &str = next()?;
+        let kernel_version_string = next()?;
+        Self::parse_kernel_version_string(kernel_version_string).map(Some)
     }
 
     fn get_debian_kernel_version(
@@ -99,17 +98,13 @@ impl KernelVersion {
         //
         // The length of the arrays in a struct utsname is unspecified (see NOTES); the fields are
         // terminated by a null byte ('\0').
-        let p = unsafe { CStr::from_ptr(info.version.as_ptr()) };
-        let p = p.to_str()?;
-        let p = match p.split_once("Debian ") {
+        let s = unsafe { CStr::from_ptr(info.version.as_ptr()) };
+        let s = s.to_str()?;
+        let kernel_version_string = match s.split_once("Debian ") {
             Some((_prefix, suffix)) => suffix,
             None => return Ok(None),
         };
-        let major: u8;
-        let minor: u8;
-        let patch: u16;
-        text_io::try_scan!(p.bytes() => "{}.{}.{}", major, minor, patch);
-        Ok(Some(Self::new(major, minor, patch)))
+        Self::parse_kernel_version_string(kernel_version_string).map(Some)
     }
 
     fn get_kernel_version() -> Result<Self, CurrentKernelVersionError> {
@@ -130,17 +125,23 @@ impl KernelVersion {
         //
         // The length of the arrays in a struct utsname is unspecified (see NOTES); the fields are
         // terminated by a null byte ('\0').
-        let p = unsafe { CStr::from_ptr(info.release.as_ptr()) };
-        let p = p.to_str()?;
-        // Unlike sscanf, text_io::try_scan! does not stop at the first non-matching character.
-        let p = match p.split_once(|c: char| c != '.' && !c.is_ascii_digit()) {
-            Some((prefix, _suffix)) => prefix,
-            None => p,
-        };
-        let major: u8;
-        let minor: u8;
-        let patch: u16;
-        text_io::try_scan!(p.bytes() => "{}.{}.{}", major, minor, patch);
+        let s = unsafe { CStr::from_ptr(info.release.as_ptr()) };
+        let s = s.to_str()?;
+        Self::parse_kernel_version_string(s)
+    }
+
+    fn parse_kernel_version_string(s: &str) -> Result<Self, CurrentKernelVersionError> {
+        fn parse<T: FromStr<Err = std::num::ParseIntError>>(s: Option<&str>) -> Option<T> {
+            match s.map(str::parse).transpose() {
+                Ok(option) => option,
+                Err(std::num::ParseIntError { .. }) => None,
+            }
+        }
+        let error = || CurrentKernelVersionError::ParseError(s.to_string());
+        let mut parts = s.split(|c: char| c == '.' || !c.is_ascii_digit());
+        let major = parse(parts.next()).ok_or_else(error)?;
+        let minor = parse(parts.next()).ok_or_else(error)?;
+        let patch = parse(parts.next()).ok_or_else(error)?;
         Ok(Self::new(major, minor, patch))
     }
 }
@@ -331,6 +332,19 @@ pub(crate) fn bytes_of_slice<T: Pod>(val: &[T]) -> &[u8] {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use assert_matches::assert_matches;
+
+    #[test]
+    fn test_parse_kernel_version_string() {
+        // WSL.
+        assert_matches!(KernelVersion::parse_kernel_version_string("5.15.90.1-microsoft-standard-WSL2"), Ok(kernel_version) => {
+            assert_eq!(kernel_version, KernelVersion::new(5, 15, 90))
+        });
+        // uname -r on Fedora.
+        assert_matches!(KernelVersion::parse_kernel_version_string("6.3.11-200.fc38.x86_64"), Ok(kernel_version) => {
+            assert_eq!(kernel_version, KernelVersion::new(6, 3, 11))
+        });
+    }
 
     #[test]
     fn test_parse_online_cpus() {


Test patch:
diff --git a/test/integration-test/Cargo.toml b/test/integration-test/Cargo.toml
index 8fb11bda0..a606ee1bf 100644
--- a/test/integration-test/Cargo.toml
+++ b/test/integration-test/Cargo.toml
@@ -6,12 +6,12 @@ publish = false
 
 [dependencies]
 anyhow = "1"
+assert_matches = "1.5.0"
 aya = { path = "../../aya" }
 aya-log = { path = "../../aya-log" }
 aya-obj = { path = "../../aya-obj" }
 libc = { version = "0.2.105" }
 log = "0.4"
-matches = "0.1.8"
 object = { version = "0.31", default-features = false, features = [
     "elf",
     "read_core",
diff --git a/test/integration-test/src/tests/rbpf.rs b/test/integration-test/src/tests/rbpf.rs
index 1f2fe2cbd..790b2fdc7 100644
--- a/test/integration-test/src/tests/rbpf.rs
+++ b/test/integration-test/src/tests/rbpf.rs
@@ -8,7 +8,7 @@ fn run_with_rbpf() {
     let object = Object::parse(crate::PASS).unwrap();
 
     assert_eq!(object.programs.len(), 1);
-    matches::assert_matches!(object.programs["pass"].section, ProgramSection::Xdp { .. });
+    assert_matches::assert_matches!(object.programs["pass"].section, ProgramSection::Xdp { .. });
     assert_eq!(object.programs["pass"].section.name(), "pass");
 
     let instructions = &object
@@ -35,7 +35,7 @@ fn use_map_with_rbpf() {
     let mut object = Object::parse(crate::MULTIMAP_BTF).unwrap();
 
     assert_eq!(object.programs.len(), 1);
-    matches::assert_matches!(
+    assert_matches::assert_matches!(
         object.programs["tracepoint"].section,
         ProgramSection::TracePoint { .. }
     );