On 8/29/26 00:20, Denis V. Lunev wrote:
> This email originated from an IP that might not be authorized by the domain 
> it was sent from.
> Do not click links or open attachments unless it is an email you expected to 
> receive.
> From: Denis V. Lunev <[email protected]>
>
> it->bs is the cursor into monitor_bdrv_states for the second phase of
> bdrv_next(), so it has to be NULL when that phase starts. Commit
> f6d38c9f6d made the first phase store the node it returns there, to
> unreference the right one when the graph changes underneath. The cursor
> is now left pointing at the last BlockBackend root, so the second phase
> resumes from there instead of from the head of the list and never
> returns a node added before it.
>
> A skipped node drops out of the vm_stop and migration handover paths:
> not flushed, not inactivated, not snapshotted. bdrv_inactivate_all()
> still reports success, so the node keeps its image lock and the
> migration target cannot open the image. Only detached nodes are hit in
> practice, as these callers also recurse into children.
>
> Reset the cursor when the second phase starts. old_bs is taken at the
> top of the function, so f6d38c9f6d keeps working.
>
> Fixes: f6d38c9f6d ("block-backend: fix edge case in bdrv_next() where BDS 
> associated to BB changes")
> Signed-off-by: Denis V. Lunev <[email protected]>
> Cc: Fiona Ebner <[email protected]>
> Cc: Kevin Wolf <[email protected]>
> ---
>
> Notes:
>     The iterator was correct from its introduction in 2016 until 2024:
>     it->bs was written only by the second phase, and the node to unreference
>     came from the BlockBackend instead.
>
>     f6d38c9f6d's own reproducer does not fire for me, 240 runs across 8
>     timings, even with its fix reverted. Tried hard.
>
>  block/block-backend.c                          |  1 +
>  tests/qemu-iotests/tests/inactive-node-nbd     | 16 ++++++++++++++++
>  tests/qemu-iotests/tests/inactive-node-nbd.out |  8 ++++++++
>  3 files changed, 25 insertions(+)
>
> diff --git a/block/block-backend.c b/block/block-backend.c
> index 37ba7e9fc4..55497c4551 100644
> --- a/block/block-backend.c
> +++ b/block/block-backend.c
> @@ -625,6 +625,7 @@ BlockDriverState *bdrv_next(BdrvNextIterator *it)
>              return bs;
>          }
>          it->phase = BDRV_NEXT_MONITOR_OWNED;
> +        it->bs = NULL;
>      }
>  
>      /* Then return the monitor-owned BDSes without a BB attached. Ignore all
> diff --git a/tests/qemu-iotests/tests/inactive-node-nbd 
> b/tests/qemu-iotests/tests/inactive-node-nbd
> index 664157bfd0..ef876ae6be 100755
> --- a/tests/qemu-iotests/tests/inactive-node-nbd
> +++ b/tests/qemu-iotests/tests/inactive-node-nbd
> @@ -47,6 +47,7 @@ def node_is_active(_vm, node_name):
>      return node['active']
>  
>  with iotests.FilePath('disk.img') as path, \
> +     iotests.FilePath('detached.img') as detached_path, \
>       iotests.FilePath('snap.qcow2') as snap_path, \
>       iotests.FilePath('snap2.qcow2') as snap2_path, \
>       iotests.FilePath('target.img') as target_path, \
> @@ -58,6 +59,7 @@ with iotests.FilePath('disk.img') as path, \
>      iotests.log('Preparing disk...')
>      iotests.qemu_img_create('-f', iotests.imgfmt, path, img_size)
>      iotests.qemu_img_create('-f', iotests.imgfmt, target_path, img_size)
> +    iotests.qemu_img_create('-f', iotests.imgfmt, detached_path, img_size)
>  
>      iotests.qemu_img_create('-f', 'qcow2', '-b', path, '-F', iotests.imgfmt,
>                              snap_path)
> @@ -68,6 +70,9 @@ with iotests.FilePath('disk.img') as path, \
>      vm.add_blockdev(f'file,node-name=disk-file,filename={path}')
>      vm.add_blockdev(f'{iotests.imgfmt},file=disk-file,node-name=disk-fmt,'
>                       'active=off')
> +    vm.add_blockdev(f'file,node-name=detached-file,filename={detached_path}')
> +    vm.add_blockdev(f'{iotests.imgfmt},file=detached-file,'
> +                     'node-name=detached-fmt')
>      vm.add_blockdev(f'file,node-name=target-file,filename={target_path}')
>      
> vm.add_blockdev(f'{iotests.imgfmt},file=target-file,node-name=target-fmt')
>      vm.add_blockdev(f'file,node-name=snap-file,filename={snap_path}')
> @@ -297,6 +302,17 @@ with iotests.FilePath('disk.img') as path, \
>      iotests.log('snap2-fmt active: %s' % node_is_active(vm, 'snap2-fmt'))
>      iotests.log('target-fmt active: %s' % node_is_active(vm, 'target-fmt'))
>  
> +    iotests.log('\n=== Inactivating all nodes at once ===')
> +
> +    # detached-fmt has no parent and no BlockBackend, so nothing can reach it
> +    # by recursion. It is only inactivated if bdrv_next() actually returns 
> it.
> +    vm.qmp_log('stop')
> +    vm.qmp_log('blockdev-set-active', active=False)
> +
> +    iotests.log('detached-fmt active: %s' % node_is_active(vm, 
> 'detached-fmt'))
> +    iotests.log('detached-file active: %s'
> +                % node_is_active(vm, 'detached-file'))
> +
>      iotests.log('\nShutting down...')
>      vm.shutdown()
>      log = vm.get_log()
> diff --git a/tests/qemu-iotests/tests/inactive-node-nbd.out 
> b/tests/qemu-iotests/tests/inactive-node-nbd.out
> index a458b4fc05..48451dc0c5 100644
> --- a/tests/qemu-iotests/tests/inactive-node-nbd.out
> +++ b/tests/qemu-iotests/tests/inactive-node-nbd.out
> @@ -235,5 +235,13 @@ snap-fmt active: True
>  snap2-fmt active: True
>  target-fmt active: True
>  
> +=== Inactivating all nodes at once ===
> +{"execute": "stop", "arguments": {}}
> +{"return": {}}
> +{"execute": "blockdev-set-active", "arguments": {"active": false}}
> +{"return": {}}
> +detached-fmt active: False
> +detached-file active: False
> +
>  Shutting down...
>  
ping

Reply via email to