Skip to content
Lesson 12.8Loaders, Kernel Services, and Trust Boundaries

Driver Requests and Device Registers

Control-code fields, device registers, interrupt work, and USB reports reveal how a driver turns a request or key press into an observable result.

Lesson 12.8
Read or listenPrint chapter or book
About 18 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 12.8Key ideas, reference and questions

A one-page summary of “Driver Requests and Device Registers” you can keep beside your notes. Plain Markdown.

Lesson 12.8 — Driver Requests and Device Registers: cheatsheet

  • Remember: Buffered I/O provides a kernel buffer, but the driver must still respect input and output lengths.
  • Also useful: A packed control code has labelled bit compartments: shifts place each field in its compartment, and masks recover those fields separately.

Key terms

  • ACK: ACK is write-only and reads as 0.
  • lParam: Bit 31 is bit 7 of the top byte, worth 2⁷ = 128 = 0x80, and bit 30 is its bit 6, worth 2⁶ = 64 = 0x40, so the top byte becomes 0x80 + 0x40 = 0xC0 and the lParam is 0xC01E_0001.
  • bool: Nothing in it touches hardware or the kernel: the register block is a Rust struct, the interrupt line is a bool, and an IRP is a small struct in a queue.

Review

  • Q: A driver defines a control code that writes to its device, with the access bits (bits 14 and 15) set to 00. Which programs can send it? A: Any program that can open a handle to the device, since access 00 means the I/O manager requires no particular rights
  • Q: The keypad driver receives the control code 0x8000_6007 instead of 0x8000_6004. What changed, and what should the driver do? A: Only the method bits changed, from 00 to 11, so the driver should refuse any code it does not define
Download cheatsheet (.md)

This lesson opens the numbers on the paths that Lesson 12.7 followed: the parameters of a DeviceIoControl request, the four fields packed into a control code, the toy keypad’s registers byte by byte, the values its interrupt handlers read, and the bytes of a real key press. It ends with a lab that runs the keypad and its driver as an ordinary program.

What a driver reads from a control request

Section titled “What a driver reads from a control request”

A DeviceIoControl request carries more parameters than a read. These are the parts of its IRP that the keypad driver reads (Windows keeps some in the IRP itself and some in a per-driver stack location attached to it):

operationIRP_MJ_DEVICE_CONTROL
control code0x80006004
output length4 bytes
system bufferkernel copy of the caller's buffer
requestoruser mode
statusset at completion
informationbytes returned
What a driver reads from an IRP for a DeviceIoControl request. Requestor says the request came from user mode, so every parameter is untrusted.

The toy control code uses buffered I/O. Its method bits tell the I/O manager to copy the caller’s input into a kernel system buffer before the driver runs, then copy the output back after completion. With this method, the driver works on that buffer rather than the program’s pointer. It still has to respect the lengths: the output length says how many bytes the caller’s buffer can hold, and writing even one byte more would overrun kernel memory.

A control code is four fields in one number

Section titled “A control code is four fields in one number”

A control code is a 32-bit number that packs four fields, built by a macro called CTL_CODE:

  • bits 16 to 31, the device type: values from 0x8000 up belong to hardware vendors, lower ones to Microsoft;
  • bits 14 and 15, the access the caller’s handle must hold: 0 for any, 1 for read, 2 for write, 3 for both;
  • bits 2 to 13, the function number: vendors use 0x800 and up;
  • bits 0 and 1, the method, which says how buffers travel: 0 means buffered.

Packing the fields into one number uses the left shift from Lesson 1.10: a shift left by n bits multiplies a number by 2ⁿ when the result fits, so a shift by 4 bits moves it exactly one hex digit, because each hex digit stands for 4 bits.

The toy keypad defines one control code, get status, with device type 0x8000, function 0x801, the buffered method, and read access. Packing it takes one shift per field:

  • 0x8000 shifted left by 16 bits, which is 16 ÷ 4 = 4 hex digits, is 0x8000_0000;
  • the access value 1 shifted left by 14 bits is 2¹⁴ = 16,384. That is 4 × 4,096, and 4,096 = 16³ is the value of a 1 in the fourth hex digit, so 16,384 is 0x4000;
  • 0x801 shifted left by 2 bits is 0x801 × 2² = 0x801 × 4 = 0x2004, because 0x800 × 4 = 0x2000 (8 × 4 = 32, which is 0x20, and the two zero digits stay behind it) and 1 × 4 = 4;
  • the method, 0, adds nothing.

0x8000_0000 + 0x4000 + 0x2004 = 0x8000_6004. The top half, 0x8000, is the device type, and the low half, 0x6004, holds the other three fields. Here are its sixteen bits, one byte at a time:

Pack four fields into one 32-bit control codeChange a field and watch the shifted pieces add up. The pieces never overlap, so the code can be taken apart again.

Fields, shifted pieces, control code

The four fields

Where each field lives
bit 31bit 0
device type0x8000 · bits 31 to 16
access1 · bits 15 to 14
function0x801 · bits 13 to 2
method0 · bits 1 to 0

Shifted into place

Each field moved to its bit position
0x800000000x8000 << 16
0x000040001 << 14
0x000020040x801 << 2
0x000000000 << 0

Control code

Add the four pieces
0x80006004what the program passes to the driver

