From nobody Sun Jul 26 11:06:39 2026 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org Return-Path: Received: from lists1p.gnu.org (lists1p.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1782998625820405.32504716794097; Thu, 2 Jul 2026 06:23:45 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists1p.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wfHO9-0006KH-Mw; Thu, 02 Jul 2026 09:23:33 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wfHNs-0005zX-8H; Thu, 02 Jul 2026 09:23:23 -0400 Received: from proxmox-new.maurer-it.com ([94.136.29.106]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wfHNp-0001v7-R2; Thu, 02 Jul 2026 09:23:15 -0400 Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 6DB7CA2DE6; Thu, 02 Jul 2026 15:23:03 +0200 (CEST) From: Fiona Ebner To: qemu-devel@nongnu.org Cc: qemu-block@nongnu.org, hreitz@redhat.com, kwolf@redhat.com, qemu-stable@nongnu.org Subject: [PATCH 1/2] block/export/fuse: fix regression with O_TRUNC when export is not growable Date: Thu, 2 Jul 2026 15:22:35 +0200 Message-ID: <20260702132256.661429-2-f.ebner@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260702132256.661429-1-f.ebner@proxmox.com> References: <20260702132256.661429-1-f.ebner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1782998575601 Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists1p.gnu.org; Received-SPF: pass client-ip=94.136.29.106; envelope-from=f.ebner@proxmox.com; helo=proxmox-new.maurer-it.com X-Spam_score_int: -18 X-Spam_score: -1.9 X-Spam_bar: - X-Spam_report: (-1.9 / 5.0 requ) BAYES_00=-1.9, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: qemu development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZM-MESSAGEID: 1782998627203158500 Content-Type: text/plain; charset="utf-8" Before commit a94a1d7699 ("fuse: Manually process requests (without libfuse)"), the O_TRUNC flag when open()-ing an export would be ignored. This is because libfuse sets FUSE_CAP_ATOMIC_O_TRUNC, so the kernel lets user space handle the O_TRUNC flag, which is ignored by the fuse code for export. After the commit, FUSE_CAP_ATOMIC_O_TRUNC is not set anymore, so the O_TRUNC flag is handled by the kernel, which executes a truncate. For blockdev-based exports, this causes a regression, because opening with O_TRUNC would previously work, but results in an ENOTSUP after commit a94a1d7699. For file-based exports, the fact that truncate is executed can be considered an improvement in general. However, in combination with growable=3Doff, this still results in a practical regression in combination with virt-fw-vars, which opens its output file with O_TRUNC and previously worked with a file-based export with growable=3Doff. After commit a94a1d7699, the file is truncated upon open and then cannot grow, meaning virt-fw-vars won't be able to write the output. To fix these regressions, while keeping the improved behavior for file-based exports with growable=3Don, set the FUSE_CAP_ATOMIC_O_TRUNC flag again if growable=3Doff. Cc: qemu-stable@nongnu.org Fixes: a94a1d7699 ("fuse: Manually process requests (without libfuse)") Signed-off-by: Fiona Ebner --- block/export/fuse.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/block/export/fuse.c b/block/export/fuse.c index c0e8dfb643..9ae22a2e00 100644 --- a/block/export/fuse.c +++ b/block/export/fuse.c @@ -859,6 +859,18 @@ fuse_co_init(FuseExport *exp, struct fuse_init_out *ou= t, uint32_t supported_flags =3D FUSE_ASYNC_READ | FUSE_ASYNC_DIO; uint32_t flags2 =3D 0; =20 + if (!exp->growable) { + /* + * Back when libfuse was used, it would always set this flag and t= hus + * the kernel did not execute a truncate itself and passed along O= _TRUNC + * to user space. Continue setting the flag for backwards compatib= ility + * when the export is not growable to avoid issues with O_TRUNC, i= .e. + * blockdev-based exports running into ENOTSUP and file-based expo= rts + * with growable=3Doff to be truncated and then stuck with size 0. + */ + supported_flags =3D FUSE_ATOMIC_O_TRUNC; + } + if (in->major !=3D 7) { error_report("FUSE major version mismatch: We have 7, but kernel h= as %" PRIu32, in->major); --=20 2.47.3 From nobody Sun Jul 26 11:06:39 2026 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org Return-Path: Received: from lists1p.gnu.org (lists1p.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1782998644222273.9823488336315; Thu, 2 Jul 2026 06:24:04 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists1p.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1wfHOA-0006Pr-Ca; Thu, 02 Jul 2026 09:23:34 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wfHNs-0005zZ-Fs; Thu, 02 Jul 2026 09:23:23 -0400 Received: from proxmox-new.maurer-it.com ([94.136.29.106]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1wfHNp-0001v6-Ud; Thu, 02 Jul 2026 09:23:16 -0400 Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 3EE9B479FE; Thu, 02 Jul 2026 15:23:03 +0200 (CEST) From: Fiona Ebner To: qemu-devel@nongnu.org Cc: qemu-block@nongnu.org, hreitz@redhat.com, kwolf@redhat.com, qemu-stable@nongnu.org Subject: [PATCH 2/2] iotests: test O_TRUNC behavior for fuse exports Date: Thu, 2 Jul 2026 15:22:36 +0200 Message-ID: <20260702132256.661429-3-f.ebner@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260702132256.661429-1-f.ebner@proxmox.com> References: <20260702132256.661429-1-f.ebner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1782998575665 Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists1p.gnu.org; Received-SPF: pass client-ip=94.136.29.106; envelope-from=f.ebner@proxmox.com; helo=proxmox-new.maurer-it.com X-Spam_score_int: -18 X-Spam_score: -1.9 X-Spam_bar: - X-Spam_report: (-1.9 / 5.0 requ) BAYES_00=-1.9, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: qemu development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZM-MESSAGEID: 1782998645318158500 Content-Type: text/plain; charset="utf-8" The test cases for the blockdev-based export and for the file-based export with growable=3Don work before commit a94a1d7699 ("fuse: Manually process requests (without libfuse)"), then are broken until commit "block/export/fuse: fix regression with O_TRUNC when export is growable". The test case for the blockdev-based export requires passwordless sudo for losetup and chmod similar to test 108. Signed-off-by: Fiona Ebner --- tests/qemu-iotests/tests/fuse-truncate | 171 +++++++++++++++++++++ tests/qemu-iotests/tests/fuse-truncate.out | 5 + 2 files changed, 176 insertions(+) create mode 100755 tests/qemu-iotests/tests/fuse-truncate create mode 100644 tests/qemu-iotests/tests/fuse-truncate.out diff --git a/tests/qemu-iotests/tests/fuse-truncate b/tests/qemu-iotests/te= sts/fuse-truncate new file mode 100755 index 0000000000..10bc1175c0 --- /dev/null +++ b/tests/qemu-iotests/tests/fuse-truncate @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +# group: rw +# +# Test how fuse exports behave with regard to O_TRUNC. +# +# Copyright (C) 2026 Proxmox Server Solutions GmbH +# +# SPDX-License-Identifier: GPL-2.0-or-later + +import os +import subprocess +from pathlib import Path + +import iotests +from iotests import qemu_img, QemuStorageDaemon + +fuse_mount_point =3D os.path.join(iotests.test_dir, 'export.fuse') +image_size =3D 1 * 1024 * 1024 +image =3D os.path.join(iotests.test_dir, 'image.' + iotests.imgfmt) + +def check_fuse_support(): + Path(fuse_mount_point).touch() + test_qsd =3D QemuStorageDaemon('--blockdev', 'null-co,node-name=3Dnode= 0', + qmp=3DTrue) + res =3D test_qsd.qmp('block-export-add', { + 'id': 'exp0', + 'type': 'fuse', + 'node-name': 'node0', + 'mountpoint': fuse_mount_point, + 'allow-other': 'off' + }) + test_qsd.stop() + os.remove(fuse_mount_point) + if 'error' in res: + assert (res['error']['desc'] =3D=3D + "Parameter 'type' does not accept value 'fuse'") + iotests.notrun('No FUSE support') + +def check_sudo_support(): + try: + subprocess.run(['sudo', '-n', 'losetup', '--version'], + capture_output=3DTrue, check=3DTrue) + except subprocess.CalledProcessError: + return False + try: + subprocess.run(['sudo', '-n', 'chmod', '--version'], + capture_output=3DTrue, check=3DTrue) + except subprocess.CalledProcessError: + return False + return True + +check_fuse_support() + +class TestTruncateBase(iotests.QMPTestCase): + growable =3D False + supports_preconditions =3D True + + def evaluate_preconditions(self): + return + + def add_blockdev(self): + qemu_img('create', '-f', iotests.imgfmt, image, str(image_size)) + self.qsd.cmd('blockdev-add', { + 'node-name': 'node0', + 'driver': iotests.imgfmt, + 'file': { + 'driver': 'file', + 'filename': image + } + }) + + def cleanup_blockdev(self): + os.remove(image) + + def add_export(self): + self.qsd.cmd('block-export-add', { + 'id': 'exp0', + 'type': 'fuse', + 'node-name': 'node0', + 'mountpoint': fuse_mount_point, + 'growable': self.growable, + 'writable': True, + 'allow-other': 'off' + }) + + def stop_qsd(self): + if self.qsd: + self.qsd.cmd('block-export-del', {'id': 'exp0'}) + self.qsd.stop() + self.qsd =3D None + + def setUp(self): + self.evaluate_preconditions() + if not self.supports_preconditions: + return + Path(fuse_mount_point).touch() + self.qsd =3D QemuStorageDaemon(qmp=3DTrue) + self.add_blockdev() + self.add_export() + + def tearDown(self): + if not self.supports_preconditions: + return + self.stop_qsd() + self.cleanup_blockdev() + os.remove(fuse_mount_point) + +class TestTruncateFileGrowable(TestTruncateBase): + growable =3D True + + def test_o_trunc(self): + with open(fuse_mount_point, 'w+b') as file: + file.seek(0, os.SEEK_END) + self.assertEqual(file.tell(), 0) + file.write(b"test") + self.stop_qsd() + +class TestTruncateFileNotGrowable(TestTruncateBase): + growable =3D False + + def test_o_trunc(self): + with open(fuse_mount_point, 'w+b') as file: + file.seek(0, os.SEEK_END) + self.assertEqual(file.tell(), image_size) + file.seek(0, os.SEEK_SET) + file.write(b"test") + self.stop_qsd() + +class TestTruncateBlockdev(TestTruncateBase): + growable =3D False + + def evaluate_preconditions(self): + self.supports_preconditions =3D check_sudo_support() + + def add_blockdev(self): + qemu_img('create', '-f', iotests.imgfmt, image, str(image_size)) + res =3D subprocess.run(['sudo', '-n', 'losetup', '--show', '-f', i= mage], + check=3DTrue, capture_output=3DTrue, text=3DT= rue) + self.loopdev =3D res.stdout.strip() + subprocess.run(['sudo', '-n', 'chmod', 'go+rw', self.loopdev], + check=3DTrue, capture_output=3DTrue) + self.qsd.cmd('blockdev-add', { + 'node-name': 'node0', + 'driver': iotests.imgfmt, + 'file': { + 'driver': 'host_device', + 'filename': self.loopdev + } + }) + + def cleanup_blockdev(self): + subprocess.run(['sudo', '-n', 'losetup', '--detach', self.loopdev], + check=3DTrue, capture_output=3DTrue) + os.remove(image) + + def test_o_trunc(self): + if not self.supports_preconditions: + iotests.case_notrun('No passwordless sudo for losetup and chmo= d') + return + + with open(fuse_mount_point, 'w+b') as file: + file.seek(0, os.SEEK_END) + self.assertEqual(file.tell(), image_size) + file.seek(0, os.SEEK_SET) + file.write(b"test") + self.stop_qsd() + +if __name__ =3D=3D '__main__': + iotests.main(supported_fmts=3D['raw'], + supported_protocols=3D['file'], + supported_platforms=3D['linux']) diff --git a/tests/qemu-iotests/tests/fuse-truncate.out b/tests/qemu-iotest= s/tests/fuse-truncate.out new file mode 100644 index 0000000000..8d7e996700 --- /dev/null +++ b/tests/qemu-iotests/tests/fuse-truncate.out @@ -0,0 +1,5 @@ +... +---------------------------------------------------------------------- +Ran 3 tests + +OK --=20 2.47.3