On 7/24/26 18:54, Kevin Wolf wrote:
> Am 16.07.2026 um 19:54 hat Denis V. Lunev geschrieben:
>> On 7/16/26 17:35, 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.
>>> qcow2_do_close() -> qcow2_inactivate() clears the dirty bit with a
>>> plain write to bs->file, unconditionally. A read-only node can still
>>> be dirty, inherited from an earlier writable session, and that write
>>> then hits a missing BLK_PERM_WRITE and asserts in
>>> bdrv_co_write_req_prepare() (block/io.c) on an entirely ordinary
>>> close -- closing is expected, the dirty bit on a read-only node
>>> is not.
>>>
>>> Skip the clear for read-only nodes, same as read access already does.
>>> Any other still-dirty node keeps the unguarded write: it is expected
>>> to hold write permission, and a missing one there is a bug worth
>>> seeing.
>>>
>>> Signed-off-by: Denis V. Lunev <[email protected]>
>>> CC: Kevin Wolf <[email protected]>
>>> CC: Hanna Reitz <[email protected]>
>>> ---
>>>  block/qcow2.c              |  6 +++++-
>>>  tests/qemu-iotests/039     | 11 +++++++++++
>>>  tests/qemu-iotests/039.out |  3 +++
>>>  3 files changed, 19 insertions(+), 1 deletion(-)
>>>
>>> diff --git a/block/qcow2.c b/block/qcow2.c
>>> index 2ab6ddd7b0..adb177e6b6 100644
>>> --- a/block/qcow2.c
>>> +++ b/block/qcow2.c
>>> @@ -2870,7 +2870,11 @@ static int GRAPH_RDLOCK 
>>> qcow2_inactivate(BlockDriverState *bs)
>>>                       strerror(-ret));
>>>      }
>>>  
>>> -    if (result == 0) {
>>> +    /*
>>> +     * A read-only node cannot resolve an inherited dirty bit here;
>>> +     * leave it dirty, same as plain read access already does.
>>> +     */
>>> +    if (result == 0 && !bdrv_is_read_only(bs)) {
>>>          qcow2_mark_clean(bs);
>>>      }
>>>  
>>> diff --git a/tests/qemu-iotests/039 b/tests/qemu-iotests/039
>>> index e43e7026ce..94a8bfe754 100755
>>> --- a/tests/qemu-iotests/039
>>> +++ b/tests/qemu-iotests/039
>>> @@ -84,6 +84,17 @@ $QEMU_IO -r -c "read -P 0x5a 0 512" "$TEST_IMG" | 
>>> _filter_qemu_io
>>>  # The dirty bit must be set
>>>  _qcow2_dump_header | grep incompatible_features
>>>  
>>> +echo
>>> +echo "== Read-only open must not crash on close =="
>>> +
>>> +# We must not try to write the QCOW2 header to a read-only image.
>>> +$QEMU_IMG info --image-opts \
>>> +    
>>> "driver=$IMGFMT,read-only=on,file.driver=file,file.filename=$TEST_IMG,file.read-only=off"
>>>  \
>>> +    > /dev/null
>>> +
>>> +# The dirty bit must still be set: this open never wrote any guest data
>>> +_qcow2_dump_header | grep incompatible_features
>>> +
>>>  echo
>>>  echo "== Repairing the image file must succeed =="
>>>  
>>> diff --git a/tests/qemu-iotests/039.out b/tests/qemu-iotests/039.out
>>> index 8fdbcc528a..c66361128f 100644
>>> --- a/tests/qemu-iotests/039.out
>>> +++ b/tests/qemu-iotests/039.out
>>> @@ -24,6 +24,9 @@ read 512/512 bytes at offset 0
>>>  512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
>>>  incompatible_features     [0]
>>>  
>>> +== Read-only open must not crash on close ==
>>> +incompatible_features     [0]
>>> +
>>>  == Repairing the image file must succeed ==
>>>  ERROR cluster 5 refcount=0 reference=1
>>>  Rebuilding refcount structure
>> Actually the problem is more subtle. The following command line results in
>>
>> qemu-storage-daemon \                                                   
>>   --blockdev
>> '{"driver":"file","filename":"dirty.qcow2","aio":"threads","node-name":"file0","cache":{"direct":false,"no-flush":false},"auto-read-only":true,"discard":"unmap"}'
>> \                    
>>   --blockdev
>> '{"node-name":"fmt0","read-only":true,"discard":"unmap","cache":{"direct":false,"no-flush":false},"driver":"qcow2","file":"file0"}'
>> \                                                   
>>   --chardev socket,id=qmp,path=master.sock,server=on,wait=off \         
>>   --monitor chardev=qmp                                                 
>>                                                          
>> via query-named-block-nodes:
>>
>>   node-name=fmt0     drv=qcow2  ro=True
>>   node-name=file0    drv=file   ro=False
>>
>> which would come to exactly configuration being asserted in the test
>> and this setup is pretty legal at my opinion. In production we have
>> had something lengthier with more snapshots.
> Yes, I don't see a problem with it.
>
> However, I also don't get a crash or anything with your QSD command line
> and a dirty image with this patch applied, so what is the part that is
> still incorrect?
>
> What is the final result that you're seeing here?

  === Image setup ===
  qemu-img create -f qcow2 -o compat=1.1,lazy_refcounts=on dirty.qcow2 64M
  qemu-io -c "write -P 0x5a 0 512" -c "sigraise 9" dirty.qcow2
  # expect: "wrote 512/512 bytes..." followed by "Killed" from the shell

  qemu-img info --output=json dirty.qcow2 | grep dirty-flag
  # expect: "dirty-flag": true

  === Launch daemon ===
  ./build/storage-daemon/qemu-storage-daemon \
    --blockdev
'{"driver":"file","filename":"dirty.qcow2","aio":"threads","node-name":"file0","cache":{"direct":false,"no-flush":false},"auto-read-only":true,"discard":"unmap"}'
\
    --blockdev
'{"node-name":"fmt0","read-only":true,"discard":"unmap","cache":{"direct":false,"no-flush":false},"driver":"qcow2","file":"file0"}'
\
    --chardev socket,id=qmp,path=master.sock,server=on,wait=off \
    --monitor chardev=qmp \
    > master.log 2>&1 &

  === Connect and drive it ===
  ./build/run qmp-shell master.sock

  (QEMU) quit

Expected result:
[1]+  Aborted (core dumped) ./build/storage-daemon/qemu-storage-daemon

Double checked this on current QEMU master.

Here I have just stated that the case inside the test I have added
in the thread starter is not artificial and this is not a mistake.
Such a command line has been generated by libvirt for me.

>> File descriptor in turn is opened in a correct way - with O_RDONLY
>> that is why the problem was hidden for a lot of time.
>>
>> Would it be more reasonable to disallow to set dirty rather than
>> to ignore is open question to me. Playing with permissions and
>> changing bdrv_is_read_only() seems more dangerous.
> I don't follow, sorry. The patch fixes a code path that marks the image
> as clean. Setting the dirty flag is the opposite operation and happens
> here before we even opened the image. How would you disallow that?
>
>> Also worth to note that bdrv_co_write_req_prepare() has wrong check.
>> It should check file descriptor rather than administrative permission.
> That doesn't sounds right. bdrv_co_write_req_prepare() is a generic
> block layer function, the file descriptor is a file-posix implementation
> detail.
OK. You say that at the bdrv_co_write_req_prepare() we check
only possible permission and that is ok. That was really
unexpected to me and that is why I have wonders. Designed this
way, no changes going in.

I can live with that :-)

Den

Reply via email to