Published on October 06, 2021
Introduction
In the previous article we took the first step towards an API to create devices with user data and our own callbacks. In this article we will be looking at implementing callbacks for reading from and writing to our device file.
Dispatching the Callbacks
Similar to how we handled the IRP_MJ_CREATE
, IRP_MJ_CLOSE
and IRP_MJ_CLEANUP
major functions in the dispatch_callback
function in the previous article, we will now have to add code to dispatch the IRP_MJ_READ
and IRP_MJ_WRITE
functions:
IRP_MJ_READ => {
let read_request = ReadRequest {
inner: request,
};
let status = data.read(&device, &read_request);
request = read_request.inner;
status
}
IRP_MJ_WRITE => {
let write_request = WriteRequest {
inner: request,
};
let status = data.write(&device, &write_request);
request = write_request.inner;
status
}
The code above is slightly different from the code we wrote before.
While it still calls into the read
and write
functions that we will define in DeviceOperations
, it wraps the IoRequest
into a ReadRequest
and WriteRequest
respectively before passing the requests to these functions.
That is because the I/O request of the ReadFile
and WriteFile
functions will provide us access to the data buffer that userspace supplied to these functions.
We now proceed to extend the DeviceOperations
trait with the read
and write
callbacks, as well as some default implementation:
fn read(&mut self, _device: &Device, request: &ReadRequest) -> Result<(), Error> {
request.complete(Ok(0));
Ok(())
}
fn write(&mut self, _device: &Device, request: &WriteRequest) -> Result<(), Error> {
request.complete(Ok(0));
Ok(())
}
Extending the Requests
When implementing the ReadRequest
and WriteRequest
types, we first want to make sure that we can still use all the functions that we implemented for IoRequest
.
Fortunately, we can wrap the IoRequest
into our ReadRequest
and WriteRequest
and then implement core::ops::Deref
:
pub struct ReadRequest {
pub(crate) inner: IoRequest,
}
impl Deref for ReadRequest {
type Target = IoRequest;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
With ReadRequest
and WriteRequest
implementing Deref
for IoRequest
, we can now access all the functions available for IoRequest
, but at the same time we can extend ReadRequest
and WriteRequest
with functions specific to those requests.
First, we will implement the function that gives us access to the user buffer that has been supplied to us from userspace for these requests.
Fortunately, thanks to Deref
we have access to the IRP and the current I/O stack location:
impl ReadRequest {
pub fn user_ptr(&self) -> UserPtr {
let stack_location = self.stack_location();
let irp = self.irp();
We need these to tell apart buffered I/O from direct I/O.
For direct I/O, we will get a Memory Descriptor List (MDL) instead.
A MDL is a set of one or more physical pages that may be discontiguous, that is they are not necessarily neighboring pages in the physical address space.
However, we can use MmGetSystemAddressForMdlSafe
to map these physical pages to a contiguous virtual area, such that our driver can access the user buffer.
Similarly, MmGetMdlByteCount
gives us the size of the MDL in bytes.
The function looks like this:
if !irp.MdlAddress.is_null() {
let ptr = unsafe {
MmGetSystemAddressForMdlSafe(irp.MdlAddress, MM_PAGE_PRIORITY::HighPagePriority as _)
};
let size = unsafe { MmGetMdlByteCount(irp.MdlAddress) } as usize;
unsafe { UserPtr::new(ptr, 0, size) }
For buffered I/O, we will get a SystemBuffer
and the length of the buffer as a parameter:
} else if !unsafe { irp.AssociatedIrp.SystemBuffer }.is_null() {
let ptr = unsafe { irp.AssociatedIrp.SystemBuffer };
let size = unsafe { stack_location.Parameters.Read }.Length as usize;
unsafe { UserPtr::new(ptr, 0, size) }
Alternatively, if the user did not set the DO_BUFFERED_IO
or DO_DIRECT_IO
flag, we would get NULL
pointers in both cases, so we will just return a UserPtr
that does not do anything:
} else {
unsafe { UserPtr::new(core::ptr::null_mut(), 0, 0) }
}
}
Similarly, we will implement an offset
function that provides us the byte offset in the file that userspace is trying to read from or write to:
pub fn offset(&self) -> i64 {
let stack_location = self.stack_location();
let irp = self.irp();
if !irp.MdlAddress.is_null() {
(unsafe { MmGetMdlByteOffset(irp.MdlAddress) }) as i64
} else if !unsafe { irp.AssociatedIrp.SystemBuffer }.is_null() {
unsafe { stack_location.Parameters.Read.ByteOffset.QuadPart }
} else {
0
}
}
}
The implementation for WriteRequest
is pretty much the same.
User Pointers
If you paid close attention, you may have noticed that we have been wrapping the pointer to the user buffer and the size of the user buffer into a UserPtr
struct.
We don't want to provide access to the raw pointer itself, but preferably a byte slice or some type instead
Therefore, we will be providing our own interface UserPtr
that owns the pointer and the size, and gives us access to a byte slice or that lets us read the data into some type T
.
The basic implementation looks as follows:
impl UserPtr {
pub unsafe fn new(
ptr: *mut cty::c_void,
read_size: usize,
write_size: usize,
) -> Self {
Self {
ptr,
read_size,
write_size,
}
}
pub fn read_size(&self) -> usize {
self.read_size
}
pub fn write_size(&self) -> usize {
self.write_size
}
To convert the pointer and the size to an immutable or mutable slice, we can use the core::slice::from_raw_parts
and core::slice::from_raw_parts_mut
functions:
pub fn as_slice(&self) -> &[u8] {
unsafe {
core::slice::from_raw_parts(self.ptr as *const u8, self.read_size)
}
}
pub fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe {
core::slice::from_raw_parts_mut(self.ptr as *mut u8, self.write_size)
}
}
In addition, we also provide functions to read the buffer into a type T
or write a type T
into the buffer, as long as the size of T
does not exceed the size of the user buffer.
We check the size of T
and if it exceeds the size of the user buffer, we return Error::INVALID_USER_BUFFER
.
Otherwise, we use core::ptr::copy_nonoverlapping
to copy over the data between T
and the user buffer.
The code for that looks as follows:
pub fn read<T: Default>(&self, obj: &mut T) -> Result<T, Error> {
if core::mem::size_of::<T>() > self.read_size {
return Err(Error::INVALID_USER_BUFFER);
}
let mut obj = T::default();
unsafe {
core::ptr::copy_nonoverlapping(
self.ptr as _,
&mut obj,
core::mem::size_of::<T>(),
);
}
Ok(obj)
}
pub fn write<T>(&mut self, obj: &T) -> Result<(), Error> {
if core::mem::size_of::<T>() > self.write_size {
return Err(Error::INVALID_USER_BUFFER);
}
unsafe {
core::ptr::copy_nonoverlapping(
obj,
self.ptr as _,
core::mem::size_of::<T>(),
);
}
Ok(())
}
}
In the implementation above we also differentiate the read_size
from the write_size
.
We won't need that right now, but it will be useful in the next article when we implement support for I/O controls.
Example
The code for this example can be found on Github.
For this example, we will be implementing the read
and write
callbacks of our DeviceOperations
trait to read the user data into a Vec<u8>
and to write whatever data is stored inside a Vec<u8>
to the user buffer:
First, we extend our MyDevice
struct to store the user data:
struct MyDevice {
data: Vec<u8>,
}
In the read
callback, we will first make sure the user specified offset does not execeed the size of our buffer.
Then we make sure the user specified size does not exceed the size of our buffer minus the offset.
After limiting both the offset and the size, we copy over the bytes and return how many bytes we copied over to userspace.
The code looks like this:
impl DeviceOperations for MyDevice {
fn read(&mut self, _device: &Device, request: &ReadRequest) -> Result<(), Error> {
let mut user_ptr = request.user_ptr();
let slice = user_ptr.as_mut_slice();
let offset = (request.offset() as usize).min(self.data.len());
let size = slice.len().min(self.data.len() - offset);
slice[0..size].copy_from_slice(&self.data[offset..offset + size]);
request.complete(Ok(size as u32));
Ok(())
}
For our write
callback, we simply assume the offset is always 0 and limit the user input to no more than 4096 bytes.
If both constraints are met, we simply take the slice of user data and turn it into a Vec<u8>
and store it:
fn write(&mut self, _device: &Device, request: &WriteRequest) -> Result<(), Error> {
let user_ptr = request.user_ptr();
if request.offset() > 0 {
return Err(Error::END_OF_FILE)?;
}
let slice = user_ptr.as_slice();
let size = slice.len().min(4096);
self.data = slice[0..size].to_vec();
request.complete(Ok(size as u32));
Ok(())
}
}
That concludes the changes to our driver.
We can load our driver as follows:
sc create example binPath=$(realpath target/x86_64-pc-windows-msvc/debug/driver.sys) type=kernel
sc start example
Userspace
The code can be found on Github. To test our driver, we will open our device file with read and write permissions. Then we write a string into the device file, which our driver will store. Up next we will allocate a buffer and try to read data from the device file into that buffer. Finally, we try to decode it as an UTF-8 string and print it if it succeeds, otherwise we simply display the data in hex:
use std::fs::OpenOptions;
use std::io::{Read, Write};
fn main() -> Result<(), std::io::Error> {
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(false)
.open("\\??\\Example")?;
file.write_all("Hello, world!".as_bytes())?;
let mut data = vec![0u8; 4096];
let size = file.read(&mut data)?;
match std::str::from_utf8(&data[..size]) {
Ok(s) => println!("read {} bytes: \"{}\"", size, s),
_ => println!("read {} bytes: {:x?}", size, &data[..size]),
}
Ok(())
}
What's Next?
In the next article, we will look at I/O controls which provide us with an alternative way to interact with our driver.
References
ReadFile
- MSDNWriteFile
- MSDNcore::ops::Deref
- Rust documentation- "Using MDLs" - MSDN
MmGetSystemAddressForMdlSafe
- MSDNMmGetMdlByteCount
- MSDNcore::slice::from_raw_parts
- Rust documentationcore::slice::from_raw_parts_mut
- Rust documentationcore::ptr::copy_nonoverlapping
- Rust documentation
If you like my work or if my work has been useful to you in any way, then feel free to donate me a cup of coffee. Any donation is much appreciated!