@@ -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}
0 commit comments