Skip to content

Commit 17d83b9

Browse files
committed
test(block): add unit tests for VIRTIO_BLK_F_WRITE_ZEROES
Add three tests parametrised over Sync and Async file engines: - test_write_zeroes_feature_and_config: VIRTIO_BLK_F_WRITE_ZEROES advertised on writable devices; max_write_zeroes_sectors, max_write_zeroes_seg, and write_zeroes_may_unmap populated as expected; read-only devices advertise none of these. - test_write_zeroes: end-to-end happy path (UNMAP=1 → PUNCH_HOLE, metric incremented, VIRTIO_BLK_S_OK) plus four error cases (reserved flag bit set, sector range out of bounds, num_sectors=0, invalid data length). - test_write_zeroes_unsupported_cached: pre-set the cache flag and verify subsequent requests return VIRTIO_BLK_S_UNSUPP without incrementing the success or invalid-request counters. The UNMAP=0 (ZERO_RANGE) happy path is not asserted in the unit tests because ZERO_RANGE support is filesystem-dependent (the host's tmpfs in some kernels returns EOPNOTSUPP); end-to-end UNMAP=0 correctness is left to the integration tests running on ext4. The unit tests cover wire-flag handling for UNMAP=0 via the invalid-flags and zero-sectors cases. Also extend test_request_type_from to assert the new VIRTIO_BLK_T_WRITE_ZEROES → RequestType::WriteZeroes mapping. Signed-off-by: Nikita Kalyazin <nikita.kalyazin@e2b.dev>
1 parent fb04ebe commit 17d83b9

2 files changed

Lines changed: 262 additions & 0 deletions

File tree

src/vmm/src/devices/virtio/block/virtio/device.rs

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2264,4 +2264,262 @@ mod tests {
22642264
assert_eq!(block.config_space.max_discard_sectors, 32);
22652265
}
22662266
}
2267+
2268+
#[test]
2269+
fn test_write_zeroes_feature_and_config() {
2270+
// Non-read-only block: VIRTIO_BLK_F_WRITE_ZEROES set, write-zeroes config
2271+
// fields populated.
2272+
for engine in [FileEngineType::Sync, FileEngineType::Async] {
2273+
let block = default_block(engine);
2274+
assert_ne!(
2275+
block.avail_features & (1u64 << VIRTIO_BLK_F_WRITE_ZEROES),
2276+
0
2277+
);
2278+
assert_eq!(block.avail_features & (1u64 << VIRTIO_BLK_F_RO), 0);
2279+
// default_block has 0x1000-byte backing file → 8 sectors.
2280+
assert_eq!(block.config_space.max_write_zeroes_sectors, 8);
2281+
assert_eq!(block.config_space.max_write_zeroes_seg, 1);
2282+
assert_eq!(block.config_space.write_zeroes_may_unmap, 1);
2283+
}
2284+
2285+
// Read-only block: VIRTIO_BLK_F_RO set, no write-zeroes.
2286+
let f = TempFile::new().unwrap();
2287+
f.as_file().set_len(0x1000).unwrap();
2288+
let config = VirtioBlockConfig {
2289+
drive_id: "test_ro_wz".to_string(),
2290+
path_on_host: f.as_path().to_str().unwrap().to_string(),
2291+
is_root_device: false,
2292+
partuuid: None,
2293+
is_read_only: true,
2294+
cache_type: CacheType::Unsafe,
2295+
rate_limiter: None,
2296+
file_engine_type: FileEngineType::default(),
2297+
};
2298+
let ro_block = VirtioBlock::new(config).unwrap();
2299+
assert_eq!(
2300+
ro_block.avail_features & (1u64 << VIRTIO_BLK_F_WRITE_ZEROES),
2301+
0
2302+
);
2303+
assert_ne!(ro_block.avail_features & (1u64 << VIRTIO_BLK_F_RO), 0);
2304+
assert_eq!(ro_block.config_space.max_write_zeroes_sectors, 0);
2305+
assert_eq!(ro_block.config_space.max_write_zeroes_seg, 0);
2306+
assert_eq!(ro_block.config_space.write_zeroes_may_unmap, 0);
2307+
}
2308+
2309+
#[test]
2310+
fn test_write_zeroes() {
2311+
for engine in [FileEngineType::Sync, FileEngineType::Async] {
2312+
let mut block = default_block(engine);
2313+
let mem = default_mem();
2314+
let interrupt = default_interrupt();
2315+
let vq = VirtQueue::new(GuestAddress(0), &mem, 16);
2316+
set_queue(&mut block, 0, vq.create_queue());
2317+
block.activate(mem.clone(), interrupt).unwrap();
2318+
read_blk_req_descriptors(&vq);
2319+
2320+
let request_type_addr = GuestAddress(vq.dtable[0].addr.get());
2321+
let data_addr = GuestAddress(vq.dtable[1].addr.get());
2322+
let status_addr = GuestAddress(vq.dtable[2].addr.get());
2323+
2324+
// Write-zeroes data descriptor is read-only (guest sends the segment list).
2325+
vq.dtable[1].flags.set(VIRTQ_DESC_F_NEXT);
2326+
vq.dtable[1].len.set(DISCARD_SEGMENT_SIZE);
2327+
2328+
// Valid write-zeroes with UNMAP=1 (PUNCH_HOLE — supported on tmpfs).
2329+
// The UNMAP=0 (ZERO_RANGE) happy path is not asserted here because
2330+
// ZERO_RANGE support is filesystem-dependent (e.g. older tmpfs returns
2331+
// EOPNOTSUPP); the wire-flag handling for UNMAP=0 is covered by the
2332+
// invalid-flag and zero-sectors cases below, and end-to-end correctness
2333+
// is left to the integration tests on ext4.
2334+
{
2335+
mem.write_obj::<u32>(VIRTIO_BLK_T_WRITE_ZEROES, request_type_addr)
2336+
.unwrap();
2337+
mem.write_obj(
2338+
DiscardWriteZeroes {
2339+
sector: 0,
2340+
num_sectors: 4,
2341+
flags: VIRTIO_BLK_WRITE_ZEROES_FLAG_UNMAP,
2342+
},
2343+
data_addr,
2344+
)
2345+
.unwrap();
2346+
2347+
check_metric_after_block!(
2348+
&block.metrics.write_zeroes_count,
2349+
1,
2350+
simulate_queue_and_async_completion_events(&mut block, true)
2351+
);
2352+
2353+
assert_eq!(vq.used.idx.get(), 1);
2354+
assert_eq!(vq.used.ring[0].get().id, 0);
2355+
assert_eq!(vq.used.ring[0].get().len, 1); // status byte only
2356+
assert_eq!(mem.read_obj::<u32>(status_addr).unwrap(), VIRTIO_BLK_S_OK);
2357+
}
2358+
2359+
// Invalid: reserved flag bit set (any bit other than UNMAP).
2360+
{
2361+
vq.used.idx.set(0);
2362+
set_queue(&mut block, 0, vq.create_queue());
2363+
mem.write_obj::<u32>(VIRTIO_BLK_T_WRITE_ZEROES, request_type_addr)
2364+
.unwrap();
2365+
mem.write_obj(
2366+
DiscardWriteZeroes {
2367+
sector: 0,
2368+
num_sectors: 4,
2369+
flags: 0x2, // reserved bit
2370+
},
2371+
data_addr,
2372+
)
2373+
.unwrap();
2374+
2375+
simulate_queue_event(&mut block, Some(true));
2376+
2377+
assert_eq!(vq.used.idx.get(), 1);
2378+
assert_eq!(
2379+
u32::from(mem.read_obj::<u8>(status_addr).unwrap()),
2380+
VIRTIO_BLK_S_IOERR
2381+
);
2382+
}
2383+
2384+
// Invalid: sector range exceeds disk bounds.
2385+
{
2386+
vq.used.idx.set(0);
2387+
set_queue(&mut block, 0, vq.create_queue());
2388+
mem.write_obj::<u32>(VIRTIO_BLK_T_WRITE_ZEROES, request_type_addr)
2389+
.unwrap();
2390+
// 8-sector disk; sector 7 + 4 = 11 > 8.
2391+
mem.write_obj(
2392+
DiscardWriteZeroes {
2393+
sector: 7,
2394+
num_sectors: 4,
2395+
flags: 0,
2396+
},
2397+
data_addr,
2398+
)
2399+
.unwrap();
2400+
2401+
simulate_queue_event(&mut block, Some(true));
2402+
2403+
assert_eq!(vq.used.idx.get(), 1);
2404+
assert_eq!(
2405+
u32::from(mem.read_obj::<u8>(status_addr).unwrap()),
2406+
VIRTIO_BLK_S_IOERR
2407+
);
2408+
}
2409+
2410+
// Invalid: num_sectors=0.
2411+
{
2412+
vq.used.idx.set(0);
2413+
set_queue(&mut block, 0, vq.create_queue());
2414+
mem.write_obj::<u32>(VIRTIO_BLK_T_WRITE_ZEROES, request_type_addr)
2415+
.unwrap();
2416+
mem.write_obj(
2417+
DiscardWriteZeroes {
2418+
sector: 0,
2419+
num_sectors: 0,
2420+
flags: 0,
2421+
},
2422+
data_addr,
2423+
)
2424+
.unwrap();
2425+
2426+
simulate_queue_event(&mut block, Some(true));
2427+
2428+
assert_eq!(vq.used.idx.get(), 1);
2429+
assert_eq!(
2430+
u32::from(mem.read_obj::<u8>(status_addr).unwrap()),
2431+
VIRTIO_BLK_S_IOERR
2432+
);
2433+
}
2434+
2435+
// Invalid data length: not a multiple of DISCARD_SEGMENT_SIZE → parse error.
2436+
{
2437+
vq.used.idx.set(0);
2438+
set_queue(&mut block, 0, vq.create_queue());
2439+
vq.dtable[1].len.set(DISCARD_SEGMENT_SIZE - 1);
2440+
mem.write_obj::<u32>(VIRTIO_BLK_T_WRITE_ZEROES, request_type_addr)
2441+
.unwrap();
2442+
2443+
simulate_queue_event(&mut block, Some(true));
2444+
2445+
// Parse error → descriptor chain discarded (used len = 0).
2446+
assert_eq!(vq.used.idx.get(), 1);
2447+
assert_eq!(vq.used.ring[0].get().len, 0);
2448+
}
2449+
}
2450+
}
2451+
2452+
#[test]
2453+
fn test_write_zeroes_unsupported_cached() {
2454+
for engine in [FileEngineType::Sync, FileEngineType::Async] {
2455+
let mut block = default_block(engine);
2456+
let mem = default_mem();
2457+
let interrupt = default_interrupt();
2458+
let vq = VirtQueue::new(GuestAddress(0), &mem, 16);
2459+
set_queue(&mut block, 0, vq.create_queue());
2460+
block.activate(mem.clone(), interrupt).unwrap();
2461+
read_blk_req_descriptors(&vq);
2462+
2463+
let request_type_addr = GuestAddress(vq.dtable[0].addr.get());
2464+
let data_addr = GuestAddress(vq.dtable[1].addr.get());
2465+
let status_addr = GuestAddress(vq.dtable[2].addr.get());
2466+
2467+
vq.dtable[1].flags.set(VIRTQ_DESC_F_NEXT);
2468+
vq.dtable[1].len.set(DISCARD_SEGMENT_SIZE);
2469+
2470+
mem.write_obj::<u32>(VIRTIO_BLK_T_WRITE_ZEROES, request_type_addr)
2471+
.unwrap();
2472+
mem.write_obj(
2473+
DiscardWriteZeroes {
2474+
sector: 0,
2475+
num_sectors: 4,
2476+
flags: 0,
2477+
},
2478+
data_addr,
2479+
)
2480+
.unwrap();
2481+
2482+
// Pre-set the flag as if a previous EOPNOTSUPP was already detected.
2483+
block.disk.write_zeroes_unsupported = true;
2484+
2485+
// Neither write_zeroes_count nor invalid_reqs_count should increment.
2486+
let wz_before = block.metrics.write_zeroes_count.count();
2487+
let invalid_before = block.metrics.invalid_reqs_count.count();
2488+
simulate_queue_and_async_completion_events(&mut block, true);
2489+
assert_eq!(block.metrics.write_zeroes_count.count(), wz_before);
2490+
assert_eq!(block.metrics.invalid_reqs_count.count(), invalid_before);
2491+
2492+
assert_eq!(vq.used.idx.get(), 1);
2493+
assert_eq!(vq.used.ring[0].get().len, 1); // status byte only
2494+
assert_eq!(
2495+
u32::from(mem.read_obj::<u8>(status_addr).unwrap()),
2496+
VIRTIO_BLK_S_UNSUPP
2497+
);
2498+
}
2499+
}
2500+
2501+
#[test]
2502+
fn test_update_disk_image_refreshes_write_zeroes_state() {
2503+
// update_disk_image() must re-arm the write_zeroes_unsupported cache (the
2504+
// new backing file may live on a fallocate-capable filesystem) and refresh
2505+
// max_write_zeroes_sectors (derived from the disk's sector count).
2506+
for engine in [FileEngineType::Sync, FileEngineType::Async] {
2507+
let mut block = default_block(engine);
2508+
// default_block uses a 0x1000-byte (8-sector) backing file.
2509+
assert_eq!(block.config_space.max_write_zeroes_sectors, 8);
2510+
2511+
// Simulate a previous EOPNOTSUPP from the old backing file.
2512+
block.disk.write_zeroes_unsupported = true;
2513+
2514+
// Hot-swap to a 16 KiB file (32 sectors).
2515+
let f = TempFile::new().unwrap();
2516+
f.as_file().set_len(0x4000).unwrap();
2517+
block
2518+
.update_disk_image(f.as_path().to_str().unwrap().to_string())
2519+
.unwrap();
2520+
2521+
assert!(!block.disk.write_zeroes_unsupported);
2522+
assert_eq!(block.config_space.max_write_zeroes_sectors, 32);
2523+
}
2524+
}
22672525
}

src/vmm/src/devices/virtio/block/virtio/request.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,10 @@ mod tests {
666666
RequestType::from(VIRTIO_BLK_T_GET_ID),
667667
RequestType::GetDeviceID
668668
);
669+
assert_eq!(
670+
RequestType::from(VIRTIO_BLK_T_WRITE_ZEROES),
671+
RequestType::WriteZeroes
672+
);
669673
assert_eq!(RequestType::from(42), RequestType::Unsupported(42));
670674
}
671675

0 commit comments

Comments
 (0)