From f82706d394d956856fdb17a4051f1ad47ba0defc Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 18 Jun 2019 22:43:52 -0400 Subject: [PATCH] Support registering a FileOperations for a chrdev --- build.rs | 33 +++++++- src/bindings_helper.h | 2 + src/c_types.rs | 3 + src/chrdev.rs | 152 ++++++++++++++++++++++++++++++++++-- src/lib.rs | 2 +- src/user_ptr.rs | 4 + tests/chrdev/Cargo.toml | 16 ++++ tests/chrdev/src/lib.rs | 47 +++++++++++ tests/chrdev/tests/tests.rs | 74 ++++++++++++++++++ 9 files changed, 325 insertions(+), 8 deletions(-) create mode 100644 tests/chrdev/Cargo.toml create mode 100644 tests/chrdev/src/lib.rs create mode 100644 tests/chrdev/tests/tests.rs diff --git a/build.rs b/build.rs index 03670a1e..c96331b1 100644 --- a/build.rs +++ b/build.rs @@ -2,12 +2,16 @@ use bindgen; use cc; use shlex; -use std::env; +use std::io::{BufRead, BufReader}; use std::path::PathBuf; use std::process::Command; +use std::{env, fs}; const INCLUDED_TYPES: &[&str] = &["file_system_type", "mode_t", "umode_t", "ctl_table"]; const INCLUDED_FUNCTIONS: &[&str] = &[ + "cdev_add", + "cdev_init", + "cdev_del", "register_filesystem", "unregister_filesystem", "krealloc", @@ -35,6 +39,7 @@ const INCLUDED_VARS: &[&str] = &[ "BINDINGS_GFP_KERNEL", "KERN_INFO", "VERIFY_WRITE", + "LINUX_VERSION_CODE", ]; const OPAQUE_TYPES: &[&str] = &[ // These need to be opaque because they're both packed and aligned, which rustc @@ -44,6 +49,30 @@ const OPAQUE_TYPES: &[&str] = &[ "xregs_state", ]; +fn kernel_version_code(major: u8, minor: u8, patch: u8) -> u64 { + ((major as u64) << 16) | ((minor as u64) << 8) | (patch as u64) +} + +fn handle_kernel_version_cfg(bindings_path: &PathBuf) { + let f = BufReader::new(fs::File::open(bindings_path).unwrap()); + let mut version = None; + for line in f.lines() { + let line = line.unwrap(); + if line.starts_with("pub const LINUX_VERSION_CODE") { + let mut parts = line.split(" = "); + parts.next(); + let raw_version = parts.next().unwrap(); + // Remove the trailing semi-colon + version = Some(raw_version[..raw_version.len() - 1].parse::().unwrap()); + break; + } + } + let version = version.expect("Couldn't find kernel version"); + if version >= kernel_version_code(4, 15, 0) { + println!("cargo:rustc-cfg=kernel_4_15_0_or_greataer") + } +} + fn main() { println!("rerun-if-env-changed=KDIR"); let output = String::from_utf8( @@ -90,6 +119,8 @@ fn main() { .write_to_file(out_path.join("bindings.rs")) .expect("Couldn't write bindings!"); + handle_kernel_version_cfg(&out_path.join("bindings.rs")); + let mut builder = cc::Build::new(); println!("cargo:rerun-if-env-changed=CLANG"); builder.compiler(env::var("CLANG").unwrap_or("clang".to_string())); diff --git a/src/bindings_helper.h b/src/bindings_helper.h index 4977a3b1..df0e387d 100644 --- a/src/bindings_helper.h +++ b/src/bindings_helper.h @@ -1,7 +1,9 @@ +#include #include #include #include #include +#include // Bindgen gets confused at certain things // diff --git a/src/c_types.rs b/src/c_types.rs index dfa21261..d7c49a1b 100644 --- a/src/c_types.rs +++ b/src/c_types.rs @@ -11,6 +11,9 @@ pub type c_ulong = u64; pub type c_ulonglong = u64; pub type c_ushort = u16; pub type c_schar = i8; +pub type c_size_t = usize; +pub type c_ssize_t = isize; + // See explanation in rust/src/libstd/os/raw.rs #[repr(u8)] pub enum c_void { diff --git a/src/chrdev.rs b/src/chrdev.rs index ac820d34..969bc0c3 100644 --- a/src/chrdev.rs +++ b/src/chrdev.rs @@ -1,25 +1,44 @@ use core::convert::TryInto; use core::ops::Range; +use core::{mem, ptr}; + +use alloc::boxed::Box; +use alloc::vec; +use alloc::vec::Vec; use crate::bindings; use crate::c_types; -use crate::error; +use crate::error::{Error, KernelResult}; +use crate::user_ptr::{UserSlicePtr, UserSlicePtrWriter}; -pub fn builder(name: &'static str, minors: Range) -> error::KernelResult { +pub fn builder(name: &'static str, minors: Range) -> KernelResult { if !name.ends_with('\x00') { - return Err(error::Error::EINVAL); + return Err(Error::EINVAL); } - return Ok(Builder { name, minors }); + return Ok(Builder { + name, + minors, + file_ops: vec![], + }); } pub struct Builder { name: &'static str, minors: Range, + file_ops: Vec<&'static FileOperationsVtable>, } impl Builder { - pub fn build(self) -> error::KernelResult { + pub fn register_device(mut self) -> Builder { + if self.file_ops.len() >= self.minors.len() { + panic!("More devices registered than minor numbers allocated.") + } + self.file_ops.push(&T::VTABLE); + return self; + } + + pub fn build(self) -> KernelResult { let mut dev: bindings::dev_t = 0; let res = unsafe { bindings::alloc_chrdev_region( @@ -30,11 +49,32 @@ impl Builder { ) }; if res != 0 { - return Err(error::Error::from_kernel_errno(res)); + return Err(Error::from_kernel_errno(res)); + } + + // Turn this into a boxed slice immediately because the kernel stores pointers into it, and + // so that data should never be moved. + let mut cdevs = vec![unsafe { mem::zeroed() }; self.file_ops.len()].into_boxed_slice(); + for (i, file_op) in self.file_ops.iter().enumerate() { + unsafe { + bindings::cdev_init(&mut cdevs[i], &file_op.0); + cdevs[i].owner = &mut bindings::__this_module; + let rc = bindings::cdev_add(&mut cdevs[i], dev + i as bindings::dev_t, 1); + if rc != 0 { + // Clean up the ones that were allocated. + for j in 0..=i { + bindings::cdev_del(&mut cdevs[j]); + } + bindings::unregister_chrdev_region(dev, self.minors.len() as _); + return Err(Error::from_kernel_errno(rc)); + } + } } + return Ok(Registration { dev, count: self.minors.len(), + cdevs, }); } } @@ -42,12 +82,112 @@ impl Builder { pub struct Registration { dev: bindings::dev_t, count: usize, + cdevs: Box<[bindings::cdev]>, } +// This is safe because Registration doesn't actually expose any methods. +unsafe impl Sync for Registration {} + impl Drop for Registration { fn drop(&mut self) { unsafe { + for dev in self.cdevs.iter_mut() { + bindings::cdev_del(dev); + } bindings::unregister_chrdev_region(self.dev, self.count as _); } } } + +pub struct FileOperationsVtable(bindings::file_operations); + +unsafe extern "C" fn open_callback( + _inode: *mut bindings::inode, + file: *mut bindings::file, +) -> c_types::c_int { + let f = match T::open() { + Ok(f) => Box::new(f), + Err(e) => return e.to_kernel_errno(), + }; + (*file).private_data = Box::into_raw(f) as *mut c_types::c_void; + return 0; +} + +unsafe extern "C" fn read_callback( + file: *mut bindings::file, + buf: *mut c_types::c_char, + len: c_types::c_size_t, + offset: *mut bindings::loff_t, +) -> c_types::c_ssize_t { + let mut data = match UserSlicePtr::new(buf as *mut c_types::c_void, len) { + Ok(ptr) => ptr.writer(), + Err(e) => return e.to_kernel_errno() as c_types::c_ssize_t, + }; + let f = &*((*file).private_data as *const T); + // TODO: Pass offset to read()? + match f.read(&mut data) { + Ok(()) => { + let written = len - data.len(); + (*offset) += written as bindings::loff_t; + return written as c_types::c_ssize_t; + } + Err(e) => return e.to_kernel_errno() as c_types::c_ssize_t, + }; +} + +unsafe extern "C" fn release_callback( + _inode: *mut bindings::inode, + file: *mut bindings::file, +) -> c_types::c_int { + let ptr = mem::replace(&mut (*file).private_data, ptr::null_mut()); + drop(Box::from_raw(ptr as *mut T)); + return 0; +} + +impl FileOperationsVtable { + pub const fn new() -> FileOperationsVtable { + return FileOperationsVtable(bindings::file_operations { + open: Some(open_callback::), + read: Some(read_callback::), + release: Some(release_callback::), + + check_flags: None, + clone_file_range: None, + compat_ioctl: None, + copy_file_range: None, + dedupe_file_range: None, + fallocate: None, + fasync: None, + flock: None, + flush: None, + fsync: None, + get_unmapped_area: None, + iterate: None, + iterate_shared: None, + llseek: None, + lock: None, + mmap: None, + #[cfg(kernel_4_15_0_or_greataer)] + mmap_supported_flags: 0, + owner: ptr::null_mut(), + poll: None, + read_iter: None, + sendpage: None, + setfl: None, + setlease: None, + show_fdinfo: None, + splice_read: None, + splice_write: None, + unlocked_ioctl: None, + write: None, + write_iter: None, + }); + } +} + +pub trait FileOperations: Sync + Sized { + const VTABLE: FileOperationsVtable; + + fn open() -> KernelResult; + fn read(&self, buf: &mut UserSlicePtrWriter) -> KernelResult<()>; +} diff --git a/src/lib.rs b/src/lib.rs index 588bfc23..2d0467d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ #![no_std] -#![feature(allocator_api, alloc_error_handler)] +#![feature(allocator_api, alloc_error_handler, const_fn)] extern crate alloc; diff --git a/src/user_ptr.rs b/src/user_ptr.rs index 3c25b10d..3ca16524 100644 --- a/src/user_ptr.rs +++ b/src/user_ptr.rs @@ -125,6 +125,10 @@ impl UserSlicePtrReader { pub struct UserSlicePtrWriter(*mut c_types::c_void, usize); impl UserSlicePtrWriter { + pub fn len(&self) -> usize { + return self.1; + } + pub fn write(&mut self, data: &[u8]) -> error::KernelResult<()> { if data.len() > self.1 || data.len() > u32::MAX as usize { return Err(error::Error::EFAULT); diff --git a/tests/chrdev/Cargo.toml b/tests/chrdev/Cargo.toml new file mode 100644 index 00000000..dd61fb50 --- /dev/null +++ b/tests/chrdev/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "chrdev-tests" +version = "0.1.0" +authors = ["Alex Gaynor ", "Geoffrey Thomas "] +edition = "2018" + +[lib] +crate-type = ["staticlib"] +test = false + +[dependencies] +linux-kernel-module = { path = "../.." } + +[dev-dependencies] +kernel-module-testlib = { path = "../../testlib" } +libc = "0.2.58" diff --git a/tests/chrdev/src/lib.rs b/tests/chrdev/src/lib.rs new file mode 100644 index 00000000..79cfca83 --- /dev/null +++ b/tests/chrdev/src/lib.rs @@ -0,0 +1,47 @@ +#![no_std] +#![feature(const_str_as_bytes)] + +use linux_kernel_module; + +struct CycleFile; + +impl linux_kernel_module::chrdev::FileOperations for CycleFile { + const VTABLE: linux_kernel_module::chrdev::FileOperationsVtable = + linux_kernel_module::chrdev::FileOperationsVtable::new::(); + + fn open() -> linux_kernel_module::KernelResult { + return Ok(CycleFile); + } + + fn read( + &self, + buf: &mut linux_kernel_module::user_ptr::UserSlicePtrWriter, + ) -> linux_kernel_module::KernelResult<()> { + for c in b"123456789".iter().cycle().take(buf.len()) { + buf.write(&[*c])?; + } + return Ok(()); + } +} + +struct ChrdevTestModule { + _chrdev_registration: linux_kernel_module::chrdev::Registration, +} + +impl linux_kernel_module::KernelModule for ChrdevTestModule { + fn init() -> linux_kernel_module::KernelResult { + let chrdev_registration = linux_kernel_module::chrdev::builder("chrdev-tests\x00", 0..1)? + .register_device::() + .build()?; + Ok(ChrdevTestModule { + _chrdev_registration: chrdev_registration, + }) + } +} + +linux_kernel_module::kernel_module!( + ChrdevTestModule, + author: "Alex Gaynor and Geoffrey Thomas", + description: "A module for testing character devices", + license: "GPL" +); diff --git a/tests/chrdev/tests/tests.rs b/tests/chrdev/tests/tests.rs new file mode 100644 index 00000000..e7df4470 --- /dev/null +++ b/tests/chrdev/tests/tests.rs @@ -0,0 +1,74 @@ +use std::io::Read; +use std::path::PathBuf; +use std::process::Command; +use std::{env, fs}; + +use libc; + +use kernel_module_testlib::with_kernel_module; + +fn get_device_major_number() -> libc::dev_t { + let devices = fs::read_to_string("/proc/devices").unwrap(); + let dev_no_line = devices + .lines() + .find(|l| l.ends_with("chrdev-tests")) + .unwrap(); + let elements = dev_no_line.rsplitn(2, " ").collect::>(); + assert_eq!(elements.len(), 2); + assert_eq!(elements[0], "chrdev-tests"); + return elements[1].trim().parse().unwrap(); +} + +fn temporary_file_path() -> PathBuf { + let mut p = env::temp_dir(); + p.push("chrdev-test-device"); + return p; +} + +struct UnlinkOnDrop<'a> { + path: &'a PathBuf, +} + +impl Drop for UnlinkOnDrop<'_> { + fn drop(&mut self) { + Command::new("sudo") + .arg("rm") + .arg(self.path.to_str().unwrap()) + .status() + .unwrap(); + } +} + +fn mknod(path: &PathBuf, device_number: libc::dev_t) -> UnlinkOnDrop { + Command::new("sudo") + .arg("mknod") + .arg(path.to_str().unwrap()) + .arg("c") + .arg(device_number.to_string()) + .arg("0") + .status() + .unwrap(); + return UnlinkOnDrop { path }; +} + +#[test] +fn test_mknod() { + with_kernel_module(|| { + let device_number = get_device_major_number(); + mknod(&temporary_file_path(), device_number); + }); +} + +#[test] +fn test_read() { + with_kernel_module(|| { + let device_number = get_device_major_number(); + let p = temporary_file_path(); + let _u = mknod(&p, device_number); + + let mut f = fs::File::open(&p).unwrap(); + let mut data = [0; 12]; + f.read_exact(&mut data).unwrap(); + assert_eq!(&data, b"123456789123") + }); +}