diff --git a/Cargo.lock b/Cargo.lock index 3dcd29f..a6a71e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,6 +255,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fail_driver_irql_violation" +version = "0.1.0" +dependencies = [ + "anyhow", + "wdk", + "wdk-alloc", + "wdk-build", + "wdk-panic", + "wdk-sys", +] + [[package]] name = "fail_driver_pool_leak" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 0f195fb..7e81c15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "general/echo/kmdf/driver/*", "general/echo/kmdf/exe", + "tools/dv/kmdf/fail_driver_irql_violation", "tools/dv/kmdf/fail_driver_pool_leak", ] resolver = "2" diff --git a/general/echo/kmdf/exe/src/main.rs b/general/echo/kmdf/exe/src/main.rs index 72ff860..bcdf6db 100644 --- a/general/echo/kmdf/exe/src/main.rs +++ b/general/echo/kmdf/exe/src/main.rs @@ -90,13 +90,13 @@ fn main() -> Result<(), Box> { } } else { eprintln!( - r##" + r" Usage: Echoapp.exe --- Send single write and read request synchronously Echoapp.exe -Async --- Send reads and writes asynchronously without terminating Echoapp.exe -Async --- Send reads and writes asynchronously Exit the app anytime by pressing Ctrl-C -"## +" ); return Err("Invalid Args".into()); } diff --git a/tools/dv/kmdf/fail_driver_irql_violation/Cargo.toml b/tools/dv/kmdf/fail_driver_irql_violation/Cargo.toml new file mode 100644 index 0000000..a753930 --- /dev/null +++ b/tools/dv/kmdf/fail_driver_irql_violation/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "fail_driver_irql_violation" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +repository.workspace = true +license.workspace = true + +[package.metadata.wdk] +# Using workspace wdk config + +[lib] +crate-type = ["cdylib"] +# Tests from root driver crates must be excluded since there's no way to prevent linker args from being passed to their unit tests: https://github.com/rust-lang/cargo/issues/12663 +test = false + +[dependencies] +wdk.workspace = true +wdk-alloc.workspace = true +wdk-panic.workspace = true +wdk-sys.workspace = true + +[build-dependencies] +anyhow.workspace = true +wdk-build.workspace = true + +[features] +default = [] +nightly = ["wdk/nightly", "wdk-sys/nightly"] diff --git a/tools/dv/kmdf/fail_driver_irql_violation/README.md b/tools/dv/kmdf/fail_driver_irql_violation/README.md new file mode 100644 index 0000000..f5e2c0c --- /dev/null +++ b/tools/dv/kmdf/fail_driver_irql_violation/README.md @@ -0,0 +1,94 @@ +# Fail_Driver_IRQL_Violdation Sample + +This sample KMDF Fail Driver demonstrates the capabilities and features of **Driver Verifier** and the **Device Fundamentals Tests**. + +It allocates a pool of memory to a global buffer when a supported device is added by the PnP Manager and intentionally does not free it before the driver is unloaded. This memory leak fault is a system vulnerability that could lead to security and performance issues and bad user experience. + +By enabling Driver Verifier on this driver, this pool leak violation can be caught before the driver is unloaded and with an active KDNET session, the bug can be analyzed further. + +NOTE: The driver uses WDM's ExAllocatePool2 API directly to allocate memory for its buffer. Ideally, such allocations should be freed by using ExFreePool API. A cleaner way to manage memory in a WDF Driver is to use [wdfmemory](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdfmemory/) + + +## Steps to reproduce the issue + +1. Clone the repository and navigate to the project root. + +2. Build the driver project using the following command in a WDK environment (or EWDK prompt) - + ``` + cargo make + ``` +3. Prepare a target system (a Hyper-V VM can be used) for testing + + Follow the below steps to setup the test system - + 1. Disable Secure boot and start the system + 2. Run "ipconfig" on the host system and note down the IP (if you are using Default Switch for the VM, note down the IP on the Default Switch) + 3. Install and open WinDbg, click on "Attach to Kernel". The key for the connection will be generated in the test system in the next steps. + 4. Connect to the test VM and run the following commands - + ``` + bcdedit /set testsigning on + bcdedit /debug on + bcdedit /dbgsettings net hostip: port:<50000-50030> + + ### Copy the key string output by the above command + ``` + 5. Paste the key in host's WinDbg prompt and connect to the kernel + 6. Restart the target/test system + ``` + shutdown -r -t 0 + ``` + +4. Copy the driver package, available under ".\target\debug\fail_driver_irql_violation_package" to the target system. + +5. Copy "devgen.exe" from host to the target system. Alternatively you may install WDK on the target system and add the directory that contains "devgen.exe" to PATH variable. + +6. Install the driver package and create the device in the target system using the below commands - + ``` + cd "fail_driver_irql_violation_package" + devgen.exe /add /bus ROOT /hardwareid "fail_driver_irql_violation" + + ## Copy the Device ID. This will be used later to run the tests + + pnputil.exe /add-driver .\fail_driver_irql_violation.inf /install + ``` +7. Enable Driver Verifier for 'fail_driver_irql_violation.sys' driver package + 1. Open run command prompt (Start + R) or cmd as administator and run "verifier" + 2. In the verifier manager, + - Create Standard Settings + - Select driver names from list + - Select 'fail_driver_irql_violation.sys' + - Finish + - Restart the system + +8. Follow the steps in https://learn.microsoft.com/en-us/windows-hardware/drivers/develop/how-to-test-a-driver-at-runtime-from-a-command-prompt to run tests against the device managed by this driver + +9. Install TAEF and WDTF on the test computer and run the following test - + ``` + cd "C:\Program Files (x86)\Windows Kits\10\Testing\Tests\Additional Tests\x64\DevFund" + TE.exe .\Devfund_PnPDTest_WLK_Certification.dll /P:"DQ=DeviceID='ROOT\DEVGEN\{PASTE-DEVICE-ID-HERE}'" --rebootResumeOption:Manual + ``` + +10. The test will lead to a Bugcheck and a BlueScreen on the target system with the following error - + ``` + DRIVER_VERIFIER_DETECTED_VIOLATION (c4) + ``` + The logs will be available in WinDbg + run ```!analyze -v``` for detailed bugcheck report + run ```!verifier 3 fail_driver_irql_violation.sys``` for info on the allocations that were leaked that caused the bugcheck. + +11. (Alternatively), the bugcheck can be observed when a device managed by this driver is removed, i.e, when the EVT_WDF_DEVICE_D0_EXIT callback function is executed. + You may use pnputil/devcon to enumerate and remove the devices - + ``` + # To enumerate the devices + pnputil /enum-devices + # To remove a device + pnputil /remove-device "DEVICE-ID" + ``` + +### References + +- [Driver Verifier](https://learn.microsoft.com/en-us/windows-hardware/drivers/devtest/driver-verifier) +- [Device Fundamentals Tests](https://learn.microsoft.com/en-us/windows-hardware/drivers/devtest/device-fundamentals-tests) +- [TAEF](https://learn.microsoft.com/en-us/windows-hardware/drivers/taef/getting-started) +- [WDTF](https://learn.microsoft.com/en-us/windows-hardware/drivers/wdtf/wdtf-runtime-library) +- [Testing a driver at runtime](https://learn.microsoft.com/en-us/windows-hardware/drivers/develop/how-to-test-a-driver-at-runtime-from-a-command-prompt) +- [Using WDF to Develop a Driver](https://learn.microsoft.com/en-us/windows-hardware/drivers/wdf/using-the-framework-to-develop-a-driver) diff --git a/tools/dv/kmdf/fail_driver_irql_violation/build.rs b/tools/dv/kmdf/fail_driver_irql_violation/build.rs new file mode 100644 index 0000000..1cfdc9b --- /dev/null +++ b/tools/dv/kmdf/fail_driver_irql_violation/build.rs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation +// License: MIT OR Apache-2.0 + +fn main() -> anyhow::Result<()> { + Ok(wdk_build::configure_wdk_binary_build()?) +} diff --git a/tools/dv/kmdf/fail_driver_irql_violation/fail_driver_irql_violation.inx b/tools/dv/kmdf/fail_driver_irql_violation/fail_driver_irql_violation.inx new file mode 100644 index 0000000..2014e99 --- /dev/null +++ b/tools/dv/kmdf/fail_driver_irql_violation/fail_driver_irql_violation.inx @@ -0,0 +1,56 @@ +;=================================================================== +; Copyright (c)2023, Microsoft Corporation +; +;Module Name: +; fail_driver_irql_violation.INF +;=================================================================== + +[Version] +Signature = "$WINDOWS NT$" +Class = SoftwareComponent +ClassGuid = {5c4c3332-344d-483c-8739-259e934c9cc8} +Provider = %ProviderString% +PnpLockDown = 1 + +[DestinationDirs] +DefaultDestDir = 13 + +[SourceDisksNames] +1 = %DiskId1%,,,"" + +[SourceDisksFiles] +fail_driver_irql_violation.sys = 1,, + +; ================= Install section ================= + +[Manufacturer] +%StdMfg%=Standard,NT$ARCH$.10.0...16299 + +[Standard.NT$ARCH$.10.0...16299] +%FAIL_DRIVER_IRQL_VIOLATION.DeviceDesc%=FAIL_DRIVER_IRQL_VIOLATION_DEVICE, fail_driver_irql_violation + +[FAIL_DRIVER_IRQL_VIOLATION_DEVICE.NT$ARCH$] +CopyFiles=Drivers_Dir + +[Drivers_Dir] +fail_driver_irql_violation.sys + +; ================= Service installation ================= +[FAIL_DRIVER_IRQL_VIOLATION_DEVICE.NT$ARCH$.Services] +AddService = fail_driver_irql_violation, %SPSVCINST_ASSOCSERVICE%, fail_driver_irql_violation_svc_ins + +[fail_driver_irql_violation_svc_ins] +DisplayName = %FAIL_DRIVER_IRQL_VIOLATION.SVCDESC% +ServiceType = 1 ; SERVICE_KERNEL_DRIVER +StartType = 3 ; SERVICE_DEMAND_START +ErrorControl = 1 ; SERVICE_ERROR_NORMAL +ServiceBinary = %13%\fail_driver_irql_violation.sys + +; ================= Strings ================= +[Strings] +SPSVCINST_ASSOCSERVICE = 0x00000002 +ProviderString = "Rust-DVFail-Sample" +StdMfg = "(Standard system devices)" +DiskId1 = "WDF DVFail Sample fail_driver_irql_violation Installation Disk #1" +FAIL_DRIVER_IRQL_VIOLATION.DeviceDesc = "DVFail Sample WDF fail_driver_irql_violation Device" +FAIL_DRIVER_IRQL_VIOLATION.SVCDESC = "DVFail Sample WDF fail_driver_irql_violation Service" \ No newline at end of file diff --git a/tools/dv/kmdf/fail_driver_irql_violation/src/driver.rs b/tools/dv/kmdf/fail_driver_irql_violation/src/driver.rs new file mode 100644 index 0000000..fc7c396 --- /dev/null +++ b/tools/dv/kmdf/fail_driver_irql_violation/src/driver.rs @@ -0,0 +1,287 @@ +// Copyright (c) Microsoft Corporation. +// License: MIT OR Apache-2.0 + +use wdk::{nt_success, paged_code, println}; +use wdk_sys::{ + call_unsafe_wdf_function_binding, + ntddk::{ExAllocatePool2, ExFreePool, KeEnterCriticalRegion, KeGetCurrentIrql}, + APC_LEVEL, + DRIVER_OBJECT, + NTSTATUS, + PCUNICODE_STRING, + PDRIVER_OBJECT, + POOL_FLAG_NON_PAGED, + SIZE_T, + ULONG, + WDFDEVICE, + WDFDEVICE_INIT, + WDFDRIVER, + WDF_DRIVER_CONFIG, + WDF_NO_HANDLE, + WDF_NO_OBJECT_ATTRIBUTES, + WDF_OBJECT_ATTRIBUTES, + WDF_PNPPOWER_EVENT_CALLBACKS, + _WDF_EXECUTION_LEVEL, + _WDF_SYNCHRONIZATION_SCOPE, +}; + +use crate::{initialize_spinlock, GLOBAL_BUFFER, GUID_DEVINTERFACE, SPINLOCK}; + +/// `DriverEntry` initializes the driver and is the first routine called by the +/// system after the driver is loaded. `DriverEntry` specifies the other entry +/// points in the function driver, such as `EvtDevice` and `DriverUnload`. +/// +/// # Arguments +/// +/// * `driver` - represents the instance of the function driver that is loaded +/// into memory. `DriverEntry` must initialize members of `DriverObject` +/// before it returns to the caller. `DriverObject` is allocated by the system +/// before the driver is loaded, and it is released by the system after the +/// system unloads the function driver from memory. +/// * `registry_path` - represents the driver specific path in the Registry. The +/// function driver can use the path to store driver related data between +/// reboots. The path does not store hardware instance specific data. +/// +/// # Return value: +/// +/// * `STATUS_SUCCESS` - if successful, +/// * `STATUS_UNSUCCESSFUL` - otherwise. +#[link_section = "INIT"] +#[export_name = "DriverEntry"] +extern "system" fn driver_entry( + driver: &mut DRIVER_OBJECT, + registry_path: PCUNICODE_STRING, +) -> NTSTATUS { + println!("Enter: driver_entry"); + + let mut driver_config = { + let wdf_driver_config_size: ULONG; + + // clippy::cast_possible_truncation cannot currently check compile-time constants: https://github.com/rust-lang/rust-clippy/issues/9613 + #[allow(clippy::cast_possible_truncation)] + { + const WDF_DRIVER_CONFIG_SIZE: usize = core::mem::size_of::(); + + // Manually assert there is not truncation since clippy doesn't work for + // compile-time constants + const { assert!(WDF_DRIVER_CONFIG_SIZE <= ULONG::MAX as usize) } + + wdf_driver_config_size = WDF_DRIVER_CONFIG_SIZE as ULONG; + } + + WDF_DRIVER_CONFIG { + Size: wdf_driver_config_size, + EvtDriverDeviceAdd: Some(evt_driver_device_add), + EvtDriverUnload: Some(evt_driver_unload), + ..WDF_DRIVER_CONFIG::default() + } + }; + + let driver_handle_output = WDF_NO_HANDLE.cast::(); + + let nt_status = unsafe { + call_unsafe_wdf_function_binding!( + WdfDriverCreate, + driver as PDRIVER_OBJECT, + registry_path, + WDF_NO_OBJECT_ATTRIBUTES, + &mut driver_config, + driver_handle_output, + ) + }; + + if !nt_success(nt_status) { + println!("Error: WdfDriverCreate failed {nt_status:#010X}"); + return nt_status; + } + + // Allocate non-paged memory pool of 1 byte (arbitrarily chosen) for the + // Global buffer + unsafe { + const LENGTH: usize = 1; + GLOBAL_BUFFER = ExAllocatePool2(POOL_FLAG_NON_PAGED, LENGTH as SIZE_T, 's' as u32); + } + + // Initialize a spinlock that can be used to synchronize access to the buffer + if let Err(status) = initialize_spinlock() { + println!("Failed to initialize spinlock: {status:#010X}"); + return status; + } + + println!("Exit: driver_entry"); + + nt_status +} + +/// `EvtDeviceAdd` is called by the framework in response to `AddDevice` +/// call from the `PnP` manager. We create and initialize a device object to +/// represent a new instance of the device. +/// +/// # Arguments: +/// +/// * `_driver` - Handle to a framework driver object created in `DriverEntry` +/// * `device_init` - Pointer to a framework-allocated `WDFDEVICE_INIT` +/// structure. +/// +/// # Return value: +/// +/// * `NTSTATUS` +#[link_section = "PAGE"] +extern "C" fn evt_driver_device_add( + _driver: WDFDRIVER, + mut device_init: *mut WDFDEVICE_INIT, +) -> NTSTATUS { + paged_code!(); + + println!("Enter: evt_driver_device_add"); + + let mut pnp_power_callbacks = WDF_PNPPOWER_EVENT_CALLBACKS { + Size: core::mem::size_of::() as ULONG, + EvtDeviceD0Entry: Some(evt_device_d0_entry), + EvtDeviceD0Exit: Some(evt_device_d0_exit), + ..WDF_PNPPOWER_EVENT_CALLBACKS::default() + }; + + let [()] = [unsafe { + call_unsafe_wdf_function_binding!( + WdfDeviceInitSetPnpPowerEventCallbacks, + device_init, + &mut pnp_power_callbacks + ); + }]; + + #[allow(clippy::cast_possible_truncation)] + let mut attributes = WDF_OBJECT_ATTRIBUTES { + Size: core::mem::size_of::() as ULONG, + ExecutionLevel: _WDF_EXECUTION_LEVEL::WdfExecutionLevelInheritFromParent, + SynchronizationScope: _WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent, + ..WDF_OBJECT_ATTRIBUTES::default() + }; + + let mut device = WDF_NO_HANDLE as WDFDEVICE; + let mut nt_status = unsafe { + call_unsafe_wdf_function_binding!( + WdfDeviceCreate, + &mut device_init, + &mut attributes, + &mut device, + ) + }; + + if !nt_success(nt_status) { + println!("Error: WdfDeviceCreate failed {nt_status:#010X}"); + return nt_status; + } + + nt_status = unsafe { + call_unsafe_wdf_function_binding!( + WdfDeviceCreateDeviceInterface, + device, + &GUID_DEVINTERFACE, + core::ptr::null_mut(), + ) + }; + + if !nt_success(nt_status) { + println!("Error: WdfDeviceCreateDeviceInterface failed {nt_status:#010X}"); + return nt_status; + } + + println!("Exit: evt_driver_device_add"); + + nt_status +} + +/// This event callback function is called before the driver is unloaded +/// +/// The `EvtDriverUnload` callback function must deallocate any +/// non-device-specific system resources that the driver's DriverEntry routine +/// allocated. +/// +/// # Argument: +/// +/// * `driver` - Handle to the framework driver object +/// +/// # Return Value: +/// +/// None +extern "C" fn evt_driver_unload(_driver: WDFDRIVER) { + println!("Enter: evt_driver_unload"); + + unsafe { ExFreePool(GLOBAL_BUFFER) }; + + println!("Exit: evt_driver_unload"); +} + +/// `EvtDeviceD0Entry` event callback function performs operations +/// that are needed when the driver's device enters the D0 power state. +/// +/// # Arguments: +/// +/// * `Device` - A handle to a framework device object. +/// * `PreviousState` - A WDF_POWER_DEVICE_STATE-typed enumerator that +/// identifies the previous device power state. +/// +/// # Return Value: +/// +/// * STATUS_SUCCESS or another status value for which NT_SUCCESS(status) equals +/// TRUE. For failures, return a status value for which NT_SUCCESS(status) +/// equals FALSE +extern "C" fn evt_device_d0_entry(_device: WDFDEVICE, _prev_state: i32) -> i32 { + println!("Enter: evt_device_d0_entry"); + unsafe { + if let Some(ref spinlock) = SPINLOCK { + spinlock.acquire(); + if !GLOBAL_BUFFER.is_null() { + core::ptr::write_bytes(GLOBAL_BUFFER, 1, 1); + } else { + println!("Global buffer is null"); + } + spinlock.release(); + } else { + println!("Spinlock is not initialized"); + return -1; + } + } + println!("Exit: evt_device_d0_entry"); + 0 +} + +/// `EvtDeviceD0Exit` event callback function performs operations +/// that are needed when the driver's device leaves the D0 power state. +/// +/// NOTE: The IRQL violation fault is injected in this callback +/// +/// # Arguments: +/// +/// * `Device` - A handle to a framework device object. +/// * `TargetState` - A WDF_POWER_DEVICE_STATE-typed enumerator that identifies +/// the device power state that the device is about to enter. +/// +/// # Return Value: +/// +/// * STATUS_SUCCESS or another status value for which NT_SUCCESS(status) equals +/// TRUE. For failures, return a status value for which NT_SUCCESS(status) +/// equals FALSE +extern "C" fn evt_device_d0_exit(_device: WDFDEVICE, _prev_state: i32) -> i32 { + println!("Enter: evt_device_d0_exit"); + unsafe { + if let Some(ref spinlock) = SPINLOCK { + spinlock.acquire(); + if !GLOBAL_BUFFER.is_null() { + core::ptr::write_bytes(GLOBAL_BUFFER, 0, 1); + // Illegal call to KeEnterCriticalRegion will lead to a + // violation of 'IrqlKeApcLte2' rule + KeEnterCriticalRegion(); + } else { + println!("Global buffer is null"); + } + spinlock.release(); + } else { + println!("Spinlock is not initialized"); + return -1; + } + } + println!("Exit: evt_device_d0_exit"); + 0 +} diff --git a/tools/dv/kmdf/fail_driver_irql_violation/src/lib.rs b/tools/dv/kmdf/fail_driver_irql_violation/src/lib.rs new file mode 100644 index 0000000..7a25b32 --- /dev/null +++ b/tools/dv/kmdf/fail_driver_irql_violation/src/lib.rs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// License: MIT OR Apache-2.0 + +//! # Abstract +//! +//! This KMDF sample contains an intentional error that is designed to +//! demonstrate the capabilities and features of Driver Verifier and the Device +//! Fundamental tests. +//! +//! The driver is designed to violate the `IrqlKeApcLte2` Rule by calling +//! KeEnterCriticalRegion() function after acquiring a spinlock. +//! +//! By enabling Driver Verifier on this driver, the IRQL violation can be caught +//! when a device that this driver manages is removed by the PnP manager. With +//! an active KDNET session, the bug can be analyzed further. + +#![no_std] +#![deny(clippy::all)] +#![warn(clippy::pedantic)] +#![warn(clippy::nursery)] +#![warn(clippy::cargo)] +#![allow(clippy::missing_safety_doc)] +#![allow(clippy::doc_markdown)] + +#[cfg(not(test))] +extern crate wdk_panic; + +#[cfg(not(test))] +use wdk_alloc::WdkAllocator; + +#[cfg(not(test))] +#[global_allocator] +static GLOBAL_ALLOCATOR: WdkAllocator = WdkAllocator; + +use wdk::{println, wdf::SpinLock}; +use wdk_sys::{ + GUID, + PVOID, + ULONG, + WDF_OBJECT_ATTRIBUTES, + _WDF_EXECUTION_LEVEL, + _WDF_SYNCHRONIZATION_SCOPE, +}; + +// {B2C3D4E5-F678-9012-3456-7890ABCDEF12} +const GUID_DEVINTERFACE: GUID = GUID { + Data1: 0xB2C3_D4E5u32, + Data2: 0xF678u16, + Data3: 0x9012u16, + Data4: [ + 0x34u8, 0x56u8, 0x78u8, 0x90u8, 0xABu8, 0xCDu8, 0xEFu8, 0x12u8, + ], +}; + +// Global Buffer for the driver +static mut GLOBAL_BUFFER: PVOID = core::ptr::null_mut(); + +// Spinlock to synchronize access to the global buffer +static mut SPINLOCK: Option = None; + +/// `initialize_spinlock` initializes a WDF Spinlock that can be used to +/// synchronize access to any shared data +/// +/// # Arguments: +/// +/// # Return Value: +/// * Returns a `Result` type - +/// - `Ok(())`: Indicates that the function executed successfully without any +/// errors. +/// - `Err(i32)`: Indicates that an error occurred during the execution of the +/// function. The `i32` value represents the error code, which can be used to +/// identify the specific error. +fn initialize_spinlock() -> Result<(), i32> { + let mut attributes = WDF_OBJECT_ATTRIBUTES { + Size: core::mem::size_of::() as ULONG, + ExecutionLevel: _WDF_EXECUTION_LEVEL::WdfExecutionLevelInheritFromParent, + SynchronizationScope: _WDF_SYNCHRONIZATION_SCOPE::WdfSynchronizationScopeInheritFromParent, + ..WDF_OBJECT_ATTRIBUTES::default() + }; + + match SpinLock::create(&mut attributes) { + Err(status) => { + println!("SpinLock create failed {status:#010X}"); + return Err(status); + } + Ok(spin_lock) => unsafe { + SPINLOCK = Some(spin_lock); + }, + } + + Ok(()) +} + +mod driver;