microsoft/openvmm
Publicmirrored fromhttps://github.com/microsoft/openvmmAvailable
Guide/src/dev_guide/tests/fuzzing/writing.md
183lines · modecode
| 1 | # Writing Fuzzers |
| 2 | |
| 3 | ## Writing a new fuzzer in OpenVMM |
| 4 | |
| 5 | The easiest way to get up and running is to look at the existing in-tree fuzzers |
| 6 | (which you can list using `cargo xtask fuzz list`), along with reading through |
| 7 | the [cargo-fuzz book](https://rust-fuzz.github.io/book/cargo-fuzz.html) (the |
| 8 | book is fairly brief and shouldn't take more than 20 minutes to read through) |
| 9 | |
| 10 | Some examples of in-tree fuzzers: |
| 11 | |
| 12 | * Simple device example: [chipset/fuzz/battery.rs][fuzz_battery_url] |
| 13 | * More complex device example: [ide/fuzz/fuzz_ide.rs][fuzz_ide_url] |
| 14 | * Abstraction over unsafe example: [ide/fuzz/fuzz_scsi_buffer.rs][fuzz_scsi_buffer_url] |
| 15 | |
| 16 | [fuzz_battery_url]: https://github.com/microsoft/openvmm/blob/main/vm/devices/chipset/fuzz/fuzz_battery.rs |
| 17 | [fuzz_ide_url]: https://github.com/microsoft/openvmm/blob/main/vm/devices/storage/ide/fuzz/fuzz_ide.rs |
| 18 | [fuzz_scsi_buffer_url]: https://github.com/microsoft/openvmm/blob/main/vm/devices/storage/scsi_buffers/fuzz/fuzz_scsi_buffers.rs |
| 19 | |
| 20 | Once you're ready to take a stab at writing your own fuzzer, spinning up a new |
| 21 | fuzzer is as easy as running: |
| 22 | |
| 23 | ```bash |
| 24 | cargo xtask fuzz init openvmm_crate_to_fuzz TEMPLATE |
| 25 | ``` |
| 26 | |
| 27 | Use `--help` for more details on the available TEMPLATE types. |
| 28 | |
| 29 | ```admonish caution |
| 30 | We don't suggest using `cargo fuzz init` (i.e: without `xtask`), as it |
| 31 | emits a template that isn't compatible with the OpenVMM repo style, and also |
| 32 | doesn't properly update the root Cargo.toml's `workspace.members` array. |
| 33 | ``` |
| 34 | |
| 35 | ## Fuzzing an abstraction over unsafe code |
| 36 | |
| 37 | Unsafe code is a prioritized target for fuzzing given its self-evident risks. |
| 38 | |
| 39 | However, it can be hard to reason about how best to exercise unsafe code in OpenVMM |
| 40 | via fuzzing as often (and correctly) the unsafe code is not directly interacting |
| 41 | with guest-controlled data. |
| 42 | |
| 43 | The approach taken with OpenVMM then is to target abstractions over unsafe code. |
| 44 | Such as interfaces and data structures like `BounceBuffers`, `guest_memory`, or |
| 45 | `ucs2`. A fuzzer for one of these will attempt to be a regular consumer of the |
| 46 | abstraction, calling those APIs declared safe and using any data structure in a |
| 47 | rust-safe way. This attempts to check that the safety guarantees the abstraction |
| 48 | is making are being upheld via the API. |
| 49 | |
| 50 | For example, let's say we want to fuzz `BounceBuffers`, here's what we may |
| 51 | want to fuzz: |
| 52 | |
| 53 |  |
| 54 | |
| 55 | Fuzz logic might then allocate a `BounceBuffer` using `new` and call methods on |
| 56 | it such as `as_mut_bytes` and `io_vecs`. Then it could access the return result |
| 57 | of both those calls: |
| 58 | |
| 59 | ```rust |
| 60 | use scsi_buffers::BounceBuffer; |
| 61 | |
| 62 | #[derive(Arbitrary)] |
| 63 | enum BouneBufferAccess { |
| 64 | AsMutBytes, |
| 65 | IoVecs |
| 66 | } |
| 67 | |
| 68 | #[derive(Arbitrary)] |
| 69 | struct FuzzCase { |
| 70 | #[arbitrary(with = |u: &mut Unstructured| u.int_in_range(0..=0x40000))] |
| 71 | size: usize, |
| 72 | accesses: Vec<BounceBufferAccess> |
| 73 | } |
| 74 | |
| 75 | fn access_mut_bytes(buf: &mut [u8]) { |
| 76 | buf.fill(b'A'); |
| 77 | // access buf in other ways to test validity of underlying memory and slice |
| 78 | } |
| 79 | |
| 80 | fn access_io_vecs(io_vecs: &[IoBuffer]) { |
| 81 | // access each IoBuffer to test validity of ptr and len |
| 82 | } |
| 83 | |
| 84 | fuzz_target!(|fuzz_case: FuzzCase| { do_fuzz(fuzz_case) }); |
| 85 | |
| 86 | fn do_fuzz(fuzz_case: FuzzCase) { |
| 87 | let mut bb = BounceBuffer::new(fuzz_case.size); |
| 88 | |
| 89 | for access in fuzz_case.accesses { |
| 90 | match access { |
| 91 | AsMutBytes => { |
| 92 | let buf = bb.as_mut_bytes(); |
| 93 | access_mut_bytes(buf); |
| 94 | }, |
| 95 | IoVecs => { |
| 96 | let io_vecs = bb.io_vecs(); |
| 97 | access_io_vecs(io_vecs) |
| 98 | } |
| 99 | } |
| 100 | } |
| 101 | }) |
| 102 | ``` |
| 103 | |
| 104 | The fuzzer should work to ensure the safe members of the API cannot be misused |
| 105 | in any way that may result in memory corruption or unsoundness. |
| 106 | |
| 107 | ## Fuzzing a chipset device |
| 108 | |
| 109 | Writing a fuzzer for a chipset device (e.g: battery, ide, serial, pic, |
| 110 | etc...) involves targeting the API that is roughly exposed to guests: the |
| 111 | device's port IO, PCI config, and MMIO interfaces. |
| 112 | |
| 113 | While it's entirely possible to hand-roll a fuzzer that is tailored to the |
| 114 | specific register configuration of a particular device, the in-repo |
| 115 | `chipset_device_fuzz` crate exports a `FuzzChipset` type that offers a |
| 116 | "plug-and-play" way to hook a chipset device up to a fuzzer: |
| 117 | |
| 118 | ```rust |
| 119 | #[derive(Arbitrary)] |
| 120 | struct StaticDeviceConfig { |
| 121 | #[arbitrary(with = |u: &mut Unstructured| u.int_in_range(0..=16))] |
| 122 | num_queues: usize, |
| 123 | } |
| 124 | |
| 125 | fn do_fuzz(u: &mut Unstructured<'_>) -> arbitrary::Result<()> { |
| 126 | // Step 1: generate a device's fixed-at-construction-time configuration |
| 127 | let static_device_config: StaticDeviceConfig = u.arbitrary()?; |
| 128 | |
| 129 | // Step 2: init the device, and wire-it-up to the fuzz chipset |
| 130 | let mut chipset = chipset_device_fuzz::FuzzChipset::default(); |
| 131 | let my_device = chipset.device_builder("my_dev").add(|services| { |
| 132 | my_dev::MyDevice::new( |
| 133 | static_device_config.num_queues, |
| 134 | &mut services.register_mmio(), // e.g: pci devices have BARs to remap their MMIO intercepts |
| 135 | ) |
| 136 | }).unwrap(); |
| 137 | |
| 138 | // Step 3: use the remaining fuzzing input to slam the device with chipset events |
| 139 | while !u.is_empty() { |
| 140 | let action = chipset.get_arbitrary_action(u)?; |
| 141 | xtask_fuzz::fuzz_eprintln!("{:x?}", action); // only prints when running a repro |
| 142 | chipset.exec_action(action).unwrap(); |
| 143 | |
| 144 | // Step 3.5: (optionally) intersperse "external stimuli" between chipset actions |
| 145 | if u.ratio(1, 10)? { |
| 146 | let event: u32 = u.arbitrary()?; |
| 147 | my_device.report_external_event(event); |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | Ok(()) |
| 152 | } |
| 153 | |
| 154 | fuzz_target!(|input: &[u8]| -> libfuzzer_sys::Corpus { |
| 155 | if do_fuzz(&mut Unstructured::new(input)).is_err() { |
| 156 | libfuzzer_sys::Corpus::Reject |
| 157 | } else { |
| 158 | libfuzzer_sys::Corpus::Keep |
| 159 | } |
| 160 | }); |
| 161 | ``` |
| 162 | |
| 163 | ## Fuzzing a vmbus device |
| 164 | |
| 165 | TBD (no such fuzzers exist in-tree today) |
| 166 | |
| 167 | ## Fuzzing `async` code |
| 168 | |
| 169 | Depending on the nature of the 'async' code in question, there are two main |
| 170 | recommended approaches to fuzzing it: |
| 171 | |
| 172 | 1. **now_or_never**: The recommended approach for individual asynchronous calls |
| 173 | is to use the `now_or_never` method from the `futures` crate. This method |
| 174 | will poll the future to completion, but will not block if the future is |
| 175 | not ready to complete. This allows you to fuzz the future without needing |
| 176 | to run it to completion, which can be useful for testing the behavior of |
| 177 | the future in various states. |
| 178 | |
| 179 | 2. **DefaultPool::run_with**: The recommended approach for more intricate |
| 180 | asynchronous requirements is to use the `DefaultPool::run_with` method from |
| 181 | our `pal_async` crate. This method takes a custom async function and runs it |
| 182 | to completion. This allows you to write custom code using regular async/await |
| 183 | syntax, combinators, `join`s, `select`s, or whatever you wish. |
| 184 | |