Each field is shifted to its own bit range and the four pieces are added: the control code is 0x80006004.

150
141
131
120
110
100
90
80
access 01
function, first 6 bits 100000
hex 6
hex 0
Bits 15 to 8 of the control code 0x8000_6004, the byte 0x60. Access 01 is read, and the function number begins here.
70
60
50
40
30
21
10
00
function, last 6 bits 000001
method 00
hex 0
hex 4
Bits 7 to 0, the byte 0x04. The function's twelve bits, 1000 0000 0001, are 0x801; method 00 is buffered.

The access field is a security check that happens before the driver runs. The I/O manager compares it with the rights on the caller’s handle and refuses the request, without ever calling the driver, if the handle lacks them. A driver that declares a powerful command with access 0, any access, lets every program that can open the device send it, which is one of the classic driver mistakes. Drivers are also expected to compare the whole 32-bit code, not just the function number, so that a code with the right function but a different method or access is refused.

Pack and unpack four control-code fields. This step-by-step trace needs JavaScript. Here is the code and where the run ends.

device_type = 0x8000; method = 0
place device_type in bits 16..31
place access in bits 14..15
place function in bits 2..13
combine the nonoverlapping fields
extract fields again and compare

Result: 0x80006000 extracts device 0x8000, access 1, function 2048, method 0. Packing did not itself grant the caller access.

Final values: access = 1; function = 2048; device_part = 0x80000000; access_part = 0x4000; function_part = 0x2000; method = 0; control_code = 0x80006000; decoded_device = 32768; decoded_access = 1; decoded_function = 2048; decoded_method = 0.

Read every step of the lesson’s example
  1. Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.

    access = 1; function = 2048.

  2. Step 2, line 2. The high 16 bits hold the device type.

    access = 1; function = 2048; device_part = 0x80000000.

  3. Step 3, line 3. Two access bits start at bit 14.

    access = 1; function = 2048; device_part = 0x80000000; access_part = 0x4000.

  4. Step 4, line 4. The 12-bit function field starts at bit 2; the two method bits remain zero.

    access = 1; function = 2048; device_part = 0x80000000; access_part = 0x4000; function_part = 0x2000; method = 0.

  5. Step 5, line 5. Combine fields that occupy different bit positions.

    access = 1; function = 2048; device_part = 0x80000000; access_part = 0x4000; function_part = 0x2000; method = 0; control_code = 0x80006000.

  6. Step 6, line 6. Result: 0x80006000 extracts device 0x8000, access 1, function 2048, method 0. Packing did not itself grant the caller access.

    access = 1; function = 2048; device_part = 0x80000000; access_part = 0x4000; function_part = 0x2000; method = 0; control_code = 0x80006000; decoded_device = 32768; decoded_access = 1; decoded_function = 2048; decoded_method = 0.

The toy keypad’s four 32-bit registers take 4 × 4 = 16 bytes in all. Suppose the firmware placed them at physical address 0xFEB0_1000: CONTROL at offset +0x00, STATUS at +0x04, DATA at +0x08, and ACK at +0x0C. STATUS therefore answers at 0xFEB0_1000 + 4 = 0xFEB0_1004. Here is the block after the driver has started the device and the A key has been pressed and released, 8 bytes at a time:

+0x0003
+0x0100
+0x0200
+0x0300
+0x0401
+0x0502
+0x0600
+0x0700
CONTROL 0x00000003
STATUS 0x00000201
The first 8 bytes of the register block. Each register is little-endian, so its lowest byte comes first.
+0x081E
+0x0900
+0x0A00
+0x0B00
+0x0C00
+0x0D00
+0x0E00
+0x0F00
DATA 0x0000001E
ACK
The last 8 bytes. DATA shows what a read would return right now, and ACK always reads as 0.

CONTROL, 0x0000_0003, is written by the driver. Bit 0, enable, makes the device scan its keys, and bit 1, interrupt enable, lets it interrupt. 3 = 2 + 1, so both bits are set. The other 30 bits are reserved and must be written as 0.

STATUS, 0x0000_0201, is read-only and describes the device at this moment. Because the register is little-endian, byte +0x04 holds bits 0 to 7 and byte +0x05 holds bits 8 to 15. Byte +0x04 is 0x01: bit 0, data ready, is set, so an event is waiting, and bit 1, overflow, is clear. Byte +0x05 is 0x02: bits 8 to 15 form an 8-bit count of waiting events, here 2. As one number, 0x0201 = 2 × 256 + 1 = 513. Dividing 513 by 256 gives 2 remainder 1: the count, and the low byte’s flags.

DATA, 0x0000_001E, holds the event at the front of the device’s queue. Bits 0 to 6 are the scan code, and bit 7 is set when the key was released rather than pressed. 0x1E is 1 × 16 + 14 = 30, binary 0001 1110. Its bit 7 is 0, so the front event is A pressed: 30 is the scan code of the A key in the numbering Windows uses, known as set 1. The event behind it is 0x9E:

bit 71
bit 60
bit 50
bit 41
bit 31
bit 21
bit 11
bit 00
released
scan code 001 1110 = 0x1E
The second event in the queue, 0x9E: the same key, released.

