Skip to content

Commit 1bd3678

Browse files
authored
RFC: storvsp: new fuzzer (#612)
First commit of a storvsp fuzzer. This begs, borrows, and steals from other test code to get something that compiles and runs. Coverage shows that the fuzzer (at this point) does hit much of the core storvsp packet processing, _except_ for the IO path. That remains a work in progress.
1 parent 8d9d904 commit 1bd3678

File tree

14 files changed

+367
-17
lines changed

14 files changed

+367
-17
lines changed

Cargo.lock

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2140,6 +2140,27 @@ dependencies = [
21402140
"xtask_fuzz",
21412141
]
21422142

2143+
[[package]]
2144+
name = "fuzz_storvsp"
2145+
version = "0.0.0"
2146+
dependencies = [
2147+
"anyhow",
2148+
"arbitrary",
2149+
"disklayer_ram",
2150+
"guestmem",
2151+
"libfuzzer-sys",
2152+
"pal_async",
2153+
"scsi_defs",
2154+
"scsidisk",
2155+
"storvsp",
2156+
"storvsp_resources",
2157+
"vmbus_async",
2158+
"vmbus_channel",
2159+
"vmbus_ring",
2160+
"xtask_fuzz",
2161+
"zerocopy",
2162+
]
2163+
21432164
[[package]]
21442165
name = "fuzz_ucs2"
21452166
version = "0.0.0"
@@ -5687,6 +5708,7 @@ dependencies = [
56875708
name = "scsi_defs"
56885709
version = "0.0.0"
56895710
dependencies = [
5711+
"arbitrary",
56905712
"bitfield-struct",
56915713
"open_enum",
56925714
"zerocopy",
@@ -6179,6 +6201,7 @@ name = "storvsp"
61796201
version = "0.0.0"
61806202
dependencies = [
61816203
"anyhow",
6204+
"arbitrary",
61826205
"async-trait",
61836206
"criterion",
61846207
"disklayer_ram",
@@ -6220,6 +6243,7 @@ dependencies = [
62206243
name = "storvsp_resources"
62216244
version = "0.0.0"
62226245
dependencies = [
6246+
"arbitrary",
62236247
"guid",
62246248
"mesh",
62256249
"vm_resource",

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ members = [
3030
"vm/devices/storage/disk_nvme/nvme_driver/fuzz",
3131
"vm/devices/storage/ide/fuzz",
3232
"vm/devices/storage/scsi_buffers/fuzz",
33+
"vm/devices/storage/storvsp/fuzz",
3334
"vm/vmcore/guestmem/fuzz",
3435
"vm/x86/x86emu/fuzz",
3536
# in-guest test bins

vm/devices/storage/scsi_defs/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ name = "scsi_defs"
66
edition = "2021"
77
rust-version.workspace = true
88

9+
[features]
10+
# Enable generating arbitrary values of types useful for fuzzing.
11+
arbitrary = ["dep:arbitrary"]
12+
913
[dependencies]
14+
arbitrary = { workspace = true, optional = true, features = ["derive"] }
1015
zerocopy.workspace = true
1116
bitfield-struct.workspace = true
1217
open_enum.workspace = true

vm/devices/storage/scsi_defs/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@ pub const SCSI_SENSEQ_OPERATING_DEFINITION_CHANGED: u8 = 0x02;
713713

714714
open_enum! {
715715
#[derive(AsBytes, FromBytes, FromZeroes)]
716+
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
716717
pub enum ScsiStatus: u8 {
717718
GOOD = 0x00,
718719
CHECK_CONDITION = 0x02,

vm/devices/storage/scsi_defs/src/srb.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use zerocopy::FromZeroes;
99

1010
#[bitfield(u8)]
1111
#[derive(AsBytes, FromBytes, FromZeroes)]
12+
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
1213
pub struct SrbStatusAndFlags {
1314
#[bits(6)]
1415
status_bits: u8,

vm/devices/storage/storvsp/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ rust-version.workspace = true
99
[features]
1010
ioperf = ["dep:disklayer_ram"]
1111

12+
# Enable generating arbitrary values of types useful for fuzzing.
13+
arbitrary = ["dep:arbitrary"]
14+
15+
# Expose some implementation details publicly, used for fuzzing.
16+
fuzz_helpers = []
17+
1218
[dependencies]
19+
arbitrary = { workspace = true, optional = true, features = ["derive"] }
20+
1321
disklayer_ram = { workspace = true, optional = true } # For `ioperf` modules
1422
scsi_buffers.workspace = true
1523
scsi_core.workspace = true
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
[package]
5+
name = "fuzz_storvsp"
6+
publish = false
7+
edition = "2021"
8+
rust-version.workspace = true
9+
10+
[dependencies]
11+
anyhow.workspace = true
12+
arbitrary = { workspace = true, features = ["derive"] }
13+
disklayer_ram.workspace = true
14+
guestmem.workspace = true
15+
pal_async.workspace = true
16+
scsi_defs = {workspace = true, features = ["arbitrary"]}
17+
scsidisk.workspace = true
18+
storvsp = {workspace = true, features = ["arbitrary", "fuzz_helpers"]}
19+
storvsp_resources = {workspace = true, features = ["arbitrary"]}
20+
vmbus_async.workspace = true
21+
vmbus_channel.workspace = true
22+
vmbus_ring.workspace = true
23+
xtask_fuzz.workspace = true
24+
zerocopy.workspace = true
25+
26+
[target.'cfg(all(target_os = "linux", target_env = "gnu"))'.dependencies]
27+
libfuzzer-sys.workspace = true
28+
29+
[package.metadata]
30+
cargo-fuzz = true
31+
32+
[package.metadata.xtask.fuzz.onefuzz-allowlist]
33+
fuzz_storvsp = ["**/*.rs", "../src/**/*.rs"]
34+
35+
[package.metadata.xtask.unused-deps]
36+
# required for the xtask_fuzz macro, but unused_deps doesn't know that
37+
ignored = ["libfuzzer-sys"]
38+
39+
[[bin]]
40+
name = "fuzz_storvsp"
41+
path = "fuzz_storvsp.rs"
42+
test = false
43+
doc = false
44+
doctest = false
45+
46+
[lints]
47+
workspace = true
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
#![cfg_attr(all(target_os = "linux", target_env = "gnu"), no_main)]
5+
6+
use arbitrary::Arbitrary;
7+
use arbitrary::Unstructured;
8+
use guestmem::ranges::PagedRange;
9+
use guestmem::GuestMemory;
10+
use pal_async::DefaultPool;
11+
use scsi_defs::Cdb10;
12+
use scsi_defs::ScsiOp;
13+
use std::sync::Arc;
14+
use storvsp::protocol;
15+
use storvsp::test_helpers::TestGuest;
16+
use storvsp::test_helpers::TestWorker;
17+
use storvsp::ScsiController;
18+
use storvsp::ScsiControllerDisk;
19+
use storvsp_resources::ScsiPath;
20+
use vmbus_async::queue::OutgoingPacket;
21+
use vmbus_async::queue::Queue;
22+
use vmbus_channel::connected_async_channels;
23+
use vmbus_ring::OutgoingPacketType;
24+
use vmbus_ring::PAGE_SIZE;
25+
use xtask_fuzz::fuzz_target;
26+
use zerocopy::AsBytes;
27+
use zerocopy::FromZeroes;
28+
29+
#[derive(Arbitrary)]
30+
enum StorvspFuzzAction {
31+
SendReadWritePacket,
32+
SendRawPacket(FuzzOutgoingPacketType),
33+
ReadCompletion,
34+
}
35+
36+
#[derive(Arbitrary)]
37+
enum FuzzOutgoingPacketType {
38+
AnyOutgoingPacket,
39+
GpaDirectPacket,
40+
}
41+
42+
/// Return an arbitrary byte length that can be sent in a GPA direct
43+
/// packet. The byte length is limited to the maximum number of pages
44+
/// that could fit into a `PagedRange` (at least with how we store the
45+
/// list of pages in the fuzzer ...).
46+
fn arbitrary_byte_len(u: &mut Unstructured<'_>) -> Result<usize, arbitrary::Error> {
47+
let max_byte_len = u.arbitrary_len::<u64>()? * PAGE_SIZE;
48+
u.int_in_range(0..=max_byte_len)
49+
}
50+
51+
/// Sends a GPA direct packet (a type of vmbus packet that references guest memory,
52+
/// the typical packet type used for SCSI requests) to storvsp.
53+
async fn send_gpa_direct_packet(
54+
guest: &mut TestGuest,
55+
payload: &[&[u8]],
56+
gpa_start: u64,
57+
byte_len: usize,
58+
transaction_id: u64,
59+
) -> Result<(), anyhow::Error> {
60+
let start_page: u64 = gpa_start / PAGE_SIZE as u64;
61+
let end_page = start_page
62+
.checked_add(byte_len.try_into()?)
63+
.map(|v| v.div_ceil(PAGE_SIZE as u64))
64+
.ok_or(arbitrary::Error::IncorrectFormat)?;
65+
66+
let gpns: Vec<u64> = (start_page..end_page).collect();
67+
let pages = PagedRange::new(gpa_start as usize % PAGE_SIZE, byte_len, gpns.as_slice())
68+
.ok_or(arbitrary::Error::IncorrectFormat)?;
69+
70+
guest
71+
.queue
72+
.split()
73+
.1
74+
.write(OutgoingPacket {
75+
packet_type: OutgoingPacketType::GpaDirect(&[pages]),
76+
transaction_id,
77+
payload,
78+
})
79+
.await
80+
.map_err(|e| e.into())
81+
}
82+
83+
/// Send a reasonably well structured read or write packet to storvsp.
84+
/// While the fuzzer should eventually discover these paths by poking at
85+
/// arbitrary GpaDirect packet payload, make the search more efficient by
86+
/// generating a packet that is more likely to pass basic parsing checks.
87+
async fn send_arbitrary_readwrite_packet(
88+
u: &mut Unstructured<'_>,
89+
guest: &mut TestGuest,
90+
) -> Result<(), anyhow::Error> {
91+
let path: ScsiPath = u.arbitrary()?;
92+
let gpa = u.arbitrary::<u64>()?;
93+
let byte_len = arbitrary_byte_len(u)?;
94+
95+
let block: u32 = u.arbitrary()?;
96+
let transaction_id: u64 = u.arbitrary()?;
97+
98+
let packet = protocol::Packet {
99+
operation: protocol::Operation::EXECUTE_SRB,
100+
flags: 0,
101+
status: protocol::NtStatus::SUCCESS,
102+
};
103+
104+
// TODO: read6, read12, read16, write6, write12, write16, etc. (READ is read10, WRITE is write10)
105+
let scsiop_choices = [ScsiOp::READ, ScsiOp::WRITE];
106+
let cdb = Cdb10 {
107+
operation_code: *(u.choose(&scsiop_choices)?),
108+
logical_block: block.into(),
109+
transfer_blocks: ((byte_len / 512) as u16).into(),
110+
..FromZeroes::new_zeroed()
111+
};
112+
113+
let mut scsi_req = protocol::ScsiRequest {
114+
target_id: path.target,
115+
path_id: path.path,
116+
lun: path.lun,
117+
length: protocol::SCSI_REQUEST_LEN_V2 as u16,
118+
cdb_length: size_of::<Cdb10>() as u8,
119+
data_transfer_length: byte_len.try_into()?,
120+
data_in: 1,
121+
..FromZeroes::new_zeroed()
122+
};
123+
124+
scsi_req.payload[0..10].copy_from_slice(cdb.as_bytes());
125+
126+
send_gpa_direct_packet(
127+
guest,
128+
&[packet.as_bytes(), scsi_req.as_bytes()],
129+
gpa,
130+
byte_len,
131+
transaction_id,
132+
)
133+
.await
134+
}
135+
136+
fn do_fuzz(u: &mut Unstructured<'_>) -> Result<(), anyhow::Error> {
137+
DefaultPool::run_with(|driver| async move {
138+
let (host, guest_channel) = connected_async_channels(16 * 1024); // TODO: [use-arbitrary-input]
139+
let guest_queue = Queue::new(guest_channel).unwrap();
140+
141+
let test_guest_mem = GuestMemory::allocate(u.int_in_range(1..=256)? * PAGE_SIZE);
142+
let controller = ScsiController::new();
143+
let disk_len_sectors = u.int_in_range(1..=1048576)?; // up to 512mb in 512 byte sectors
144+
let disk = scsidisk::SimpleScsiDisk::new(
145+
disklayer_ram::ram_disk(disk_len_sectors * 512, false).unwrap(),
146+
Default::default(),
147+
);
148+
controller.attach(u.arbitrary()?, ScsiControllerDisk::new(Arc::new(disk)))?;
149+
150+
let _test_worker = TestWorker::start(
151+
controller,
152+
driver.clone(),
153+
test_guest_mem.clone(),
154+
host,
155+
None,
156+
);
157+
158+
let mut guest = TestGuest {
159+
queue: guest_queue,
160+
transaction_id: 0,
161+
};
162+
163+
if u.ratio(9, 10)? {
164+
// TODO: [use-arbitrary-input] (e.g., munge the negotiation packets)
165+
guest.perform_protocol_negotiation().await;
166+
}
167+
168+
while !u.is_empty() {
169+
let action = u.arbitrary::<StorvspFuzzAction>()?;
170+
match action {
171+
StorvspFuzzAction::SendReadWritePacket => {
172+
send_arbitrary_readwrite_packet(u, &mut guest).await?;
173+
}
174+
StorvspFuzzAction::SendRawPacket(packet_type) => {
175+
match packet_type {
176+
FuzzOutgoingPacketType::AnyOutgoingPacket => {
177+
let packet_types = [
178+
OutgoingPacketType::InBandNoCompletion,
179+
OutgoingPacketType::InBandWithCompletion,
180+
OutgoingPacketType::Completion,
181+
];
182+
let payload = u.arbitrary::<protocol::Packet>()?;
183+
// TODO: [use-arbitrary-input] (send a byte blob of arbitrary length rather
184+
// than a fixed-size arbitrary packet)
185+
let packet = OutgoingPacket {
186+
transaction_id: u.arbitrary()?,
187+
packet_type: *u.choose(&packet_types)?,
188+
payload: &[payload.as_bytes()], // TODO: [use-arbitrary-input]
189+
};
190+
191+
guest.queue.split().1.write(packet).await?;
192+
}
193+
FuzzOutgoingPacketType::GpaDirectPacket => {
194+
let header = u.arbitrary::<protocol::Packet>()?;
195+
let scsi_req = u.arbitrary::<protocol::ScsiRequest>()?;
196+
197+
send_gpa_direct_packet(
198+
&mut guest,
199+
&[header.as_bytes(), scsi_req.as_bytes()],
200+
u.arbitrary()?,
201+
arbitrary_byte_len(u)?,
202+
u.arbitrary()?,
203+
)
204+
.await?
205+
}
206+
}
207+
}
208+
StorvspFuzzAction::ReadCompletion => {
209+
// Read completion(s) from the storvsp -> guest queue. This shouldn't
210+
// evoke any specific storvsp behavior, but is important to eventually
211+
// allow forward progress of various code paths.
212+
//
213+
// Ignore the result, since vmbus returns error if the queue is empty,
214+
// but that's fine for the fuzzer ...
215+
let _ = guest.queue.split().0.try_read();
216+
}
217+
}
218+
}
219+
220+
Ok::<(), anyhow::Error>(())
221+
})?;
222+
223+
Ok::<(), anyhow::Error>(())
224+
}
225+
226+
fuzz_target!(|input: &[u8]| {
227+
xtask_fuzz::init_tracing_if_repro();
228+
229+
let _ = do_fuzz(&mut Unstructured::new(input));
230+
231+
// Always keep the corpus, since errors are a reasonable outcome.
232+
// A future optimization would be to reject any corpus entries that
233+
// result in the inability to generate arbitrary data from the Unstructured...
234+
});

vm/devices/storage/storvsp/src/ioperf.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ impl PerfTester {
5454
let test_guest_mem = GuestMemory::allocate(16 * 1024);
5555

5656
let worker = TestWorker::start(
57-
controller.state.clone(),
57+
controller,
5858
driver,
5959
test_guest_mem.clone(),
6060
host,

0 commit comments

Comments
 (0)