0x9E is 9 × 16 + 14 = 158. Bit 7 is worth 2⁷ = 128 = 0x80, and 158 − 128 = 30, so the remaining bits name the A key again: A released.

ACK is write-only and reads as 0. Writing a 1 to bit 0 tells the device its interrupt has been handled, and the device lowers its interrupt signal; writing a 1 to bit 1 clears the overflow flag. Bits written as 0 change nothing. This write-1-to-clear convention is common because it lets a driver clear one flag without first reading the register to preserve the others.

Lesson 12.7 split a driver’s interrupt handling into a short interrupt service routine and a deferred procedure call that does the real work. Here is that sequence for the two events above, with the value each register returns:

I/O managerDriver DPCDriver ISRKeypad registersI/O managerDriver DPCDriver ISRKeypad registerstwo events queued,interrupt line raisedinterrupt line loweredinterruptread STATUS0x00000201, data ready, 2 waitingwrite 0x1 to ACKqueue the DPC and returnread STATUS, then DATA0x1E, A pressedread STATUS, then DATA0x9E, A releasedread STATUS0x00000000, nothing leftcomplete the waiting IRP, 2 bytes

The ISR writes 0x1 to ACK. Only bit 0 is set, so the device lowers its interrupt line and leaves the overflow flag, bit 1, as it was. The last read of STATUS returns 0: both events have been read, so the queue is empty, and the overflow flag was never set. The 2 bytes the DPC collected, 0x1E and 0x9E, are what the parked read receives when the DPC completes it.

Two stops on the path from a USB keyboard to a game, the keyboard’s report and the message the game finally receives, pack their facts into bits.

In the simple boot format that every PC understands, a keyboard’s HID report is 8 bytes:

byte 002
byte 100
byte 204
byte 300
byte 400
byte 500
byte 600
byte 700
modifiers
reserved
up to six keys held
A boot-format report while left Shift and A are held.

Byte 0 holds one bit per modifier key: bit 0 is left Ctrl, bit 1 left Shift, bit 2 left Alt, bit 3 the left Windows key, and bits 4 to 7 the same four keys on the right. 0x02 is binary 0000 0010, so only bit 1 is set: left Shift. Bytes 2 to 7 list up to six keys currently held, each as a HID usage number. The letters A to Z are usages 4 to 4 + 25 = 29, or 0x04 to 0x1D (1 × 16 + 13 = 29), so 0x04 is A. Six slots is also why a keyboard reporting in boot format cannot report a seventh key held at the same time.

KBDHID.SYS turns usage 0x04 into scan code 0x1E. The toy keypad marked a release by setting bit 7, 0x80, which is how PS/2 keyboards sent it over their wire. The events that KBDCLASS.SYS queues keep the scan code on its own and mark a release with a separate break flag instead.

When the game’s WM_KEYDOWN message arrives, its lParam packs the details of the key press into 32 bits:

0
0
1
E
0
0
0
1
flags
scan code
repeat count
0x00
0x1E
0x0001 = 1
The lParam of the WM_KEYDOWN for a first press of A, 0x001E_0001, one hex digit per box.

0x1E shifted left by 16 bits, which is 4 hex digits, is 0x001E_0000, and adding a repeat count of 1 gives 0x001E_0001. The top byte, bits 24 to 31, holds flags. For the matching WM_KEYUP, bit 30 (the key was down before) and bit 31 (the key is being released) are set. Bit 31 is bit 7 of the top byte, worth 2⁷ = 128 = 0x80, and bit 30 is its bit 6, worth 2⁶ = 64 = 0x40, so the top byte becomes 0x80 + 0x40 = 0xC0 and the lParam is 0xC01E_0001.

The lab toy_driver_lab.rs builds the toy keypad and a driver for it as an ordinary user-mode program. Nothing in it touches hardware or the kernel: the register block is a Rust struct, the interrupt line is a bool, and an IRP is a small struct in a queue. What it keeps from the real thing are the request checks and register behavior described in Lesson 12.7 and here.

toy_driver_lab.rs

rust-labs/src/bin/ · Rust, 733 lines

Show the codeHide the code
//! Toy driver lab for Lesson 14.8.
//!
//! An ordinary user-mode simulation of a device and the driver that owns it.
//! `ToyKeypad` plays the hardware: a 16-byte block of registers and a small
//! queue of key events. `ToyDriver` plays the kernel driver: it validates
//! requests, parks reads that cannot finish yet, and completes them when the
//! device interrupts. Nothing here touches real hardware or the kernel. The
//! register block is a struct, the interrupt line is a `bool`, and an I/O
//! request packet is a small struct in a queue.

use std::collections::VecDeque;
use std::error::Error;
use std::fmt;

// The register map: byte offsets from the start of the register block.

/// Read and write. Bit 0 enables the device, bit 1 enables its interrupt.
const CONTROL: usize = 0x00;
/// Read only. Bit 0: data ready. Bit 1: overflow. Bits 8 to 15: events waiting.
const STATUS: usize = 0x04;
/// Read only, with a side effect: reading removes the event at the front.
/// Bits 0 to 6 hold a scan code; bit 7 is set when the key was released.
const DATA: usize = 0x08;
/// Write only, write 1 to clear. Bit 0 lowers the interrupt line and bit 1
/// clears the overflow flag. Bits written as 0 change nothing.
const ACK: usize = 0x0C;
const BLOCK_BYTES: usize = 16;

const CONTROL_ENABLE: u32 = 0x1;
const CONTROL_IRQ_ENABLE: u32 = 0x2;
const CONTROL_DEFINED: u32 = CONTROL_ENABLE | CONTROL_IRQ_ENABLE;

const STATUS_DATA_READY: u32 = 0x1;
const STATUS_OVERFLOW: u32 = 0x2;
const STATUS_COUNT_SHIFT: u32 = 8;

const DATA_SCAN_CODE: u8 = 0x7F;
const DATA_RELEASED: u8 = 0x80;

const ACK_INTERRUPT: u32 = 0x1;
const ACK_OVERFLOW: u32 = 0x2;
const ACK_DEFINED: u32 = ACK_INTERRUPT | ACK_OVERFLOW;

/// How many events the device can hold before it starts dropping them.
const QUEUE_CAPACITY: usize = 4;

/// The set-1 scan code for the A key.
const SCAN_A: u8 = 0x1E;

/// The access right a handle needs to read from the device.
const FILE_READ_DATA: u32 = 0x1;
const METHOD_BUFFERED: u32 = 0;
/// Device types from `0x8000` up belong to hardware vendors, not Microsoft.
const TOY_DEVICE_TYPE: u32 = 0x8000;
/// The keypad's only control code: copy the STATUS register to the caller.
const IOCTL_TOY_GET_STATUS: u32 = ctl_code(TOY_DEVICE_TYPE, 0x801, METHOD_BUFFERED, FILE_READ_DATA);

/// Packs a control code the way the Windows `CTL_CODE` macro does.
const fn ctl_code(device_type: u32, function: u32, method: u32, access: u32) -> u32 {
    (device_type << 16) | (access << 14) | (function << 2) | method
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct IoctlFields {
    device_type: u32,
    access: u32,
    function: u32,
    method: u32,
}

fn decode_ioctl(code: u32) -> IoctlFields {
    IoctlFields {
        device_type: code >> 16,
        access: (code >> 14) & 0x3,
        function: (code >> 2) & 0xFFF,
        method: code & 0x3,
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct StatusFields {
    data_ready: bool,
    overflow: bool,
    count: u32,
}

fn decode_status(value: u32) -> StatusFields {
    StatusFields {
        data_ready: value & STATUS_DATA_READY != 0,
        overflow: value & STATUS_OVERFLOW != 0,
        count: (value >> STATUS_COUNT_SHIFT) & 0xFF,
    }
}

fn describe_event(event: u8) -> String {
    let scan_code = event & DATA_SCAN_CODE;
    let action = if event & DATA_RELEASED == 0 {
        "pressed"
    } else {
        "released"
    };
    format!("0x{event:02X}: scan code 0x{scan_code:02X} {action}")
}

/// What the simulated bus reports for an access that names no register.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BusError {
    Unaligned(usize),
    OutOfRange(usize),
    ReadOnly(usize),
    ReservedBits { offset: usize, value: u32 },
}

impl fmt::Display for BusError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            BusError::Unaligned(offset) => {
                write!(formatter, "offset 0x{offset:02X} is not 4-byte aligned")
            }
            BusError::OutOfRange(offset) => {
                write!(
                    formatter,
                    "offset 0x{offset:02X} is past the register block"
                )
            }
            BusError::ReadOnly(offset) => {
                write!(
                    formatter,
                    "the register at 0x{offset:02X} cannot be written"
                )
            }
            BusError::ReservedBits { offset, value } => write!(
                formatter,
                "0x{value:08X} sets reserved bits of the register at 0x{offset:02X}"
            ),
        }
    }
}

impl Error for BusError {}

/// Registers are 4 bytes wide and start every 4 bytes.
fn register(offset: usize) -> Result<usize, BusError> {
    if offset >= BLOCK_BYTES {
        Err(BusError::OutOfRange(offset))
    } else if offset.is_multiple_of(4) {
        Ok(offset)
    } else {
        Err(BusError::Unaligned(offset))
    }
}

/// The simulated hardware.
#[derive(Debug, Default)]
struct ToyKeypad {
    control: u32,
    events: VecDeque<u8>,
    overflow: bool,
    interrupt_line: bool,
}

impl ToyKeypad {
    /// The physical side of the device: a key changed state.
    fn key_event(&mut self, scan_code: u8, released: bool) {
        if self.control & CONTROL_ENABLE == 0 {
            return; // a disabled device does not scan its keys
        }
        if self.events.len() == QUEUE_CAPACITY {
            self.overflow = true; // the event is lost, and the flag says so
        } else {
            let released_bit = if released { DATA_RELEASED } else { 0 };
            self.events
                .push_back((scan_code & DATA_SCAN_CODE) | released_bit);
        }
        if self.control & CONTROL_IRQ_ENABLE != 0 {
            self.interrupt_line = true;
        }
    }

    fn interrupt_pending(&self) -> bool {
        self.interrupt_line
    }

    fn status(&self) -> u32 {
        let count = u32::try_from(self.events.len())
            .unwrap_or(u32::MAX)
            .min(0xFF);
        let mut status = count << STATUS_COUNT_SHIFT;
        if !self.events.is_empty() {
            status |= STATUS_DATA_READY;
        }
        if self.overflow {
            status |= STATUS_OVERFLOW;
        }
        status
    }

    /// One 32-bit register read, as the driver's processor would perform it.
    fn read32(&mut self, offset: usize) -> Result<u32, BusError> {
        match register(offset)? {
            CONTROL => Ok(self.control),
            STATUS => Ok(self.status()),
            // Reading DATA is not a pure read: it removes the front event.
            DATA => Ok(self.events.pop_front().map_or(0, u32::from)),
            _ => Ok(0), // ACK is write only and reads as zero
        }
    }

    /// One 32-bit register write.
    fn write32(&mut self, offset: usize, value: u32) -> Result<(), BusError> {
        match register(offset)? {
            CONTROL if value & !CONTROL_DEFINED == 0 => {
                self.control = value;
                Ok(())
            }
            ACK if value & !ACK_DEFINED == 0 => {
                if value & ACK_INTERRUPT != 0 {
                    self.interrupt_line = false;
                }
                if value & ACK_OVERFLOW != 0 {
                    self.overflow = false;
                }
                Ok(())
            }
            CONTROL | ACK => Err(BusError::ReservedBits { offset, value }),
            _ => Err(BusError::ReadOnly(offset)),
        }
    }

    /// Every register as 16 little-endian bytes, without the side effect a
    /// real DATA read would have. Real hardware offers no such view; the lab
    /// uses it only to print the register block the lesson draws.
    fn debug_view(&self) -> [u8; BLOCK_BYTES] {
        let front = self.events.front().copied().map_or(0, u32::from);
        let words = [self.control, self.status(), front, 0];
        let mut bytes = [0_u8; BLOCK_BYTES];
        let (chunks, _) = bytes.as_chunks_mut::<4>();
        for (chunk, word) in chunks.iter_mut().zip(words) {
            *chunk = word.to_le_bytes();
        }
        bytes
    }
}

/// What a request asks for: the major function of the packet.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MajorFunction {
    /// `ReadFile`: return key events, one byte each.
    Read,
    /// `DeviceIoControl`: the control code names the operation.
    DeviceControl { code: u32 },
}

/// A simulated I/O request packet: the I/O manager's work order.
#[derive(Debug, Clone, PartialEq, Eq)]
struct Irp {
    id: u32,
    major: MajorFunction,
    /// The rights granted to the caller's handle when it opened the device.
    granted_access: u32,
    /// The size of the caller's output buffer, in bytes.
    output_length: usize,
}

fn read_request(id: u32, output_length: usize) -> Irp {
    Irp {
        id,
        major: MajorFunction::Read,
        granted_access: FILE_READ_DATA,
        output_length,
    }
}

fn control_request(id: u32, code: u32, granted_access: u32, output_length: usize) -> Irp {
    Irp {
        id,
        major: MajorFunction::DeviceControl { code },
        granted_access,
        output_length,
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Status {
    Success,
    Pending,
    AccessDenied,
    InvalidDeviceRequest,
    BufferTooSmall,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct Completion {
    id: u32,
    status: Status,
    bytes: Vec<u8>,
}

/// The I/O manager's check, made before any driver sees the request: the
/// handle must hold every right that the request's access bits demand.
fn io_manager_allows(irp: &Irp) -> bool {
    let needed = match irp.major {
        MajorFunction::Read => FILE_READ_DATA,
        MajorFunction::DeviceControl { code } => decode_ioctl(code).access,
    };
    irp.granted_access & needed == needed
}

/// The path of one request: the I/O manager's check, then the driver.
fn submit(driver: &mut ToyDriver, device: &mut ToyKeypad, irp: Irp) -> Result<Status, BusError> {
    if io_manager_allows(&irp) {
        driver.dispatch(device, irp)
    } else {
        Ok(Status::AccessDenied)
    }
}

/// The simulated driver.
#[derive(Debug, Default)]
struct ToyDriver {
    /// Read requests waiting for key events, oldest first.
    pending_reads: VecDeque<Irp>,
    /// Events taken from the device but not yet handed to a request.
    buffered_events: VecDeque<u8>,
    /// Requests finished since the I/O manager last collected them.
    completed: Vec<Completion>,
    dpc_queued: bool,
    overflows_seen: u32,
}

impl ToyDriver {
    /// Starts the device: enable its key scanning and its interrupt.
    fn start(device: &mut ToyKeypad) -> Result<Self, BusError> {
        device.write32(CONTROL, CONTROL_ENABLE | CONTROL_IRQ_ENABLE)?;
        Ok(ToyDriver::default())
    }

    /// The dispatch routine: validate a request, then finish it now or park it.
    fn dispatch(&mut self, device: &mut ToyKeypad, irp: Irp) -> Result<Status, BusError> {
        match irp.major {
            MajorFunction::Read => {
                if irp.output_length == 0 {
                    return Ok(self.complete(irp.id, Status::BufferTooSmall, Vec::new()));
                }
                if self.buffered_events.is_empty() {
                    self.pending_reads.push_back(irp);
                    return Ok(Status::Pending);
                }
                let bytes = self.take_events(irp.output_length);
                Ok(self.complete(irp.id, Status::Success, bytes))
            }
            MajorFunction::DeviceControl { code } => {
                // Compare the whole 32-bit code, not only its function number.
                if code != IOCTL_TOY_GET_STATUS {
                    return Ok(self.complete(irp.id, Status::InvalidDeviceRequest, Vec::new()));
                }
                // Check the length before copying anything into the buffer.
                if irp.output_length < 4 {
                    return Ok(self.complete(irp.id, Status::BufferTooSmall, Vec::new()));
                }
                let status = device.read32(STATUS)?;
                Ok(self.complete(irp.id, Status::Success, status.to_le_bytes().to_vec()))
            }
        }
    }

    /// The interrupt service routine. Other work on its core waits while it
    /// runs, so it does the minimum: confirm the interrupt came from this
    /// device, acknowledge it so the device lowers its line, and queue the DPC.
    fn isr(&mut self, device: &mut ToyKeypad) -> Result<bool, BusError> {
        let status = device.read32(STATUS)?;
        if status & STATUS_DATA_READY == 0 {
            return Ok(false); // the line is shared, and another device raised it
        }
        device.write32(ACK, ACK_INTERRUPT)?;
        self.dpc_queued = true;
        Ok(true)
    }

    /// The deferred procedure call: drain the device, then complete waiting reads.
    fn dpc(&mut self, device: &mut ToyKeypad) -> Result<(), BusError> {
        if !std::mem::take(&mut self.dpc_queued) {
            return Ok(());
        }
        while device.read32(STATUS)? & STATUS_DATA_READY != 0 {
            let [event, ..] = device.read32(DATA)?.to_le_bytes();
            self.buffered_events.push_back(event);
        }
        if device.read32(STATUS)? & STATUS_OVERFLOW != 0 {
            self.overflows_seen += 1;
            device.write32(ACK, ACK_OVERFLOW)?;
        }
        while !self.buffered_events.is_empty() {
            let Some(irp) = self.pending_reads.pop_front() else {
                break;
            };
            let bytes = self.take_events(irp.output_length);
            self.complete(irp.id, Status::Success, bytes);
        }
        Ok(())
    }

    fn take_events(&mut self, limit: usize) -> Vec<u8> {
        let count = limit.min(self.buffered_events.len());
        self.buffered_events.drain(..count).collect()
    }

    fn complete(&mut self, id: u32, status: Status, bytes: Vec<u8>) -> Status {
        self.completed.push(Completion { id, status, bytes });
        status
    }

    fn take_completions(&mut self) -> Vec<Completion> {
        std::mem::take(&mut self.completed)
    }
}

fn print_block(bytes: &[u8; BLOCK_BYTES]) {
    let names = ["CONTROL", "STATUS", "DATA", "ACK"];
    let (words, _) = bytes.as_chunks::<4>();
    for (offset, (name, chunk)) in names.iter().zip(words).enumerate() {
        let word = u32::from_le_bytes(*chunk);
        let hex: Vec<String> = chunk.iter().map(|byte| format!("{byte:02X}")).collect();
        println!(
            "   +0x{:02X} {name:<7} bytes {}  = 0x{word:08X}",
            offset * 4,
            hex.join(" ")
        );
    }
}

fn print_completions(driver: &mut ToyDriver) {
    for completion in driver.take_completions() {
        println!(
            "   request {} completed with {:?}, {} bytes",
            completion.id,
            completion.status,
            completion.bytes.len()
        );
        for event in completion.bytes {
            println!("     {}", describe_event(event));
        }
    }
}

fn pending_read_scenario(driver: &mut ToyDriver, device: &mut ToyKeypad) -> Result<(), BusError> {
    println!("1. A program asks for key events before any key is pressed");
    let status = submit(driver, device, read_request(1, 8))?;
    println!("   dispatch returned {status:?}; the request waits in the driver");

    println!("\n2. A is pressed and released; the device queues two events");
    device.key_event(SCAN_A, false);
    device.key_event(SCAN_A, true);
    let view = device.debug_view();
    print_block(&view);
    let fields = decode_status(u32::from_le_bytes([view[4], view[5], view[6], view[7]]));
    println!(
        "   STATUS: data ready {}, overflow {}, {} events waiting",
        fields.data_ready, fields.overflow, fields.count
    );

    println!("\n3. The interrupt line is up, so the ISR runs, then the DPC");
    if device.interrupt_pending() && driver.isr(device)? {
        println!("   ISR: the interrupt was ours; acknowledged, DPC queued");
        driver.dpc(device)?;
    }
    print_completions(driver);
    Ok(())
}

fn control_code_scenario(driver: &mut ToyDriver, device: &mut ToyKeypad) -> Result<(), BusError> {
    println!("\n4. Control codes are checked by the I/O manager and by the driver");
    let fields = decode_ioctl(IOCTL_TOY_GET_STATUS);
    println!(
        "   0x{IOCTL_TOY_GET_STATUS:08X}: device type 0x{:04X}, access {}, function 0x{:03X}, method {}",
        fields.device_type, fields.access, fields.function, fields.method
    );
    let cases = [
        (
            "handle without read access",
            control_request(2, IOCTL_TOY_GET_STATUS, 0, 4),
        ),
        (
            "unknown control code",
            control_request(3, IOCTL_TOY_GET_STATUS | 0x3, FILE_READ_DATA, 4),
        ),
        (
            "2-byte output buffer",
            control_request(4, IOCTL_TOY_GET_STATUS, FILE_READ_DATA, 2),
        ),
        (
            "valid request",
            control_request(5, IOCTL_TOY_GET_STATUS, FILE_READ_DATA, 4),
        ),
    ];
    for (label, irp) in cases {
        let status = submit(driver, device, irp)?;
        println!("   {label:<27} -> {status:?}");
    }
    driver.take_completions();
    Ok(())
}

fn overflow_scenario(driver: &mut ToyDriver, device: &mut ToyKeypad) -> Result<(), BusError> {
    println!("\n5. Six events arrive before the driver runs; the device holds four");
    for released in [false, true, false, true, false, true] {
        device.key_event(SCAN_A, released);
    }
    let fields = decode_status(device.read32(STATUS)?);
    println!(
        "   STATUS: data ready {}, overflow {}, {} events waiting",
        fields.data_ready, fields.overflow, fields.count
    );
    if driver.isr(device)? {
        driver.dpc(device)?;
    }
    println!(
        "   the DPC buffered {} events and saw {} overflow",
        driver.buffered_events.len(),
        driver.overflows_seen
    );
    Ok(())
}

fn main() -> Result<(), Box<dyn Error>> {
    let mut device = ToyKeypad::default();
    let mut driver = ToyDriver::start(&mut device)?;
    pending_read_scenario(&mut driver, &mut device)?;
    control_code_scenario(&mut driver, &mut device)?;
    overflow_scenario(&mut driver, &mut device)?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    fn started() -> (ToyKeypad, ToyDriver) {
        let mut device = ToyKeypad::default();
        let driver = ToyDriver::start(&mut device).unwrap();
        (device, driver)
    }

    #[test]
    fn register_block_matches_the_lesson_figure() {
        let (mut device, _driver) = started();
        device.key_event(SCAN_A, false);
        device.key_event(SCAN_A, true);
        assert_eq!(
            device.debug_view(),
            [
                0x03, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x00, 0x00,
                0x00, 0x00,
            ]
        );
        assert_eq!(
            decode_status(0x0201),
            StatusFields {
                data_ready: true,
                overflow: false,
                count: 2,
            }
        );
    }

    #[test]
    fn reading_data_removes_the_front_event() {
        let (mut device, _driver) = started();
        device.key_event(SCAN_A, false);
        device.key_event(SCAN_A, true);
        assert_eq!(device.read32(DATA), Ok(0x1E));
        assert_eq!(device.read32(DATA), Ok(0x9E));
        assert_eq!(device.read32(DATA), Ok(0));
        assert_eq!(device.read32(STATUS), Ok(0));
    }

    #[test]
    fn ack_is_write_one_to_clear() {
        let (mut device, _driver) = started();
        device.key_event(SCAN_A, false);
        assert!(device.interrupt_pending());
        device.write32(ACK, 0).unwrap();
        assert!(device.interrupt_pending(), "writing 0 must change nothing");
        device.write32(ACK, ACK_INTERRUPT).unwrap();
        assert!(!device.interrupt_pending());
    }

    #[test]
    fn a_full_queue_drops_events_and_says_so() {
        let (mut device, _driver) = started();
        for _ in 0..QUEUE_CAPACITY + 2 {
            device.key_event(SCAN_A, false);
        }
        assert_eq!(
            decode_status(device.read32(STATUS).unwrap()),
            StatusFields {
                data_ready: true,
                overflow: true,
                count: 4,
            }
        );
    }

    #[test]
    fn bad_register_accesses_are_rejected() {
        let (mut device, _driver) = started();
        assert_eq!(device.read32(0x02), Err(BusError::Unaligned(0x02)));
        assert_eq!(device.read32(0x10), Err(BusError::OutOfRange(0x10)));
        assert_eq!(device.write32(STATUS, 1), Err(BusError::ReadOnly(STATUS)));
        assert_eq!(
            device.write32(CONTROL, 0x4),
            Err(BusError::ReservedBits {
                offset: CONTROL,
                value: 0x4,
            })
        );
    }

    #[test]
    fn a_disabled_device_ignores_keys() {
        let mut device = ToyKeypad::default();
        device.key_event(SCAN_A, false);
        assert_eq!(device.read32(STATUS), Ok(0));
        assert!(!device.interrupt_pending());
    }

    #[test]
    fn the_control_code_decodes_into_its_fields() {
        assert_eq!(IOCTL_TOY_GET_STATUS, 0x8000_6004);
        assert_eq!(
            decode_ioctl(IOCTL_TOY_GET_STATUS),
            IoctlFields {
                device_type: 0x8000,
                access: FILE_READ_DATA,
                function: 0x801,
                method: METHOD_BUFFERED,
            }
        );
    }

    #[test]
    fn the_io_manager_refuses_a_handle_without_read_access() {
        let (mut device, mut driver) = started();
        let irp = control_request(7, IOCTL_TOY_GET_STATUS, 0, 4);
        assert_eq!(
            submit(&mut driver, &mut device, irp),
            Ok(Status::AccessDenied)
        );
        assert!(
            driver.take_completions().is_empty(),
            "the driver never saw it"
        );
    }

    #[test]
    fn the_driver_compares_the_whole_control_code() {
        let (mut device, mut driver) = started();
        let near_miss = IOCTL_TOY_GET_STATUS | 0x3;
        let irp = control_request(8, near_miss, FILE_READ_DATA, 4);
        assert_eq!(
            submit(&mut driver, &mut device, irp),
            Ok(Status::InvalidDeviceRequest)
        );
    }

    #[test]
    fn a_short_buffer_is_refused_before_anything_is_copied() {
        let (mut device, mut driver) = started();
        let irp = control_request(9, IOCTL_TOY_GET_STATUS, FILE_READ_DATA, 2);
        assert_eq!(
            submit(&mut driver, &mut device, irp),
            Ok(Status::BufferTooSmall)
        );
        assert_eq!(
            driver.take_completions(),
            vec![Completion {
                id: 9,
                status: Status::BufferTooSmall,
                bytes: Vec::new(),
            }]
        );
    }

    #[test]
    fn a_pending_read_completes_after_the_interrupt() {
        let (mut device, mut driver) = started();
        assert_eq!(
            submit(&mut driver, &mut device, read_request(1, 8)),
            Ok(Status::Pending)
        );
        device.key_event(SCAN_A, false);
        device.key_event(SCAN_A, true);
        assert_eq!(driver.isr(&mut device), Ok(true));
        assert!(!device.interrupt_pending());
        driver.dpc(&mut device).unwrap();
        assert_eq!(
            driver.take_completions(),
            vec![Completion {
                id: 1,
                status: Status::Success,
                bytes: vec![0x1E, 0x9E],
            }]
        );
    }

    #[test]
    fn the_isr_ignores_an_interrupt_it_did_not_cause() {
        let (mut device, mut driver) = started();
        assert_eq!(driver.isr(&mut device), Ok(false));
    }

    #[test]
    fn reads_complete_in_order_and_respect_buffer_sizes() {
        let (mut device, mut driver) = started();
        submit(&mut driver, &mut device, read_request(1, 1)).unwrap();
        submit(&mut driver, &mut device, read_request(2, 8)).unwrap();
        for released in [false, true, false] {
            device.key_event(SCAN_A, released);
        }
        assert_eq!(driver.isr(&mut device), Ok(true));
        driver.dpc(&mut device).unwrap();
        let completions = driver.take_completions();
        assert_eq!(completions.len(), 2);
        assert_eq!(
            (completions[0].id, completions[0].bytes.clone()),
            (1, vec![0x1E])
        );
        assert_eq!(
            (completions[1].id, completions[1].bytes.clone()),
            (2, vec![0x9E, 0x1E])
        );
    }
}

The device’s register read shows DATA’s side effect in one line:

fn read32(&mut self, offset: usize) -> Result<u32, BusError> {
match register(offset)? {
CONTROL => Ok(self.control),
STATUS => Ok(self.status()),
// Reading DATA is not a pure read: it removes the front event.
DATA => Ok(self.events.pop_front().map_or(0, u32::from)),
_ => Ok(0), // ACK is write only and reads as zero
}
}

register refuses an offset that is not a multiple of 4 or that lies past the 16-byte block, so a mistyped offset becomes an error instead of a read of the wrong register. The driver’s ISR is as short as Lesson 12.7 says it should be:

Optional: practise the named operations and their inputs with a few whole-token blanks.

fn isr(&mut self, device: &mut ToyKeypad) -> Result<bool, BusError> {
let status = device.read32(STATUS)?;
if status & STATUS_DATA_READY == 0 {
return Ok(false); // the line is shared, and another device raised it
}
device.write32(ACK, ACK_INTERRUPT)?;
self.dpc_queued = true;
Ok(true)
}

The dispatch routine compares the whole control code and checks the output length before it copies anything, and a separate submit function plays the I/O manager, refusing any request whose handle lacks the access bits its code demands.

Build it, then run it from the rust-labs folder:

Terminal window
cargo run --bin toy_driver_lab

The program parks a read request, presses and releases A, prints the register block drawn above, runs the ISR and the DPC, and shows the read completing with 0x1E and 0x9E. It then sends four control requests: one from a handle without read access, one with the near-miss code 0x8000_6007, one with a 2-byte buffer, and a valid one. Finally it presses keys six times with no driver running in between, to show the four-slot queue setting its overflow flag. The tests beside it check the register bytes from this lesson, the write-1-to-clear rule, the overflow, every refused request, and that waiting reads complete in order.

Then set QUEUE_CAPACITY to 2 and work out which tests must now fail before running them.

The toy kept driver mechanics inside an ordinary program: control codes, buffers, register side effects, and interrupt work were all visible without installing privileged code. The next lesson asks what changes when those mechanics belong to a real loaded driver, and builds an inventory of the components already trusted by Windows.

References: defining I/O control codes, security issues for I/O control codes, keyboard and mouse HID client drivers, and WM_KEYDOWN.

For the specific buffered method used in the toy code, see Microsoft’s IOCTL buffer descriptions.

Knowledge check

What the access bits protect

A driver defines a control code that writes to its device, with the access bits (bits 14 and 15) set to 00. Which programs can send it?

Finished Lesson 12.8?

Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.

Chapter 12: 10 lessons

This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.