All Virtuozzo development lists (kernel + QEMU)
 help / color / mirror / Atom feed
Search results ordered by [date|relevance]  view[summary|nested|Atom feed]
thread overview below | download mbox.gz: |
* Re: [Devel] [PATCH VZ10 v9 8/8] selftests/ve: Add mount accounting selftest
  @ 2026-09-18 15:06  0%   ` Vladimir Riabchun
  2026-09-25 16:04  0%   ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-18 15:06 UTC (permalink / raw)
  To: devel

Sashiko review:

On 9/15/26 19:22, Vladimir Riabchun wrote:
> There are 6 test cases, covered in the new test:
> 1. Simple mount accouting correctness, just mount/umount.
> 2. Verification of correct limit hits and changes, including
>     negative values.
> 3. Partial mounts test, when mount limit is hit in the middle
>     of creation.
> 4. Test that enabled pseudosuper allows overuse.
> 5. Test that pseudosuper doesn't affect mount accounting.
> 6. Failcount feature verification.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-135520
> 
> Feature: per-ve failcounters
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> ---
>   tools/testing/selftests/ve/.gitignore         |   1 +
>   tools/testing/selftests/ve/Makefile           |   1 +
>   .../selftests/ve/ve_mount_accounting_test.c   | 550 ++++++++++++++++++
>   tools/testing/selftests/ve/ve_selftest.h      |  21 +
>   4 files changed, 573 insertions(+)
>   create mode 100644 tools/testing/selftests/ve/ve_mount_accounting_test.c
> 
> diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
> index afa4c568c2c9..3df4d05888dc 100644
> --- a/tools/testing/selftests/ve/.gitignore
> +++ b/tools/testing/selftests/ve/.gitignore
> @@ -1,2 +1,3 @@
>   ve_ns_owner_test
>   ve_perms_test
> +ve_mount_accounting_test
> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
> index ec40cbc7b3a1..c6efe7c4b4fb 100644
> --- a/tools/testing/selftests/ve/Makefile
> +++ b/tools/testing/selftests/ve/Makefile
> @@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
>   
>   TEST_GEN_PROGS += ve_ns_owner_test
>   TEST_GEN_PROGS += ve_perms_test
> +TEST_GEN_PROGS += ve_mount_accounting_test
>   
>   include ../lib.mk
> diff --git a/tools/testing/selftests/ve/ve_mount_accounting_test.c b/tools/testing/selftests/ve/ve_mount_accounting_test.c
> new file mode 100644
> index 000000000000..6ba13dcdcc87
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
> @@ -0,0 +1,550 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * ve_mount_accounting selftests
> + *
> + * Tests to check the correctness of mount accounting.
> + */
> +#define _GNU_SOURCE
> +#include <asm/unistd.h>
> +#include <linux/sched.h>
> +#include <linux/limits.h>
> +#include <sys/wait.h>
> +#include <sys/syscall.h>
> +#include <sys/stat.h>
> +#include <sys/mount.h>
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <sched.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <unistd.h>
> +#include <string.h>
> +#include <time.h>
> +
> +#include "../kselftest_harness.h"
> +#include "ve_selftest.h"
> +
> +#define TMP_DIR			"/ve-mnt-tmp/"
> +#define VE_MOUNTS_MAX		128
> +
> +/*
> + * These values take a while to calculate, the first test will
> + * do this and store the values for those who come after.
> + */
> +int *need_avail;
> +
> +FIXTURE(ve_mnt_acc)
> +{
> +	int cgv2_fd;
> +	int ctid;
> +	struct namespace_ctx ns_ctx;
> +	int orig_have_mnt;
> +};
> +
> +/*
> + * free_vfsmnt is called via call_rcu, so we need to ensure
> + * that all pending call_rcu tasks are finished.
> + * However, one barrier is not enough:
> + * mntput_no_expire
> + *   init_task_work(&mnt->mnt_rcu, __cleanup_mnt);
> + *   task_work_add(task, &mnt->mnt_rcu, TWA_RESUME)
> + *     OR
> + *   schedule_delayed_work(&delayed_mntput_work, 1)
> + *   # Both later call cleanup_mnt
> + *     cleanup_mnt
> + *       call_rcu(&mnt->mnt_rcu, delayed_free_vfsmnt);
> + *
> + * We can't enforce workqueue flush from userspace, so instead
> + * read several times until the value is the one we expect.
> + */

Sashiko says:
  This segment of the `wait_on_mnt_avail` function can potentially lead
  to an infinite wait if the value `mnt_avail_nr` does not match
  the expected value before timeout.
  While this isn't a bug introduced by this patch, could we include
  some mechanism  to handle timeouts more gracefully, such as an error code
  return or retry limit?

Something is off here.
1. There was no bug before this patch.
2. Retry and error return mechanism is what this code does.

> +static int wait_on_mnt_avail(FIXTURE_DATA(ve_mnt_acc) * self, int expected)
> +{
> +#define WAIT_TIMEOUT_NS		(4096UL * 1000 * 1000)
> +	char path_avail[64];
> +	int mnt_avail_nr = 0;
> +	struct timespec delay = { .tv_nsec = 1000000 };
> +
> +	snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
> +
> +	while (delay.tv_nsec <= WAIT_TIMEOUT_NS) {
> +		if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr)) {
> +			fprintf(stderr, "Failed to read from %s\n", path_avail);
> +			return 1;
> +		}
> +		if (mnt_avail_nr == expected)
> +			return 0;
> +
> +		do_rcu_barrier();
> +		if (nanosleep(&delay, NULL)) {
> +			fprintf(stderr, "nanosleep failed: %s\n", strerror(errno));
> +			return 1;
> +		}
> +		delay.tv_nsec *= 2;
> +	}
> +	fprintf(stderr, "Didn't get expected value at %s\n", path_avail);
> +	fprintf(stderr, "Expected %d, last read %d\n", expected, mnt_avail_nr);
> +	return -1;
> +}
> +
> +static int set_pseudosuper(FIXTURE_DATA(ve_mnt_acc) * self, int value)
> +{
> +	char path[64];
> +
> +	snprintf(path, sizeof(path), "%d/ve.pseudosuper", self->ctid);
> +	return write_u64_at(self->cgv2_fd, path, value);
> +}
> +

Sashiko says:
  In `_create_mount`, can the clean-up logic be more robust to ensure
  that filesystem resources are consistently freed in the event of an error
  or unexpected behavior?

No more cleanup here is possible.

> +static int _create_mount(void *id_ptr)
> +{
> +	char path[PATH_MAX];
> +	int id = *(int *)id_ptr, ret;
> +
> +	snprintf(path, sizeof(path), TMP_DIR "%d", id);
> +
> +	if (mkdir(path, 0755) < 0) {
> +		fprintf(stderr, "Failed to create directory %s: %s\n", path, strerror(errno));
> +		return -1;
> +	}
> +	ret = mount("tmpfs", path, "tmpfs", 0, "size=1M");
> +	if (!ret)
> +		return 0;
> +	fprintf(stderr, "Failed to mount tmpfs to %s: %s\n", path, strerror(errno));
> +
> +	rmdir(path);
> +	return -1;
> +}
> +
> +/* Create a mount and wait until mount counter is valid */
> +static int create_mount(FIXTURE_DATA(ve_mnt_acc) * self, int id)
> +{
> +	int ret, avail_before;
> +	char path[64];
> +
> +	snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
> +
> +	if (read_s32_at(self->cgv2_fd, path, &avail_before)) {
> +		fprintf(stderr, "Failed to read mnt_avail_nr in create_mount\n");
> +		return 1;
> +	}
> +

Sashiko says:
  Would it be beneficial to include comprehensive error handling in `create_mount`
  to cover cases when `run_in_ve` fails?

No, it wouldn't. Nothing we can do in this case and nothing
needs to be handled - no resources created.

> +	ret = run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
> +		CLONE_NEWVE | CLONE_NEWNS, _create_mount, &id);
> +
> +	if (ret)
> +		wait_on_mnt_avail(self, avail_before);
> +	else
> +		ret = wait_on_mnt_avail(self, avail_before - 1);
> +	return ret;
> +}
> +

Sashiko says:
  Does the `_destroy_mount` function correctly handle subsequent errors that may
  occur during the unmount and directory removal stages?

Yes, it does.

> +static int _destroy_mount(void *id_ptr)
> +{
> +	char path[PATH_MAX];
> +	struct stat st;
> +	int id = *(int *)id_ptr;
> +
> +	snprintf(path, sizeof(path), TMP_DIR "%d", id);
> +
> +	if (stat(path, &st))
> +		return 1;
> +	if (umount(path)) {
> +		fprintf(stderr, "failed to umount directory %s: %s\n", path, strerror(errno));
> +		return -1;
> +	}
> +	if (rmdir(path)) {
> +		fprintf(stderr, "failed to remove directory %s: %s\n", path, strerror(errno));
> +		return -1;
> +	}
> +	return 0;
> +}
> +
> ...
> +
> +int main(int argc, char **argv)
> +{
> +	need_avail = mmap(NULL, sizeof(int),
> +		PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
> +
> +	if (need_avail == MAP_FAILED) {
> +		fprintf(stderr, "Failed to mmap shared region: %s\n", strerror(errno));
> +		return 1;
> +	}
> +	*need_avail = -1;
> +
> +	return test_harness_run(argc, argv);

Sashiko says:
  Does the `main` function properly handle the memory allocated by `mmap`,
  especially considering that there might not be a corresponding `munmap` to free
  the memory?

Doesn't matter, we can free need_avail only when test_harness_run finishes.
It is easier to just exit.

> +}
> diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
> index bf9b89b59a6c..8cc9b3238aaf 100644
> --- a/tools/testing/selftests/ve/ve_selftest.h
> +++ b/tools/testing/selftests/ve/ve_selftest.h
> @@ -413,4 +413,25 @@ static inline int is_mounted(const char *path)
>   	return errno == EXDEV ? 1 : -1;
>   }
>   

Sashiko says:
  Could write operations within `do_rcu_barrier` be improperly assumed to be atomic
  leading to potential scheduling issues when invoked in contexts that might sleep?

No, they couldn't. This is user-space code.

> +static inline int do_rcu_barrier(void)
> +{
> +	int fd;
> +
> +	errno = 0;
> +	fd = open("/sys/module/rcutree/parameters/do_rcu_barrier", O_WRONLY);
> +
> +	if (fd < 0) {
> +		fprintf(stderr, "Failed to open do_rcu_barrier: %s\n", strerror(errno));
> +		return -1;
> +	}
> +	errno = 0;
> +	if (write(fd, "1", 2) != 2) {
> +		fprintf(stderr, "Failed to issue rcu_barrier: %s\n", strerror(errno));
> +		close(fd);
> +		return -1;
> +	}
> +	close(fd);
> +	return 0;
> +}
> +
>   #endif /* __SELFTESTS_VE_VE_SELFTEST_H */

-- 
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH vz10 3/7] selftests: net: skip what this kernel and iproute2 do not have
  @ 2026-09-21 14:51  0%   ` Vladimir Riabchun
  0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-21 14:51 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

> Five cases report a failure where a feature is simply not there.
> 
> tls asks for every cipher it knows and fails on the ones the kernel does
> not implement, SM4 and ARIA, which answer ENOENT at setsockopt
> time. Skip a cipher the kernel refuses that way; the run then reports
> 632 passed, 264 skipped and none failed.
> 
> rtnetlink.sh sets up an erspan tunnel, which this kernel does not have
> and which fails with "Unknown device type", and checks a netconf dump
> whose exit status iproute2 gets wrong: 6.11 exits 2 on a dump that
> worked, 6.17 exits 0. Probe for both and skip when unusable.
> 
> rtnetlink.py asks for an IPv4 multicast address dump the kernel answers
> with EOPNOTSUPP, and ip_local_port_range asks for protocols it does not
> have. Skip those too.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-139651
> Feature: fix selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/net/ip_local_port_range.c b/tools/testing/selftests/net/ip_local_port_range.c
> index 29451d2244b7..2d7b9c852b2b 100644
> --- a/tools/testing/selftests/net/ip_local_port_range.c
> +++ b/tools/testing/selftests/net/ip_local_port_range.c
> @@ -118,10 +118,6 @@ static int get_ip_local_port_range(int fd, __u32 *range)
>  
>  FIXTURE(ip_local_port_range) {};
>  
> -FIXTURE_SETUP(ip_local_port_range)
> -{
> -}
> -
>  FIXTURE_TEARDOWN(ip_local_port_range)
>  {
>  }
> @@ -180,6 +176,19 @@ FIXTURE_VARIANT_ADD(ip_local_port_range, ip6_mptcp) {
>  	.so_protocol	= IPPROTO_MPTCP,
>  };
>  
> +FIXTURE_SETUP(ip_local_port_range)
> +{
> +	int fd;
> +
> +	/* Not every protocol under test is built into every kernel. */
> +	fd = socket(variant->so_domain, variant->so_type, variant->so_protocol);
> +	if (fd < 0 && (errno == EPROTONOSUPPORT || errno == ESOCKTNOSUPPORT ||
> +		       errno == EAFNOSUPPORT))
> +		SKIP(return, "%s", strerror(errno));
> +	ASSERT_GE(fd, 0) TH_LOG("socket failed");
> +	close(fd);
> +}
> +
>  TEST_F(ip_local_port_range, invalid_option_value)
>  {
>  	__u16 val16;
> diff --git a/tools/testing/selftests/net/rtnetlink.py b/tools/testing/selftests/net/rtnetlink.py
> index baa7b33f313e..425fc7a3941b 100755
> --- a/tools/testing/selftests/net/rtnetlink.py
> +++ b/tools/testing/selftests/net/rtnetlink.py
> @@ -1,7 +1,9 @@
>  #! /usr/bin/python3 -sP
>  # SPDX-License-Identifier: GPL-2.0
>  
> -from lib.py import ksft_exit, ksft_run, ksft_ge, RtnlAddrFamily
> +from lib.py import ksft_exit, ksft_run, ksft_ge, KsftSkipEx
> +from lib.py import NlError, RtnlAddrFamily
> +import errno
>  import socket
>  
>  IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01'
> @@ -12,7 +14,12 @@ def dump_mcaddr_check(rtnl: RtnlAddrFamily) -> None:
>      At least the loopback interface should have this address.
>      """
>  
> -    addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True)
> +    try:
> +        addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True)
> +    except NlError as e:
> +        if e.nl_msg.error == -errno.EOPNOTSUPP:
> +            raise KsftSkipEx("IPv4 multicast address dump is not supported")
> +        raise
>  
>      all_host_multicasts = [
>          addr for addr in addresses if addr['multicast'] == IPV4_ALL_HOSTS_MULTICAST
> diff --git a/tools/testing/selftests/net/rtnetlink.sh b/tools/testing/selftests/net/rtnetlink.sh
> index 5f72b447e940..cc4e94b90002 100755
> --- a/tools/testing/selftests/net/rtnetlink.sh
> +++ b/tools/testing/selftests/net/rtnetlink.sh
> @@ -142,10 +142,30 @@ kci_del_dummy()
>  	run_cmd ip link del dev "$devdummy"
>  }
>  
> +# Some iproute2 versions exit non-zero from a netconf dump that worked,
> +# printing the devconf and then failing anyway.  Nothing about the kernel
> +# can be learned from the exit status of such an ip, so find out once.
> +netconf_exit_status_usable()
> +{
> +	local out
> +
> +	out=$(ip -4 netconf show dev lo 2>/dev/null)
> +	if [ $? -ne 0 ] && [ -n "$out" ]; then
> +		return 1
> +	fi
> +	return 0
> +}
> +
>  kci_test_netconf()
>  {
>  	dev="$1"
>  	r=$ret
> +
> +	if ! netconf_exit_status_usable; then
> +		end_test "SKIP: ip netconf $dev: iproute2 fails a dump that worked"
> +		return $ksft_skip
> +	fi
> +
>  	run_cmd ip netconf show dev "$dev"
>  	for f in 4 6; do
>  		run_cmd ip -$f netconf show dev "$dev"
> @@ -920,6 +940,17 @@ kci_test_erspan()
>  		return $ksft_skip
>  	fi
>  
> +	# the kernel can be built without erspan, rtnetlink then has no ops
> +	# for the type and says so.  A kernel that has it gets past this and
> +	# fails on the attributes the probe leaves out.
> +	if ip -netns "$testns" link add dev "$DEV_NS" type erspan 2>&1 | \
> +	   grep -q "Unknown device type"; then
> +		end_test "SKIP: erspan: not supported by the kernel"
> +		ip netns del "$testns"
> +		return $ksft_skip
> +	fi
> +	ip -netns "$testns" link del dev "$DEV_NS" 2>/dev/null
> +
>  	# test native tunnel erspan v1
>  	run_cmd ip -netns "$testns" link add dev "$DEV_NS" type erspan seq \
>  		key 102 local 172.16.1.100 remote 172.16.1.200 \
> diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
> index c9b0b4337e12..07f2b454d755 100644
> --- a/tools/testing/selftests/net/tls.c
> +++ b/tools/testing/selftests/net/tls.c
> @@ -413,6 +413,8 @@ FIXTURE_SETUP(tls)
>  		return;
>  
>  	ret = setsockopt(self->fd, SOL_TLS, TLS_TX, &tls12, tls12.len);
> +	if (ret < 0 && errno == ENOENT)
> +		SKIP(return, "Cipher not built into the kernel");

Just a small question: ENOENT error is not in setsockopt man.
It seems more like ENOPROTOOPT, but if the fix is here, it probably
helped. Could you tell where ENOENT comes from?
Maybe we should test for ENOPROTOOPT as well here?

>  	ASSERT_EQ(ret, 0);
>  
>  	ret = setsockopt(self->cfd, SOL_TLS, TLS_RX, &tls12, tls12.len);

Reviewed-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

-- 
Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* [PATCH 2/2] UefiCpuPkg/MpInitLib: resend the Start-up IPI when an AP misses it #VSTOR-146639
  @ 2026-09-21 15:44  3% ` Denis V. Lunev
  2026-09-23 13:30  0%   ` Andrey Drobyshev
  0 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-09-21 15:44 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, Denis V. Lunev

From: Denis V. Lunev <den@openvz.org>

WaitApWakeup spins until the AP clears its wakeup signal, with no
timeout and no way out. If the AP never consumes the Start-up IPI, the
BSP spins forever and the boot is dead.

A hypervisor can lose that IPI. Under KVM, INIT is latched while a
vCPU is in SMM but the Start-up IPI is discarded outright, so an AP
that takes an SMI just before the BSP broadcasts INIT-SIPI-SIPI is
left in wait-for-SIPI with nothing left to start it. The window is the
few instructions between an AP releasing the BSP at the end of the SMM
rendezvous and its own RSM, which a scheduled vCPU can be preempted
inside of. KVM holds that delivering the SIPI is software's job, so
the firmware has to cope.

Resend rather than spin. Address the AP individually so that APs
already running the procedure are not disturbed, and pass the reset
vector only on the paths that sent a Start-up IPI in the first place,
so the SEV-SNP and hand-off paths keep waiting as before. A Start-up
IPI is ignored unless the target is in wait-for-SIPI, so resending
costs nothing when the AP is merely slow.

The failure this fixes looks like a guest that never boots: firmware
starts the boot manager, paints nothing further, and one CPU spins
while the other APs sit idle.

Signed-off-by: Denis V. Lunev <den@openvz.org>
Cc: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 UefiCpuPkg/Library/MpInitLib/MpLib.c | 85 +++++++++++++++++++++++++---
 UefiCpuPkg/Library/MpInitLib/MpLib.h |  2 +
 2 files changed, 80 insertions(+), 7 deletions(-)

diff --git a/UefiCpuPkg/Library/MpInitLib/MpLib.c b/UefiCpuPkg/Library/MpInitLib/MpLib.c
index 426d998b9a..87e8f824b6 100644
--- a/UefiCpuPkg/Library/MpInitLib/MpLib.c
+++ b/UefiCpuPkg/Library/MpInitLib/MpLib.c
@@ -942,16 +942,63 @@ DxeApEntryPoint (
   ApWakeupFunction (CpuMpData, ProcessorNumber);
 }
 
+/**
+  Calculate timeout value and return the current performance counter value.
+
+  @param[in]  TimeoutInMicroseconds  The timeout value in microseconds.
+  @param[out] CurrentTime            Returns the current value of the performance counter.
+
+  @return Expected timestamp counter for timeout, or 0 if TimeoutInMicroseconds is 0.
+**/
+UINT64
+CalculateTimeout (
+  IN  UINTN   TimeoutInMicroseconds,
+  OUT UINT64  *CurrentTime
+  );
+
+/**
+  Checks whether timeout expires.
+
+  @param[in, out]  PreviousTime   On input, the requested time. On output, the elapsed time.
+  @param[in]       TotalTime      The total amount of elapsed time.
+  @param[in]       Timeout        The timeout ticks, 0 means infinity.
+
+  @retval TRUE   A timeout occurred.
+  @retval FALSE  No timeout occurred.
+**/
+BOOLEAN
+CheckTimeout (
+  IN OUT UINT64  *PreviousTime,
+  IN     UINT64  *TotalTime,
+  IN     UINT64  Timeout
+  );
+
 /**
   Wait for AP wakeup and write AP start-up signal till AP is waken up.
 
   @param[in] ApStartupSignalBuffer  Pointer to AP wakeup signal
+  @param[in] ApicId                 Local APIC ID of the AP being woken
+  @param[in] StartupRoutine         AP reset vector, or 0 if the AP was not
+                                    woken by INIT-SIPI-SIPI and no Start-up
+                                    IPI may be resent
 **/
 VOID
 WaitApWakeup (
-  IN volatile UINT32  *ApStartupSignalBuffer
+  IN volatile UINT32  *ApStartupSignalBuffer,
+  IN UINT32           ApicId,
+  IN UINT32           StartupRoutine
   )
 {
+  UINT64  CurrentTime;
+  UINT64  TotalTime;
+  UINT64  Timeout;
+
+  Timeout = CalculateTimeout (
+              (StartupRoutine != 0) ? AP_SIPI_RESEND_TIMEOUT_US : 0,
+              &CurrentTime
+              );
+  TotalTime = 0;
+
   //
   // If AP is waken up, StartupApSignal should be cleared.
   // Otherwise, write StartupApSignal again till AP waken up.
@@ -962,6 +1009,12 @@ WaitApWakeup (
            WAKEUP_AP_SIGNAL
            ) != 0)
   {
+    if (CheckTimeout (&CurrentTime, &TotalTime, Timeout)) {
+      SendStartupIpi (ApicId, StartupRoutine);
+      Timeout   = CalculateTimeout (AP_SIPI_RESEND_TIMEOUT_US, &CurrentTime);
+      TotalTime = 0;
+    }
+
     CpuPause ();
   }
 }
@@ -1256,8 +1309,10 @@ WakeUpAP (
   CPU_AP_DATA                    *CpuData;
   BOOLEAN                        ResetVectorRequired;
   CPU_INFO_IN_HOB                *CpuInfoInHob;
+  UINT32                         SipiVector;
 
   CpuMpData->FinishedCount = 0;
+  SipiVector               = 0;
   ResetVectorRequired      = FALSE;
 
   if (CpuMpData->WakeUpByInitSipiSipi ||
@@ -1320,13 +1375,14 @@ WakeUpAP (
       if (CanUseSevSnpCreateAP (CpuMpData)) {
         SevSnpCreateAP (CpuMpData, -1);
       } else {
+        SipiVector = (UINT32)ExchangeInfo->BufferStart;
         if ((CpuMpData->InitFlag == ApInitConfig) && FixedPcdGetBool (PcdFirstTimeWakeUpAPsBySipi)) {
           //
           // SIPI can be used for the first time wake up after reset to reduce boot time.
           //
-          SendStartupIpiAllExcludingSelf ((UINT32)ExchangeInfo->BufferStart);
+          SendStartupIpiAllExcludingSelf (SipiVector);
         } else {
-          SendInitSipiSipiAllExcludingSelf ((UINT32)ExchangeInfo->BufferStart);
+          SendInitSipiSipiAllExcludingSelf (SipiVector);
         }
       }
     }
@@ -1398,10 +1454,15 @@ WakeUpAP (
       //
       // Wait all APs waken up if this is not the 1st broadcast of SIPI
       //
+      CpuInfoInHob = (CPU_INFO_IN_HOB *)(UINTN)CpuMpData->CpuInfoInHob;
       for (Index = 0; Index < CpuMpData->CpuCount; Index++) {
         CpuData = &CpuMpData->CpuData[Index];
         if (Index != CpuMpData->BspNumber) {
-          WaitApWakeup (CpuData->StartupApSignal);
+          WaitApWakeup (
+            CpuData->StartupApSignal,
+            CpuInfoInHob[Index].ApicId,
+            SipiVector
+            );
         }
       }
     }
@@ -1430,9 +1491,10 @@ WakeUpAP (
       if (CanUseSevSnpCreateAP (CpuMpData)) {
         SevSnpCreateAP (CpuMpData, (INTN)ProcessorNumber);
       } else {
+        SipiVector = (UINT32)ExchangeInfo->BufferStart;
         SendInitSipiSipi (
           CpuInfoInHob[ProcessorNumber].ApicId,
-          (UINT32)ExchangeInfo->BufferStart
+          SipiVector
           );
       }
     }
@@ -1440,7 +1502,12 @@ WakeUpAP (
     //
     // Wait specified AP waken up
     //
-    WaitApWakeup (CpuData->StartupApSignal);
+    CpuInfoInHob = (CPU_INFO_IN_HOB *)(UINTN)CpuMpData->CpuInfoInHob;
+    WaitApWakeup (
+      CpuData->StartupApSignal,
+      CpuInfoInHob[ProcessorNumber].ApicId,
+      SipiVector
+      );
   }
 
   if (ResetVectorRequired) {
@@ -2017,7 +2084,11 @@ SwitchApContext (
   {
     for (Index = 0; Index < MpHandOff->CpuCount; Index++) {
       if (MpHandOff->ProcessorIndex + Index != BspNumber) {
-        WaitApWakeup ((UINT32 *)(UINTN)(MpHandOff->Info[Index].StartupSignalAddress));
+        WaitApWakeup (
+          (UINT32 *)(UINTN)(MpHandOff->Info[Index].StartupSignalAddress),
+          0,
+          0
+          );
       }
     }
   }
diff --git a/UefiCpuPkg/Library/MpInitLib/MpLib.h b/UefiCpuPkg/Library/MpInitLib/MpLib.h
index 5d10516cd8..94bda7f1ea 100644
--- a/UefiCpuPkg/Library/MpInitLib/MpLib.h
+++ b/UefiCpuPkg/Library/MpInitLib/MpLib.h
@@ -44,6 +44,8 @@
 #include "MpHandOff.h"
 
 #define WAKEUP_AP_SIGNAL  SIGNATURE_32 ('S', 'T', 'A', 'P')
+
+#define AP_SIPI_RESEND_TIMEOUT_US  10000
 //
 // To trigger the start-up signal, BSP writes the specified
 // StartupSignalValue to the StartupSignalAddress of each processor.
-- 
2.53.0


^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH vz10 v2 1/3] ms/selftests/posix_timers: Use CLOCK_THREAD_CPUTIME_ID for ITIMER_PROF measurements
  @ 2026-09-21 20:46  4% ` Eva Kurchatova
  0 siblings, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-21 20:46 UTC (permalink / raw)
  To: vasileios.almpanis; +Cc: devel

From: John Stultz <jstultz@google.com>

It was reported that the posix_timers test was at times seeing failures
with ITIMER_PROF timers, specifically in cases where the RCU_SOFTIRQ was
taking up significant amounts of time.

Analysis showed that as the time in softirq isn't included in the task
stime + utime accounting used to trigger the SIGPROF so delays from softirq
work could cause it to appear that the signal was incorrectly delayed.

Contributing to this is that the test uses gettimeofday() to measure
itimers, which also means any scheduling delay can also cause failures (as
the task may not be running the entire time).

To fix this, convert all the itimer measurements to use clock_gettime(),
tweaking the logic to use nsecs instead of usecs. Then for ITIMER_PROF
timers, utilize the CLOCK_THREAD_CPUTIME_ID clockid so that it is similarly
measuring the time the task was running.

Signed-off-by: John Stultz <jstultz@google.com>
Signed-off-by: Thomas Gleixner <tglx@kernel.org>
Link: https://patch.msgid.link/20260428173957.1394265-1-jstultz@google.com
The ITIMER_PROF case of it fails on the CI machines, which are two
cpu guests under load:

  # Diff too high: 2561992..not ok 3 ITIMER_PROF

2.56 seconds of wall time for the 2 seconds of cpu time the timer
counts, so the task had about four fifths of a cpu.

(cherry picked from commit b00385b8d081ce74f36ea178e04e1b106505fb36)

https://virtuozzo.atlassian.net/browse/VSTOR-143363
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/timers/posix_timers.c | 55 ++++++++++---------
 1 file changed, 28 insertions(+), 27 deletions(-)

diff --git a/tools/testing/selftests/timers/posix_timers.c b/tools/testing/selftests/timers/posix_timers.c
index f0eceb0faf34..077a98fb1b1d 100644
--- a/tools/testing/selftests/timers/posix_timers.c
+++ b/tools/testing/selftests/timers/posix_timers.c
@@ -77,19 +77,25 @@ static void sig_handler(int nr)
 	done = 1;
 }
 
+static inline int64_t calcdiff_ns(struct timespec t1, struct timespec t2)
+{
+	int64_t diff;
+
+	diff = NSEC_PER_SEC * (int64_t)((int) t1.tv_sec - (int) t2.tv_sec);
+	diff += ((int) t1.tv_nsec - (int) t2.tv_nsec);
+	return diff;
+}
+
 /*
  * Check the expected timer expiration matches the GTOD elapsed delta since
  * we armed the timer. Keep a 0.5 sec error margin due to various jitter.
  */
-static int check_diff(struct timeval start, struct timeval end)
+static int check_diff(struct timespec start, struct timespec end)
 {
-	long long diff;
-
-	diff = end.tv_usec - start.tv_usec;
-	diff += (end.tv_sec - start.tv_sec) * USEC_PER_SEC;
+	long long diff = calcdiff_ns(end, start);
 
-	if (llabs(diff - DELAY * USEC_PER_SEC) > USEC_PER_SEC / 2) {
-		printf("Diff too high: %lld..", diff);
+	if (llabs(diff - DELAY * NSEC_PER_SEC) > NSEC_PER_SEC / 2) {
+		printf("Diff too high: %lld ns..", diff);
 		return -1;
 	}
 
@@ -98,22 +104,25 @@ static int check_diff(struct timeval start, struct timeval end)
 
 static void check_itimer(int which, const char *name)
 {
-	struct timeval start, end;
+	struct timespec start, end;
 	struct itimerval val = {
 		.it_value.tv_sec = DELAY,
 	};
+	int clock_id = CLOCK_REALTIME;
 
 	done = 0;
 
 	if (which == ITIMER_VIRTUAL)
 		signal(SIGVTALRM, sig_handler);
-	else if (which == ITIMER_PROF)
+	else if (which == ITIMER_PROF) {
+		clock_id = CLOCK_THREAD_CPUTIME_ID;
 		signal(SIGPROF, sig_handler);
+	}
 	else if (which == ITIMER_REAL)
 		signal(SIGALRM, sig_handler);
 
-	if (gettimeofday(&start, NULL) < 0)
-		fatal_error(name, "gettimeofday()");
+	if (clock_gettime(clock_id, &start))
+		fatal_error(name, "clock_gettime()");
 
 	if (setitimer(which, &val, NULL) < 0)
 		fatal_error(name, "setitimer()");
@@ -125,18 +134,19 @@ static void check_itimer(int which, const char *name)
 	else if (which == ITIMER_REAL)
 		idle_loop();
 
-	if (gettimeofday(&end, NULL) < 0)
-		fatal_error(name, "gettimeofday()");
+	if (clock_gettime(clock_id, &end))
+		fatal_error(name, "clock_gettime()");
 
 	ksft_test_result(check_diff(start, end) == 0, "%s\n", name);
 }
 
 static void check_timer_create(int which, const char *name)
 {
-	struct timeval start, end;
+	struct timespec start, end;
 	struct itimerspec val = {
 		.it_value.tv_sec = DELAY,
 	};
+	int clock_id = CLOCK_REALTIME;
 	timer_t id;
 
 	done = 0;
@@ -147,16 +157,16 @@ static void check_timer_create(int which, const char *name)
 	if (signal(SIGALRM, sig_handler) == SIG_ERR)
 		fatal_error(name, "signal()");
 
-	if (gettimeofday(&start, NULL) < 0)
-		fatal_error(name, "gettimeofday()");
+	if (clock_gettime(clock_id, &start))
+		fatal_error(name, "clock_gettime()");
 
 	if (timer_settime(id, 0, &val, NULL) < 0)
 		fatal_error(name, "timer_settime()");
 
 	user_loop();
 
-	if (gettimeofday(&end, NULL) < 0)
-		fatal_error(name, "gettimeofday()");
+	if (clock_gettime(clock_id, &end))
+		fatal_error(name, "clock_gettime()");
 
 	ksft_test_result(check_diff(start, end) == 0,
 			 "timer_create() per %s\n", name);
@@ -444,15 +454,6 @@ static void check_delete(void)
 	ksft_test_result(!tsig.signals, "check_delete\n");
 }
 
-static inline int64_t calcdiff_ns(struct timespec t1, struct timespec t2)
-{
-	int64_t diff;
-
-	diff = NSEC_PER_SEC * (int64_t)((int) t1.tv_sec - (int) t2.tv_sec);
-	diff += ((int) t1.tv_nsec - (int) t2.tv_nsec);
-	return diff;
-}
-
 static void check_sigev_none(int which, const char *name)
 {
 	struct timespec start, now;
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH vz10 v2 2/6] selftests: net: skip what this kernel and iproute2 do not have
  @ 2026-09-22 12:02  2% ` Eva Kurchatova
  2026-09-22 12:02 16% ` [Devel] [PATCH vz10 v2 6/6] selftests: net: let the bridged PMTU tests take the ICMP they ask for Eva Kurchatova
  1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-22 12:02 UTC (permalink / raw)
  To: vasileios.almpanis; +Cc: devel

Five cases report a failure where a feature is simply not there.

tls asks for every cipher it knows and fails on the ones the kernel does
not implement, SM4 and ARIA, which answer ENOENT at setsockopt
time. Skip a cipher the kernel refuses that way; the run then reports
632 passed, 264 skipped and none failed.

rtnetlink.sh sets up an erspan tunnel, which this kernel does not have
and which fails with "Unknown device type", and checks a netconf dump
whose exit status iproute2 gets wrong: 6.11 exits 2 on a dump that
worked, 6.17 exits 0. Probe for both and skip when unusable.

rtnetlink.py asks for an IPv4 multicast address dump the kernel answers
with EOPNOTSUPP, and ip_local_port_range asks for protocols it does not
have. Skip those too.

https://virtuozzo.atlassian.net/browse/VSTOR-139651
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 .../selftests/net/ip_local_port_range.c       | 17 +++++++---
 tools/testing/selftests/net/rtnetlink.py      | 11 +++++--
 tools/testing/selftests/net/rtnetlink.sh      | 31 +++++++++++++++++++
 tools/testing/selftests/net/tls.c             |  2 ++
 4 files changed, 55 insertions(+), 6 deletions(-)

diff --git a/tools/testing/selftests/net/ip_local_port_range.c b/tools/testing/selftests/net/ip_local_port_range.c
index 29451d2244b7..2d7b9c852b2b 100644
--- a/tools/testing/selftests/net/ip_local_port_range.c
+++ b/tools/testing/selftests/net/ip_local_port_range.c
@@ -118,10 +118,6 @@ static int get_ip_local_port_range(int fd, __u32 *range)
 
 FIXTURE(ip_local_port_range) {};
 
-FIXTURE_SETUP(ip_local_port_range)
-{
-}
-
 FIXTURE_TEARDOWN(ip_local_port_range)
 {
 }
@@ -180,6 +176,19 @@ FIXTURE_VARIANT_ADD(ip_local_port_range, ip6_mptcp) {
 	.so_protocol	= IPPROTO_MPTCP,
 };
 
+FIXTURE_SETUP(ip_local_port_range)
+{
+	int fd;
+
+	/* Not every protocol under test is built into every kernel. */
+	fd = socket(variant->so_domain, variant->so_type, variant->so_protocol);
+	if (fd < 0 && (errno == EPROTONOSUPPORT || errno == ESOCKTNOSUPPORT ||
+		       errno == EAFNOSUPPORT))
+		SKIP(return, "%s", strerror(errno));
+	ASSERT_GE(fd, 0) TH_LOG("socket failed");
+	close(fd);
+}
+
 TEST_F(ip_local_port_range, invalid_option_value)
 {
 	__u16 val16;
diff --git a/tools/testing/selftests/net/rtnetlink.py b/tools/testing/selftests/net/rtnetlink.py
index e9ad5e88da97..e0b44902c602 100755
--- a/tools/testing/selftests/net/rtnetlink.py
+++ b/tools/testing/selftests/net/rtnetlink.py
@@ -1,7 +1,9 @@
 #!/usr/bin/env python3
 # SPDX-License-Identifier: GPL-2.0
 
-from lib.py import ksft_exit, ksft_run, ksft_ge, RtnlAddrFamily
+from lib.py import ksft_exit, ksft_run, ksft_ge, KsftSkipEx
+from lib.py import NlError, RtnlAddrFamily
+import errno
 import socket
 
 IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01'
@@ -12,7 +14,12 @@ def dump_mcaddr_check(rtnl: RtnlAddrFamily) -> None:
     At least the loopback interface should have this address.
     """
 
-    addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True)
+    try:
+        addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True)
+    except NlError as e:
+        if e.nl_msg.error == -errno.EOPNOTSUPP:
+            raise KsftSkipEx("IPv4 multicast address dump is not supported")
+        raise
 
     all_host_multicasts = [
         addr for addr in addresses if addr['multicast'] == IPV4_ALL_HOSTS_MULTICAST
diff --git a/tools/testing/selftests/net/rtnetlink.sh b/tools/testing/selftests/net/rtnetlink.sh
index 5f72b447e940..cc4e94b90002 100755
--- a/tools/testing/selftests/net/rtnetlink.sh
+++ b/tools/testing/selftests/net/rtnetlink.sh
@@ -142,10 +142,30 @@ kci_del_dummy()
 	run_cmd ip link del dev "$devdummy"
 }
 
+# Some iproute2 versions exit non-zero from a netconf dump that worked,
+# printing the devconf and then failing anyway.  Nothing about the kernel
+# can be learned from the exit status of such an ip, so find out once.
+netconf_exit_status_usable()
+{
+	local out
+
+	out=$(ip -4 netconf show dev lo 2>/dev/null)
+	if [ $? -ne 0 ] && [ -n "$out" ]; then
+		return 1
+	fi
+	return 0
+}
+
 kci_test_netconf()
 {
 	dev="$1"
 	r=$ret
+
+	if ! netconf_exit_status_usable; then
+		end_test "SKIP: ip netconf $dev: iproute2 fails a dump that worked"
+		return $ksft_skip
+	fi
+
 	run_cmd ip netconf show dev "$dev"
 	for f in 4 6; do
 		run_cmd ip -$f netconf show dev "$dev"
@@ -920,6 +940,17 @@ kci_test_erspan()
 		return $ksft_skip
 	fi
 
+	# the kernel can be built without erspan, rtnetlink then has no ops
+	# for the type and says so.  A kernel that has it gets past this and
+	# fails on the attributes the probe leaves out.
+	if ip -netns "$testns" link add dev "$DEV_NS" type erspan 2>&1 | \
+	   grep -q "Unknown device type"; then
+		end_test "SKIP: erspan: not supported by the kernel"
+		ip netns del "$testns"
+		return $ksft_skip
+	fi
+	ip -netns "$testns" link del dev "$DEV_NS" 2>/dev/null
+
 	# test native tunnel erspan v1
 	run_cmd ip -netns "$testns" link add dev "$DEV_NS" type erspan seq \
 		key 102 local 172.16.1.100 remote 172.16.1.200 \
diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
index c9b0b4337e12..07f2b454d755 100644
--- a/tools/testing/selftests/net/tls.c
+++ b/tools/testing/selftests/net/tls.c
@@ -413,6 +413,8 @@ FIXTURE_SETUP(tls)
 		return;
 
 	ret = setsockopt(self->fd, SOL_TLS, TLS_TX, &tls12, tls12.len);
+	if (ret < 0 && errno == ENOENT)
+		SKIP(return, "Cipher not built into the kernel");
 	ASSERT_EQ(ret, 0);
 
 	ret = setsockopt(self->cfd, SOL_TLS, TLS_RX, &tls12, tls12.len);
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 2%]

* [Devel] [PATCH vz10 v2 6/6] selftests: net: let the bridged PMTU tests take the ICMP they ask for
    2026-09-22 12:02  2% ` [Devel] [PATCH vz10 v2 2/6] selftests: net: skip what this kernel and iproute2 do not have Eva Kurchatova
@ 2026-09-22 12:02 16% ` Eva Kurchatova
  2026-09-22 14:27  0%   ` Konstantin Khorenko
  1 sibling, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-09-22 12:02 UTC (permalink / raw)
  To: vasileios.almpanis; +Cc: devel

The bridged tunnel PMTU tests send a packet that is too big to make the
tunnel answer with "Frag needed", which is how the route exception they
check gets created. The ping that does it from the namespace holding
the bridge is given a deadline, and ping(8) says of it:

  In this case ping does not stop after count packet are sent, it waits
  either for deadline expire or until count probes are answered or for
  some error notification from network.

so it stops at that very ICMP and reports the loss:

  # ping -M want -i 0.1 -w 1 -s 4500 192.168.2.2
  From 192.168.2.2 icmp_seq=2 Frag needed and DF set (mtu = 3950)
  2 packets transmitted, 0 received, +1 errors, 100% packet loss

  TEST: IPv4, bridged vxlan4: PMTU exceptions                    [FAIL]

The exception is created all the same, and with the right value, the
test never gets to look at it. Ask for a count of pings instead, as
the line above this one already does for the other namespace, so the
error is counted and the run goes on.

All 16 bridged cases pass with this, vxlan and geneve, over IPv4 and
IPv6.

https://virtuozzo.atlassian.net/browse/VSTOR-139651
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/net/pmtu.sh | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/net/pmtu.sh b/tools/testing/selftests/net/pmtu.sh
index 6c651c880fe8..d607429840cf 100755
--- a/tools/testing/selftests/net/pmtu.sh
+++ b/tools/testing/selftests/net/pmtu.sh
@@ -1409,7 +1409,12 @@ test_pmtu_ipvX_over_bridged_vxlanY_or_geneveY_exception() {
 	mtu "${ns_b}" ${type}_b $((${ll_mtu} + 1000))
 
 	run_cmd ${ns_c} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1
-	run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -w 1  -s $((${ll_mtu} + 500)) ${dst} || return 1
+	# This ping is meant to draw the ICMP that creates the exception,
+	# and ping stops on "some error notification from network" where a
+	# deadline is given, see ping(8), so it would always report the
+	# loss and fail here.  Send a count of them instead, as the ping
+	# above does.
+	run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1
 
 	# Check that exceptions were created
 	pmtu="$(route_get_dst_pmtu_from_exception "${ns_c}" ${dst})"
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 16%]

* Re: [Devel] [PATCH vz10 v2 6/6] selftests: net: let the bridged PMTU tests take the ICMP they ask for
  2026-09-22 12:02 16% ` [Devel] [PATCH vz10 v2 6/6] selftests: net: let the bridged PMTU tests take the ICMP they ask for Eva Kurchatova
@ 2026-09-22 14:27  0%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-22 14:27 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

  I am not taking this patch. Instead I will cherry-pick the upstream commit
  it is a piece of:

    b58d74963320 ("tunnels: Drop stale dst when building an ICMP error for
    PMTUD")

  Why the test fails here at all

    Upstream commit 8930424777e4 ("tunnels: Accept PACKET_HOST in
    skb_tunnel_check_pmtu()") made bridged vxlan/geneve build a "Frag needed"
    ICMP for locally generated packets too, not only for forwarded ones.
    Our tree has it via the RHEL 6.12.0-211.16.1 import (see the blame of
    net/ipv4/ip_tunnel_core.c:425). That is the ICMP ns_a's own ping now
    receives, and that is what makes ping stop on the -w deadline.

  Why the test change alone is not enough

    8930424777e4 also introduced a bug. The ICMP error is built around the
    original packet, which for a routed or locally generated overlay packet
    still carries a valid dst. The error is then re-injected into the Rx
    path with that stale dst, so the IP layer skips the route lookup and the
    ICMP is either dropped or sent back out of the tunnel instead of being
    delivered to the overlay source.

    b58d74963320 fixes that: iptunnel_pmtud_build_icmp() and
    iptunnel_pmtud_build_icmpv6() drop the dst, so the ICMP gets a proper
    route lookup. It is marked Cc: stable and has Fixes: 8930424777e4.

  What this patch is

    The pmtu.sh hunk here is byte for byte the selftest part of that same
    upstream commit. Taking the test half as a vz patch and leaving the
    kernel half out would make the test pass while the kernel keeps
    misrouting PMTUD errors on bridged tunnels.

  So: one ms/ cherry-pick of b58d74963320, kernel fix and test change
  together, instead of a vz-only change to the test.

--
Best regards,

Konstantin Khorenko,
Virtuozzo Linux Kernel Team

On 9/22/26 14:02, Eva Kurchatova wrote:
> The bridged tunnel PMTU tests send a packet that is too big to make the
> tunnel answer with "Frag needed", which is how the route exception they
> check gets created. The ping that does it from the namespace holding
> the bridge is given a deadline, and ping(8) says of it:
> 
>   In this case ping does not stop after count packet are sent, it waits
>   either for deadline expire or until count probes are answered or for
>   some error notification from network.
> 
> so it stops at that very ICMP and reports the loss:
> 
>   # ping -M want -i 0.1 -w 1 -s 4500 192.168.2.2
>   From 192.168.2.2 icmp_seq=2 Frag needed and DF set (mtu = 3950)
>   2 packets transmitted, 0 received, +1 errors, 100% packet loss
> 
>   TEST: IPv4, bridged vxlan4: PMTU exceptions                    [FAIL]
> 
> The exception is created all the same, and with the right value, the
> test never gets to look at it. Ask for a count of pings instead, as
> the line above this one already does for the other namespace, so the
> error is counted and the run goes on.
> 
> All 16 bridged cases pass with this, vxlan and geneve, over IPv4 and
> IPv6.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-139651
> Feature: fix selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> ---
>  tools/testing/selftests/net/pmtu.sh | 7 ++++++-
>  1 file changed, 6 insertions(+), 1 deletion(-)
> 
> diff --git a/tools/testing/selftests/net/pmtu.sh b/tools/testing/selftests/net/pmtu.sh
> index 6c651c880fe8..d607429840cf 100755
> --- a/tools/testing/selftests/net/pmtu.sh
> +++ b/tools/testing/selftests/net/pmtu.sh
> @@ -1409,7 +1409,12 @@ test_pmtu_ipvX_over_bridged_vxlanY_or_geneveY_exception() {
>  	mtu "${ns_b}" ${type}_b $((${ll_mtu} + 1000))
>  
>  	run_cmd ${ns_c} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1
> -	run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -w 1  -s $((${ll_mtu} + 500)) ${dst} || return 1
> +	# This ping is meant to draw the ICMP that creates the exception,
> +	# and ping stops on "some error notification from network" where a
> +	# deadline is given, see ping(8), so it would always report the
> +	# loss and fail here.  Send a count of them instead, as the ping
> +	# above does.
> +	run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1
>  
>  	# Check that exceptions were created
>  	pmtu="$(route_get_dst_pmtu_from_exception "${ns_c}" ${dst})"

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* [Devel] [PATCH hci-8.1 v3 2/5] selftests: net: skip what this kernel and iproute2 do not have
  @ 2026-09-22 15:00  2% ` Konstantin Khorenko
  2026-09-22 15:00 10% ` [Devel] [PATCH hci-8.1 v3 5/5] ms/tunnels: Drop stale dst when building an ICMP error for PMTUD Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-22 15:00 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

From: Eva Kurchatova <eva.kurchatova@virtuozzo.com>

Five cases report a failure where a feature is simply not there.

tls asks for every cipher it knows and fails on the ones the kernel does
not implement, SM4 and ARIA, which answer ENOENT at setsockopt
time. Skip a cipher the kernel refuses that way; the run then reports
632 passed, 264 skipped and none failed.

rtnetlink.sh sets up an erspan tunnel, which this kernel does not have
and which fails with "Unknown device type", and checks a netconf dump
whose exit status iproute2 gets wrong: 6.11 exits 2 on a dump that
worked, 6.17 exits 0. Probe for both and skip when unusable.

rtnetlink.py asks for an IPv4 multicast address dump the kernel answers
with EOPNOTSUPP, and ip_local_port_range asks for protocols it does not
have. Skip those too.

https: //virtuozzo.atlassian.net/browse/VSTOR-139651
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
Reviewed-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
 tools/testing/selftests/net/ip_local_port_range.c | 17 ++++++++++---
 tools/testing/selftests/net/rtnetlink.py          | 11 ++++++--
 tools/testing/selftests/net/rtnetlink.sh          | 31 +++++++++++++++++++++++
 tools/testing/selftests/net/tls.c                 |  2 ++
 4 files changed, 55 insertions(+), 6 deletions(-)

diff --git a/tools/testing/selftests/net/ip_local_port_range.c b/tools/testing/selftests/net/ip_local_port_range.c
index 29451d2244b75..2d7b9c852b2b1 100644
--- a/tools/testing/selftests/net/ip_local_port_range.c
+++ b/tools/testing/selftests/net/ip_local_port_range.c
@@ -118,10 +118,6 @@ static int get_ip_local_port_range(int fd, __u32 *range)
 
 FIXTURE(ip_local_port_range) {};
 
-FIXTURE_SETUP(ip_local_port_range)
-{
-}
-
 FIXTURE_TEARDOWN(ip_local_port_range)
 {
 }
@@ -180,6 +176,19 @@ FIXTURE_VARIANT_ADD(ip_local_port_range, ip6_mptcp) {
 	.so_protocol	= IPPROTO_MPTCP,
 };
 
+FIXTURE_SETUP(ip_local_port_range)
+{
+	int fd;
+
+	/* Not every protocol under test is built into every kernel. */
+	fd = socket(variant->so_domain, variant->so_type, variant->so_protocol);
+	if (fd < 0 && (errno == EPROTONOSUPPORT || errno == ESOCKTNOSUPPORT ||
+		       errno == EAFNOSUPPORT))
+		SKIP(return, "%s", strerror(errno));
+	ASSERT_GE(fd, 0) TH_LOG("socket failed");
+	close(fd);
+}
+
 TEST_F(ip_local_port_range, invalid_option_value)
 {
 	__u16 val16;
diff --git a/tools/testing/selftests/net/rtnetlink.py b/tools/testing/selftests/net/rtnetlink.py
index e9ad5e88da97f..e0b44902c602d 100755
--- a/tools/testing/selftests/net/rtnetlink.py
+++ b/tools/testing/selftests/net/rtnetlink.py
@@ -1,7 +1,9 @@
 #!/usr/bin/env python3
 # SPDX-License-Identifier: GPL-2.0
 
-from lib.py import ksft_exit, ksft_run, ksft_ge, RtnlAddrFamily
+from lib.py import ksft_exit, ksft_run, ksft_ge, KsftSkipEx
+from lib.py import NlError, RtnlAddrFamily
+import errno
 import socket
 
 IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01'
@@ -12,7 +14,12 @@ IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01'
     At least the loopback interface should have this address.
     """
 
-    addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True)
+    try:
+        addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True)
+    except NlError as e:
+        if e.nl_msg.error == -errno.EOPNOTSUPP:
+            raise KsftSkipEx("IPv4 multicast address dump is not supported")
+        raise
 
     all_host_multicasts = [
         addr for addr in addresses if addr['multicast'] == IPV4_ALL_HOSTS_MULTICAST
diff --git a/tools/testing/selftests/net/rtnetlink.sh b/tools/testing/selftests/net/rtnetlink.sh
index 5f72b447e9400..cc4e94b90002b 100755
--- a/tools/testing/selftests/net/rtnetlink.sh
+++ b/tools/testing/selftests/net/rtnetlink.sh
@@ -142,10 +142,30 @@ kci_del_dummy()
 	run_cmd ip link del dev "$devdummy"
 }
 
+# Some iproute2 versions exit non-zero from a netconf dump that worked,
+# printing the devconf and then failing anyway.  Nothing about the kernel
+# can be learned from the exit status of such an ip, so find out once.
+netconf_exit_status_usable()
+{
+	local out
+
+	out=$(ip -4 netconf show dev lo 2>/dev/null)
+	if [ $? -ne 0 ] && [ -n "$out" ]; then
+		return 1
+	fi
+	return 0
+}
+
 kci_test_netconf()
 {
 	dev="$1"
 	r=$ret
+
+	if ! netconf_exit_status_usable; then
+		end_test "SKIP: ip netconf $dev: iproute2 fails a dump that worked"
+		return $ksft_skip
+	fi
+
 	run_cmd ip netconf show dev "$dev"
 	for f in 4 6; do
 		run_cmd ip -$f netconf show dev "$dev"
@@ -920,6 +940,17 @@ kci_test_erspan()
 		return $ksft_skip
 	fi
 
+	# the kernel can be built without erspan, rtnetlink then has no ops
+	# for the type and says so.  A kernel that has it gets past this and
+	# fails on the attributes the probe leaves out.
+	if ip -netns "$testns" link add dev "$DEV_NS" type erspan 2>&1 | \
+	   grep -q "Unknown device type"; then
+		end_test "SKIP: erspan: not supported by the kernel"
+		ip netns del "$testns"
+		return $ksft_skip
+	fi
+	ip -netns "$testns" link del dev "$DEV_NS" 2>/dev/null
+
 	# test native tunnel erspan v1
 	run_cmd ip -netns "$testns" link add dev "$DEV_NS" type erspan seq \
 		key 102 local 172.16.1.100 remote 172.16.1.200 \
diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
index c9b0b4337e127..07f2b454d755b 100644
--- a/tools/testing/selftests/net/tls.c
+++ b/tools/testing/selftests/net/tls.c
@@ -413,6 +413,8 @@ FIXTURE_SETUP(tls)
 		return;
 
 	ret = setsockopt(self->fd, SOL_TLS, TLS_TX, &tls12, tls12.len);
+	if (ret < 0 && errno == ENOENT)
+		SKIP(return, "Cipher not built into the kernel");
 	ASSERT_EQ(ret, 0);
 
 	ret = setsockopt(self->cfd, SOL_TLS, TLS_RX, &tls12, tls12.len);

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 2%]

* [Devel] [PATCH hci-8.1 v3 5/5] ms/tunnels: Drop stale dst when building an ICMP error for PMTUD
    2026-09-22 15:00  2% ` [Devel] [PATCH hci-8.1 v3 2/5] selftests: net: skip what this kernel and iproute2 do not have Konstantin Khorenko
@ 2026-09-22 15:00 10% ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-22 15:00 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

From: Ido Schimmel <idosch@nvidia.com>

Bridged UDP tunnels such as VXLAN and GENEVE build an ICMP error packet
around an overlay packet if the packet is going to exceed the underlay
path MTU. The ICMP error packet is then injected back into the Rx path
with the source and destination addresses swapped, so that it will be
delivered to the overlay source.

If the overlay packet was routed to the UDP tunnel or locally generated,
then it is already carrying a valid dst entry and this entry is not
dropped when transforming the packet to an ICMP error packet. This
causes the IP layer to reuse the dst entry, leading to the ICMP error
packet being dropped or routed out of the UDP tunnel interface in case
of forwarding.

Prior to the blamed commit this could not happen, as
skb_tunnel_check_pmtu() did not build ICMP errors for PACKET_HOST
packets. Such packets were instead encapsulated and, unless the DF bit
was set in the outer header, fragmented by the underlay.

Fix this by making sure that the ICMP error packet does not have a valid
dst entry, thereby forcing the IP layer to perform a route lookup.

Adjust the bridged PMTU exception selftests accordingly. When the
local sender in ns_a pings the overlay destination with a deadline
(-w), ping exits on the first socket error before any reply is
received and returns a non-zero exit code. The test therefore only
passed because the ICMP error was never delivered. Use a packet count
(-c) like the ns_c line above it, so that the ICMP error counts
against the packet budget and the exit code depends on whether echo
replies were received. This passes with and without the fix.

Fixes: 8930424777e4 ("tunnels: Accept PACKET_HOST in skb_tunnel_check_pmtu().")
Cc: stable@vger.kernel.org
Reported-by: Laika Price <laikabcprice@gmail.com>
Closes: https://lore.kernel.org/netdev/20260614-master-v3-1-9f5060ba1ed1@gmail.com/
Reported-by: Yaroslav Dudkov <aroslavdudkov622@gmail.com>
Closes: https://lore.kernel.org/netdev/20260901081825.287173-1-aroslavdudkov622@gmail.com/
Reported-by: Charles Bordet <rough.rock3059@datachamp.fr>
Closes: https://lore.kernel.org/netdev/aHVhQLPJIhq-SYPM@eldamar.lan/
Signed-off-by: Ido Schimmel <idosch@nvidia.com>
Tested-by: Yaroslav Dudkov <aroslavdudkov622@gmail.com>
Reviewed-by: David Ahern <dsahern@kernel.org>
Reviewed-by: Stefano Brivio <sbrivio@redhat.com>
Reviewed-by: Guillaume Nault <gnault@redhat.com>
Link: https://patch.msgid.link/20260902190112.4126199-1-idosch@nvidia.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
(cherry picked from commit b58d749633203d92c265317b45fccee555090352)

https://virtuozzo.atlassian.net/browse/VSTOR-139651
Feature: fix ms/net
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 net/ipv4/ip_tunnel_core.c           | 6 ++++++
 tools/testing/selftests/net/pmtu.sh | 2 +-
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/net/ipv4/ip_tunnel_core.c b/net/ipv4/ip_tunnel_core.c
index 8392d304a72eb..7e0f0291b7d56 100644
--- a/net/ipv4/ip_tunnel_core.c
+++ b/net/ipv4/ip_tunnel_core.c
@@ -252,6 +252,9 @@ static int iptunnel_pmtud_build_icmp(struct sk_buff *skb, int mtu)
 	eth_header(skb, skb->dev, ntohs(eh.h_proto), eh.h_source, eh.h_dest, 0);
 	skb_reset_mac_header(skb);
 
+	if (skb_valid_dst(skb))
+		skb_dst_drop(skb);
+
 	return skb->len;
 }
 
@@ -348,6 +351,9 @@ static int iptunnel_pmtud_build_icmpv6(struct sk_buff *skb, int mtu)
 	eth_header(skb, skb->dev, ntohs(eh.h_proto), eh.h_source, eh.h_dest, 0);
 	skb_reset_mac_header(skb);
 
+	if (skb_valid_dst(skb))
+		skb_dst_drop(skb);
+
 	return skb->len;
 }
 
diff --git a/tools/testing/selftests/net/pmtu.sh b/tools/testing/selftests/net/pmtu.sh
index 6c651c880fe83..19b9e099b06bd 100755
--- a/tools/testing/selftests/net/pmtu.sh
+++ b/tools/testing/selftests/net/pmtu.sh
@@ -1409,7 +1409,7 @@ test_pmtu_ipvX_over_bridged_vxlanY_or_geneveY_exception() {
 	mtu "${ns_b}" ${type}_b $((${ll_mtu} + 1000))
 
 	run_cmd ${ns_c} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1
-	run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -w 1  -s $((${ll_mtu} + 500)) ${dst} || return 1
+	run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1
 
 	# Check that exceptions were created
 	pmtu="$(route_get_dst_pmtu_from_exception "${ns_c}" ${dst})"

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 10%]

* Re: [Devel] [PATCH vz10 v2 1/2] selftests: ve: grant the directories the files sit in
    @ 2026-09-22 17:47  6% ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-22 17:47 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

> The sysfs variant names nested leaves, kernel/vmcoreinfo and
> kernel/notes, while the proc one names files at the top of the
> filesystem. Both enforcement tests grant the leaves and nothing else,
> so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
> cannot traverse, and every open answers ENOENT:
> 
>   not ok 14 ve_perms.sysfs.enforce_allows
>   not ok 15 ve_perms.sysfs.enforce_denies
> 
> Grant x on each directory along the way, taken from the path the
> variant already gives. A file at the top of the filesystem has none
> and needs nothing, so the proc variant is unaffected.
> 
> The suite then passes 16 of 16, where it passed 14 before.

This line is redundant, please remove.
In general, please try to avoid numbers in commit messages which (numbers) will
change soon - this time it's enough just to add one more test and that's it.

> 
> Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-144955
> 
> Feature: fix vz selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
> index 25ffbd42c3802..2b542dc301394 100644
> --- a/tools/testing/selftests/ve/ve_perms_test.c
> +++ b/tools/testing/selftests/ve/ve_perms_test.c
> @@ -348,6 +348,37 @@ TEST_F(ve_perms, write_parser)
>   * read, and granting x on the directory lets it be traversed (the lookup then
>   * fails ENOENT on the missing child, not EACCES).
>   */

Those helpers were inserted between a test comment and the test itself.

> +/*
> + * A leaf is only reachable once every directory above it can be traversed.
> + * The variants name their files relative to the mount point, so grant x on
> + * each directory along the way; a file at the top of the filesystem has
> + * none and needs nothing.
> + */
> +static int allow_path(int cgv2_fd, const char *path, const char *file)
> +{
> +	char dir[240];	/* set_entry() writes "<dir> <mask>" into 256 */

The 240 is tied to the 256 inside set_entry() by a comment only, and
set_entry() itself still truncates silently: a long entry is written to
the cgroup file cut short instead of failing. The file already has the
idiom for this in ve_open_rel(), which checks the snprintf() result
against sizeof(). Apply the same in set_entry() and the caller no longer
needs its own length arithmetic:

         static int set_entry(int cgv2_fd, const char *path, const char *entry,
                             const char *mask)
         {
        -       char line[256];
        +       char line[PATH_MAX];

        -       snprintf(line, sizeof(line), "%s %s", entry, mask);
        +       if (snprintf(line, sizeof(line), "%s %s", entry, mask) >=
        +           (int)sizeof(line))
        +               return -1;
                return write_file_at(cgv2_fd, path, line);
         }

         static int allow_path(int cgv2_fd, const char *path, const char *file)
         {
        -       char dir[240];  /* set_entry() writes "<dir> <mask>" into 256 */
        +       char dir[PATH_MAX];
                const char *sep;

                for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
        -               if (sep - file >= (long)sizeof(dir))
        -                       return -1;
        -               snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file), file);
        +               if (snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file),
        +                            file) >= (int)sizeof(dir))
        +                       return -1;
                        if (set_entry(cgv2_fd, path, dir, "x") < 0)
                                return -1;
                }
                return 0;
         }

The set_entry() part fixes a pre-existing function, so it can go in its
own commit if you prefer, but it is small enough to stay here.

-- 
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 6%]

* Re: [Devel] [PATCH vz10 v2 2/2] selftests: ve: introduce vtty pair teardown stress test
  @ 2026-09-22 17:47  3%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-22 17:47 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

> A vtty pair is created by the first master open and torn down by the
> last close. The close racing the peer's close used to let both sides
> decide they were the final one and call release_tty() twice, and a
> master opening again while a teardown was in flight used to hand out a
> reference to a tty_struct about to be freed.
> 
> Hammer both ends. The master comes from the vzctl ioctl, which does
> not look the VE up. The slave is only reachable from inside the VE, as
> TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
> vttys index minor - 1, so the test builds a VE with cgroup2, sets
> ve.veid to the cgroup it made, and runs the slave side there.
> 
> One of the symtoms of a buggy kernel is dmesg warning, not just an
> eventual crash, so the test checks that. A crashed node should also
> report test failure in CI, but that's less verbose, hence the check.

Typo: symtoms. The second sentence argues against itself ("should also
report ... hence the check"). One sentence carries the point: a warning
does not stop the node, so the test reads the kernel log.

> 
> https://virtuozzo.atlassian.net/browse/VSTOR-144771
> 
> Feature: ve selftest
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
> index afa4c568c2c9b..9d8d860938273 100644
> --- a/tools/testing/selftests/ve/.gitignore
> +++ b/tools/testing/selftests/ve/.gitignore
> @@ -1,2 +1,3 @@
>  ve_ns_owner_test
>  ve_perms_test
> +ve_vtty_stress
> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
> index ec40cbc7b3a1b..6cd5823c4d932 100644
> --- a/tools/testing/selftests/ve/Makefile
> +++ b/tools/testing/selftests/ve/Makefile
> @@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
>  
>  TEST_GEN_PROGS += ve_ns_owner_test
>  TEST_GEN_PROGS += ve_perms_test
> +TEST_GEN_PROGS += ve_vtty_stress
>  
>  include ../lib.mk
> diff --git a/tools/testing/selftests/ve/settings b/tools/testing/selftests/ve/settings
> new file mode 100644
> index 0000000000000..e2206265f67c7
> --- /dev/null
> +++ b/tools/testing/selftests/ve/settings
> @@ -0,0 +1 @@
> +timeout=900
> diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
> new file mode 100644
> index 0000000000000..57edbdeef0c84
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_vtty_stress.c
> @@ -0,0 +1,250 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * ve_vtty_stress - close a vtty pair from both ends at once.
> + *
> + * A vtty pair is created by the first master open and torn down by the
> + * last close. A close racing the peer's close used to let both sides
> + * decide they were the final one and call release_tty() twice, and a
> + * master opened again while a teardown was still in flight used to hand
> + * out a reference to a tty_struct that was about to be freed. Either
> + * way the next close walked freed memory.
> + *
> + * The master comes from the vzctl ioctl, which does not look the VE up.
> + * The slave is only reachable from inside the VE, as TTY_MAJOR minor
> + * 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to vttys index
> + * minor - 1. So the test makes a VE with cgroup2, hammers the master
> + * from the host and the slave from a task inside it, and fails if the
> + * kernel logs a complaint about the teardown.
> + *
> + * Surviving is not a pass on its own: the first symptom is a warning,
> + * which stops nothing by itself.

This is the commit message pasted into the file. The history of the
double release_tty() and the freed tty_struct belongs in the commit,
where git keeps it next to the change. The header needs what the test
does, how it reaches both ends, and the usage line. Also, "created by the
first master open" is not the whole story: the slave open creates the
pair too, since vtty_lookup() returns NULL for a missing map and
tty_init_dev(vttys_driver) then runs vtty_install(). In this test the
slave wins that race often.

> + *
> + * Usage: ve_vtty_stress [seconds] (default 600, or $VE_VTTY_SECS)
> + */
> +#define _GNU_SOURCE
> +#include <sched.h>
> +#include <stdbool.h>
> +#include <sys/ioctl.h>
> +#include <linux/major.h>
> +#include <sys/sysmacros.h>
> +#include <sys/wait.h>
> +#include <time.h>
> +
> +#include "ve_selftest.h"
> +#include "../kselftest.h"
> +
> +/* The ioctl argument; the trailing data[] the uapi struct has is unused. */
> +struct vzctl_ve_configure {
> +	unsigned int veid, key, val, size;
> +};
> +
> +#define VZCTL_VE_CONFIGURE _IOW('.', 15, struct vzctl_ve_configure)
> +#define VE_CONFIGURE_OPEN_TTY 5
> +
> +#define VTTY_IDX 0 /* the pair both ends work on */
> +#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
> +#define DEFAULT_SECS 600

ve_vtty_stress is in TEST_GEN_PROGS, so every "make run_tests" of the ve
directory now takes ten minutes longer, and the settings file lifts the
timeout for the whole directory to make room for that. Nothing in the
commit says how long the original bug took to show up. If it reproduced
in seconds, a default in the tens of seconds keeps the test in the
default run and the long soak stays available through VE_VTTY_SECS or
argv[1]. If it really needs minutes, that deserves a sentence in the
commit message.

> +
> +static int open_master(int ctl_fd, unsigned int veid)
> +{
> +	struct vzctl_ve_configure c = {
> +		.veid = veid,
> +		.key = VE_CONFIGURE_OPEN_TTY,
> +		.val = VTTY_IDX,
> +	};
> +
> +	return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
> +}
> +
> +/*
> + * The slave end, inside the VE. Every error here is one the race is
> + * expected to produce: the pair is gone once the master has closed it.
> + */
> +static int hammer_slave(time_t deadline, unsigned long *opened)
> +{
> +	char node[] = "/tmp/ve_vtty_slave";

A fixed name under /tmp is not that good.
A name with the pid in it, or a node under a private directory, could be better.

> +
> +	unlink(node);
> +	if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0)
> +		return -1;
> +
> +	while (time(NULL) < deadline) {
> +		int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
> +
> +		if (fd >= 0) {
> +			(*opened)++;
> +			close(fd);
> +		}
> +	}

The comment is not true for EPERM, EACCES, ENOENT or ENODEV: none of
those comes from the race, all of them mean the slave cannot be opened
at all (a /tmp mounted nodev gives EACCES from may_open(), for one). The
loop ignores errno, so a broken setup spins for the full run and only
then fails with "no slave was ever opened". Bail out on the first errno
that is not EBUSY/EIO/ENXIO and report it. The master loop has the same
shape and the same fix.

> +	unlink(node);
> +	return 0;
> +}
> +
> +/* The kernel log, since a warning fails nothing by itself. */
> +static int kmsg_open(void)
> +{
> +	int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
> +
> +	if (fd >= 0)
> +		lseek(fd, 0, SEEK_END);
> +	return fd;
> +}
> +
> +static int kmsg_check(int fd)
> +{
> +	static const char *const ours[] = {
> +		"tty_release",	    "release_tty",   "check_tty_count",
> +		"vtty_open_master", "vtty_shutdown",
> +	};
> +	char buf[8192];
> +	int found = 0;
> +	ssize_t r;
> +	size_t i;
> +
> +	while (fd >= 0 && (r = read(fd, buf, sizeof(buf) - 1)) > 0) {
> +		buf[r] = '\0';
> +		if (!strstr(buf, "WARNING") && !strstr(buf, "BUG") &&
> +		    !strstr(buf, "Oops") && !strstr(buf, "general protection"))
> +			continue;
> +		for (i = 0; i < ARRAY_SIZE(ours); i++)
> +			if (strstr(buf, ours[i])) {
> +				ksft_print_msg("kernel log: %s", buf);
> +				found++;
> +				break;
> +			}
> +	}
> +	return found;
> +}

/dev/kmsg returns one record per read(), and the keyword and the
function name have to be in the same record for this to match. They
rarely are:

  - "WARNING: CPU: 0 PID: 1 at drivers/tty/pty.c:NNN vtty_map_clear+0x.."
    names vtty_map_clear(), the function holding the WARN_ON(). It
    matches vtty_shutdown only if the compiler inlined the static helper.
    The WARN_ON()s in vtty_map_set() and vtty_lookup() never match.
  - "Oops: 0000 [#1] ..." and "general protection fault, ..." carry no
    function name at all; that is in the RIP: record that follows.
  - "BUG: KASAN: slab-use-after-free in tty_lock+0x.." or a
    "list_del corruption" splat name none of the five.
  - check_tty_count() prints
    "vtty_slave vttys0: tty_release: tty->count(N) != (#fd's(M) + ..."
    with no WARNING/BUG/Oops in it, so the first strstr() chain skips the
    record before the names are tried, and the entry cannot match.

The test can therefore pass on the kernel it is meant to catch. The node
does nothing else during the run, so any WARNING/BUG/Oops/GPF record is a
failure; drop the function filter, and add the "tty->count(" text as a
match of its own.

One more thing here:
The read loop ends on the first error, and /dev/kmsg returns -EPIPE once
when the reader's position has been overwritten (devkmsg_read(), "our
last seen message is gone, return error and reset"), then continues from
the oldest record. Nothing drains the log during the run, so a buggy
kernel that WARNs on every double teardown for ten minutes overruns the
ring, the first read() after the deadline fails with EPIPE, kmsg_check()
returns 0 and the test passes. Skip EPIPE with a continue, or drain the
fd from the main loop.

> +
> +int main(int argc, char **argv)
> +{
> +	unsigned long master_opened = 0, slave_opened = 0;
> +	int cg, ctid, ctl, kmsg, ready[2], count[2], status;
> +	const char *env = getenv("VE_VTTY_SECS");
> +	char path[PATH_MAX], val[16], c;
> +	time_t deadline;
> +	long secs;
> +	pid_t pid;
> +
> +	ksft_print_header();
> +	secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
> +	if (secs <= 0)
> +		secs = DEFAULT_SECS;
> +
> +	ctl = open("/dev/vzctl", O_RDWR);
> +	if (ctl < 0)
> +		ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
> +			       "(vzdev, vzmon) are needed\n",
> +			       strerror(errno));
> +
> +	cg = mount_cg2_fd();
> +	if (cg < 0)
> +		ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
> +	write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS);
> +
> +	ctid = make_ve(cg, CTID_MIN);
> +	if (ctid < 0)
> +		ksft_exit_skip("cannot make a VE cgroup\n");
> +
> +	/*
> +	 * The pair is filed under the veid of the VE, so the two ends only
> +	 * meet if the cgroup carries the one the master is opened for.
> +	 */
> +	snprintf(path, sizeof(path), "%d/ve.veid", ctid);
> +	snprintf(val, sizeof(val), "%d", ctid);

ve_id_write() does not check that a veid is unique (the FIXME in
kernel/ve/ve.c says as much), and the vtty map is a global idr keyed by
veid. make_ve() only guarantees that no cgroup of that name exists in the
hierarchy, not that no running container carries that veid. The
108..200 range makes a clash unlikely on a test node; a sentence in the
commit message saying the test assumes it is enough.

> +	if (write_file_at(cg, path, val) < 0) {
> +		destroy_ve(cg, ctid);
> +		ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
> +	}
> +
> +	/* Do not report a failure where the vtty pair cannot be had at all. */
> +	status = open_master(ctl, ctid);
> +	if (status < 0 && errno != EBUSY) {
> +		destroy_ve(cg, ctid);
> +		ksft_exit_skip("no vtty master: %s\n", strerror(errno));
> +	}
> +	if (status >= 0)
> +		close(status);
> +
> +	ksft_set_plan(1);
> +	kmsg = kmsg_open();
> +	if (kmsg < 0)
> +		ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
> +			       "be noticed\n",
> +			       strerror(errno));
> +
> +	if (pipe(ready) || pipe(count))
> +		ksft_exit_fail_msg("pipe: %s\n", strerror(errno));

Every other exit after make_ve() calls destroy_ve() first; this one
leaves the cgroup behind.

-- 
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [PATCH 2/2] UefiCpuPkg/MpInitLib: resend the Start-up IPI when an AP misses it #VSTOR-146639
  2026-09-21 15:44  3% ` [PATCH 2/2] UefiCpuPkg/MpInitLib: resend the Start-up IPI when an AP misses it #VSTOR-146639 Denis V. Lunev
@ 2026-09-23 13:30  0%   ` Andrey Drobyshev
  0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-23 13:30 UTC (permalink / raw)
  To: Denis V. Lunev, svt-core

Ack
Overall LGTM, see some minor nits below

On 9/21/26 6:44 PM, Denis V. Lunev wrote:
> From: Denis V. Lunev <den@openvz.org>
> 
> WaitApWakeup spins until the AP clears its wakeup signal, with no
> timeout and no way out. If the AP never consumes the Start-up IPI, the
> BSP spins forever and the boot is dead.
> 
> A hypervisor can lose that IPI. Under KVM, INIT is latched while a
> vCPU is in SMM but the Start-up IPI is discarded outright, so an AP
> that takes an SMI just before the BSP broadcasts INIT-SIPI-SIPI is
> left in wait-for-SIPI with nothing left to start it. The window is the
> few instructions between an AP releasing the BSP at the end of the SMM
> rendezvous and its own RSM, which a scheduled vCPU can be preempted
> inside of. KVM holds that delivering the SIPI is software's job, so
> the firmware has to cope.
> 
> Resend rather than spin. Address the AP individually so that APs
> already running the procedure are not disturbed, and pass the reset
> vector only on the paths that sent a Start-up IPI in the first place,
> so the SEV-SNP and hand-off paths keep waiting as before. A Start-up
> IPI is ignored unless the target is in wait-for-SIPI, so resending
> costs nothing when the AP is merely slow.
> 
> The failure this fixes looks like a guest that never boots: firmware
> starts the boot manager, paints nothing further, and one CPU spins
> while the other APs sit idle.
> 
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> Cc: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
> ---
>  UefiCpuPkg/Library/MpInitLib/MpLib.c | 85 +++++++++++++++++++++++++---
>  UefiCpuPkg/Library/MpInitLib/MpLib.h |  2 +
>  2 files changed, 80 insertions(+), 7 deletions(-)
> 
> diff --git a/UefiCpuPkg/Library/MpInitLib/MpLib.c b/UefiCpuPkg/Library/MpInitLib/MpLib.c
> index 426d998b9a..87e8f824b6 100644
> --- a/UefiCpuPkg/Library/MpInitLib/MpLib.c
> +++ b/UefiCpuPkg/Library/MpInitLib/MpLib.c
> @@ -942,16 +942,63 @@ DxeApEntryPoint (
>    ApWakeupFunction (CpuMpData, ProcessorNumber);
>  }
>  
> +/**
> +  Calculate timeout value and return the current performance counter value.
> +
> +  @param[in]  TimeoutInMicroseconds  The timeout value in microseconds.
> +  @param[out] CurrentTime            Returns the current value of the performance counter.
> +
> +  @return Expected timestamp counter for timeout, or 0 if TimeoutInMicroseconds is 0.
> +**/
> +UINT64
> +CalculateTimeout (
> +  IN  UINTN   TimeoutInMicroseconds,
> +  OUT UINT64  *CurrentTime
> +  );
> +
> +/**
> +  Checks whether timeout expires.
> +
> +  @param[in, out]  PreviousTime   On input, the requested time. On output, the elapsed time.
> +  @param[in]       TotalTime      The total amount of elapsed time.
> +  @param[in]       Timeout        The timeout ticks, 0 means infinity.
> +
> +  @retval TRUE   A timeout occurred.
> +  @retval FALSE  No timeout occurred.
> +**/
> +BOOLEAN
> +CheckTimeout (
> +  IN OUT UINT64  *PreviousTime,
> +  IN     UINT64  *TotalTime,
> +  IN     UINT64  Timeout
> +  );
> +

These functions are only used here in this translation unit, yet they
aren't declared static.  Should we wither declare them static and move
their definitions above (without the need for forward declarations), or
just declare them in MpLib.h?

>  /**
>    Wait for AP wakeup and write AP start-up signal till AP is waken up.
>  
>    @param[in] ApStartupSignalBuffer  Pointer to AP wakeup signal
> +  @param[in] ApicId                 Local APIC ID of the AP being woken
> +  @param[in] StartupRoutine         AP reset vector, or 0 if the AP was not
> +                                    woken by INIT-SIPI-SIPI and no Start-up
> +                                    IPI may be resent
>  **/
>  VOID
>  WaitApWakeup (
> -  IN volatile UINT32  *ApStartupSignalBuffer
> +  IN volatile UINT32  *ApStartupSignalBuffer,
> +  IN UINT32           ApicId,
> +  IN UINT32           StartupRoutine
>    )
>  {
> +  UINT64  CurrentTime;
> +  UINT64  TotalTime;
> +  UINT64  Timeout;
> +
> +  Timeout = CalculateTimeout (
> +              (StartupRoutine != 0) ? AP_SIPI_RESEND_TIMEOUT_US : 0,
> +              &CurrentTime
> +              );
> +  TotalTime = 0;
> +
>    //
>    // If AP is waken up, StartupApSignal should be cleared.
>    // Otherwise, write StartupApSignal again till AP waken up.
> @@ -962,6 +1009,12 @@ WaitApWakeup (
>             WAKEUP_AP_SIGNAL
>             ) != 0)
>    {
> +    if (CheckTimeout (&CurrentTime, &TotalTime, Timeout)) {
> +      SendStartupIpi (ApicId, StartupRoutine);
> +      Timeout   = CalculateTimeout (AP_SIPI_RESEND_TIMEOUT_US, &CurrentTime);
> +      TotalTime = 0;
> +    }

This re-send of IPI should happen rarely.  Wouldn't we wanna know when
it does happen?  How about adding some debug info when IPI is re-issued?

Andrey

> +
>      CpuPause ();
>    }
>  }
> @@ -1256,8 +1309,10 @@ WakeUpAP (
>    CPU_AP_DATA                    *CpuData;
>    BOOLEAN                        ResetVectorRequired;
>    CPU_INFO_IN_HOB                *CpuInfoInHob;
> +  UINT32                         SipiVector;
>  
>    CpuMpData->FinishedCount = 0;
> +  SipiVector               = 0;
>    ResetVectorRequired      = FALSE;
>  
>    if (CpuMpData->WakeUpByInitSipiSipi ||
> @@ -1320,13 +1375,14 @@ WakeUpAP (
>        if (CanUseSevSnpCreateAP (CpuMpData)) {
>          SevSnpCreateAP (CpuMpData, -1);
>        } else {
> +        SipiVector = (UINT32)ExchangeInfo->BufferStart;
>          if ((CpuMpData->InitFlag == ApInitConfig) && FixedPcdGetBool (PcdFirstTimeWakeUpAPsBySipi)) {
>            //
>            // SIPI can be used for the first time wake up after reset to reduce boot time.
>            //
> -          SendStartupIpiAllExcludingSelf ((UINT32)ExchangeInfo->BufferStart);
> +          SendStartupIpiAllExcludingSelf (SipiVector);
>          } else {
> -          SendInitSipiSipiAllExcludingSelf ((UINT32)ExchangeInfo->BufferStart);
> +          SendInitSipiSipiAllExcludingSelf (SipiVector);
>          }
>        }
>      }
> @@ -1398,10 +1454,15 @@ WakeUpAP (
>        //
>        // Wait all APs waken up if this is not the 1st broadcast of SIPI
>        //
> +      CpuInfoInHob = (CPU_INFO_IN_HOB *)(UINTN)CpuMpData->CpuInfoInHob;
>        for (Index = 0; Index < CpuMpData->CpuCount; Index++) {
>          CpuData = &CpuMpData->CpuData[Index];
>          if (Index != CpuMpData->BspNumber) {
> -          WaitApWakeup (CpuData->StartupApSignal);
> +          WaitApWakeup (
> +            CpuData->StartupApSignal,
> +            CpuInfoInHob[Index].ApicId,
> +            SipiVector
> +            );
>          }
>        }
>      }
> @@ -1430,9 +1491,10 @@ WakeUpAP (
>        if (CanUseSevSnpCreateAP (CpuMpData)) {
>          SevSnpCreateAP (CpuMpData, (INTN)ProcessorNumber);
>        } else {
> +        SipiVector = (UINT32)ExchangeInfo->BufferStart;
>          SendInitSipiSipi (
>            CpuInfoInHob[ProcessorNumber].ApicId,
> -          (UINT32)ExchangeInfo->BufferStart
> +          SipiVector
>            );
>        }
>      }
> @@ -1440,7 +1502,12 @@ WakeUpAP (
>      //
>      // Wait specified AP waken up
>      //
> -    WaitApWakeup (CpuData->StartupApSignal);
> +    CpuInfoInHob = (CPU_INFO_IN_HOB *)(UINTN)CpuMpData->CpuInfoInHob;
> +    WaitApWakeup (
> +      CpuData->StartupApSignal,
> +      CpuInfoInHob[ProcessorNumber].ApicId,
> +      SipiVector
> +      );
>    }
>  
>    if (ResetVectorRequired) {
> @@ -2017,7 +2084,11 @@ SwitchApContext (
>    {
>      for (Index = 0; Index < MpHandOff->CpuCount; Index++) {
>        if (MpHandOff->ProcessorIndex + Index != BspNumber) {
> -        WaitApWakeup ((UINT32 *)(UINTN)(MpHandOff->Info[Index].StartupSignalAddress));
> +        WaitApWakeup (
> +          (UINT32 *)(UINTN)(MpHandOff->Info[Index].StartupSignalAddress),
> +          0,
> +          0
> +          );
>        }
>      }
>    }
> diff --git a/UefiCpuPkg/Library/MpInitLib/MpLib.h b/UefiCpuPkg/Library/MpInitLib/MpLib.h
> index 5d10516cd8..94bda7f1ea 100644
> --- a/UefiCpuPkg/Library/MpInitLib/MpLib.h
> +++ b/UefiCpuPkg/Library/MpInitLib/MpLib.h
> @@ -44,6 +44,8 @@
>  #include "MpHandOff.h"
>  
>  #define WAKEUP_AP_SIGNAL  SIGNATURE_32 ('S', 'T', 'A', 'P')
> +
> +#define AP_SIPI_RESEND_TIMEOUT_US  10000
>  //
>  // To trigger the start-up signal, BSP writes the specified
>  // StartupSignalValue to the StartupSignalAddress of each processor.


^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH vz10 v2] selftests: pci_endpoint: skip when the test device is absent
  @ 2026-09-23 18:03  0% ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-23 18:03 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

changelog v1-> v2?

--
Best regards,

Konstantin Khorenko,
Virtuozzo Linux Kernel Team

On 9/10/26 05:21, Eva Kurchatova wrote:
> Every fixture opens /dev/pci-endpoint-test.0 and asserts on the result,
> so on a machine that has no PCI endpoint test device all 17 cases fail
> in fixture setup rather than saying the hardware is not there.
> 
> Fix by checking the device once before handing over to the harness.
> Doing it that way returns one SKIP line from the kselftest.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-142446
> Feature: fix selftests
> 
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> ---
>  .../selftests/pci_endpoint/pci_endpoint_test.c | 18 +++++++++++++++++-
>  1 file changed, 17 insertions(+), 1 deletion(-)
> 
> diff --git a/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c b/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
> index da0db0e7c969..a6106b6c52e3 100644
> --- a/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
> +++ b/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
> @@ -257,4 +257,20 @@ TEST_F(pcie_ep_doorbell, DOORBELL_TEST)
>  	pci_ep_ioctl(PCITEST_DOORBELL, 0);
>  	EXPECT_FALSE(ret) TH_LOG("Test failed for Doorbell\n");
>  }
> -TEST_HARNESS_MAIN
> +
> +static bool test_device_available(void)
> +{
> +	return access(test_device, F_OK) == 0;
> +}
> +
> +int main(int argc, char **argv)
> +{
> +	/*
> +	 * Only the plain invocation, which is the one kselftest uses, turns
> +	 * into a skip, so that -l and -h keep working without the hardware.
> +	 */
> +	if (argc == 1 && !test_device_available())
> +		ksft_exit_skip("no PCI endpoint test device %s\n", test_device);
> +
> +	return test_harness_run(argc, argv);
> +}

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH vz10 v2] selftests: binderfs: skip the stress test without binderfs
  @ 2026-09-23 18:03  0% ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-23 18:03 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

changelog?

--
Best regards,

Konstantin Khorenko,
Virtuozzo Linux Kernel Team

On 9/10/26 05:10, Eva Kurchatova wrote:
> binderfs_stress mounts binderfs and asserts on the first mount, so on
> a kernel without CONFIG_ANDROID_BINDERFS the case fails rather than
> reporting that the filesystem is not there.
> 
> Ask /proc/filesystems whether the kernel has it, the way
> supports_filesystem() in the landlock tests and check_resctrlfs_support()
> in the resctrl ones already do, and skip when it does not.
> 
> Probing with a mount instead would not work here: the test is meant to
> run unprivileged, and path_mount() rejects an unprivileged caller in
> may_mount() before do_new_mount() ever looks the filesystem type up, so
> the probe would see EPERM whether binderfs is there or not.  The child
> would then still reach the real mount inside its userns and fail.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-142449
> Feature: fix selftests
> 
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> ---
>  .../filesystems/binderfs/binderfs_test.c      | 25 +++++++++++++++++++
>  1 file changed, 25 insertions(+)
> 
> diff --git a/tools/testing/selftests/filesystems/binderfs/binderfs_test.c b/tools/testing/selftests/filesystems/binderfs/binderfs_test.c
> index 319567f0fae1..fd1f9b6e5d46 100644
> --- a/tools/testing/selftests/filesystems/binderfs/binderfs_test.c
> +++ b/tools/testing/selftests/filesystems/binderfs/binderfs_test.c
> @@ -36,6 +36,28 @@
>  		fd = -EBADF;        \
>  	}
>  
> +static bool binderfs_supported(void)
> +{
> +	char line[128];
> +	bool ret = false;
> +	FILE *f;
> +
> +	f = fopen("/proc/filesystems", "r");
> +	if (!f)
> +		return true;	/* Cannot tell, let the test run and report. */
> +
> +	while (fgets(line, sizeof(line), f)) {
> +		/* binderfs has no backing device, hence the "nodev" prefix. */
> +		if (!strcmp(line, "nodev\tbinder\n")) {
> +			ret = true;
> +			break;
> +		}
> +	}
> +
> +	fclose(f);
> +	return ret;
> +}
> +
>  static void change_mountns(struct __test_metadata *_metadata)
>  {
>  	int ret;
> @@ -388,6 +410,9 @@ TEST(binderfs_stress)
>  	char binderfs_mntpt[] = P_tmpdir "/binderfs_XXXXXX",
>  		device_path[sizeof(P_tmpdir "/binderfs_XXXXXX/") + BINDERFS_MAX_NAME];
>  
> +	if (!binderfs_supported())
> +		SKIP(return, "The Android binderfs filesystem is not available");
> +
>  	ret = socketpair(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, syncfds);
>  	ASSERT_EQ(ret, 0) {
>  		TH_LOG("%s - Failed to create socket pair", strerror(errno));

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH vz10 v2 1/2] selftests: pstore: skip when no backend is registered
  @ 2026-09-23 18:03  0% ` Konstantin Khorenko
  2026-09-23 18:37  0% ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-23 18:03 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

changelog?

--
Best regards,

Konstantin Khorenko,
Virtuozzo Linux Kernel Team

On 9/10/26 05:17, Eva Kurchatova wrote:
> An unregistered pstore backend reads back "(null)", and every latter
> check fails. This also contradicts the pstore_crash_test documentation.
> 
> Fix by skipping an unregistered backend in common_tests
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-142447
> Feature: fix selftests
> 
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> ---
>  tools/testing/selftests/pstore/common_tests       | 15 +++++++++++++--
>  .../selftests/pstore/pstore_post_reboot_tests     |  2 --
>  2 files changed, 13 insertions(+), 4 deletions(-)
> 
> diff --git a/tools/testing/selftests/pstore/common_tests b/tools/testing/selftests/pstore/common_tests
> index 4509f0cc9c91..71cd4473f45a 100755
> --- a/tools/testing/selftests/pstore/common_tests
> +++ b/tools/testing/selftests/pstore/common_tests
> @@ -55,6 +55,8 @@ operate_files() { # tested value, files, operation
>  
>  # Parameters
>  TEST_STRING_PATTERN="Testing pstore: uuid="
> +# Kselftest framework requirement - SKIP code is 4.
> +ksft_skip=4
>  UUID=`cat /proc/sys/kernel/random/uuid`
>  TOP_DIR=`absdir $0`
>  LOG_DIR=$TOP_DIR/logs/`date +%Y%m%d-%H%M%S`_${UUID}/
> @@ -74,8 +76,17 @@ prlog "=== Pstore unit tests (`basename $0`) ==="
>  prlog "UUID="$UUID
>  
>  prlog -n "Checking pstore backend is registered ... "
> -backend=`cat /sys/module/pstore/parameters/backend`
> -show_result $?
> +# A tristate pstore leaves no parameter file at all when it is not built
> +# or not loaded, and an unregistered backend reads back as "(null)".
> +backend=`cat /sys/module/pstore/parameters/backend 2>/dev/null`
> +if [ -z "$backend" ] || [ "$backend" = "(null)" ]; then
> +    prlog "SKIP"
> +    prlog -e "\tbackend=${backend:-<none>}"
> +    prlog -e "\tcmdline=`cat /proc/cmdline`"
> +    prlog "pstore backend is not registered, skipping"
> +    exit $ksft_skip
> +fi
> +show_result 0
>  prlog -e "\tbackend=${backend}"
>  prlog -e "\tcmdline=`cat /proc/cmdline`"
>  if [ $rc -ne 0 ]; then
> diff --git a/tools/testing/selftests/pstore/pstore_post_reboot_tests b/tools/testing/selftests/pstore/pstore_post_reboot_tests
> index d6da5e86efbf..a5464407f5b2 100755
> --- a/tools/testing/selftests/pstore/pstore_post_reboot_tests
> +++ b/tools/testing/selftests/pstore/pstore_post_reboot_tests
> @@ -7,8 +7,6 @@
>  #  Written by Hiraku Toyooka <hiraku.toyooka.gu@hitachi.com>
>  #
>  
> -# Kselftest framework requirement - SKIP code is 4.
> -ksft_skip=4
>  
>  . ./common_tests
>  

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH vz10 v2 1/2] selftests: pstore: skip when no backend is registered
    2026-09-23 18:03  0% ` Konstantin Khorenko
@ 2026-09-23 18:37  0% ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-23 18:37 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

> An unregistered pstore backend reads back "(null)", and every latter

latter -> later

> check fails. This also contradicts the pstore_crash_test documentation.
> 
> Fix by skipping an unregistered backend in common_tests
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-142447
> Feature: fix selftests
> 
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/pstore/common_tests b/tools/testing/selftests/pstore/common_tests
> index 4509f0cc9c918..71cd4473f45a5 100755
> --- a/tools/testing/selftests/pstore/common_tests
> +++ b/tools/testing/selftests/pstore/common_tests
> @@ -55,6 +55,8 @@ operate_files() { # tested value, files, operation
>  
>  # Parameters
>  TEST_STRING_PATTERN="Testing pstore: uuid="
> +# Kselftest framework requirement - SKIP code is 4.
> +ksft_skip=4
>  UUID=`cat /proc/sys/kernel/random/uuid`
>  TOP_DIR=`absdir $0`
>  LOG_DIR=$TOP_DIR/logs/`date +%Y%m%d-%H%M%S`_${UUID}/
> @@ -74,8 +76,17 @@ prlog "=== Pstore unit tests (`basename $0`) ==="
>  prlog "UUID="$UUID
>  
>  prlog -n "Checking pstore backend is registered ... "
> -backend=`cat /sys/module/pstore/parameters/backend`
> -show_result $?
> +# A tristate pstore leaves no parameter file at all when it is not built
> +# or not loaded, and an unregistered backend reads back as "(null)".
> +backend=`cat /sys/module/pstore/parameters/backend 2>/dev/null`
> +if [ -z "$backend" ] || [ "$backend" = "(null)" ]; then
> +    prlog "SKIP"
> +    prlog -e "\tbackend=${backend:-<none>}"
> +    prlog -e "\tcmdline=`cat /proc/cmdline`"
> +    prlog "pstore backend is not registered, skipping"
> +    exit $ksft_skip
> +fi
> +show_result 0
>  prlog -e "\tbackend=${backend}"
>  prlog -e "\tcmdline=`cat /proc/cmdline`"
>  if [ $rc -ne 0 ]; then

After the patch and "show_result 0" $rc here cannot become 0.

  -show_result 0
  +prlog "ok"
   prlog -e "\tbackend=${backend}"
   prlog -e "\tcmdline=`cat /proc/cmdline`"
  -if [ $rc -ne 0 ]; then
  -    exit 1
  -fi

> diff --git a/tools/testing/selftests/pstore/pstore_post_reboot_tests b/tools/testing/selftests/pstore/pstore_post_reboot_tests
> index d6da5e86efbf8..a5464407f5b21 100755
> --- a/tools/testing/selftests/pstore/pstore_post_reboot_tests
> +++ b/tools/testing/selftests/pstore/pstore_post_reboot_tests
> @@ -7,8 +7,6 @@
>  #  Written by Hiraku Toyooka <hiraku.toyooka.gu@hitachi.com>
>  #
>  
> -# Kselftest framework requirement - SKIP code is 4.
> -ksft_skip=4
>  

After remove, we have 2 empty lines here, drop one.

-- 
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH vz10 3/3] selftests: drv-net: read the channel count over netlink
  @ 2026-09-24 11:53  0%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-24 11:53 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

> napi_threaded needs the number of combined channels of the device and
> asks ethtool to print it as JSON. Not every ethtool prints that one:
> ethtool 6.11 does it for -k, -a, -c, -g and -x, but not for -l, and
> the test ends in the first case:
> 
>   CmdExitFailure: Command failed: ethtool --json -l eth4
>   STDERR: b'ethtool: bad command line argument(s)
>   JSON output not available for this subcommand
> 
>   not ok 1 napi_threaded.napi_init
> 
> Ask the kernel for the count over netlink, which is where ethtool reads
> it too, so the test no longer depends on the ethtool version installed.
> Changing the count stays with ethtool -L, which needs no JSON. No other
> test in the group asks ethtool for something it cannot print, so this is
> the only one that needs it.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-139651
> Feature: fix selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/drivers/net/napi_threaded.py b/tools/testing/selftests/drivers/net/napi_threaded.py
> index ed66efa481b0f..73c81a319b49f 100755
> --- a/tools/testing/selftests/drivers/net/napi_threaded.py
> +++ b/tools/testing/selftests/drivers/net/napi_threaded.py
> @@ -7,7 +7,7 @@ Test napi threaded states.
>  
>  from lib.py import ksft_run, ksft_exit
>  from lib.py import ksft_eq, ksft_ne, ksft_ge
> -from lib.py import NetDrvEnv, NetdevFamily
> +from lib.py import EthtoolFamily, NetDrvEnv, NetdevFamily
>  from lib.py import cmd, defer, ethtool
>  
>  
> @@ -28,7 +28,13 @@ from lib.py import cmd, defer, ethtool
>  
>  
>  def _setup_deferred_cleanup(cfg) -> None:
> -    combined = ethtool(f"-l {cfg.ifname}", json=True)[0].get("combined", 0)
> +    # Not every ethtool prints the channel counts as JSON, the one on
> +    # the test machine here does not:
> +    #   ethtool: bad command line argument(s)
> +    #   JSON output not available for this subcommand
> +    # Ask the kernel for them instead, it is the same number.

The long description with error messages examples should be in the commit
message, not in the code comment. A short comment like
"not every ethtool prints -l as JSON, ask the kernel"
should be enough.

-- 
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* [PATCH hci-8.0/hci-8.1 1/2] migration/block-dirty-bitmap: fix postcopy abort with bitmaps on several nodes #VSTOR-147066
  @ 2026-09-24 12:13 35% ` Denis V. Lunev
  0 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-24 12:13 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den, Denis V. Lunev

From: Denis V. Lunev <den@openvz.org>

With dirty bitmaps on more than one node, the source switches nodes
during the postcopy bulk phase and sends the node name again. The
destination reads those chunks in the postcopy listen thread, which does
not hold the BQL, and resolves the name through bdrv_lookup_bs():

  qemu-system-x86_64: ../block.c:6400: bdrv_lookup_bs: Assertion
  `qemu_in_main_thread()' failed.

The bdrv_find_dirty_bitmap() call right after it needs the BQL or the
dirty bitmap lock as well. Taking the BQL there would deadlock: the
listen thread holds s->lock, and dirty_bitmap_mig_before_vm_start()
takes s->lock with the BQL held.

Every node and bitmap that a non-START chunk refers to was set up by a
START chunk, and those are loaded under the BQL during setup. Record the
node alias there, and resolve all other chunks against s->bitmaps
instead of the block graph.

Bitmaps on a single node never resend the node name after setup, which
is why migrate-bitmaps-postcopy-test does not catch this.

Fixes: f791bf7f93f2 ("assertions for block global state API")
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 migration/block-dirty-bitmap.c | 59 +++++++++++++++++++++++++++++++---
 1 file changed, 55 insertions(+), 4 deletions(-)

diff --git a/migration/block-dirty-bitmap.c b/migration/block-dirty-bitmap.c
index 9c8c308773d..d17eec412bb 100644
--- a/migration/block-dirty-bitmap.c
+++ b/migration/block-dirty-bitmap.c
@@ -136,6 +136,7 @@ typedef struct DBMSaveState {
 } DBMSaveState;
 
 typedef struct LoadBitmapState {
+    char node_alias[256];
     BlockDriverState *bs;
     BdrvDirtyBitmap *bitmap;
     bool migrated;
@@ -861,6 +862,7 @@ static int dirty_bitmap_load_start(QEMUFile *f, DBMLoadState *s)
     }
 
     b = g_new(LoadBitmapState, 1);
+    g_strlcpy(b->node_alias, s->node_alias, sizeof(b->node_alias));
     b->bs = s->bs;
     b->bitmap = s->bitmap;
     b->migrated = false;
@@ -1058,6 +1060,54 @@ static int dirty_bitmap_load_bits(QEMUFile *f, DBMLoadState *s)
     return 0;
 }
 
+/*
+ * Only START chunks are guaranteed to be loaded under the BQL; the rest may
+ * come from the postcopy listen thread and must not touch the block graph.
+ */
+static BlockDriverState *dirty_bitmap_load_find_bs(DBMLoadState *s,
+                                                   const char *device,
+                                                   const char *node_name,
+                                                   Error **errp)
+{
+    GSList *item;
+
+    if (s->flags & DIRTY_BITMAP_MIG_FLAG_START) {
+        return bdrv_lookup_bs(device, node_name, errp);
+    }
+
+    for (item = s->bitmaps; item; item = g_slist_next(item)) {
+        LoadBitmapState *b = item->data;
+
+        if (!strcmp(b->node_alias, s->node_alias)) {
+            return b->bs;
+        }
+    }
+
+    error_setg(errp, "Error: no migrated bitmaps on node alias '%s'",
+               s->node_alias);
+    return NULL;
+}
+
+static BdrvDirtyBitmap *dirty_bitmap_load_find_bitmap(DBMLoadState *s)
+{
+    GSList *item;
+
+    if (s->flags & DIRTY_BITMAP_MIG_FLAG_START) {
+        return bdrv_find_dirty_bitmap(s->bs, s->bitmap_name);
+    }
+
+    for (item = s->bitmaps; item; item = g_slist_next(item)) {
+        LoadBitmapState *b = item->data;
+
+        if (b->bs == s->bs &&
+            !strcmp(bdrv_dirty_bitmap_name(b->bitmap), s->bitmap_name)) {
+            return b->bitmap;
+        }
+    }
+
+    return NULL;
+}
+
 static int dirty_bitmap_load_header(QEMUFile *f, DBMLoadState *s,
                                     GHashTable *alias_map)
 {
@@ -1086,11 +1136,12 @@ static int dirty_bitmap_load_header(QEMUFile *f, DBMLoadState *s,
                     s->bs = NULL;
                 } else {
                     bitmap_alias_map = amin->subtree;
-                    s->bs = bdrv_lookup_bs(NULL, amin->string, &local_err);
+                    s->bs = dirty_bitmap_load_find_bs(s, NULL, amin->string,
+                                                      &local_err);
                 }
             } else {
-                s->bs = bdrv_lookup_bs(s->node_alias, s->node_alias,
-                                       &local_err);
+                s->bs = dirty_bitmap_load_find_bs(s, s->node_alias,
+                                                  s->node_alias, &local_err);
             }
             if (!s->bs) {
                 error_report_err(local_err);
@@ -1141,7 +1192,7 @@ static int dirty_bitmap_load_header(QEMUFile *f, DBMLoadState *s,
 
         if (!s->cancelled) {
             g_strlcpy(s->bitmap_name, bitmap_name, sizeof(s->bitmap_name));
-            s->bitmap = bdrv_find_dirty_bitmap(s->bs, s->bitmap_name);
+            s->bitmap = dirty_bitmap_load_find_bitmap(s);
 
             /*
              * bitmap may be NULL here, it wouldn't be an error if it is the
-- 
2.53.0


^ permalink raw reply	[relevance 35%]

* Re: [Devel] [PATCH vz10 4/4] selftests: drv-net-hw: skip the tests that need a real interface
  @ 2026-09-24 13:23  3%   ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-24 13:23 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

> ethtool, ethtool_extended_state, hw_stats_l3 and hw_stats_l3_gre need a
> device that can set link speed and autonegotiation, report an extended
> link state, or offload L3 statistics. Where none is configured, lib.sh
> creates a veth pair instead, which supports none of that, so every case
> fails on something the kernel correctly refuses.
> 
> Upstream answers this by keeping such tests in drivers/net/hw and not
> running them on veth at all: commit 0c499a351777 ("selftests:
> forwarding: Ditch skip_on_veth()") removed the equivalent check once the
> directory was split out. This guard is therefore not for upstream, it
> only stops a run that substitutes veth pairs from reporting failures.
> 
> Skip before sourcing lib.sh, because lib.sh makes the veth pair as it is
> sourced. Exiting afterwards leaves the devices behind - vrf_cleanup()
> removes them and a test that skips never gets there - and a later test
> that adds a veth by one of those names then cannot. hw_stats_l3_gre
> asks for six interfaces, so one run left veth0 to veth5 in the initial
> namespace and the netfilter suite failed on it.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-139651
> Feature: fix selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/drivers/net/hw/ethtool.sh b/tools/testing/selftests/drivers/net/hw/ethtool.sh
> index fa6953de6b6dd..b1744758627c2 100755
> --- a/tools/testing/selftests/drivers/net/hw/ethtool.sh
> +++ b/tools/testing/selftests/drivers/net/hw/ethtool.sh
> @@ -11,6 +11,22 @@ ALL_TESTS="
>  "
>  NUM_NETIFS=2
>  lib_dir=$(dirname "$0")
> +
> +# The test needs a device capable of setting link speed and
> +# autonegotiation. Where no real interfaces are configured, lib.sh makes
> +# a veth pair instead, which cannot, and every case then fails on
> +# something the kernel correctly refuses. Check this before sourcing
> +# lib.sh: it creates that pair as it is sourced, so exiting afterwards
> +# leaves the devices behind, and the next test that wants one of those
> +# names cannot create it.

This is the commit message pasted into the test, four times. The
rationale belongs in the commit message only. In the code one line is
enough, something like:


> +
> +: "${NETIF_CREATE:=yes}"
> +: "${NETIF_TYPE:=veth}"
> +if [[ "$NETIF_CREATE" = yes && "$NETIF_TYPE" = veth ]]; then
> +	echo "SKIP: needs a real interface, set NETIFS in forwarding.config"
> +	exit 4 # ksft_skip, lib.sh is not sourced yet
> +fi
> +
>  source "$lib_dir"/../../../net/forwarding/lib.sh
>  source ethtool_lib.sh
>  
> diff --git a/tools/testing/selftests/drivers/net/hw/ethtool_extended_state.sh b/tools/testing/selftests/drivers/net/hw/ethtool_extended_state.sh
> index a7584448416ea..c6c96117b1f12 100755
> --- a/tools/testing/selftests/drivers/net/hw/ethtool_extended_state.sh
> +++ b/tools/testing/selftests/drivers/net/hw/ethtool_extended_state.sh
> @@ -9,6 +9,22 @@ ALL_TESTS="
>  
>  NUM_NETIFS=2
>  lib_dir=$(dirname "$0")
> +
> +# The test needs a device capable of reporting an extended link state.
> +# Where no real interfaces are configured, lib.sh makes a veth pair
> +# instead, which cannot, and every case then fails on something the
> +# kernel correctly refuses. Check this before sourcing lib.sh: it
> +# creates that pair as it is sourced, so exiting afterwards leaves the
> +# devices behind, and the next test that wants one of those names cannot
> +# create it.
> +
> +: "${NETIF_CREATE:=yes}"
> +: "${NETIF_TYPE:=veth}"
> +if [[ "$NETIF_CREATE" = yes && "$NETIF_TYPE" = veth ]]; then
> +	echo "SKIP: needs a real interface, set NETIFS in forwarding.config"
> +	exit 4 # ksft_skip, lib.sh is not sourced yet
> +fi
> +
>  source "$lib_dir"/../../../net/forwarding/lib.sh
>  source ethtool_lib.sh
>  
> diff --git a/tools/testing/selftests/drivers/net/hw/hw_stats_l3.sh b/tools/testing/selftests/drivers/net/hw/hw_stats_l3.sh
> index 67fafefc80bea..1776a4954601b 100755
> --- a/tools/testing/selftests/drivers/net/hw/hw_stats_l3.sh
> +++ b/tools/testing/selftests/drivers/net/hw/hw_stats_l3.sh
> @@ -49,6 +49,22 @@ ALL_TESTS="
>  "
>  NUM_NETIFS=4
>  lib_dir=$(dirname "$0")
> +
> +# The test needs a device capable of offloading L3 statistics. Where no
> +# real interfaces are configured, lib.sh makes a veth pair instead,
> +# which cannot, and every case then fails on something the kernel
> +# correctly refuses. Check this before sourcing lib.sh: it creates that
> +# pair as it is sourced, so exiting afterwards leaves the devices
> +# behind, and the next test that wants one of those names cannot create
> +# it.
> +
> +: "${NETIF_CREATE:=yes}"
> +: "${NETIF_TYPE:=veth}"
> +if [[ "$NETIF_CREATE" = yes && "$NETIF_TYPE" = veth ]]; then
> +	echo "SKIP: needs a real interface, set NETIFS in forwarding.config"
> +	exit 4 # ksft_skip, lib.sh is not sourced yet
> +fi
> +

The check does not see forwarding.config. lib.sh sets these defaults
(lib.sh:54-55), then sources net/forwarding/forwarding.config
(lib.sh:126), and only then creates the interfaces (lib.sh:433). Here
lib.sh has not been sourced yet, so the config file has not been read
and the two variables fall back to yes/veth unless they are exported in
the environment.

So a user who does what the SKIP line tells them to, puts real NETIFS
into forwarding.config and sets NETIF_CREATE=no there, still gets the
skip: on real hardware all four tests never run. Only an exported
NETIF_CREATE=no would get past it, and forwarding.config is not
sourced into the environment anywhere.

Read the same file lib.sh reads before deciding:

forwarding_dir=$lib_dir/../../../net/forwarding
[[ -f $forwarding_dir/forwarding.config ]] && \
      source "$forwarding_dir/forwarding.config"
: "${NETIF_CREATE:=yes}"
: "${NETIF_TYPE:=veth}"

Since the same block goes into four files, put it into a small helper
next to ethtool_lib.sh and source that before lib.sh, instead of
copying it.

>  source "$lib_dir"/../../../net/forwarding/lib.sh
>  source "$lib_dir"/../../../net/forwarding/tc_common.sh
>  

Two more notes on the set of tests:

ethtool_rmon.sh in the same directory reads rmon histogram counters via
"ethtool -S --groups rmon" and has no skip of its own, so on veth it
fails the same way. If it fails in your runs too, it belongs in this
patch. ethtool_mm.sh and loopback.sh already skip by themselves.

The reference to 0c499a351777 is correct and the "why not upstream"
paragraph is useful, keep it.

-- 
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in
@ 2026-09-24 15:01  3% Eva Kurchatova
  2026-09-24 15:01  6% ` [Devel] [PATCH hci-8.1 v3 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
  2026-09-24 16:17  0% ` [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in Konstantin Khorenko
  0 siblings, 2 replies; 200+ results
From: Eva Kurchatova @ 2026-09-24 15:01 UTC (permalink / raw)
  To: vasileios.almpanis; +Cc: devel

The sysfs variant names nested leaves, kernel/vmcoreinfo and
kernel/notes, while the proc one names files at the top of the
filesystem. Both enforcement tests grant the leaves and nothing else,
so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
cannot traverse, and every open answers ENOENT:

  not ok 14 ve_perms.sysfs.enforce_allows
  not ok 15 ve_perms.sysfs.enforce_denies

Grant x on each directory along the way, taken from the path the
variant already gives.

Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")

https://virtuozzo.atlassian.net/browse/VSTOR-144955

Feature: fix vz selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/ve/ve_perms_test.c | 33 ++++++++++++++++++++++
 1 file changed, 33 insertions(+)

diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 25ffbd42c380..2b542dc30139 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -348,6 +348,37 @@ TEST_F(ve_perms, write_parser)
  * read, and granting x on the directory lets it be traversed (the lookup then
  * fails ENOENT on the missing child, not EACCES).
  */
+/*
+ * A leaf is only reachable once every directory above it can be traversed.
+ * The variants name their files relative to the mount point, so grant x on
+ * each directory along the way; a file at the top of the filesystem has
+ * none and needs nothing.
+ */
+static int allow_path(int cgv2_fd, const char *path, const char *file)
+{
+	char dir[240];	/* set_entry() writes "<dir> <mask>" into 256 */
+	const char *sep;
+
+	for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
+		if (sep - file >= (long)sizeof(dir))
+			return -1;
+		snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file), file);
+		if (set_entry(cgv2_fd, path, dir, "x") < 0)
+			return -1;
+	}
+	return 0;
+}
+
+static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
+{
+	int i;
+
+	for (i = 0; i < NR_FILES; i++)
+		if (allow_path(cgv2_fd, path, files[i]) < 0)
+			return -1;
+	return 0;
+}
+
 TEST_F(ve_perms, enforce_allows)
 {
 	char path[PATH_MAX], absent[128];
@@ -358,6 +389,7 @@ TEST_F(ve_perms, enforce_allows)
 
 	ve_file(path, sizeof(path), self->ctid_a, variant->fs);
 
+	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
 	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
 	for (i = 0; i < NR_FILES; i++)
 		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
@@ -400,6 +432,7 @@ TEST_F(ve_perms, enforce_denies)
 				      O_RDONLY), 0);
 
 	/* Granted w only: visible but not readable. */
+	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
 	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
 	for (i = 0; i < NR_FILES; i++)
 		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH hci-8.1 v3 2/2] selftests: ve: introduce vtty pair teardown stress test
  2026-09-24 15:01  3% [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
@ 2026-09-24 15:01  6% ` Eva Kurchatova
  2026-09-24 16:17  0% ` [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-24 15:01 UTC (permalink / raw)
  To: vasileios.almpanis; +Cc: devel

Either end of a vtty pair creates it, and the last close tears it down.

Race condition in older VZKernel vtty implementation allowed both sides
to decide they were the final one and call release_tty() twice, and a
master opening again while a teardown was in flight used to hand out a
reference to a tty_struct about to be freed. Either way, this caused
use-after-free.

Hammer both ends for ownership correctness & thread safety verification.

Sixty seconds by default, enough to bring down a buggy kernel.

https://virtuozzo.atlassian.net/browse/VSTOR-144771

Feature: ve selftest
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/ve/.gitignore       |   1 +
 tools/testing/selftests/ve/Makefile         |   1 +
 tools/testing/selftests/ve/settings         |   1 +
 tools/testing/selftests/ve/ve_vtty_stress.c | 319 ++++++++++++++++++++
 4 files changed, 322 insertions(+)
 create mode 100644 tools/testing/selftests/ve/settings
 create mode 100644 tools/testing/selftests/ve/ve_vtty_stress.c

diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index afa4c568c2c9..9d8d86093827 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,2 +1,3 @@
 ve_ns_owner_test
 ve_perms_test
+ve_vtty_stress
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index ec40cbc7b3a1..6cd5823c4d93 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
 
 TEST_GEN_PROGS += ve_ns_owner_test
 TEST_GEN_PROGS += ve_perms_test
+TEST_GEN_PROGS += ve_vtty_stress
 
 include ../lib.mk
diff --git a/tools/testing/selftests/ve/settings b/tools/testing/selftests/ve/settings
new file mode 100644
index 000000000000..694d70710ff0
--- /dev/null
+++ b/tools/testing/selftests/ve/settings
@@ -0,0 +1 @@
+timeout=300
diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
new file mode 100644
index 000000000000..4de40a45428b
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_vtty_stress.c
@@ -0,0 +1,319 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_vtty_stress - open and close a vtty pair from both ends at once.
+ *
+ * Either end of a vtty pair creates it, and the last close tears it down.
+ *
+ * Race condition in older VZKernel vtty implementation allowed both sides
+ * to decide they were the final one and call release_tty() twice, and a
+ * master opening again while a teardown was in flight used to hand out a
+ * reference to a tty_struct about to be freed. Either way, this caused
+ * use-after-free.
+ *
+ * Hammer both ends. The master comes from the vzctl ioctl, which does
+ * not look the VE up. The slave is reachable only from inside the VE, as
+ * TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
+ * vttys index minor - 1, so the test builds a VE with cgroup2, sets
+ * ve.veid to the cgroup it made, and runs the slave side there.
+ *
+ * A warning does not stop the host, so the run reads the kernel log too.
+ * One read() of /dev/kmsg is one record, so the test matches vtty driver
+ * splats, as well as generic kernel warning/oops/kasan reports.
+ *
+ * Usage: ve_vtty_stress [seconds] (default 60, or $VE_VTTY_SECS)
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <stdbool.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <linux/major.h>
+#include <sys/sysmacros.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "ve_selftest.h"
+#include "../kselftest.h"
+
+/* The ioctl argument; the trailing data[] the uapi struct has is unused. */
+struct vzctl_ve_configure {
+	unsigned int veid, key, val, size;
+};
+
+#define VZCTL_VE_CONFIGURE _IOW('.', 15, struct vzctl_ve_configure)
+#define VE_CONFIGURE_OPEN_TTY 5
+
+#define VTTY_IDX 0 /* the pair both ends work on */
+#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
+#define DEFAULT_SECS 60
+
+/*
+ * Expected open errno codes: the pair may be closing, or gone already,
+ * when an open lands on it.
+ * Anything else is an environment issue or a bug.
+ */
+static bool expected_errno(int err)
+{
+	return err == EBUSY || err == EIO || err == ENXIO;
+}
+
+static int open_master(int ctl_fd, unsigned int veid)
+{
+	struct vzctl_ve_configure c = {
+		.veid = veid,
+		.key = VE_CONFIGURE_OPEN_TTY,
+		.val = VTTY_IDX,
+	};
+
+	return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
+}
+
+/* The slave end, inside the VE. Returns the errno that stopped it, or 0. */
+static int hammer_slave(const char *node, time_t deadline,
+			unsigned long *opened)
+{
+	int err = 0;
+
+	while (time(NULL) < deadline) {
+		int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
+
+		if (fd >= 0) {
+			(*opened)++;
+			close(fd);
+		} else if (!expected_errno(errno)) {
+			err = errno;
+			break;
+		}
+	}
+
+	return err;
+}
+
+/* The kernel log, since a warning fails nothing by itself. */
+static int kmsg_open(void)
+{
+	int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+
+	if (fd >= 0)
+		lseek(fd, 0, SEEK_END);
+	return fd;
+}
+
+/*
+ * Check for kernel log warnings, one read() per record, hence
+ * the function name is not checked.
+ */
+static int kmsg_check(int fd)
+{
+	char buf[8192];
+	int found = 0;
+	ssize_t r;
+
+	if (fd < 0)
+		return 0;
+
+	for (;;) {
+		r = read(fd, buf, sizeof(buf) - 1);
+		if (r < 0) {
+			/* Ring overrun, continue with the oldest existing record */
+			if (errno == EPIPE)
+				continue;
+			break;
+		}
+		if (r == 0)
+			break;
+		buf[r] = '\0';
+		/* Known symptoms of a buggy kernel */
+		if (strstr(buf, "vtty") && (strstr(buf, "tty->count") ||
+					    strstr(buf, "bad slave count"))) {
+			ksft_print_msg("vtty mismatched count log: %s", buf);
+			found++;
+		}
+		if (strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") ||
+		    strstr(buf, ";BUG: ") || strstr(buf, "soft lockup - ")) {
+			ksft_print_msg("kernel log: %s", buf);
+			found++;
+		}
+	}
+	return found;
+}
+
+int main(int argc, char **argv)
+{
+	unsigned long master_opened = 0, slave_opened = 0;
+	int cg, ctid, ctl, kmsg, ready[2], count[2], status;
+	int master_err = 0, slave_err;
+	const char *env = getenv("VE_VTTY_SECS");
+	char path[PATH_MAX], val[16], c;
+	char dir[] = "/tmp/ve_vtty_XXXXXX";
+	char node[sizeof(dir) + 6];
+	time_t deadline;
+	long secs;
+	pid_t pid;
+
+	ksft_print_header();
+	secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
+	if (secs <= 0)
+		secs = DEFAULT_SECS;
+
+	ctl = open("/dev/vzctl", O_RDWR);
+	if (ctl < 0)
+		ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
+			       "(vzdev, vzmon) are needed\n",
+			       strerror(errno));
+
+	cg = mount_cg2_fd();
+	if (cg < 0)
+		ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
+	write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS);
+
+	ctid = make_ve(cg, CTID_MIN);
+	if (ctid < 0)
+		ksft_exit_skip("cannot make a VE cgroup\n");
+
+	/* The ve.veid must match between master / slave */
+	snprintf(path, sizeof(path), "%d/ve.veid", ctid);
+	snprintf(val, sizeof(val), "%d", ctid);
+	if (write_file_at(cg, path, val) < 0) {
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
+	}
+
+	/* Do not report a failure where the vtty pair cannot be had at all */
+	status = open_master(ctl, ctid);
+	if (status < 0 && errno != EBUSY) {
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("no vtty master: %s\n", strerror(errno));
+	}
+	if (status >= 0)
+		close(status);
+
+	/*
+	 * Use temp directory for the node the slave end opens.
+	 * Cleaned up by the master side instead of relying on the slave.
+	 */
+	if (!mkdtemp(dir)) {
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("mkdtemp: %s\n", strerror(errno));
+	}
+	snprintf(node, sizeof(node), "%s/slave", dir);
+	if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0) {
+		status = errno;
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("mknod: %s\n", strerror(status));
+	}
+
+	ksft_set_plan(1);
+	kmsg = kmsg_open();
+	if (kmsg < 0)
+		ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
+			       "be noticed\n",
+			       strerror(errno));
+
+	if (pipe(ready) || pipe(count)) {
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("pipe: %s\n", strerror(errno));
+	}
+
+	deadline = time(NULL) + secs;
+	ksft_print_msg("VE %d, pair %d, %ld seconds\n", ctid, VTTY_IDX, secs);
+
+	pid = fork();
+	if (pid == 0) {
+		close(ready[0]);
+		close(count[0]);
+		if (enter_cgroup(cg, ctid) < 0)
+			_exit(errno ? errno : 1);
+		/* CLONE_NEWVE puts the task in the VE of the cgroup */
+		if (unshare(CLONE_NEWVE | CLONE_NEWPID | CLONE_NEWNS) < 0)
+			_exit(errno ? errno : 1);
+		if (fork() == 0) {
+			unsigned long n = 0;
+			int err;
+
+			/*
+			 * Hammering on after the run has given up would hold
+			 * the cgroup open against destroy_ve().
+			 */
+			prctl(PR_SET_PDEATHSIG, SIGKILL);
+
+			c = 'r';
+			if (write(ready[1], &c, 1) != 1)
+				_exit(errno);
+			err = hammer_slave(node, deadline, &n);
+			/* Send the count either way: it says how far it got. */
+			if (write(count[1], &n, sizeof(n)) != sizeof(n))
+				_exit(errno);
+			_exit(err);
+		}
+		wait(&status);
+		_exit(WIFEXITED(status) ? WEXITSTATUS(status) : 1);
+	} else if (pid < 0) {
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("fork: %s\n", strerror(errno));
+	}
+	close(ready[1]);
+	close(count[1]);
+
+	if (read(ready[0], &c, 1) != 1) {
+		waitpid(pid, &status, 0);
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("the VE side never came up\n");
+	}
+
+	while (time(NULL) < deadline) {
+		int fd = open_master(ctl, ctid);
+
+		if (fd >= 0) {
+			master_opened++;
+			close(fd);
+		} else if (!expected_errno(errno)) {
+			master_err = errno;
+			break;
+		}
+	}
+
+	if (master_err) {
+		/* The other end runs to its own deadline, stop it */
+		kill(pid, SIGKILL);
+		waitpid(pid, &status, 0);
+		slave_err = 0;
+	} else {
+		if (read(count[0], &slave_opened, sizeof(slave_opened)) !=
+		    sizeof(slave_opened))
+			slave_opened = 0;
+		waitpid(pid, &status, 0);
+		slave_err = WIFEXITED(status) ? WEXITSTATUS(status) : EINTR;
+	}
+
+	ksft_print_msg("%lu master opens, %lu slave opens\n", master_opened,
+		       slave_opened);
+
+	status = kmsg_check(kmsg);
+	unlink(node);
+	rmdir(dir);
+	destroy_ve(cg, ctid);
+
+	if (master_err)
+		ksft_exit_fail_msg("the master end stopped: %s\n",
+				   strerror(master_err));
+	if (slave_err)
+		ksft_exit_fail_msg("the slave end stopped: %s\n",
+				   strerror(slave_err));
+	if (!master_opened)
+		ksft_exit_fail_msg("no master was ever opened\n");
+	if (!slave_opened)
+		ksft_exit_fail_msg("no slave was ever opened\n");
+	if (status)
+		ksft_exit_fail_msg("kernel complained %d time(s)\n", status);
+
+	ksft_test_result_pass("vtty pair closed from both ends\n");
+	ksft_finished();
+}
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 6%]

* Re: [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in
  2026-09-24 15:01  3% [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
  2026-09-24 15:01  6% ` [Devel] [PATCH hci-8.1 v3 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
@ 2026-09-24 16:17  0% ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-24 16:17 UTC (permalink / raw)
  To: Eva Kurchatova; +Cc: devel

changelog v2->v3 ?

+ none of my comments are fixed in both those patches.

NAK for both.

And ask Claude + Fable to review your patches before sending them in the list.

--
Best regards,

Konstantin Khorenko,
Virtuozzo Linux Kernel Team

On 9/24/26 17:01, Eva Kurchatova wrote:
> The sysfs variant names nested leaves, kernel/vmcoreinfo and
> kernel/notes, while the proc one names files at the top of the
> filesystem. Both enforcement tests grant the leaves and nothing else,
> so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
> cannot traverse, and every open answers ENOENT:
> 
>   not ok 14 ve_perms.sysfs.enforce_allows
>   not ok 15 ve_perms.sysfs.enforce_denies
> 
> Grant x on each directory along the way, taken from the path the
> variant already gives.
> 
> Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-144955
> 
> Feature: fix vz selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> ---
>  tools/testing/selftests/ve/ve_perms_test.c | 33 ++++++++++++++++++++++
>  1 file changed, 33 insertions(+)
> 
> diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
> index 25ffbd42c380..2b542dc30139 100644
> --- a/tools/testing/selftests/ve/ve_perms_test.c
> +++ b/tools/testing/selftests/ve/ve_perms_test.c
> @@ -348,6 +348,37 @@ TEST_F(ve_perms, write_parser)
>   * read, and granting x on the directory lets it be traversed (the lookup then
>   * fails ENOENT on the missing child, not EACCES).
>   */
> +/*
> + * A leaf is only reachable once every directory above it can be traversed.
> + * The variants name their files relative to the mount point, so grant x on
> + * each directory along the way; a file at the top of the filesystem has
> + * none and needs nothing.
> + */
> +static int allow_path(int cgv2_fd, const char *path, const char *file)
> +{
> +	char dir[240];	/* set_entry() writes "<dir> <mask>" into 256 */
> +	const char *sep;
> +
> +	for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
> +		if (sep - file >= (long)sizeof(dir))
> +			return -1;
> +		snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file), file);
> +		if (set_entry(cgv2_fd, path, dir, "x") < 0)
> +			return -1;
> +	}
> +	return 0;
> +}
> +
> +static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
> +{
> +	int i;
> +
> +	for (i = 0; i < NR_FILES; i++)
> +		if (allow_path(cgv2_fd, path, files[i]) < 0)
> +			return -1;
> +	return 0;
> +}
> +
>  TEST_F(ve_perms, enforce_allows)
>  {
>  	char path[PATH_MAX], absent[128];
> @@ -358,6 +389,7 @@ TEST_F(ve_perms, enforce_allows)
>  
>  	ve_file(path, sizeof(path), self->ctid_a, variant->fs);
>  
> +	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
>  	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
>  	for (i = 0; i < NR_FILES; i++)
>  		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
> @@ -400,6 +432,7 @@ TEST_F(ve_perms, enforce_denies)
>  				      O_RDONLY), 0);
>  
>  	/* Granted w only: visible but not readable. */
> +	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
>  	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
>  	for (i = 0; i < NR_FILES; i++)
>  		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* [PATCH QEMU HCI-8.0 v4 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
  @ 2026-09-24 16:41 31% ` Andrey Drobyshev
  2026-09-24 16:41 13% ` [PATCH QEMU HCI-8.0 v4 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
                   ` (8 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
  To: svt-core; +Cc: Andrey Drobyshev, den

The FD passed into usb_host_open() for a wrapped device is only stored in
s->hostfd after libusb_wrap_sys_device() succeeds, and the failure path
doesn't close it.  if wrapping fails, or a later step of the open sequence
fails (e.g. usb_device_attach), the FD is leaked, as usb_host_close() only
runs for fully opened devices, and the realize error path doesn't clean it
up either.

Store the FD in s->hostfd before wrapping and close it on the failure
path.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b74670ae256..83645fbe0ce 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -954,6 +954,11 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
     int rc;
     Error *local_err = NULL;
 
+    /* Make sure failure path frees caller-owned FD */
+    if (!dev) {
+        s->hostfd = hostfd;
+    }
+
     if (s->bh_postld_pending) {
         return -1;
     }
@@ -978,7 +983,6 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
         if (rc != 0) {
             goto fail;
         }
-        s->hostfd  = hostfd;
         dev = libusb_get_device(s->dh);
         bus_num = libusb_get_bus_number(dev);
         addr = libusb_get_device_address(dev);
@@ -1066,6 +1070,10 @@ fail:
         s->dh = NULL;
         s->dev = NULL;
     }
+    if (s->hostfd != -1) {
+        close(s->hostfd);
+        s->hostfd = -1;
+    }
     return -1;
 }
 

-- 
2.47.1


^ permalink raw reply	[relevance 31%]

* [PATCH QEMU HCI-8.0 v4 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800
    2026-09-24 16:41 31% ` [PATCH QEMU HCI-8.0 v4 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 13% ` Andrey Drobyshev
  2026-09-24 16:41 26% ` [PATCH QEMU HCI-8.0 v4 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
                   ` (7 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
  To: svt-core; +Cc: Andrey Drobyshev, den

We're about to add support for CPR migration to usb-host in the upcoming
commits.  Right now CPR is crashing with usb-host devices.  Let's add
migration blocker upfront, for both cpr-exec and cpr-transfer.  The
blocker is added unconditionally, so that we resuse CPR migration instead
of crashing on it.  That is for the sake of bisectability of the upcoming
commits.  It is going to be lifted (conditioned) in a following patch
once CPR support is added.  Also, blocker is added at the end of
usb_host_realize(), so we have to cleanup all the side effects done
earlier in .realize() in case migrate_add_blocker_modes() fails.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 83645fbe0ce..5c3da00d8eb 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -46,6 +46,7 @@
 #endif
 
 #include "qapi/error.h"
+#include "migration/blocker.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -103,6 +104,7 @@ struct USBHostDevice {
     char                             port[16];
 
     int                              hostfd;
+    Error                            *cpr_blocker;
     libusb_device                    *dev;
     libusb_device_handle             *dh;
     struct libusb_device_descriptor  ddesc;
@@ -1258,6 +1260,18 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
 
     s->exit.notify = usb_host_exit_notifier;
     qemu_add_exit_notifier(&s->exit);
+
+    error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+               DEVICE(s)->id ?: "(anonymous)");
+    if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+                                  MIG_MODE_CPR_TRANSFER,
+                                  MIG_MODE_CPR_EXEC, -1) < 0) {
+        qemu_remove_exit_notifier(&s->exit);
+        if (s->needs_autoscan) {
+            QTAILQ_REMOVE(&hostdevs, s, next);
+        }
+        usb_host_close(s);
+    }
 }
 
 static void usb_host_instance_init(Object *obj)
@@ -1279,6 +1293,7 @@ static void usb_host_unrealize(USBDevice *udev)
         QTAILQ_REMOVE(&hostdevs, s, next);
     }
     usb_host_close(s);
+    migrate_del_blocker(&s->cpr_blocker);
 }
 
 static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)

-- 
2.47.1


^ permalink raw reply	[relevance 13%]

* [PATCH QEMU HCI-8.0 v4 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800
    2026-09-24 16:41 31% ` [PATCH QEMU HCI-8.0 v4 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
  2026-09-24 16:41 13% ` [PATCH QEMU HCI-8.0 v4 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 26% ` Andrey Drobyshev
  2026-09-24 16:41 25% ` [PATCH QEMU HCI-8.0 v4 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
                   ` (6 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
  To: svt-core; +Cc: Andrey Drobyshev, den

In .realize(), save freshly opened host device FD into the CPR registry,
and then reuse it on CPR target.  Also, add deletion of that FD from
registry to usb_host_open() / usb_host_close() cleanup paths.

For this to work, we also need to skip the .post-load() code which
closes, detaches the device, and then rescans the host bus to reopen
matching hostdevs.  For the CPR-case migration we don't want any of that
as the hostdev FD stays preserved, and the guest shouldn't notice the
switchover.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 33 ++++++++++++++++++++++++++++++++-
 1 file changed, 32 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 5c3da00d8eb..b946b0ef350 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -47,6 +47,7 @@
 
 #include "qapi/error.h"
 #include "migration/blocker.h"
+#include "migration/cpr.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -947,6 +948,21 @@ static void usb_host_ep_update(USBHostDevice *s)
     libusb_free_config_descriptor(conf);
 }
 
+static char *usb_host_cpr_fd_name(USBHostDevice *s)
+{
+    /* Prefix the key so it can't clash with another CPR label */
+    return DEVICE(s)->id ? g_strdup_printf("usb-host/%s", DEVICE(s)->id)
+                         : NULL;
+}
+
+static void usb_host_cpr_delete_fd(USBHostDevice *s)
+{
+    g_autofree char *name = usb_host_cpr_fd_name(s);
+    if (name) {
+        cpr_delete_fd(name, 0);
+    }
+}
+
 static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
 {
     USBDevice *udev = USB_DEVICE(s);
@@ -1073,6 +1089,7 @@ fail:
         s->dev = NULL;
     }
     if (s->hostfd != -1) {
+        usb_host_cpr_delete_fd(s);
         close(s->hostfd);
         s->hostfd = -1;
     }
@@ -1134,6 +1151,7 @@ static int usb_host_close(USBHostDevice *s)
     s->dev = NULL;
 
     if (s->hostfd != -1) {
+        usb_host_cpr_delete_fd(s);
         close(s->hostfd);
         s->hostfd = -1;
     }
@@ -1220,9 +1238,12 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
 
 #if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
     if (s->hostdevice) {
+        g_autofree char *name = usb_host_cpr_fd_name(s);
         int fd;
+
         s->needs_autoscan = false;
-        fd = qemu_open(s->hostdevice, O_RDWR, errp);
+        fd = name ? cpr_open_fd(s->hostdevice, O_RDWR, name, 0, errp)
+                  : qemu_open(s->hostdevice, O_RDWR, errp);
         if (fd < 0) {
             return;
         }
@@ -1761,6 +1782,16 @@ static int usb_host_post_load(void *opaque, int version_id)
 {
     USBHostDevice *dev = opaque;
 
+    /*
+     * For CPR migration, device wasn't released/reset, and the guest
+     * is unaware of the switchover.  The detach/rescan performed in
+     * usb_host_post_load_bh() only exists for cross-host migration.
+     * Skip it for CPR.
+     */
+    if (cpr_is_incoming()) {
+        return 0;
+    }
+
     if (!dev->bh_postld) {
         dev->bh_postld = qemu_bh_new_guarded(usb_host_post_load_bh, dev,
                                              &DEVICE(dev)->mem_reentrancy_guard);

-- 
2.47.1


^ permalink raw reply	[relevance 26%]

* [PATCH QEMU HCI-8.0 v4 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800
                     ` (2 preceding siblings ...)
  2026-09-24 16:41 26% ` [PATCH QEMU HCI-8.0 v4 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 25% ` Andrey Drobyshev
  2026-09-24 16:41 23% ` [PATCH QEMU HCI-8.0 v4 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
                   ` (5 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
  To: svt-core; +Cc: Andrey Drobyshev, den

usb_host_abort_xfers() cancels every pending request and then pumps
libusb events until all of them are reaped, with a bounded wait.
Split the reap-wait loop into usb_host_reap_xfers(), so that the
following commit can reuse it to reap canceled transfers without going
through the abort path.  No functional change.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b946b0ef350..9b70d6deef9 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1096,15 +1096,11 @@ fail:
     return -1;
 }
 
-static void usb_host_abort_xfers(USBHostDevice *s)
+static void usb_host_reap_xfers(USBHostDevice *s)
 {
     USBHostRequest *r, *rtmp;
     int limit = 100;
 
-    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
-        usb_host_req_abort(r);
-    }
-
     while (QTAILQ_FIRST(&s->requests) != NULL) {
         struct timeval tv;
         memset(&tv, 0, sizeof(tv));
@@ -1126,6 +1122,16 @@ static void usb_host_abort_xfers(USBHostDevice *s)
     }
 }
 
+static void usb_host_abort_xfers(USBHostDevice *s)
+{
+    USBHostRequest *r, *rtmp;
+
+    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+        usb_host_req_abort(r);
+    }
+    usb_host_reap_xfers(s);
+}
+
 static int usb_host_close(USBHostDevice *s)
 {
     USBDevice *udev = USB_DEVICE(s);

-- 
2.47.1


^ permalink raw reply	[relevance 25%]

* [PATCH QEMU HCI-8.0 v4 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800
                     ` (3 preceding siblings ...)
  2026-09-24 16:41 25% ` [PATCH QEMU HCI-8.0 v4 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 23% ` Andrey Drobyshev
  2026-09-24 16:41 21% ` [PATCH QEMU HCI-8.0 v4 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
                   ` (4 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
  To: svt-core; +Cc: Andrey Drobyshev, den

URBs submitted by the old QEMU must not outlive it across the
switchover: the kernel would complete them into buffers of an address
space which is gone after exec, and the new libusb context would reap
URB pointers it never submitted.

So, in .pre_save() for the CPR modes, cancel and reap every pending
transfer, without completing its packet.  The packets stay ASYNC and
their TDs remain on the transfer rings, so the target re-executes them
when usb_xhci_post_load() kicks the endpoints - the same in-flight
replay as on regular live migration.  A partially executed transfer is
simply re-run from the start of the TD.

Isochronous URBs are not on the request list but on their own rings,
and freeing them while in-flight would leave them queued on the
preserved FD.  Cancel them so they are unlinked and reaped: an iso URB
in flight when the guest stops may never complete on its own.  The
target reconstructs the iso stream from the guest's transfer ring.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 94 insertions(+), 3 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 9b70d6deef9..4ea469de2d9 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -48,6 +48,7 @@
 #include "qapi/error.h"
 #include "migration/blocker.h"
 #include "migration/cpr.h"
+#include "migration/misc.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -403,7 +404,10 @@ static void usb_host_req_free(USBHostRequest *r)
 #ifdef CONFIG_WIN32
     request_count--;
 #endif
-    QTAILQ_REMOVE(&r->host->requests, r, next);
+    /* Skip if a timed-out reap already unlinked it (avoid a double remove) */
+    if (QTAILQ_IN_USE(r, next)) {
+        QTAILQ_REMOVE(&r->host->requests, r, next);
+    }
     libusb_free_transfer(r->xfer);
     g_free(r->buffer);
     g_free(r);
@@ -1096,7 +1100,7 @@ fail:
     return -1;
 }
 
-static void usb_host_reap_xfers(USBHostDevice *s)
+static bool usb_host_reap_xfers(USBHostDevice *s)
 {
     USBHostRequest *r, *rtmp;
     int limit = 100;
@@ -1117,9 +1121,10 @@ static void usb_host_reap_xfers(USBHostDevice *s)
             QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
                 QTAILQ_REMOVE(&s->requests, r, next);
             }
-            return;
+            return false;
         }
     }
+    return true;
 }
 
 static void usb_host_abort_xfers(USBHostDevice *s)
@@ -1132,6 +1137,76 @@ static void usb_host_abort_xfers(USBHostDevice *s)
     usb_host_reap_xfers(s);
 }
 
+static bool usb_host_iso_inflight(USBHostDevice *s)
+{
+    USBHostIsoRing *ring;
+
+    QTAILQ_FOREACH(ring, &s->isorings, next) {
+        if (!QTAILQ_EMPTY(&ring->inflight)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/*
+ * Quiesce the device for a CPR switchover.  Cancel and reap all URBs,
+ * so that no URB submitted by this process can outlive it.
+ */
+static int usb_host_cpr_drain_xfers(USBHostDevice *s)
+{
+    USBHostRequest *r, *rtmp;
+    USBHostIsoRing *ring;
+    USBHostIsoXfer *xfer;
+    int limit;
+
+    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+        if (r->p) {
+            /*
+             * Clear r->p so the reap callback early-outs on it: the
+             * callback must still run (it frees the request), but it
+             * must not complete the packet.
+             */
+            r->p = NULL;
+        }
+        libusb_cancel_transfer(r->xfer);
+    }
+
+    if (!usb_host_reap_xfers(s)) {
+        /*
+         * No URB can be left live on the FD across CPR migration.  If
+         * we can't reap them -> abort the migration.
+         */
+        return -1;
+    }
+
+    /*
+     * Iso URBs are on the rings, not s->requests.  Cancel them: one in
+     * flight when the guest stops may never complete on its own, and none
+     * may be left on the preserved fd.  The target replays from the ring.
+     */
+    QTAILQ_FOREACH(ring, &s->isorings, next) {
+        QTAILQ_FOREACH(xfer, &ring->inflight, next) {
+            libusb_cancel_transfer(xfer->xfer);
+        }
+    }
+
+    /*
+     * Cap the reap at 2x a full ring:
+     * iso_urb_count URBs x iso_urb_frames packets each
+     */
+    limit = 2 * s->iso_urb_count * s->iso_urb_frames;
+    while (usb_host_iso_inflight(s)) {
+        struct timeval tv = { .tv_usec = 1000 };
+        libusb_handle_events_timeout(ctx, &tv);
+        if (limit-- == 0) {
+            return -1;
+        }
+    }
+    usb_host_iso_free_all(s);
+    return 0;
+}
+
 static int usb_host_close(USBHostDevice *s)
 {
     USBDevice *udev = USB_DEVICE(s);
@@ -1807,10 +1882,26 @@ static int usb_host_post_load(void *opaque, int version_id)
     return 0;
 }
 
+static int usb_host_pre_save(void *opaque)
+{
+    USBHostDevice *s = opaque;
+    MigMode mode = migrate_mode();
+
+    /* URBs submitted by this process must not outlive CPR migration */
+    if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
+        s->dh) {
+        if (usb_host_cpr_drain_xfers(s) < 0) {
+            return -1;
+        }
+    }
+    return 0;
+}
+
 static const VMStateDescription vmstate_usb_host = {
     .name = "usb-host",
     .version_id = 1,
     .minimum_version_id = 1,
+    .pre_save = usb_host_pre_save,
     .post_load = usb_host_post_load,
     .fields = (const VMStateField[]) {
         VMSTATE_USB_DEVICE(parent_obj, USBHostDevice),

-- 
2.47.1


^ permalink raw reply	[relevance 23%]

* [PATCH QEMU HCI-8.0 v4 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
                     ` (4 preceding siblings ...)
  2026-09-24 16:41 23% ` [PATCH QEMU HCI-8.0 v4 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 21% ` Andrey Drobyshev
  2026-09-24 16:41 22% ` [PATCH QEMU HCI-8.0 v4 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
                   ` (3 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
  To: svt-core; +Cc: Andrey Drobyshev, den

The previous commit drains the in-flight URBs in .pre_save() and leaves
their packets ASYNC for the target to replay.  If migration fails, the
source resumes instead, with those packets still owned by the
controller and nothing left to complete them.

Remember the drained packets in a list, and re-issue them on the
preserved FD from a MIG_EVENT_PRECOPY_FAILED notifier.  Control
transfers can't be reconstructed from the packet alone, so complete
those as errors and let the guest driver retry.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 76 insertions(+), 2 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 4ea469de2d9..1e261554609 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -107,6 +107,9 @@ struct USBHostDevice {
 
     int                              hostfd;
     Error                            *cpr_blocker;
+    NotifierWithReturn               cpr_notifier;
+    GSList                           *cpr_inflight;
+    bool                             cpr_drained;
     libusb_device                    *dev;
     libusb_device_handle             *dh;
     struct libusb_device_descriptor  ddesc;
@@ -1150,8 +1153,13 @@ static bool usb_host_iso_inflight(USBHostDevice *s)
 }
 
 /*
- * Quiesce the device for a CPR switchover.  Cancel and reap all URBs,
- * so that no URB submitted by this process can outlive it.
+ * Quiesce the device for a CPR switchover.  Cancel and reap all URBs
+ * without completing their packets, so that no URB submitted by this
+ * process can outlive it.  CPR target re-executes them in
+ * usb_xhci_post_load() kicking the running endpoints.
+ *
+ * Keep the list of packets so that they can be re-issued on this side
+ * instead if MIG_EVENT_PRECOPY_FAILED fires.
  */
 static int usb_host_cpr_drain_xfers(USBHostDevice *s)
 {
@@ -1162,6 +1170,10 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
 
     QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
         if (r->p) {
+            if (r->p->state == USB_PACKET_ASYNC) {
+                /* In-flight req, in submission order, for the failure path */
+                s->cpr_inflight = g_slist_append(s->cpr_inflight, r->p);
+            }
             /*
              * Clear r->p so the reap callback early-outs on it: the
              * callback must still run (it frees the request), but it
@@ -1171,6 +1183,7 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
         }
         libusb_cancel_transfer(r->xfer);
     }
+    s->cpr_drained = true;
 
     if (!usb_host_reap_xfers(s)) {
         /*
@@ -1287,6 +1300,53 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
     return ret;
 }
 
+static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+
+/*
+ * A failed CPR migration resumes the source VM with the drained
+ * packets still owned by the host controller as in-flight.  Re-issue
+ * them on the preserved fd.  Control transfers cannot be reconstructed
+ * from the packet alone; complete them as errors and let the guest
+ * driver retry.
+ */
+static int usb_host_cpr_notifier(NotifierWithReturn *notifier,
+                                 MigrationEvent *e, Error **errp)
+{
+    USBHostDevice *s = container_of(notifier, USBHostDevice, cpr_notifier);
+    USBDevice *udev = USB_DEVICE(s);
+    GSList *it;
+    USBPacket *p;
+
+    if (e->type != MIG_EVENT_PRECOPY_FAILED || !s->cpr_drained) {
+        return 0;
+    }
+
+    for (it = s->cpr_inflight; it; it = it->next) {
+        p = it->data;
+        /* Replay from the start of the TD */
+        p->actual_length = 0;
+        if (p->ep->nr == 0) {
+            /*
+             * p->ep[0] is control endpoint.  Control transfers can't
+             * be reconstructed reliably, so complete them as errors
+             * and let the guest retry.
+             */
+            p->status = USB_RET_IOERROR;
+            usb_generic_async_ctrl_complete(udev, p);
+        } else {
+            usb_host_handle_data(udev, p);
+            if (p->status != USB_RET_ASYNC) {
+                /* Completed synchronously with error */
+                usb_packet_complete(udev, p);
+            }
+        }
+    }
+    g_slist_free(s->cpr_inflight);
+    s->cpr_inflight = NULL;
+    s->cpr_drained = false;
+    return 0;
+}
+
 static void usb_host_realize(USBDevice *udev, Error **errp)
 {
     USBHostDevice *s = USB_HOST_DEVICE(udev);
@@ -1363,6 +1423,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
     s->exit.notify = usb_host_exit_notifier;
     qemu_add_exit_notifier(&s->exit);
 
+#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
+    if (s->hostdevice && DEVICE(s)->id) {
+        migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
+                                     MIG_MODE_CPR_TRANSFER,
+                                     MIG_MODE_CPR_EXEC, -1);
+    }
+#endif
+
     error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
                DEVICE(s)->id ?: "(anonymous)");
     if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
@@ -1396,6 +1464,9 @@ static void usb_host_unrealize(USBDevice *udev)
     }
     usb_host_close(s);
     migrate_del_blocker(&s->cpr_blocker);
+    migration_remove_notifier(&s->cpr_notifier);
+    g_slist_free(s->cpr_inflight);
+    s->cpr_inflight = NULL;
 }
 
 static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1403,6 +1474,9 @@ static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
     USBHostDevice *s = USB_HOST_DEVICE(udev);
     USBHostRequest *r;
 
+    /* Packet is being cancelled -> it mustn't be re-issued on failed CPR */
+    s->cpr_inflight = g_slist_remove(s->cpr_inflight, p);
+
     if (p->combined) {
         usb_combined_packet_cancel(udev, p);
         return;

-- 
2.47.1


^ permalink raw reply	[relevance 21%]

* [PATCH QEMU HCI-8.0 v4 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800
                     ` (5 preceding siblings ...)
  2026-09-24 16:41 21% ` [PATCH QEMU HCI-8.0 v4 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 22% ` Andrey Drobyshev
  2026-09-24 16:41 26% ` [PATCH QEMU HCI-8.0 v4 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
                   ` (2 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
  To: svt-core; +Cc: Andrey Drobyshev, den

usb_host_open() reads the product string descriptor from the device
with a synchronous control transfer.  On the CPR target this runs while
the device is being handed over, and the transfer can block there,
hanging the incoming migration.

The string only fills the cosmetic product_desc, so skip the read when
we're the CPR target and fall back to the synthetic "host:bus.addr"
name.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 1e261554609..0108ccba72e 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1067,7 +1067,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
     udev->speed = speed_map[libusb_speed];
     usb_host_speed_compat(s);
 
-    if (s->ddesc.iProduct) {
+    if (s->ddesc.iProduct && !cpr_is_incoming()) {
         libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
                                            (unsigned char *)udev->product_desc,
                                            sizeof(udev->product_desc));

-- 
2.47.1


^ permalink raw reply	[relevance 22%]

* [PATCH QEMU HCI-8.0 v4 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800
                     ` (6 preceding siblings ...)
  2026-09-24 16:41 22% ` [PATCH QEMU HCI-8.0 v4 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 26% ` Andrey Drobyshev
  2026-09-24 16:41 11% ` [PATCH QEMU HCI-8.0 v4 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
  2026-09-24 16:41 20% ` [PATCH QEMU HCI-8.0 v4 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
  To: svt-core; +Cc: Andrey Drobyshev, den

Unlike cpr-exec, cpr-transfer keeps the source QEMU alive: it hands the
usbfs FD to the new QEMU over the migration channel and only exits once
the target is up.  While the handoff is in flight both processes hold the
same FD, and only one may reap URB events on it - if both do, one reaps
the other's in-flight completions and wedges an isochronous transfer that
cannot be retried.  So reaping is handed off along with the FD:

  * The target doesn't reap until it takes over: it stops events right
    after opening the FD in .realize() and restarts in .post_load().
  * The source stops reaping in .pre_save() and, since it stays alive,
    restarts from the MIG_EVENT_PRECOPY_FAILED notifier on failure.
  * On success the source's .exit() leaves the device untouched - no
    reset, interface release or host-driver rebind - and just closes its
    own copy of the FD.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 94 ++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 83 insertions(+), 11 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 0108ccba72e..57984e72e45 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -110,6 +110,7 @@ struct USBHostDevice {
     NotifierWithReturn               cpr_notifier;
     GSList                           *cpr_inflight;
     bool                             cpr_drained;
+    bool                             cpr_handed_off;
     libusb_device                    *dev;
     libusb_device_handle             *dh;
     struct libusb_device_descriptor  ddesc;
@@ -1230,16 +1231,19 @@ static int usb_host_close(USBHostDevice *s)
 
     trace_usb_host_close(s->bus_num, s->addr);
 
-    usb_host_abort_xfers(s);
-    usb_host_iso_free_all(s);
+    if (!s->cpr_handed_off) {
+        usb_host_abort_xfers(s);
+        usb_host_iso_free_all(s);
 
-    if (udev->attached) {
-        usb_device_detach(udev);
+        if (udev->attached) {
+            usb_device_detach(udev);
+        }
+
+        usb_host_release_interfaces(s);
+        libusb_reset_device(s->dh);
+        usb_host_attach_kernel(s);
     }
 
-    usb_host_release_interfaces(s);
-    libusb_reset_device(s->dh);
-    usb_host_attach_kernel(s);
     libusb_close(s->dh);
     s->dh = NULL;
     s->dev = NULL;
@@ -1274,10 +1278,17 @@ static void usb_host_exit_notifier(struct Notifier *n, void *data)
     USBHostDevice *s = container_of(n, USBHostDevice, exit);
 
     if (s->dh) {
-        usb_host_abort_xfers(s);
-        usb_host_release_interfaces(s);
-        libusb_reset_device(s->dh);
-        usb_host_attach_kernel(s);
+        /*
+         * Handed to new QEMU across cpr-transfer: do not reset, release
+         * interfaces or rebind the host driver underneath it; just close
+         * our handle.
+         */
+        if (!s->cpr_handed_off) {
+            usb_host_abort_xfers(s);
+            usb_host_release_interfaces(s);
+            libusb_reset_device(s->dh);
+            usb_host_attach_kernel(s);
+        }
         libusb_close(s->dh);
     }
 }
@@ -1301,6 +1312,8 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
 }
 
 static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+static void usb_host_cpr_stop_events(void);
+static void usb_host_cpr_restart_events(void);
 
 /*
  * A failed CPR migration resumes the source VM with the drained
@@ -1321,6 +1334,11 @@ static int usb_host_cpr_notifier(NotifierWithReturn *notifier,
         return 0;
     }
 
+    if (s->cpr_handed_off) {
+        usb_host_cpr_restart_events();
+        s->cpr_handed_off = false;
+    }
+
     for (it = s->cpr_inflight; it; it = it->next) {
         p = it->data;
         /* Replay from the start of the TD */
@@ -1393,6 +1411,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
             error_setg(errp, "failed to open host usb device %s", s->hostdevice);
             return;
         }
+        if (cpr_is_incoming()) {
+            /*
+             * The source still owns the shared fd until it hands off.  Don't
+             * reap URBs on it yet, or we would steal the source's in-flight
+             * completions; .post_load() restarts events once we take over.
+             */
+            usb_host_cpr_stop_events();
+        }
     } else
 #endif
     if (s->match.addr && s->match.bus_num &&
@@ -1944,6 +1970,7 @@ static int usb_host_post_load(void *opaque, int version_id)
      * Skip it for CPR.
      */
     if (cpr_is_incoming()) {
+        usb_host_cpr_restart_events();
         return 0;
     }
 
@@ -1956,6 +1983,47 @@ static int usb_host_post_load(void *opaque, int version_id)
     return 0;
 }
 
+#ifndef CONFIG_WIN32
+
+/*
+ * cpr-transfer: the usbfs fd is shared with the new QEMU via SCM_RIGHTS.
+ * Stop this (source) process from reaping URBs on it, so the new QEMU can
+ * drive the device without both processes racing on the same fd.  Restart
+ * on migration failure, when the source resumes.
+ */
+static void usb_host_cpr_stop_events(void)
+{
+    const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+    libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
+    if (poll) {
+        for (int i = 0; poll[i] != NULL; i++) {
+            usb_host_del_fd(poll[i]->fd, ctx);
+        }
+        free(poll);
+    }
+}
+
+static void usb_host_cpr_restart_events(void)
+{
+    const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+    libusb_set_pollfd_notifiers(ctx, usb_host_add_fd, usb_host_del_fd, ctx);
+    if (poll) {
+        for (int i = 0; poll[i] != NULL; i++) {
+            usb_host_add_fd(poll[i]->fd, poll[i]->events, ctx);
+        }
+        free(poll);
+    }
+}
+
+#else
+
+static void usb_host_cpr_stop_events(void) {}
+static void usb_host_cpr_restart_events(void) {}
+
+#endif
+
 static int usb_host_pre_save(void *opaque)
 {
     USBHostDevice *s = opaque;
@@ -1967,6 +2035,10 @@ static int usb_host_pre_save(void *opaque)
         if (usb_host_cpr_drain_xfers(s) < 0) {
             return -1;
         }
+        if (mode == MIG_MODE_CPR_TRANSFER) {
+            usb_host_cpr_stop_events();
+            s->cpr_handed_off = true;
+        }
     }
     return 0;
 }

-- 
2.47.1


^ permalink raw reply	[relevance 26%]

* [PATCH QEMU HCI-8.0 v4 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
                     ` (7 preceding siblings ...)
  2026-09-24 16:41 26% ` [PATCH QEMU HCI-8.0 v4 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 11% ` Andrey Drobyshev
  2026-09-24 16:41 20% ` [PATCH QEMU HCI-8.0 v4 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
  To: svt-core; +Cc: Andrey Drobyshev, den

On CPR incoming we skip the usb_host_open() rescan, so libusb only sets
up the endpoints from the device's default configuration.  If the guest
had enabled an endpoint by selecting a non-zero altsetting (e.g. an
isochronous streaming endpoint), that endpoint is now missing, and the
guest's transfers to it simply stall.

The altsetting itself is restored by vmload now, so let's call
usb_host_ep_update() on the CPR target to rebuild the endpoints out of
it.

The kernel claims are still there on the preserved FD, but libusb's own
handle knows nothing about them, so a later SET_INTERFACE would fail with
LIBUSB_ERROR_NOT_FOUND.  Let's re-claim the interfaces on the target as
well to bring libusb's view back in sync - on the preserved FD this is a
no-op at the usbfs level anyway.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 82 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 57984e72e45..eb349915eee 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -907,7 +907,7 @@ static void usb_host_ep_update(USBHostDevice *s)
         alt = udev->altsetting[intf->bInterfaceNumber];
 
         if (alt != 0) {
-            assert(alt < conf->interface[i].num_altsetting);
+            assert(alt >= 0 && alt < conf->interface[i].num_altsetting);
             intf = &conf->interface[i].altsetting[alt];
         }
 
@@ -1959,6 +1959,75 @@ static void usb_host_post_load_bh(void *opaque)
     usb_host_auto_check(NULL);
 }
 
+/*
+ * On the CPR target libusb knows nothing about the interface claims the
+ * preserved fd still holds, so a later SET_INTERFACE would fail with
+ * NOT_FOUND.  Re-claim them to bring libusb's view back in sync; on the
+ * preserved fd it's a no-op at the usbfs level anyway.
+ */
+static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
+{
+    USBDevice *udev = USB_DEVICE(s);
+    struct libusb_config_descriptor *conf;
+    int n, rc;
+
+    rc = libusb_get_active_config_descriptor(s->dev, &conf);
+    if (rc != 0) {
+        usb_host_libusb_error("libusb_get_active_config_descriptor", rc);
+        return -1;
+    }
+
+    n = MIN(conf->bNumInterfaces, USB_MAX_INTERFACES);
+    for (int i = 0; i < n; i++) {
+        rc = libusb_claim_interface(s->dh, i);
+        if (rc != 0) {
+            usb_host_libusb_error("libusb_claim_interface", rc);
+            libusb_free_config_descriptor(conf);
+            return -1;
+        }
+        s->ifs[i].claimed = true;
+    }
+
+    udev->ninterfaces = conf->bNumInterfaces;
+    udev->configuration = conf->bConfigurationValue;
+    libusb_free_config_descriptor(conf);
+
+    return 0;
+}
+
+/*
+ * During CPR the altsetting array is included in the migration stream.
+ * Reject a value this device can't honour, so that usb_host_ep_update()
+ * on the target never sees an out of range altsetting.  In this case
+ * we fail the incoming migration instead of aborting on it.
+ */
+static int usb_host_cpr_check_altsettings(USBHostDevice *s)
+{
+    USBDevice *udev = USB_DEVICE(s);
+    struct libusb_config_descriptor *conf;
+
+    if (libusb_get_active_config_descriptor(s->dev, &conf) != 0) {
+        error_report("usb-host: cannot read config descriptor on "
+                     "CPR incoming");
+        return -1;
+    }
+
+    for (int i = 0; i < conf->bNumInterfaces; i++) {
+        int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
+        int alt = udev->altsetting[ifnum];
+
+        if (alt < 0 || alt >= conf->interface[i].num_altsetting) {
+            error_report("usb-host: migrated altsetting %d out of range for "
+                         "interface %d", alt, ifnum);
+            libusb_free_config_descriptor(conf);
+            return -1;
+        }
+    }
+
+    libusb_free_config_descriptor(conf);
+    return 0;
+}
+
 static int usb_host_post_load(void *opaque, int version_id)
 {
     USBHostDevice *dev = opaque;
@@ -1970,6 +2039,18 @@ static int usb_host_post_load(void *opaque, int version_id)
      * Skip it for CPR.
      */
     if (cpr_is_incoming()) {
+        /*
+         * We kept the device open across CPR, so libusb still needs its
+         * interface claims and endpoints rebuilt here.  And we only start
+         * reaping now, once the source has handed the fd off to us.
+         */
+        if (usb_host_cpr_check_altsettings(dev) < 0) {
+            return -1;
+        }
+        if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
+            return -1;
+        }
+        usb_host_ep_update(dev);
         usb_host_cpr_restart_events();
         return 0;
     }

-- 
2.47.1


^ permalink raw reply	[relevance 11%]

* [PATCH QEMU HCI-8.0 v4 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800
                     ` (8 preceding siblings ...)
  2026-09-24 16:41 11% ` [PATCH QEMU HCI-8.0 v4 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 20% ` Andrey Drobyshev
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
  To: svt-core; +Cc: Andrey Drobyshev, den

A previous commit added an unconditional blocker for CPR migrations.
That is to refuse CPR gracefully instead of crashing and for the previous
commits to remain bisectable.

Now the restore path is complete: the FD is preserved, in-flight URBs
are drained and replayed, the device is handed off across cpr-transfer,
and the endpoints and isochronous streams are reconstructed on the
target.  So let's lift the CPR blocker for the devices which satisfy the
preconditions.

Still, the remaining preconditions for CPR to work are:

  * We must open host device FD ourselves, so that libusb owns the FD.
    This way we're able to preserve the FD during CPR;
  * Device ID must be provided as a unique stable key for CPR FD registry.

Use them to guard the blocker addition.  Apart from that, prohibit CPR on
WIN32 platform and on hosts with older libusb versions.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 50 +++++++++++++++++++++++++++++++++++---------------
 1 file changed, 35 insertions(+), 15 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index eb349915eee..8582da02567 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1449,24 +1449,44 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
     s->exit.notify = usb_host_exit_notifier;
     qemu_add_exit_notifier(&s->exit);
 
-#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
-    if (s->hostdevice && DEVICE(s)->id) {
-        migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
-                                     MIG_MODE_CPR_TRANSFER,
-                                     MIG_MODE_CPR_EXEC, -1);
+#ifdef CONFIG_WIN32
+    error_setg(&s->cpr_blocker,
+               "usb-host device %s does not support CPR: "
+               "WIN32 platform is not supported",
+               DEVICE(s)->id ?: "(anonymous)");
+#elif LIBUSB_API_VERSION < 0x01000107
+    error_setg(&s->cpr_blocker,
+               "usb-host device %s does not support CPR: "
+               "libusb API version 0x%08x is too old",
+               DEVICE(s)->id ?: "(anonymous)", (unsigned) LIBUSB_API_VERSION);
+#else
+    if (!(s->hostdevice && DEVICE(s)->id)) {
+        /*
+         * Host FD must be owned by libusb to support CPR migration.  That's
+         * equivalent to hostdevice= property being present.   Also device ID
+         * is required as stable key for CPR FD registry.
+         */
+        error_setg(&s->cpr_blocker,
+                   "usb-host device %s does not support CPR: "
+                   "hostdevice= and a device id are required",
+                   DEVICE(s)->id ?: "(anonymous)");
     }
 #endif
 
-    error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
-               DEVICE(s)->id ?: "(anonymous)");
-    if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
-                                  MIG_MODE_CPR_TRANSFER,
-                                  MIG_MODE_CPR_EXEC, -1) < 0) {
-        qemu_remove_exit_notifier(&s->exit);
-        if (s->needs_autoscan) {
-            QTAILQ_REMOVE(&hostdevs, s, next);
-        }
-        usb_host_close(s);
+    if (s->cpr_blocker) {
+        if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+                                      MIG_MODE_CPR_TRANSFER,
+                                      MIG_MODE_CPR_EXEC, -1) < 0) {
+            qemu_remove_exit_notifier(&s->exit);
+            if (s->needs_autoscan) {
+                QTAILQ_REMOVE(&hostdevs, s, next);
+            }
+            usb_host_close(s);
+        }
+    } else {
+        migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
+                                     MIG_MODE_CPR_TRANSFER,
+                                     MIG_MODE_CPR_EXEC, -1);
     }
 }
 

-- 
2.47.1


^ permalink raw reply	[relevance 20%]

* [Devel] [PATCH VZ10 v4 2/3] selftests/ve: Add namespace management helpers
  2026-09-25 12:47  3% [Devel] [PATCH VZ10 v4 0/3] Improvements for ve selftests Konstantin Khorenko
  2026-09-25 12:47  4% ` [Devel] [PATCH VZ10 v4 1/3] ve_namespace: Allow entering non-running VE namespace Konstantin Khorenko
@ 2026-09-25 12:47  6% ` Konstantin Khorenko
  2026-09-25 12:47  5% ` [Devel] [PATCH VZ10 v4 3/3] selftests/ve: Add more helpers Konstantin Khorenko
  2 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 12:47 UTC (permalink / raw)
  To: Vladimir Riabchun, Vasileios Almpanis; +Cc: devel

From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

When mount namespace is created in a child, it is destroyed
when the child dies. We may need to keep it for a while to check
how VE handles resources.

Instead of keeping a thread which lives in a new ve+mnt namespaces,
let's store open namespace file descriptors to enter them later
without keeping children alive.

https://virtuozzo.atlassian.net/browse/VSTOR-144504

Feature: ve selftests
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Reviewed-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
Reviewed-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/selftests/ve/ve_selftest.h | 130 +++++++++++++++++++++++++++++++
 1 file changed, 130 insertions(+)

diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index c53bf7900d208..bc9dc28a8d074 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -31,6 +31,136 @@
 #define VE_CONTROLLERS \
 	"+cpuset +cpu +cpuacct +io +memory +hugetlb +pids +rdma +misc +ve"
 
+struct namespace_ctx {
+	int ns_save_mask;	/* Mask of stored CLONE_NEW* namespaces. */
+	/* Namespace file descriptors. Name should match /proc/PID/ns/NAME */
+	int ve_fd;
+	int mnt_fd;
+};
+
+static inline void close_namespace_fds(struct namespace_ctx *ctx)
+{
+	if (ctx->ns_save_mask & CLONE_NEWVE)
+		close(ctx->ve_fd);
+	if (ctx->ns_save_mask & CLONE_NEWNS)
+		close(ctx->mnt_fd);
+
+	ctx->ns_save_mask = 0;
+}
+
+static inline int save_namespace_fds(struct namespace_ctx *ctx, int flags)
+{
+#define OPEN_NS(name, flag)                                                \
+do {                                                                       \
+	if (flags & (flag)) {                                              \
+		ctx->name##_fd = open("/proc/self/ns/" #name, O_RDONLY);   \
+		if (ctx->name##_fd < 0) {                                  \
+			fprintf(stderr, "%s: could not open fd %s: %s\n",  \
+				__func__, #name, strerror(errno));         \
+			goto err;                                          \
+		}                                                          \
+		ctx->ns_save_mask |= (flag);                               \
+	}                                                                  \
+} while (0)
+
+	ctx->ns_save_mask = 0;
+
+	OPEN_NS(ve, CLONE_NEWVE);
+	OPEN_NS(mnt, CLONE_NEWNS);
+
+#undef OPEN_NS
+	return 0;
+
+err:
+	close_namespace_fds(ctx);
+	return -1;
+}
+
+static inline int enter_namespaces(struct namespace_ctx *ctx, int flags)
+{
+#define JOIN_NS(name, flag)                                                 \
+do {                                                                        \
+	if (flags & (flag)) {                                               \
+		if (!(ctx->ns_save_mask & (flag))) {                        \
+			fprintf(stderr, "%s: could not switch ns %s:"       \
+				" no fd in context.\n", __func__, #name);   \
+			return -1;                                          \
+		}                                                           \
+		if (setns(ctx->name##_fd, (flag))) {                        \
+			fprintf(stderr, "%s: could not switch ns %s: %s\n", \
+				__func__, #name, strerror(errno));          \
+			return -1;                                          \
+		}                                                           \
+	}                                                                   \
+} while (0)
+
+	JOIN_NS(ve, CLONE_NEWVE);
+	JOIN_NS(mnt, CLONE_NEWNS);
+
+#undef JOIN_NS
+	return 0;
+}
+
+static inline int enter_cgroup(int cgv2_fd, int ctid);
+
+/*
+ * Create new namespaces and store them in ctx.
+ * CLONE_NEWVE is treated specially - before unshare corresponding
+ * CT cgroup is entered, after it root cgroup is entered (not the original one).
+ */
+static inline int prepare_namespaces(struct namespace_ctx *ctx, int flags,
+	int cgv2_fd, int ctid)
+{
+	struct namespace_ctx tmp_ctx;
+	int ret = 0;
+
+	ctx->ns_save_mask = 0;
+
+	if (save_namespace_fds(&tmp_ctx, flags) < 0)
+		return -1;
+
+	if (flags & CLONE_NEWVE) {
+		if (enter_cgroup(cgv2_fd, ctid) < 0) {
+			fprintf(stderr, "%s: failed to enter CT cgroup\n", __func__);
+			ret = -1;
+			goto restore_ns;
+		}
+	}
+
+	if (unshare(flags) < 0) {
+		fprintf(stderr, "%s: failed to unshare, flags %x: %s\n",
+			__func__, flags, strerror(errno));
+		ret = -1;
+		goto restore_cg;
+	}
+
+	if (save_namespace_fds(ctx, flags)) {
+		fprintf(stderr, "%s: failed to save new namespaces, flags %x\n",
+			__func__, flags);
+		ret = -1;
+	}
+
+restore_cg:
+	if (flags & CLONE_NEWVE) {
+		if (enter_cgroup(cgv2_fd, 0) < 0) {
+			fprintf(stderr, "%s: failed to restore root cgroup\n", __func__);
+			ret = -1;
+		}
+	}
+
+restore_ns:
+	if (enter_namespaces(&tmp_ctx, flags) < 0) {
+		fprintf(stderr, "%s: failed to restore old namespaces, flags %x\n",
+			__func__, flags);
+		ret = -1;
+	}
+
+	close_namespace_fds(&tmp_ctx);
+	if (ret < 0)
+		close_namespace_fds(ctx);
+	return ret;
+}
+
 static inline int write_file_at(int dirfd, const char *path, const char *val)
 {
 	int fd, ret;

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 6%]

* [Devel] [PATCH VZ10 v4 0/3] Improvements for ve selftests
@ 2026-09-25 12:47  3% Konstantin Khorenko
  2026-09-25 12:47  4% ` [Devel] [PATCH VZ10 v4 1/3] ve_namespace: Allow entering non-running VE namespace Konstantin Khorenko
                   ` (2 more replies)
  0 siblings, 3 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 12:47 UTC (permalink / raw)
  To: Vladimir Riabchun, Vasileios Almpanis; +Cc: devel

This change adds helpers for creating and preserving various
namespaces and running functions inside them.
This appeared useful while implementing ve mount accounting
selftests - it required keeping mount namespace alive and
doing different things at different moments of time.

To make setns work with CLONE_NEWVE without starting VE,
the first patch changes the -EINVAL return to a warning message.

https://virtuozzo.atlassian.net/browse/VSTOR-144504

Feature: ve selftests

Changes in v4:
- Moved the ve_namespace patch to the front, before the selftest
  helpers that rely on it.
- ve_namespace: the warning follows the "CT: %s:" format of other
  ve.c messages and prints the VE state and the offender's comm/pid;
  the commit message now explains why the warning is kept.
- namespace management helpers: prepare_namespaces() leaves ctx
  empty on any failure (mask zeroed on entry, saved fds closed on
  error), so unconditional cleanup in callers is safe; fixed a
  comment typo.
- more helpers: dropped a stray blank line in run_in_ve().
- Collected Reviewed-by tags.
- Link to v3: http://lore.virtuozzo.com/20260909-ve-selftest-improv-v3-0-4b41980d9a45@virtuozzo.com

Changes in v3:
- Patch 1: wrap flag -> (flag) in macros, undef them after use.
- Patch 2: Fix comments in run_in_ve function.
- Link to v2: http://lore.virtuozzo.com/20260909-ve-selftest-improv-v2-0-aad0a25d6569@virtuozzo.com

Changes in v2:
- Patch 1: fixed JOIN_NS order in enter_namespaces, added proper
  cleanup in prepare_namespaces.
- Patch 2: increased buf size in write_u64_at, added range check in
  read_s32_at, separated setup failures in run_in_ve from fn setup
  failures.
- Patch 3: Removed kselftests mention.
- Link to v1: http://lore.virtuozzo.com/20260907-ve-selftest-improv-v1-0-a17847abbd48@virtuozzo.com

Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
Vladimir Riabchun (3):
      ve_namespace: Allow entering non-running VE namespace
      selftests/ve: Add namespace management helpers
      selftests/ve: Add more helpers

 kernel/ve/ve_namespace.c                 |   4 +-
 tools/testing/selftests/ve/ve_selftest.h | 214 ++++++++++++++++++++++++++++++-
 2 files changed, 210 insertions(+), 8 deletions(-)
---
base-commit: f7ba712715849bef1a54e1f13fab79b094ccc0df
change-id: 20260907-ve-selftest-improv-2d043d021cae

Best regards,
--  
Konstantin Khorenko <khorenko@virtuozzo.com>

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH VZ10 v4 1/3] ve_namespace: Allow entering non-running VE namespace
  2026-09-25 12:47  3% [Devel] [PATCH VZ10 v4 0/3] Improvements for ve selftests Konstantin Khorenko
@ 2026-09-25 12:47  4% ` Konstantin Khorenko
  2026-09-25 12:47  6% ` [Devel] [PATCH VZ10 v4 2/3] selftests/ve: Add namespace management helpers Konstantin Khorenko
  2026-09-25 12:47  5% ` [Devel] [PATCH VZ10 v4 3/3] selftests/ve: Add more helpers Konstantin Khorenko
  2 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 12:47 UTC (permalink / raw)
  To: Vladimir Riabchun, Vasileios Almpanis; +Cc: devel

From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

The restriction doesn't bring a lot of value and makes tests more
complex: to enter a VE namespace, the VE has to be actually started.

In vzctl we have locking to prevent parallel creations.
If someone decides to skip locks and start one CT
in parallel - big mistake, but not ours.

Still, entering a non-running CT is a strange thing to do. Our
selftests do it on purpose, but if some of our userspace does the
same on a production node, that is a bug we want to know about and
fix, so leave a warning in place of the check.

https://virtuozzo.atlassian.net/browse/VSTOR-144504

Feature: ve selftests
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Reviewed-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
Reviewed-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 kernel/ve/ve_namespace.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/kernel/ve/ve_namespace.c b/kernel/ve/ve_namespace.c
index 3dbe0dde2645d..6b12bcd605ea4 100644
--- a/kernel/ve/ve_namespace.c
+++ b/kernel/ve/ve_namespace.c
@@ -227,7 +227,9 @@ static int ve_ns_install(struct nsset *nsset, struct ns_common *new)
 		return -EPERM;
 
 	if (!VE_IS_RUNNING(ve_ns->ve))
-		return -EINVAL;
+		pr_warn_once("CT: %s: setns to the VE namespace of a non-running CT (state %d) by %s (%d)\n",
+			     ve_name(ve_ns->ve), ve_ns->ve->state,
+			     current->comm, task_pid_nr(current));
 
 	/*
 	 * Don't need to put_ve_ns(nsset->ve_ns) here,

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH VZ10 v4 3/3] selftests/ve: Add more helpers
  2026-09-25 12:47  3% [Devel] [PATCH VZ10 v4 0/3] Improvements for ve selftests Konstantin Khorenko
  2026-09-25 12:47  4% ` [Devel] [PATCH VZ10 v4 1/3] ve_namespace: Allow entering non-running VE namespace Konstantin Khorenko
  2026-09-25 12:47  6% ` [Devel] [PATCH VZ10 v4 2/3] selftests/ve: Add namespace management helpers Konstantin Khorenko
@ 2026-09-25 12:47  5% ` Konstantin Khorenko
  2 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 12:47 UTC (permalink / raw)
  To: Vladimir Riabchun, Vasileios Almpanis; +Cc: devel

From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

Some more read/write helpers may be useful.

Also, add a helper to execute functions in child process with
switched namespaces and cgroup.

https://virtuozzo.atlassian.net/browse/VSTOR-144504

Feature: ve selftests
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Reviewed-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
Reviewed-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/selftests/ve/ve_selftest.h | 84 +++++++++++++++++++++++++++++---
 1 file changed, 77 insertions(+), 7 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index bc9dc28a8d074..7b45f91c1d561 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -175,6 +175,14 @@ static inline int write_file_at(int dirfd, const char *path, const char *val)
 	return (ret == (int)len) ? 0 : -1;
 }
 
+static inline int write_u64_at(int dirfd, const char *path, unsigned long long val)
+{
+	char s[32];
+
+	snprintf(s, sizeof(s), "%llu", val);
+	return write_file_at(dirfd, path, s);
+}
+
 static inline int read_file_at(int dirfd, const char *path, char *buf,
 			       size_t buflen)
 {
@@ -205,14 +213,9 @@ static inline int read_u64_at(int dirfd, const char *path,
 			      unsigned long long *out)
 {
 	char buf[32] = {0}, *end;
-	int fd, ret;
-
-	fd = openat(dirfd, path, O_RDONLY);
-	if (fd < 0)
-		return -1;
+	int ret;
 
-	ret = read(fd, buf, sizeof(buf) - 1);
-	close(fd);
+	ret = read_file_at(dirfd, path, buf, sizeof(buf));
 	if (ret <= 0)
 		return -1;
 
@@ -223,6 +226,25 @@ static inline int read_u64_at(int dirfd, const char *path,
 	return 0;
 }
 
+static inline int read_s32_at(int dirfd, const char *path,
+			      int *out)
+{
+	char buf[32] = {0}, *end;
+	long tmp;
+	int ret;
+
+	ret = read_file_at(dirfd, path, buf, sizeof(buf));
+	if (ret <= 0)
+		return -1;
+
+	errno = 0;
+	tmp = strtol(buf, &end, 10);
+	if (errno || end == buf || tmp < INT_MIN || tmp > INT_MAX)
+		return -1;
+	*out = tmp;
+	return 0;
+}
+
 /* A fresh, private cgroup2 mount, returned as an fd to use with *at() calls. */
 static inline int mount_cg2_fd(void)
 {
@@ -266,6 +288,54 @@ static inline int enter_cgroup(int cgv2_fd, int ctid)
 	return ret;
 }
 
+/*
+ * Run function in VE cgroup and given namespaces.
+ *
+ * Namespaces are provided via namespace_ctx and entered based on flags mask.
+ *
+ * Return values:
+ *  -  0 if function returns zero
+ *  - -1 if function returns negative value
+ *  -  1 if function returns positive value
+ *  -  2 if setup fails
+ */
+static inline int run_in_ve(int cgv2_fd, int ctid, struct namespace_ctx *ctx,
+		int flags, int (*fn)(void *), void *arg)
+{
+	int status;
+	pid_t pid;
+
+	pid = fork();
+	if (pid < 0) {
+		fprintf(stderr, "%s: fork failed\n", __func__);
+		return 2;
+	}
+	if (pid == 0) {
+		int ret;
+
+		if (enter_cgroup(cgv2_fd, ctid) < 0) {
+			fprintf(stderr, "%s: enter_cgroup failed\n", __func__);
+			_exit(254);
+		}
+		if (enter_namespaces(ctx, flags) < 0)
+			_exit(254);
+
+		ret = fn(arg);
+		if (ret < 0)
+			ret = 1;
+		else if (ret > 0)
+			ret = 255;
+		_exit(ret);
+	}
+	if (waitpid(pid, &status, 0) < 0 || !WIFEXITED(status) || WEXITSTATUS(status) == 254)
+		return 2;
+	if (WEXITSTATUS(status) == 255)
+		return 1;
+	if (WEXITSTATUS(status))
+		return -1;
+	return 0;
+}
+
 /*
  * Create a fresh VE cgroup at the first free id at or after @from and unhide
  * its ve.* control files. Return the new id, or -1.

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 5%]

* [Devel] [PATCH hci-8.1 v3 1/2] selftests: pstore: skip when no backend is registered
  @ 2026-09-25 13:18  4% ` Eva Kurchatova
  0 siblings, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-25 13:18 UTC (permalink / raw)
  To: khorenko; +Cc: devel

An unregistered pstore backend reads back "(null)", and every latter
check fails. This also contradicts the pstore_crash_test documentation.

pstore_crash_test has carried the comment, but never implemented it:

  # exit if pstore backend is not registered

Fix by skipping an unregistered backend in common_tests.

https://virtuozzo.atlassian.net/browse/VSTOR-142447
Feature: fix selftests

Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/pstore/common_tests     | 17 ++++++++++++-----
 .../selftests/pstore/pstore_post_reboot_tests   |  3 ---
 2 files changed, 12 insertions(+), 8 deletions(-)

diff --git a/tools/testing/selftests/pstore/common_tests b/tools/testing/selftests/pstore/common_tests
index 4509f0cc9c91..8f140fa572af 100755
--- a/tools/testing/selftests/pstore/common_tests
+++ b/tools/testing/selftests/pstore/common_tests
@@ -55,6 +55,8 @@ operate_files() { # tested value, files, operation
 
 # Parameters
 TEST_STRING_PATTERN="Testing pstore: uuid="
+# Kselftest framework requirement - SKIP code is 4.
+ksft_skip=4
 UUID=`cat /proc/sys/kernel/random/uuid`
 TOP_DIR=`absdir $0`
 LOG_DIR=$TOP_DIR/logs/`date +%Y%m%d-%H%M%S`_${UUID}/
@@ -74,10 +76,15 @@ prlog "=== Pstore unit tests (`basename $0`) ==="
 prlog "UUID="$UUID
 
 prlog -n "Checking pstore backend is registered ... "
-backend=`cat /sys/module/pstore/parameters/backend`
-show_result $?
+# No file at all where pstore is not loaded, "(null)" where none registered
+backend=`cat /sys/module/pstore/parameters/backend 2>/dev/null`
+if [ -z "$backend" ] || [ "$backend" = "(null)" ]; then
+    prlog "SKIP"
+    prlog -e "\tbackend=${backend:-<none>}"
+    prlog -e "\tcmdline=`cat /proc/cmdline`"
+    prlog "pstore backend is not registered, skipping"
+    exit $ksft_skip
+fi
+prlog "ok"
 prlog -e "\tbackend=${backend}"
 prlog -e "\tcmdline=`cat /proc/cmdline`"
-if [ $rc -ne 0 ]; then
-    exit 1
-fi
diff --git a/tools/testing/selftests/pstore/pstore_post_reboot_tests b/tools/testing/selftests/pstore/pstore_post_reboot_tests
index d6da5e86efbf..2edd48a87dda 100755
--- a/tools/testing/selftests/pstore/pstore_post_reboot_tests
+++ b/tools/testing/selftests/pstore/pstore_post_reboot_tests
@@ -7,9 +7,6 @@
 #  Written by Hiraku Toyooka <hiraku.toyooka.gu@hitachi.com>
 #
 
-# Kselftest framework requirement - SKIP code is 4.
-ksft_skip=4
-
 . ./common_tests
 
 if [ -e $REBOOT_FLAG  ]; then
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* [PATCH QEMU HCI-8.0 v5 01/13] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
  @ 2026-09-25 14:11 31% ` Andrey Drobyshev
  2026-09-25 14:11 13% ` [PATCH QEMU HCI-8.0 v5 02/13] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
                   ` (8 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

The FD passed into usb_host_open() for a wrapped device is only stored in
s->hostfd after libusb_wrap_sys_device() succeeds, and the failure path
doesn't close it.  if wrapping fails, or a later step of the open sequence
fails (e.g. usb_device_attach), the FD is leaked, as usb_host_close() only
runs for fully opened devices, and the realize error path doesn't clean it
up either.

Store the FD in s->hostfd before wrapping and close it on the failure
path.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b74670ae256..83645fbe0ce 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -954,6 +954,11 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
     int rc;
     Error *local_err = NULL;
 
+    /* Make sure failure path frees caller-owned FD */
+    if (!dev) {
+        s->hostfd = hostfd;
+    }
+
     if (s->bh_postld_pending) {
         return -1;
     }
@@ -978,7 +983,6 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
         if (rc != 0) {
             goto fail;
         }
-        s->hostfd  = hostfd;
         dev = libusb_get_device(s->dh);
         bus_num = libusb_get_bus_number(dev);
         addr = libusb_get_device_address(dev);
@@ -1066,6 +1070,10 @@ fail:
         s->dh = NULL;
         s->dev = NULL;
     }
+    if (s->hostfd != -1) {
+        close(s->hostfd);
+        s->hostfd = -1;
+    }
     return -1;
 }
 

-- 
2.47.1


^ permalink raw reply	[relevance 31%]

* [PATCH QEMU HCI-8.0 v5 03/13] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800
    2026-09-25 14:11 31% ` [PATCH QEMU HCI-8.0 v5 01/13] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
  2026-09-25 14:11 13% ` [PATCH QEMU HCI-8.0 v5 02/13] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 26% ` Andrey Drobyshev
  2026-09-25 14:11 25% ` [PATCH QEMU HCI-8.0 v5 04/13] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
                   ` (6 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

In .realize(), save freshly opened host device FD into the CPR registry,
and then reuse it on CPR target.  Also, add deletion of that FD from
registry to usb_host_open() / usb_host_close() cleanup paths.

For this to work, we also need to skip the .post-load() code which
closes, detaches the device, and then rescans the host bus to reopen
matching hostdevs.  For the CPR-case migration we don't want any of that
as the hostdev FD stays preserved, and the guest shouldn't notice the
switchover.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 33 ++++++++++++++++++++++++++++++++-
 1 file changed, 32 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 5c3da00d8eb..b946b0ef350 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -47,6 +47,7 @@
 
 #include "qapi/error.h"
 #include "migration/blocker.h"
+#include "migration/cpr.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -947,6 +948,21 @@ static void usb_host_ep_update(USBHostDevice *s)
     libusb_free_config_descriptor(conf);
 }
 
+static char *usb_host_cpr_fd_name(USBHostDevice *s)
+{
+    /* Prefix the key so it can't clash with another CPR label */
+    return DEVICE(s)->id ? g_strdup_printf("usb-host/%s", DEVICE(s)->id)
+                         : NULL;
+}
+
+static void usb_host_cpr_delete_fd(USBHostDevice *s)
+{
+    g_autofree char *name = usb_host_cpr_fd_name(s);
+    if (name) {
+        cpr_delete_fd(name, 0);
+    }
+}
+
 static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
 {
     USBDevice *udev = USB_DEVICE(s);
@@ -1073,6 +1089,7 @@ fail:
         s->dev = NULL;
     }
     if (s->hostfd != -1) {
+        usb_host_cpr_delete_fd(s);
         close(s->hostfd);
         s->hostfd = -1;
     }
@@ -1134,6 +1151,7 @@ static int usb_host_close(USBHostDevice *s)
     s->dev = NULL;
 
     if (s->hostfd != -1) {
+        usb_host_cpr_delete_fd(s);
         close(s->hostfd);
         s->hostfd = -1;
     }
@@ -1220,9 +1238,12 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
 
 #if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
     if (s->hostdevice) {
+        g_autofree char *name = usb_host_cpr_fd_name(s);
         int fd;
+
         s->needs_autoscan = false;
-        fd = qemu_open(s->hostdevice, O_RDWR, errp);
+        fd = name ? cpr_open_fd(s->hostdevice, O_RDWR, name, 0, errp)
+                  : qemu_open(s->hostdevice, O_RDWR, errp);
         if (fd < 0) {
             return;
         }
@@ -1761,6 +1782,16 @@ static int usb_host_post_load(void *opaque, int version_id)
 {
     USBHostDevice *dev = opaque;
 
+    /*
+     * For CPR migration, device wasn't released/reset, and the guest
+     * is unaware of the switchover.  The detach/rescan performed in
+     * usb_host_post_load_bh() only exists for cross-host migration.
+     * Skip it for CPR.
+     */
+    if (cpr_is_incoming()) {
+        return 0;
+    }
+
     if (!dev->bh_postld) {
         dev->bh_postld = qemu_bh_new_guarded(usb_host_post_load_bh, dev,
                                              &DEVICE(dev)->mem_reentrancy_guard);

-- 
2.47.1


^ permalink raw reply	[relevance 26%]

* [PATCH QEMU HCI-8.0 v5 02/13] usb-host: add migration blocker for CPR modes #VSTOR-137800
    2026-09-25 14:11 31% ` [PATCH QEMU HCI-8.0 v5 01/13] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 13% ` Andrey Drobyshev
  2026-09-25 14:11 26% ` [PATCH QEMU HCI-8.0 v5 03/13] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
                   ` (7 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

We're about to add support for CPR migration to usb-host in the upcoming
commits.  Right now CPR is crashing with usb-host devices.  Let's add
migration blocker upfront, for both cpr-exec and cpr-transfer.  The
blocker is added unconditionally, so that we resuse CPR migration instead
of crashing on it.  That is for the sake of bisectability of the upcoming
commits.  It is going to be lifted (conditioned) in a following patch
once CPR support is added.  Also, blocker is added at the end of
usb_host_realize(), so we have to cleanup all the side effects done
earlier in .realize() in case migrate_add_blocker_modes() fails.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 83645fbe0ce..5c3da00d8eb 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -46,6 +46,7 @@
 #endif
 
 #include "qapi/error.h"
+#include "migration/blocker.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -103,6 +104,7 @@ struct USBHostDevice {
     char                             port[16];
 
     int                              hostfd;
+    Error                            *cpr_blocker;
     libusb_device                    *dev;
     libusb_device_handle             *dh;
     struct libusb_device_descriptor  ddesc;
@@ -1258,6 +1260,18 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
 
     s->exit.notify = usb_host_exit_notifier;
     qemu_add_exit_notifier(&s->exit);
+
+    error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+               DEVICE(s)->id ?: "(anonymous)");
+    if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+                                  MIG_MODE_CPR_TRANSFER,
+                                  MIG_MODE_CPR_EXEC, -1) < 0) {
+        qemu_remove_exit_notifier(&s->exit);
+        if (s->needs_autoscan) {
+            QTAILQ_REMOVE(&hostdevs, s, next);
+        }
+        usb_host_close(s);
+    }
 }
 
 static void usb_host_instance_init(Object *obj)
@@ -1279,6 +1293,7 @@ static void usb_host_unrealize(USBDevice *udev)
         QTAILQ_REMOVE(&hostdevs, s, next);
     }
     usb_host_close(s);
+    migrate_del_blocker(&s->cpr_blocker);
 }
 
 static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)

-- 
2.47.1


^ permalink raw reply	[relevance 13%]

* [PATCH QEMU HCI-8.0 v5 05/13] usb-host: drain in-flight URBs across CPR #VSTOR-137800
                     ` (3 preceding siblings ...)
  2026-09-25 14:11 25% ` [PATCH QEMU HCI-8.0 v5 04/13] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 25% ` Andrey Drobyshev
  2026-09-25 14:11 21% ` [PATCH QEMU HCI-8.0 v5 07/13] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
                   ` (4 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

URBs submitted by the old QEMU must not outlive it across the
switchover: the kernel would complete them into buffers of an address
space which is gone after exec, and the new libusb context would reap
URB pointers it never submitted.

So, in .pre_save() for the CPR modes, cancel and reap every pending
transfer.  One that already completed in the kernel is delivered
normally - its data is real and the guest must see it.  One still in
flight is suppressed: its packet stays ASYNC and its TD stays on the
transfer ring, so the target re-executes it from the start of the TD
when usb_xhci_post_load() kicks the endpoints.  Completing a transfer
can make xHCI queue the next TD, so keep cancelling until the request
list stays empty.

If the device won't release its URBs within the bounded wait, error
the still in-flight packets out -> the resumed guest retries them --
leak the requests, since libusb still owns their transfers, and abort
the migration.

Isochronous URBs are not on the request list but on their own rings,
and freeing them while in-flight would leave them queued on the
preserved FD.  Cancel them so they are unlinked and reaped: an iso URB
in flight when the guest stops may never complete on its own.  The
target reconstructs the iso stream from the guest's transfer ring.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 158 +++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 135 insertions(+), 23 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 9b70d6deef9..ebad04ec305 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -48,6 +48,7 @@
 #include "qapi/error.h"
 #include "migration/blocker.h"
 #include "migration/cpr.h"
+#include "migration/misc.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -403,7 +404,10 @@ static void usb_host_req_free(USBHostRequest *r)
 #ifdef CONFIG_WIN32
     request_count--;
 #endif
-    QTAILQ_REMOVE(&r->host->requests, r, next);
+    /* Skip if a timed-out reap already unlinked it (avoid a double remove) */
+    if (QTAILQ_IN_USE(r, next)) {
+        QTAILQ_REMOVE(&r->host->requests, r, next);
+    }
     libusb_free_transfer(r->xfer);
     g_free(r->buffer);
     g_free(r);
@@ -430,6 +434,9 @@ static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
     if (r->p == NULL) {
         goto out; /* request was canceled */
     }
+    if (xfer->status == LIBUSB_TRANSFER_CANCELLED) {
+        goto out; /* CPR drain: the target will replay this packet */
+    }
 
     r->p->status = status_map[xfer->status];
     r->p->actual_length = xfer->actual_length;
@@ -480,6 +487,9 @@ static void LIBUSB_CALL usb_host_req_complete_data(struct libusb_transfer *xfer)
     if (r->p == NULL) {
         goto out; /* request was canceled */
     }
+    if (xfer->status == LIBUSB_TRANSFER_CANCELLED) {
+        goto out; /* CPR drain: the target will replay this packet */
+    }
 
     r->p->status = status_map[xfer->status];
     if (r->in && xfer->actual_length) {
@@ -500,22 +510,29 @@ out:
     }
 }
 
-static void usb_host_req_abort(USBHostRequest *r)
+/* Fail r's in-flight packet with an error status and detach it from r. */
+static void usb_host_req_complete_err(USBHostRequest *r, int status)
 {
-    USBHostDevice  *s = r->host;
-    bool inflight = (r->p && r->p->state == USB_PACKET_ASYNC);
+    USBHostDevice *s = r->host;
 
-    if (inflight) {
-        r->p->status = USB_RET_NODEV;
-        trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
-                                    r->p->status, r->p->actual_length);
-        if (r->p->ep->nr == 0) {
-            usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
-        } else {
-            usb_packet_complete(USB_DEVICE(s), r->p);
-        }
-        r->p = NULL;
+    if (!r->p || r->p->state != USB_PACKET_ASYNC) {
+        return;
+    }
+    r->p->status = status;
+    trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
+                                r->p->status, r->p->actual_length);
+    if (r->p->ep->nr == 0) {
+        usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
+    } else {
+        usb_packet_complete(USB_DEVICE(s), r->p);
+    }
+    r->p = NULL;
+}
 
+static void usb_host_req_abort(USBHostRequest *r)
+{
+    if (r->p && r->p->state == USB_PACKET_ASYNC) {
+        usb_host_req_complete_err(r, USB_RET_NODEV);
         libusb_cancel_transfer(r->xfer);
     }
 }
@@ -1096,9 +1113,18 @@ fail:
     return -1;
 }
 
-static void usb_host_reap_xfers(USBHostDevice *s)
+/* libusb still owns cancelled-but-unreaped transfers: unlink, don't free. */
+static void usb_host_drop_requests(USBHostDevice *s)
 {
     USBHostRequest *r, *rtmp;
+
+    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+        QTAILQ_REMOVE(&s->requests, r, next);
+    }
+}
+
+static void usb_host_reap_xfers(USBHostDevice *s)
+{
     int limit = 100;
 
     while (QTAILQ_FIRST(&s->requests) != NULL) {
@@ -1108,15 +1134,10 @@ static void usb_host_reap_xfers(USBHostDevice *s)
         libusb_handle_events_timeout(ctx, &tv);
         if (--limit == 0) {
             /*
-             * Don't wait forever for libusb calling the complete
-             * callback (which will unlink and free the request).
-             *
-             * Leaking memory here, to make sure libusb will not
-             * access memory which we have released already.
+             * Don't wait forever for libusb to call the complete callback
+             * (which unlinks and frees the request).
              */
-            QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
-                QTAILQ_REMOVE(&s->requests, r, next);
-            }
+            usb_host_drop_requests(s);
             return;
         }
     }
@@ -1132,6 +1153,81 @@ static void usb_host_abort_xfers(USBHostDevice *s)
     usb_host_reap_xfers(s);
 }
 
+static bool usb_host_iso_inflight(USBHostDevice *s)
+{
+    USBHostIsoRing *ring;
+
+    QTAILQ_FOREACH(ring, &s->isorings, next) {
+        if (!QTAILQ_EMPTY(&ring->inflight)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/*
+ * Quiesce the device for a CPR switchover.  Cancel and reap all URBs,
+ * so that no URB submitted by this process can outlive it.
+ */
+static int usb_host_cpr_drain_xfers(USBHostDevice *s)
+{
+    USBHostRequest *r, *rtmp;
+    USBHostIsoRing *ring;
+    USBHostIsoXfer *xfer;
+    int limit = 100;
+
+    /*
+     * Cancel and reap every URB.  Completing a reaped URB can make xHCI queue
+     * the next TD, so re-cancel whatever reappears until the queue stays empty.
+     */
+    while (!QTAILQ_EMPTY(&s->requests)) {
+        struct timeval tv = { .tv_usec = 2500 };
+
+        QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+            libusb_cancel_transfer(r->xfer);
+        }
+        libusb_handle_events_timeout(ctx, &tv);
+        if (--limit == 0) {
+            /*
+             * The device won't release these URBs.  Error the packets out so
+             * the resumed guest retries them, leak the requests (libusb still
+             * owns their transfers) and abort the migration.
+             */
+            QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+                usb_host_req_complete_err(r, USB_RET_IOERROR);
+            }
+            usb_host_drop_requests(s);
+            return -1;
+        }
+    }
+
+    /*
+     * Iso URBs are on the rings, not s->requests.  Cancel them: one in
+     * flight when the guest stops may never complete on its own, and none
+     * may be left on the preserved fd.  The target replays from the ring.
+     */
+    QTAILQ_FOREACH(ring, &s->isorings, next) {
+        QTAILQ_FOREACH(xfer, &ring->inflight, next) {
+            libusb_cancel_transfer(xfer->xfer);
+        }
+    }
+
+    /*
+     * Cap the reap at 2x a full ring:
+     * iso_urb_count URBs x iso_urb_frames packets each
+     */
+    limit = 2 * s->iso_urb_count * s->iso_urb_frames;
+    while (usb_host_iso_inflight(s)) {
+        struct timeval tv = { .tv_usec = 1000 };
+        libusb_handle_events_timeout(ctx, &tv);
+        if (limit-- == 0) {
+            return -1;
+        }
+    }
+    usb_host_iso_free_all(s);
+    return 0;
+}
+
 static int usb_host_close(USBHostDevice *s)
 {
     USBDevice *udev = USB_DEVICE(s);
@@ -1807,10 +1903,26 @@ static int usb_host_post_load(void *opaque, int version_id)
     return 0;
 }
 
+static int usb_host_pre_save(void *opaque)
+{
+    USBHostDevice *s = opaque;
+    MigMode mode = migrate_mode();
+
+    /* URBs submitted by this process must not outlive CPR migration */
+    if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
+        s->dh) {
+        if (usb_host_cpr_drain_xfers(s) < 0) {
+            return -1;
+        }
+    }
+    return 0;
+}
+
 static const VMStateDescription vmstate_usb_host = {
     .name = "usb-host",
     .version_id = 1,
     .minimum_version_id = 1,
+    .pre_save = usb_host_pre_save,
     .post_load = usb_host_post_load,
     .fields = (const VMStateField[]) {
         VMSTATE_USB_DEVICE(parent_obj, USBHostDevice),

-- 
2.47.1


^ permalink raw reply	[relevance 25%]

* [PATCH QEMU HCI-8.0 v5 04/13] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800
                     ` (2 preceding siblings ...)
  2026-09-25 14:11 26% ` [PATCH QEMU HCI-8.0 v5 03/13] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 25% ` Andrey Drobyshev
  2026-09-25 14:11 25% ` [PATCH QEMU HCI-8.0 v5 05/13] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
                   ` (5 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

usb_host_abort_xfers() cancels every pending request and then pumps
libusb events until all of them are reaped, with a bounded wait.  Split
that reap-wait loop out into usb_host_reap_xfers().  No functional
change.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b946b0ef350..9b70d6deef9 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1096,15 +1096,11 @@ fail:
     return -1;
 }
 
-static void usb_host_abort_xfers(USBHostDevice *s)
+static void usb_host_reap_xfers(USBHostDevice *s)
 {
     USBHostRequest *r, *rtmp;
     int limit = 100;
 
-    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
-        usb_host_req_abort(r);
-    }
-
     while (QTAILQ_FIRST(&s->requests) != NULL) {
         struct timeval tv;
         memset(&tv, 0, sizeof(tv));
@@ -1126,6 +1122,16 @@ static void usb_host_abort_xfers(USBHostDevice *s)
     }
 }
 
+static void usb_host_abort_xfers(USBHostDevice *s)
+{
+    USBHostRequest *r, *rtmp;
+
+    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+        usb_host_req_abort(r);
+    }
+    usb_host_reap_xfers(s);
+}
+
 static int usb_host_close(USBHostDevice *s)
 {
     USBDevice *udev = USB_DEVICE(s);

-- 
2.47.1


^ permalink raw reply	[relevance 25%]

* [PATCH QEMU HCI-8.0 v5 09/13] usb-host: hand off the device across cpr-transfer #VSTOR-137800
                     ` (6 preceding siblings ...)
  2026-09-25 14:11 22% ` [PATCH QEMU HCI-8.0 v5 08/13] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 26% ` Andrey Drobyshev
  2026-09-25 14:11 11% ` [PATCH QEMU HCI-8.0 v5 11/13] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
  2026-09-25 14:11 20% ` [PATCH QEMU HCI-8.0 v5 13/13] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

Unlike cpr-exec, cpr-transfer keeps the source QEMU alive: it hands the
usbfs FD to the new QEMU over the migration channel and only exits once
the target is up.  While the handoff is in flight both processes hold the
same FD, and only one may reap URB events on it - if both do, one reaps
the other's in-flight completions and wedges an isochronous transfer that
cannot be retried.  So reaping is handed off along with the FD:

  * The target doesn't reap until it takes over: it stops events right
    after opening the FD in .realize() and restarts in .post_load().
  * The source stops reaping in .pre_save() and, since it stays alive,
    restarts from the MIG_EVENT_PRECOPY_FAILED notifier on failure.
  * On success the source's .exit() leaves the device untouched - no
    reset, interface release or host-driver rebind - and just closes its
    own copy of the FD.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 94 ++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 83 insertions(+), 11 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 6b8c0cf0a82..742844ce426 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -110,6 +110,7 @@ struct USBHostDevice {
     NotifierWithReturn               cpr_notifier;
     GSList                           *cpr_inflight;
     bool                             cpr_drained;
+    bool                             cpr_handed_off;
     libusb_device                    *dev;
     libusb_device_handle             *dh;
     struct libusb_device_descriptor  ddesc;
@@ -1265,16 +1266,19 @@ static int usb_host_close(USBHostDevice *s)
 
     trace_usb_host_close(s->bus_num, s->addr);
 
-    usb_host_abort_xfers(s);
-    usb_host_iso_free_all(s);
+    if (!s->cpr_handed_off) {
+        usb_host_abort_xfers(s);
+        usb_host_iso_free_all(s);
 
-    if (udev->attached) {
-        usb_device_detach(udev);
+        if (udev->attached) {
+            usb_device_detach(udev);
+        }
+
+        usb_host_release_interfaces(s);
+        libusb_reset_device(s->dh);
+        usb_host_attach_kernel(s);
     }
 
-    usb_host_release_interfaces(s);
-    libusb_reset_device(s->dh);
-    usb_host_attach_kernel(s);
     libusb_close(s->dh);
     s->dh = NULL;
     s->dev = NULL;
@@ -1309,10 +1313,17 @@ static void usb_host_exit_notifier(struct Notifier *n, void *data)
     USBHostDevice *s = container_of(n, USBHostDevice, exit);
 
     if (s->dh) {
-        usb_host_abort_xfers(s);
-        usb_host_release_interfaces(s);
-        libusb_reset_device(s->dh);
-        usb_host_attach_kernel(s);
+        /*
+         * Handed to new QEMU across cpr-transfer: do not reset, release
+         * interfaces or rebind the host driver underneath it; just close
+         * our handle.
+         */
+        if (!s->cpr_handed_off) {
+            usb_host_abort_xfers(s);
+            usb_host_release_interfaces(s);
+            libusb_reset_device(s->dh);
+            usb_host_attach_kernel(s);
+        }
         libusb_close(s->dh);
     }
 }
@@ -1336,6 +1347,8 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
 }
 
 static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+static void usb_host_cpr_stop_events(void);
+static void usb_host_cpr_restart_events(void);
 
 /*
  * A failed CPR migration resumes the source VM with the drained
@@ -1356,6 +1369,11 @@ static int usb_host_cpr_notifier(NotifierWithReturn *notifier,
         return 0;
     }
 
+    if (s->cpr_handed_off) {
+        usb_host_cpr_restart_events();
+        s->cpr_handed_off = false;
+    }
+
     for (it = s->cpr_inflight; it; it = it->next) {
         p = it->data;
         /* Replay from the start of the TD */
@@ -1428,6 +1446,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
             error_setg(errp, "failed to open host usb device %s", s->hostdevice);
             return;
         }
+        if (cpr_is_incoming()) {
+            /*
+             * The source still owns the shared fd until it hands off.  Don't
+             * reap URBs on it yet, or we would steal the source's in-flight
+             * completions; .post_load() restarts events once we take over.
+             */
+            usb_host_cpr_stop_events();
+        }
     } else
 #endif
     if (s->match.addr && s->match.bus_num &&
@@ -1979,6 +2005,7 @@ static int usb_host_post_load(void *opaque, int version_id)
      * Skip it for CPR.
      */
     if (cpr_is_incoming()) {
+        usb_host_cpr_restart_events();
         return 0;
     }
 
@@ -1991,6 +2018,47 @@ static int usb_host_post_load(void *opaque, int version_id)
     return 0;
 }
 
+#ifndef CONFIG_WIN32
+
+/*
+ * cpr-transfer: the usbfs fd is shared with the new QEMU via SCM_RIGHTS.
+ * Stop this (source) process from reaping URBs on it, so the new QEMU can
+ * drive the device without both processes racing on the same fd.  Restart
+ * on migration failure, when the source resumes.
+ */
+static void usb_host_cpr_stop_events(void)
+{
+    const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+    libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
+    if (poll) {
+        for (int i = 0; poll[i] != NULL; i++) {
+            usb_host_del_fd(poll[i]->fd, ctx);
+        }
+        free(poll);
+    }
+}
+
+static void usb_host_cpr_restart_events(void)
+{
+    const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+    libusb_set_pollfd_notifiers(ctx, usb_host_add_fd, usb_host_del_fd, ctx);
+    if (poll) {
+        for (int i = 0; poll[i] != NULL; i++) {
+            usb_host_add_fd(poll[i]->fd, poll[i]->events, ctx);
+        }
+        free(poll);
+    }
+}
+
+#else
+
+static void usb_host_cpr_stop_events(void) {}
+static void usb_host_cpr_restart_events(void) {}
+
+#endif
+
 static int usb_host_pre_save(void *opaque)
 {
     USBHostDevice *s = opaque;
@@ -2002,6 +2070,10 @@ static int usb_host_pre_save(void *opaque)
         if (usb_host_cpr_drain_xfers(s) < 0) {
             return -1;
         }
+        if (mode == MIG_MODE_CPR_TRANSFER) {
+            usb_host_cpr_stop_events();
+            s->cpr_handed_off = true;
+        }
     }
     return 0;
 }

-- 
2.47.1


^ permalink raw reply	[relevance 26%]

* [PATCH QEMU HCI-8.0 v5 08/13] usb-host: skip product-string read on CPR incoming #VSTOR-137800
                     ` (5 preceding siblings ...)
  2026-09-25 14:11 21% ` [PATCH QEMU HCI-8.0 v5 07/13] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 22% ` Andrey Drobyshev
  2026-09-25 14:11 26% ` [PATCH QEMU HCI-8.0 v5 09/13] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
                   ` (2 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

usb_host_open() reads the product string descriptor from the device
with a synchronous control transfer.  On the CPR target this runs while
the device is being handed over, and the transfer can block there,
hanging the incoming migration.

The string only fills the cosmetic product_desc, so skip the read when
we're the CPR target and fall back to the synthetic "host:bus.addr"
name.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index a14a3748838..6b8c0cf0a82 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1097,7 +1097,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
     udev->speed = speed_map[libusb_speed];
     usb_host_speed_compat(s);
 
-    if (s->ddesc.iProduct) {
+    if (s->ddesc.iProduct && !cpr_is_incoming()) {
         libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
                                            (unsigned char *)udev->product_desc,
                                            sizeof(udev->product_desc));

-- 
2.47.1


^ permalink raw reply	[relevance 22%]

* [PATCH QEMU HCI-8.0 v5 07/13] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
                     ` (4 preceding siblings ...)
  2026-09-25 14:11 25% ` [PATCH QEMU HCI-8.0 v5 05/13] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 21% ` Andrey Drobyshev
  2026-09-25 14:11 22% ` [PATCH QEMU HCI-8.0 v5 08/13] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
                   ` (3 subsequent siblings)
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

The previous commit drains the in-flight URBs in .pre_save() and leaves
their packets ASYNC for the target to replay.  If migration fails, the
source resumes instead, with those packets still owned by the
controller and nothing left to complete them.

Remember the drained packets in a list, and re-issue them on the
preserved FD from a MIG_EVENT_PRECOPY_FAILED notifier.  Control
transfers can't be reconstructed from the packet alone, so complete
those as errors and let the guest driver retry.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 96 +++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 92 insertions(+), 4 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 9e9ee7d305f..a14a3748838 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -107,6 +107,9 @@ struct USBHostDevice {
 
     int                              hostfd;
     Error                            *cpr_blocker;
+    NotifierWithReturn               cpr_notifier;
+    GSList                           *cpr_inflight;
+    bool                             cpr_drained;
     libusb_device                    *dev;
     libusb_device_handle             *dh;
     struct libusb_device_descriptor  ddesc;
@@ -425,6 +428,23 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
     return NULL;
 }
 
+/*
+ * A CANCELLED reap with r->p still set -> the CPR drain cancelled it (every
+ * other path clears r->p first).  Don't complete it, let the target replay it.
+ * Returns true if the caller should just free the request.
+ */
+static bool usb_host_req_cpr_replay(USBHostRequest *r,
+                                    struct libusb_transfer *xfer)
+{
+    if (xfer->status != LIBUSB_TRANSFER_CANCELLED) {
+        return false;
+    }
+    if (r->p->state == USB_PACKET_ASYNC) {
+        r->host->cpr_inflight = g_slist_append(r->host->cpr_inflight, r->p);
+    }
+    return true;
+}
+
 static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
 {
     USBHostRequest *r = xfer->user_data;
@@ -434,7 +454,7 @@ static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
     if (r->p == NULL) {
         goto out; /* request was canceled */
     }
-    if (xfer->status == LIBUSB_TRANSFER_CANCELLED) {
+    if (usb_host_req_cpr_replay(r, xfer)) {
         goto out; /* CPR drain: the target will replay this packet */
     }
 
@@ -487,7 +507,7 @@ static void LIBUSB_CALL usb_host_req_complete_data(struct libusb_transfer *xfer)
     if (r->p == NULL) {
         goto out; /* request was canceled */
     }
-    if (xfer->status == LIBUSB_TRANSFER_CANCELLED) {
+    if (usb_host_req_cpr_replay(r, xfer)) {
         goto out; /* CPR drain: the target will replay this packet */
     }
 
@@ -1166,8 +1186,13 @@ static bool usb_host_iso_inflight(USBHostDevice *s)
 }
 
 /*
- * Quiesce the device for a CPR switchover.  Cancel and reap all URBs,
- * so that no URB submitted by this process can outlive it.
+ * Quiesce the device for a CPR switchover.  Cancel and reap all URBs
+ * without completing their packets, so that no URB submitted by this
+ * process can outlive it.  CPR target re-executes them in
+ * usb_xhci_post_load() kicking the running endpoints.
+ *
+ * Keep the list of packets so that they can be re-issued on this side
+ * instead if MIG_EVENT_PRECOPY_FAILED fires.
  */
 static int usb_host_cpr_drain_xfers(USBHostDevice *s)
 {
@@ -1176,6 +1201,8 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
     USBHostIsoXfer *xfer;
     int limit = 100;
 
+    s->cpr_drained = true;
+
     /*
      * Cancel and reap every URB.  Completing a reaped URB can make xHCI queue
      * the next TD, so re-cancel whatever reappears until the queue stays empty.
@@ -1308,6 +1335,53 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
     return ret;
 }
 
+static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+
+/*
+ * A failed CPR migration resumes the source VM with the drained
+ * packets still owned by the host controller as in-flight.  Re-issue
+ * them on the preserved fd.  Control transfers cannot be reconstructed
+ * from the packet alone; complete them as errors and let the guest
+ * driver retry.
+ */
+static int usb_host_cpr_notifier(NotifierWithReturn *notifier,
+                                 MigrationEvent *e, Error **errp)
+{
+    USBHostDevice *s = container_of(notifier, USBHostDevice, cpr_notifier);
+    USBDevice *udev = USB_DEVICE(s);
+    GSList *it;
+    USBPacket *p;
+
+    if (e->type != MIG_EVENT_PRECOPY_FAILED || !s->cpr_drained) {
+        return 0;
+    }
+
+    for (it = s->cpr_inflight; it; it = it->next) {
+        p = it->data;
+        /* Replay from the start of the TD */
+        p->actual_length = 0;
+        if (p->ep->nr == 0) {
+            /*
+             * p->ep[0] is control endpoint.  Control transfers can't
+             * be reconstructed reliably, so complete them as errors
+             * and let the guest retry.
+             */
+            p->status = USB_RET_IOERROR;
+            usb_generic_async_ctrl_complete(udev, p);
+        } else {
+            usb_host_handle_data(udev, p);
+            if (p->status != USB_RET_ASYNC) {
+                /* Completed synchronously with error */
+                usb_packet_complete(udev, p);
+            }
+        }
+    }
+    g_slist_free(s->cpr_inflight);
+    s->cpr_inflight = NULL;
+    s->cpr_drained = false;
+    return 0;
+}
+
 static void usb_host_realize(USBDevice *udev, Error **errp)
 {
     USBHostDevice *s = USB_HOST_DEVICE(udev);
@@ -1384,6 +1458,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
     s->exit.notify = usb_host_exit_notifier;
     qemu_add_exit_notifier(&s->exit);
 
+#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
+    if (s->hostdevice && DEVICE(s)->id) {
+        migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
+                                     MIG_MODE_CPR_TRANSFER,
+                                     MIG_MODE_CPR_EXEC, -1);
+    }
+#endif
+
     error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
                DEVICE(s)->id ?: "(anonymous)");
     if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
@@ -1417,6 +1499,9 @@ static void usb_host_unrealize(USBDevice *udev)
     }
     usb_host_close(s);
     migrate_del_blocker(&s->cpr_blocker);
+    migration_remove_notifier(&s->cpr_notifier);
+    g_slist_free(s->cpr_inflight);
+    s->cpr_inflight = NULL;
 }
 
 static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1424,6 +1509,9 @@ static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
     USBHostDevice *s = USB_HOST_DEVICE(udev);
     USBHostRequest *r;
 
+    /* Packet is being cancelled -> it mustn't be re-issued on failed CPR */
+    s->cpr_inflight = g_slist_remove(s->cpr_inflight, p);
+
     if (p->combined) {
         usb_combined_packet_cancel(udev, p);
         return;

-- 
2.47.1


^ permalink raw reply	[relevance 21%]

* [PATCH QEMU HCI-8.0 v5 13/13] usb-host: make CPR migration blocker conditional #VSTOR-137800
                     ` (8 preceding siblings ...)
  2026-09-25 14:11 11% ` [PATCH QEMU HCI-8.0 v5 11/13] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 20% ` Andrey Drobyshev
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

A previous commit added an unconditional blocker for CPR migrations.
That is to refuse CPR gracefully instead of crashing and for the previous
commits to remain bisectable.

Now the restore path is complete: the FD is preserved, in-flight URBs
are drained and replayed, the device is handed off across cpr-transfer,
and the endpoints and isochronous streams are reconstructed on the
target.  So let's lift the CPR blocker for the devices which satisfy the
preconditions.

Still, the remaining preconditions for CPR to work are:

  * We must open host device FD ourselves, so that libusb owns the FD.
    This way we're able to preserve the FD during CPR;
  * Device ID must be provided as a unique stable key for CPR FD registry.

Use them to guard the blocker addition.  Apart from that, prohibit CPR on
WIN32 platform and on hosts with older libusb versions.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 50 +++++++++++++++++++++++++++++++++++---------------
 1 file changed, 35 insertions(+), 15 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 8078de01260..0623d6a7183 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1484,24 +1484,44 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
     s->exit.notify = usb_host_exit_notifier;
     qemu_add_exit_notifier(&s->exit);
 
-#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
-    if (s->hostdevice && DEVICE(s)->id) {
-        migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
-                                     MIG_MODE_CPR_TRANSFER,
-                                     MIG_MODE_CPR_EXEC, -1);
+#ifdef CONFIG_WIN32
+    error_setg(&s->cpr_blocker,
+               "usb-host device %s does not support CPR: "
+               "WIN32 platform is not supported",
+               DEVICE(s)->id ?: "(anonymous)");
+#elif LIBUSB_API_VERSION < 0x01000107
+    error_setg(&s->cpr_blocker,
+               "usb-host device %s does not support CPR: "
+               "libusb API version 0x%08x is too old",
+               DEVICE(s)->id ?: "(anonymous)", (unsigned) LIBUSB_API_VERSION);
+#else
+    if (!(s->hostdevice && DEVICE(s)->id)) {
+        /*
+         * Host FD must be owned by libusb to support CPR migration.  That's
+         * equivalent to hostdevice= property being present.   Also device ID
+         * is required as stable key for CPR FD registry.
+         */
+        error_setg(&s->cpr_blocker,
+                   "usb-host device %s does not support CPR: "
+                   "hostdevice= and a device id are required",
+                   DEVICE(s)->id ?: "(anonymous)");
     }
 #endif
 
-    error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
-               DEVICE(s)->id ?: "(anonymous)");
-    if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
-                                  MIG_MODE_CPR_TRANSFER,
-                                  MIG_MODE_CPR_EXEC, -1) < 0) {
-        qemu_remove_exit_notifier(&s->exit);
-        if (s->needs_autoscan) {
-            QTAILQ_REMOVE(&hostdevs, s, next);
-        }
-        usb_host_close(s);
+    if (s->cpr_blocker) {
+        if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+                                      MIG_MODE_CPR_TRANSFER,
+                                      MIG_MODE_CPR_EXEC, -1) < 0) {
+            qemu_remove_exit_notifier(&s->exit);
+            if (s->needs_autoscan) {
+                QTAILQ_REMOVE(&hostdevs, s, next);
+            }
+            usb_host_close(s);
+        }
+    } else {
+        migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
+                                     MIG_MODE_CPR_TRANSFER,
+                                     MIG_MODE_CPR_EXEC, -1);
     }
 }
 

-- 
2.47.1


^ permalink raw reply	[relevance 20%]

* [PATCH QEMU HCI-8.0 v5 11/13] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
                     ` (7 preceding siblings ...)
  2026-09-25 14:11 26% ` [PATCH QEMU HCI-8.0 v5 09/13] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 11% ` Andrey Drobyshev
  2026-09-25 14:11 20% ` [PATCH QEMU HCI-8.0 v5 13/13] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
  9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

On CPR incoming we skip the usb_host_open() rescan, so libusb only sets
up the endpoints from the device's default configuration.  If the guest
had enabled an endpoint by selecting a non-zero altsetting (e.g. an
isochronous streaming endpoint), that endpoint is now missing, and the
guest's transfers to it simply stall.

The altsetting itself is restored by vmload now, so let's call
usb_host_ep_update() on the CPR target to rebuild the endpoints out of
it.

The kernel claims are still there on the preserved FD, but libusb's own
handle knows nothing about them, so a later SET_INTERFACE would fail with
LIBUSB_ERROR_NOT_FOUND.  Let's re-claim the interfaces on the target as
well to bring libusb's view back in sync - on the preserved FD this is a
no-op at the usbfs level anyway.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 82 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 742844ce426..8078de01260 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -937,7 +937,7 @@ static void usb_host_ep_update(USBHostDevice *s)
         alt = udev->altsetting[intf->bInterfaceNumber];
 
         if (alt != 0) {
-            assert(alt < conf->interface[i].num_altsetting);
+            assert(alt >= 0 && alt < conf->interface[i].num_altsetting);
             intf = &conf->interface[i].altsetting[alt];
         }
 
@@ -1994,6 +1994,75 @@ static void usb_host_post_load_bh(void *opaque)
     usb_host_auto_check(NULL);
 }
 
+/*
+ * On the CPR target libusb knows nothing about the interface claims the
+ * preserved fd still holds, so a later SET_INTERFACE would fail with
+ * NOT_FOUND.  Re-claim them to bring libusb's view back in sync; on the
+ * preserved fd it's a no-op at the usbfs level anyway.
+ */
+static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
+{
+    USBDevice *udev = USB_DEVICE(s);
+    struct libusb_config_descriptor *conf;
+    int n, rc;
+
+    rc = libusb_get_active_config_descriptor(s->dev, &conf);
+    if (rc != 0) {
+        usb_host_libusb_error("libusb_get_active_config_descriptor", rc);
+        return -1;
+    }
+
+    n = MIN(conf->bNumInterfaces, USB_MAX_INTERFACES);
+    for (int i = 0; i < n; i++) {
+        rc = libusb_claim_interface(s->dh, i);
+        if (rc != 0) {
+            usb_host_libusb_error("libusb_claim_interface", rc);
+            libusb_free_config_descriptor(conf);
+            return -1;
+        }
+        s->ifs[i].claimed = true;
+    }
+
+    udev->ninterfaces = conf->bNumInterfaces;
+    udev->configuration = conf->bConfigurationValue;
+    libusb_free_config_descriptor(conf);
+
+    return 0;
+}
+
+/*
+ * During CPR the altsetting array is included in the migration stream.
+ * Reject a value this device can't honour, so that usb_host_ep_update()
+ * on the target never sees an out of range altsetting.  In this case
+ * we fail the incoming migration instead of aborting on it.
+ */
+static int usb_host_cpr_check_altsettings(USBHostDevice *s)
+{
+    USBDevice *udev = USB_DEVICE(s);
+    struct libusb_config_descriptor *conf;
+
+    if (libusb_get_active_config_descriptor(s->dev, &conf) != 0) {
+        error_report("usb-host: cannot read config descriptor on "
+                     "CPR incoming");
+        return -1;
+    }
+
+    for (int i = 0; i < conf->bNumInterfaces; i++) {
+        int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
+        int alt = udev->altsetting[ifnum];
+
+        if (alt < 0 || alt >= conf->interface[i].num_altsetting) {
+            error_report("usb-host: migrated altsetting %d out of range for "
+                         "interface %d", alt, ifnum);
+            libusb_free_config_descriptor(conf);
+            return -1;
+        }
+    }
+
+    libusb_free_config_descriptor(conf);
+    return 0;
+}
+
 static int usb_host_post_load(void *opaque, int version_id)
 {
     USBHostDevice *dev = opaque;
@@ -2005,6 +2074,18 @@ static int usb_host_post_load(void *opaque, int version_id)
      * Skip it for CPR.
      */
     if (cpr_is_incoming()) {
+        /*
+         * We kept the device open across CPR, so libusb still needs its
+         * interface claims and endpoints rebuilt here.  And we only start
+         * reaping now, once the source has handed the fd off to us.
+         */
+        if (usb_host_cpr_check_altsettings(dev) < 0) {
+            return -1;
+        }
+        if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
+            return -1;
+        }
+        usb_host_ep_update(dev);
         usb_host_cpr_restart_events();
         return 0;
     }

-- 
2.47.1


^ permalink raw reply	[relevance 11%]

* Re: [Devel] [PATCH VZ10 v9 6/8] ve: Introduce per-VE failcount
  @ 2026-09-25 16:02  0%   ` Konstantin Khorenko
  2026-09-25 16:08  4%   ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:02 UTC (permalink / raw)
  To: Vladimir Riabchun, devel

i will drop neighbor related failcounter from this patch as it increases the amount of printks inside irq

--
Best regards,

Konstantin Khorenko,
Virtuozzo Linux Kernel Team

On 9/15/26 19:22, Vladimir Riabchun wrote:
> It may be useful to have a history of resource limit hits for every VE,
> this may simplify debugging and provide some information about the
> resources usage.
> 
> This information is provided by ve.failcount file, any write to it
> resets all failcounts.
> 
> To add a new failcounter we need to create a new atomic_t field
> name_failcount in ve structure and add a new VE_FC_ENTRY in
> ve_failcounts array.
> 
> One change, unrelated to failcounts: aio fields are now initialized
> in ve0.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-135520
> 
> Feature: per-ve failcounters
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> ---
>  fs/aio.c                 |  1 +
>  fs/namespace.c           |  2 ++
>  include/linux/ve.h       | 16 +++++++++
>  kernel/bpf/syscall.c     |  1 +
>  kernel/ve/ve.c           | 70 ++++++++++++++++++++++++++++++++++++++++
>  net/core/dev.c           | 10 ++----
>  net/core/neighbour.c     |  3 +-
>  net/core/net_namespace.c |  4 ++-
>  8 files changed, 96 insertions(+), 11 deletions(-)
> 
> diff --git a/fs/aio.c b/fs/aio.c
> index cb63416af135..4ae06bcd47f2 100644
> --- a/fs/aio.c
> +++ b/fs/aio.c
> @@ -814,6 +814,7 @@ static struct kioctx *ioctx_alloc(unsigned nr_events)
>  	spin_lock(&ve->aio_nr_lock);
>  	if (ve->aio_nr + ctx->max_reqs > ve->aio_max_nr ||
>  	    ve->aio_nr + ctx->max_reqs < ve->aio_nr) {
> +		ve_failcount_inc(ve, aio);
>  		spin_unlock(&ve->aio_nr_lock);
>  		err = -EAGAIN;
>  		goto err_ctx;
> diff --git a/fs/namespace.c b/fs/namespace.c
> index 1d6e66cd7174..ea01664ff5f2 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -3380,6 +3380,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
>  
>  	if (ret)
>  		get_ve(ve);
> +	else
> +		ve_failcount_inc(ve, mnt);
>  	return ret;
>  }
>  
> diff --git a/include/linux/ve.h b/include/linux/ve.h
> index fabc608e31f5..065129b42768 100644
> --- a/include/linux/ve.h
> +++ b/include/linux/ve.h
> @@ -80,12 +80,15 @@ struct ve_struct {
>  	struct kmapset_key	proc_perms_key;
>  
>  	atomic_t		netns_avail_nr;
> +	atomic_t		netns_failcount;
>  	int			netns_max_nr;
>  
>  	atomic_t		netif_avail_nr;
> +	atomic_t		netif_failcount;
>  	int			netif_max_nr;
>  
>  	atomic_t		bpf_prog_avail_nr;
> +	atomic_t		bpf_prog_failcount;
>  	int			bpf_prog_max_nr;
>  
>  	atomic64_t		_uevent_seqnum;
> @@ -94,6 +97,7 @@ struct ve_struct {
>  
>  	atomic_t		arp_neigh_nr;
>  	atomic_t		nd_neigh_nr;
> +	atomic_t		neigh_tbl_failcount;
>  	unsigned long		meminfo_val;
>  
>  	/*
> @@ -102,6 +106,7 @@ struct ve_struct {
>  	 * other containers.
>  	 */
>  	atomic_t		mnt_avail_nr; /* number of available VE mounts */
> +	atomic_t		mnt_failcount;
>  	int			mnt_max_nr;
>  
>  #ifdef CONFIG_COREDUMP
> @@ -129,6 +134,7 @@ struct ve_struct {
>  	spinlock_t		aio_nr_lock;
>  	unsigned long		aio_nr;
>  	unsigned long		aio_max_nr;
> +	atomic_t		aio_failcount;
>  #endif
>  	struct vfsmount		*devtmpfs_mnt;
>  };
> @@ -151,6 +157,16 @@ extern int nr_ve;
>  #define BPF_PROG_MAX_NR_DEFAULT	256	/* number of loaded BPF progs per-VE */
>  #define MNT_MAX_NR_DEFAULT	4096	/* number of mounts per-VE */
>  
> +#define ve_failcount_inc(ve, name)					\
> +do {									\
> +	struct ve_struct *__ve = (ve);					\
> +									\
> +	atomic_inc(&__ve->name##_failcount);				\
> +	ve_pr_warn_ratelimited(VE_LOG_BOTH, "CT%s: hits the " #name	\
> +		" limit, see the '" #name "' counter in ve.failcount "	\
> +		"of the container's ve cgroup\n", ve_name(__ve));	\
> +} while (0)
> +
>  #ifdef CONFIG_VE
>  static inline void ve_set_state(struct ve_struct *ve, int new_state)
>  {
> diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
> index c94d4240e3d3..8498bc9df7ea 100644
> --- a/kernel/bpf/syscall.c
> +++ b/kernel/bpf/syscall.c
> @@ -2891,6 +2891,7 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
>  	if (!bpf_cap && type == BPF_PROG_TYPE_CGROUP_DEVICE) {
>  		load_ve = get_exec_env();
>  		if (atomic_dec_if_positive(&load_ve->bpf_prog_avail_nr) < 0) {
> +			ve_failcount_inc(load_ve, bpf_prog);
>  			load_ve = NULL;
>  			err = -ENOSPC;
>  			goto put_token;
> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
> index 5ebdb4b7baa7..9261565208e6 100644
> --- a/kernel/ve/ve.c
> +++ b/kernel/ve/ve.c
> @@ -100,10 +100,13 @@ struct ve_struct ve0 = {
>  	.features		= -1,
>  	.sched_lat_ve.cur	= &ve0_lat_stats,
>  	.netns_avail_nr		= ATOMIC_INIT(INT_MAX),
> +	.netns_failcount	= ATOMIC_INIT(0),
>  	.netns_max_nr		= INT_MAX,
>  	.netif_avail_nr		= ATOMIC_INIT(INT_MAX),
> +	.netif_failcount	= ATOMIC_INIT(0),
>  	.netif_max_nr		= INT_MAX,
>  	.bpf_prog_avail_nr	= ATOMIC_INIT(INT_MAX),
> +	.bpf_prog_failcount	= ATOMIC_INIT(0),
>  	.bpf_prog_max_nr	= INT_MAX,
>  	.fsync_enable		= FSYNC_FILTERED,
>  	._randomize_va_space	=
> @@ -115,8 +118,16 @@ struct ve_struct ve0 = {
>  
>  	.arp_neigh_nr		= ATOMIC_INIT(0),
>  	.nd_neigh_nr		= ATOMIC_INIT(0),
> +	.neigh_tbl_failcount	= ATOMIC_INIT(0),
>  	.mnt_avail_nr		= ATOMIC_INIT(INT_MAX),
>  	.mnt_max_nr		= INT_MAX,
> +	.mnt_failcount		= ATOMIC_INIT(0),
> +#ifdef CONFIG_AIO
> +	.aio_nr_lock		= __SPIN_LOCK_UNLOCKED(aio_nr_lock),
> +	.aio_nr			= 0,
> +	.aio_max_nr		= AIO_MAX_NR_DEFAULT,
> +	.aio_failcount		= ATOMIC_INIT(0),
> +#endif
>  	.meminfo_val		= VE_MEMINFO_SYSTEM,
>  #ifdef CONFIG_X86
>  	.umh_running_helpers	= ATOMIC_INIT(0),
> @@ -787,12 +798,15 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
>  	ve->fsync_enable = FSYNC_FILTERED;
>  
>  	atomic_set(&ve->netns_avail_nr, NETNS_MAX_NR_DEFAULT);
> +	atomic_set(&ve->netns_failcount, 0);
>  	ve->netns_max_nr = NETNS_MAX_NR_DEFAULT;
>  
>  	atomic_set(&ve->netif_avail_nr, NETIF_MAX_NR_DEFAULT);
> +	atomic_set(&ve->netif_failcount, 0);
>  	ve->netif_max_nr = NETIF_MAX_NR_DEFAULT;
>  
>  	atomic_set(&ve->bpf_prog_avail_nr, BPF_PROG_MAX_NR_DEFAULT);
> +	atomic_set(&ve->bpf_prog_failcount, 0);
>  	ve->bpf_prog_max_nr = BPF_PROG_MAX_NR_DEFAULT;
>  
>  	err = ve_log_init(ve);
> @@ -821,7 +835,9 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
>  
>  	atomic_set(&ve->arp_neigh_nr, 0);
>  	atomic_set(&ve->nd_neigh_nr, 0);
> +	atomic_set(&ve->neigh_tbl_failcount, 0);
>  	ve->mnt_max_nr = MNT_MAX_NR_DEFAULT;
> +	atomic_set(&ve->mnt_failcount, 0);
>  	atomic_set(&ve->mnt_avail_nr, MNT_MAX_NR_DEFAULT);
>  
>  #ifdef CONFIG_COREDUMP
> @@ -834,6 +850,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
>  	spin_lock_init(&ve->aio_nr_lock);
>  	ve->aio_nr = 0;
>  	ve->aio_max_nr = AIO_MAX_NR_DEFAULT;
> +	atomic_set(&ve->aio_failcount, 0);
>  #endif
>  
>  	return &ve->css;
> @@ -1074,6 +1091,53 @@ VE_RESOURCE(mnt);
>  VE_RESOURCE(netif);
>  VE_RESOURCE(bpf_prog);
>  
> +static const struct ve_failcount_entry {
> +	const char *name;
> +	size_t offset;
> +} ve_failcounts[] = {
> +#define VE_FC_ENTRY(name) { #name, offsetof(struct ve_struct, name##_failcount) }
> +	VE_FC_ENTRY(netns),
> +	VE_FC_ENTRY(mnt),
> +	VE_FC_ENTRY(netif),
> +	VE_FC_ENTRY(bpf_prog),
> +	VE_FC_ENTRY(neigh_tbl),
> +#ifdef CONFIG_AIO
> +	VE_FC_ENTRY(aio),
> +#endif
> +	{}
> +};
> +
> +static int ve_failcount_read(struct seq_file *sf, void *v)
> +{
> +	struct ve_struct *ve = css_to_ve(seq_css(sf));
> +	const struct ve_failcount_entry *entry;
> +	atomic_t *fc;
> +
> +	for (entry = ve_failcounts; entry->name; entry++) {
> +		fc = (void *)ve + entry->offset;
> +		seq_printf(sf, "%s: %d\n", entry->name, atomic_read(fc));
> +	}
> +	return 0;
> +}
> +
> +static ssize_t ve_failcount_write(struct kernfs_open_file *of, char *buf,
> +				  size_t nbytes, loff_t off)
> +{
> +	struct ve_struct *ve = css_to_ve(of_css(of));
> +	const struct ve_failcount_entry *entry;
> +	atomic_t *fc;
> +
> +	if (!ve_is_super(get_exec_env()) && !ve->is_pseudosuper)
> +		return -EPERM;
> +
> +	for (entry = ve_failcounts; entry->name; entry++) {
> +		fc = (void *)ve + entry->offset;
> +		atomic_set(fc, 0);
> +	}
> +
> +	return nbytes;
> +}
> +
>  static int ve_os_release_read(struct seq_file *sf, void *v)
>  {
>  	struct cgroup_subsys_state *css = seq_css(sf);
> @@ -1613,6 +1677,12 @@ static struct cftype ve_cftypes[] = {
>  		.flags			= CFTYPE_NOT_ON_ROOT,
>  		.write_u64		= ve_rpc_kill_write,
>  	},
> +	{
> +		.name			= "failcount",
> +		.flags			= CFTYPE_NOT_ON_ROOT,
> +		.seq_show		= ve_failcount_read,
> +		.write			= ve_failcount_write,
> +	},
>  	{ }
>  };
>  
> diff --git a/net/core/dev.c b/net/core/dev.c
> index c7dddb200489..7632ad1fd015 100644
> --- a/net/core/dev.c
> +++ b/net/core/dev.c
> @@ -10997,10 +10997,7 @@ int register_netdevice(struct net_device *dev)
>  
>  	ret = -ENOMEM;
>  	if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
> -		ve_pr_warn_ratelimited(VE_LOG_BOTH,
> -			"CT%s: hits max number of network devices, "
> -			"increase ve::netif_max_nr parameter\n",
> -			ve_name(net->owner_ve));
> +		ve_failcount_inc(net->owner_ve, netif);
>  		return ret;
>  	}
>  
> @@ -12211,10 +12208,7 @@ int __dev_change_net_namespace(struct net_device *dev, struct net *net,
>  
>  	err = -ENOMEM;
>  	if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
> -		ve_pr_warn_ratelimited(VE_LOG_BOTH,
> -			"CT%s: hits max number of network devices, "
> -			"increase ve::netif_max_nr parameter\n",
> -			ve_name(net->owner_ve));
> +		ve_failcount_inc(net->owner_ve, netif);
>  		goto out;
>  	}
>  	atomic_inc(&dev_net(dev)->owner_ve->netif_avail_nr);
> diff --git a/net/core/neighbour.c b/net/core/neighbour.c
> index f90deb17fb25..c7d5e6df50c2 100644
> --- a/net/core/neighbour.c
> +++ b/net/core/neighbour.c
> @@ -520,8 +520,7 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
>  	    (glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
>  	     time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
>  		if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
> -			net_info_ratelimited("%s: neighbor table overflow!\n",
> -					     tbl->id);
> +			ve_failcount_inc(ve, neigh_tbl);
>  			NEIGH_CACHE_STAT_INC(tbl, table_fulls);
>  			goto out_entries;
>  		}
> diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c
> index b3d54cad984a..328e1c96b6f4 100644
> --- a/net/core/net_namespace.c
> +++ b/net/core/net_namespace.c
> @@ -486,8 +486,10 @@ void net_drop_ns(void *p)
>  #ifdef CONFIG_VE
>  static int dec_netns_avail(struct ve_struct *ve)
>  {
> -	if (atomic_dec_if_positive(&ve->netns_avail_nr) < 0)
> +	if (atomic_dec_if_positive(&ve->netns_avail_nr) < 0) {
> +		ve_failcount_inc(ve, netns);
>  		return -ENOSPC;
> +	}
>  	return 0;
>  }
>  

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH VZ10 v9 8/8] selftests/ve: Add mount accounting selftest
    2026-09-18 15:06  0%   ` Vladimir Riabchun
@ 2026-09-25 16:04  0%   ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:04 UTC (permalink / raw)
  To: Vladimir Riabchun, devel

On 9/15/26 19:22, Vladimir Riabchun wrote:
> There are 6 test cases, covered in the new test:
> 1. Simple mount accouting correctness, just mount/umount.
> 2. Verification of correct limit hits and changes, including
>    negative values.
> 3. Partial mounts test, when mount limit is hit in the middle
>    of creation.
> 4. Test that enabled pseudosuper allows overuse.
> 5. Test that pseudosuper doesn't affect mount accounting.
> 6. Failcount feature verification.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-135520
> 
> Feature: per-ve failcounters
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> ---
>  tools/testing/selftests/ve/.gitignore         |   1 +
>  tools/testing/selftests/ve/Makefile           |   1 +
>  .../selftests/ve/ve_mount_accounting_test.c   | 550 ++++++++++++++++++
>  tools/testing/selftests/ve/ve_selftest.h      |  21 +
>  4 files changed, 573 insertions(+)
>  create mode 100644 tools/testing/selftests/ve/ve_mount_accounting_test.c
> 

<skipped>

> + * We can't enforce workqueue flush from userspace, so instead
> + * read several times until the value is the one we expect.
> + */
> +static int wait_on_mnt_avail(FIXTURE_DATA(ve_mnt_acc) * self, int expected)
> +{
> +#define WAIT_TIMEOUT_NS		(4096UL * 1000 * 1000)
> +	char path_avail[64];
> +	int mnt_avail_nr = 0;
> +	struct timespec delay = { .tv_nsec = 1000000 };
> +
> +	snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
> +
> +	while (delay.tv_nsec <= WAIT_TIMEOUT_NS) {
> +		if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr)) {
> +			fprintf(stderr, "Failed to read from %s\n", path_avail);
> +			return 1;
> +		}
> +		if (mnt_avail_nr == expected)
> +			return 0;
> +
> +		do_rcu_barrier();
> +		if (nanosleep(&delay, NULL)) {
> +			fprintf(stderr, "nanosleep failed: %s\n", strerror(errno));
> +			return 1;
> +		}
> +		delay.tv_nsec *= 2;

  nanosleep() requires tv_nsec < 1e9 and fails with EINVAL otherwise.
  The doubling goes 1ms, 2ms, ..., 512ms, and the next step is 1024ms,
  which no longer fits tv_nsec. So the loop never gets anywhere near
  WAIT_TIMEOUT_NS: after ~1s of total sleeping the next iteration bails
  out with "nanosleep failed: Invalid argument" and returns 1, instead
  of the intended ~8s of waiting and the timeout report with the
  expected/read values.

  This defeats the purpose of the helper: on a loaded machine, where
  the counter takes a while to settle, the test fails early with a
  misleading error - exactly the case this function was reworked for
  in v9.

  Please keep the delay in a plain nanoseconds variable and normalize
  it before sleeping:

        delay.tv_sec = ns / 1000000000;
        delay.tv_nsec = ns % 1000000000;

  As a side note, tv_nsec is long, so 2048ms would overflow it on a
  32-bit build; the normalization fixes that too.

<skipped>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH VZ10 v9 6/8] ve: Introduce per-VE failcount
    2026-09-25 16:02  0%   ` Konstantin Khorenko
@ 2026-09-25 16:08  4%   ` Konstantin Khorenko
  1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:08 UTC (permalink / raw)
  To: Vladimir Riabchun, devel


On 9/15/26 19:22, Vladimir Riabchun wrote:
> It may be useful to have a history of resource limit hits for every VE,
> this may simplify debugging and provide some information about the
> resources usage.
> 
> This information is provided by ve.failcount file, any write to it
> resets all failcounts.
> 
> To add a new failcounter we need to create a new atomic_t field
> name_failcount in ve structure and add a new VE_FC_ENTRY in
> ve_failcounts array.
> 
> One change, unrelated to failcounts: aio fields are now initialized
> in ve0.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-135520
> 
> Feature: per-ve failcounters
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> ---
>  fs/aio.c                 |  1 +
>  fs/namespace.c           |  2 ++
>  include/linux/ve.h       | 16 +++++++++
>  kernel/bpf/syscall.c     |  1 +
>  kernel/ve/ve.c           | 70 ++++++++++++++++++++++++++++++++++++++++
>  net/core/dev.c           | 10 ++----
>  net/core/neighbour.c     |  3 +-
>  net/core/net_namespace.c |  4 ++-
>  8 files changed, 96 insertions(+), 11 deletions(-)
> 
> diff --git a/fs/aio.c b/fs/aio.c
> index cb63416af135..4ae06bcd47f2 100644
> --- a/fs/aio.c
> +++ b/fs/aio.c
> @@ -814,6 +814,7 @@ static struct kioctx *ioctx_alloc(unsigned nr_events)
>  	spin_lock(&ve->aio_nr_lock);
>  	if (ve->aio_nr + ctx->max_reqs > ve->aio_max_nr ||
>  	    ve->aio_nr + ctx->max_reqs < ve->aio_nr) {
> +		ve_failcount_inc(ve, aio);
>  		spin_unlock(&ve->aio_nr_lock);
>  		err = -EAGAIN;
>  		goto err_ctx;
> diff --git a/fs/namespace.c b/fs/namespace.c
> index 1d6e66cd7174..ea01664ff5f2 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -3380,6 +3380,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
>  
>  	if (ret)
>  		get_ve(ve);
> +	else
> +		ve_failcount_inc(ve, mnt);
>  	return ret;
>  }

  Now every failed mount produces two ratelimited warnings in the host
  log: the pre-existing one in alloc_vfsmnt()

        pr_warn_ratelimited("CT#%s reached the limit on mounts.\n", ...);

  and the one printed by ve_failcount_inc(). They are ratelimited
  independently, so the log gets both messages for the same event.

  Please drop the alloc_vfsmnt() one in this patch: the
  ve_failcount_inc() message carries strictly more information (it also
  names the counter to look at) and additionally goes to the container
  log.

>  
> diff --git a/include/linux/ve.h b/include/linux/ve.h
> index fabc608e31f5..065129b42768 100644
> --- a/include/linux/ve.h
> +++ b/include/linux/ve.h
> @@ -80,12 +80,15 @@ struct ve_struct {
>  	struct kmapset_key	proc_perms_key;
>  
>  	atomic_t		netns_avail_nr;
> +	atomic_t		netns_failcount;
>  	int			netns_max_nr;
>  
>  	atomic_t		netif_avail_nr;
> +	atomic_t		netif_failcount;
>  	int			netif_max_nr;
>  
>  	atomic_t		bpf_prog_avail_nr;
> +	atomic_t		bpf_prog_failcount;
>  	int			bpf_prog_max_nr;
>  
>  	atomic64_t		_uevent_seqnum;
> @@ -94,6 +97,7 @@ struct ve_struct {
>  
>  	atomic_t		arp_neigh_nr;
>  	atomic_t		nd_neigh_nr;
> +	atomic_t		neigh_tbl_failcount;
>  	unsigned long		meminfo_val;
>  
>  	/*
> @@ -102,6 +106,7 @@ struct ve_struct {
>  	 * other containers.
>  	 */
>  	atomic_t		mnt_avail_nr; /* number of available VE mounts */
> +	atomic_t		mnt_failcount;
>  	int			mnt_max_nr;
>  
>  #ifdef CONFIG_COREDUMP
> @@ -129,6 +134,7 @@ struct ve_struct {
>  	spinlock_t		aio_nr_lock;
>  	unsigned long		aio_nr;
>  	unsigned long		aio_max_nr;
> +	atomic_t		aio_failcount;
>  #endif
>  	struct vfsmount		*devtmpfs_mnt;
>  };
> @@ -151,6 +157,16 @@ extern int nr_ve;
>  #define BPF_PROG_MAX_NR_DEFAULT	256	/* number of loaded BPF progs per-VE */
>  #define MNT_MAX_NR_DEFAULT	4096	/* number of mounts per-VE */
>  
> +#define ve_failcount_inc(ve, name)					\
> +do {									\
> +	struct ve_struct *__ve = (ve);					\
> +									\
> +	atomic_inc(&__ve->name##_failcount);				\
> +	ve_pr_warn_ratelimited(VE_LOG_BOTH, "CT%s: hits the " #name	\
> +		" limit, see the '" #name "' counter in ve.failcount "	\
> +		"of the container's ve cgroup\n", ve_name(__ve));	\
> +} while (0)
> +
>  #ifdef CONFIG_VE
>  static inline void ve_set_state(struct ve_struct *ve, int new_state)
>  {
> diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
> index c94d4240e3d3..8498bc9df7ea 100644
> --- a/kernel/bpf/syscall.c
> +++ b/kernel/bpf/syscall.c
> @@ -2891,6 +2891,7 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
>  	if (!bpf_cap && type == BPF_PROG_TYPE_CGROUP_DEVICE) {
>  		load_ve = get_exec_env();
>  		if (atomic_dec_if_positive(&load_ve->bpf_prog_avail_nr) < 0) {
> +			ve_failcount_inc(load_ve, bpf_prog);
>  			load_ve = NULL;
>  			err = -ENOSPC;
>  			goto put_token;
> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
> index 5ebdb4b7baa7..9261565208e6 100644
> --- a/kernel/ve/ve.c
> +++ b/kernel/ve/ve.c
> @@ -100,10 +100,13 @@ struct ve_struct ve0 = {
>  	.features		= -1,
>  	.sched_lat_ve.cur	= &ve0_lat_stats,
>  	.netns_avail_nr		= ATOMIC_INIT(INT_MAX),
> +	.netns_failcount	= ATOMIC_INIT(0),
>  	.netns_max_nr		= INT_MAX,
>  	.netif_avail_nr		= ATOMIC_INIT(INT_MAX),
> +	.netif_failcount	= ATOMIC_INIT(0),
>  	.netif_max_nr		= INT_MAX,
>  	.bpf_prog_avail_nr	= ATOMIC_INIT(INT_MAX),
> +	.bpf_prog_failcount	= ATOMIC_INIT(0),
>  	.bpf_prog_max_nr	= INT_MAX,
>  	.fsync_enable		= FSYNC_FILTERED,
>  	._randomize_va_space	=
> @@ -115,8 +118,16 @@ struct ve_struct ve0 = {
>  
>  	.arp_neigh_nr		= ATOMIC_INIT(0),
>  	.nd_neigh_nr		= ATOMIC_INIT(0),
> +	.neigh_tbl_failcount	= ATOMIC_INIT(0),
>  	.mnt_avail_nr		= ATOMIC_INIT(INT_MAX),
>  	.mnt_max_nr		= INT_MAX,
> +	.mnt_failcount		= ATOMIC_INIT(0),
> +#ifdef CONFIG_AIO
> +	.aio_nr_lock		= __SPIN_LOCK_UNLOCKED(aio_nr_lock),
> +	.aio_nr			= 0,
> +	.aio_max_nr		= AIO_MAX_NR_DEFAULT,
> +	.aio_failcount		= ATOMIC_INIT(0),
> +#endif
>  	.meminfo_val		= VE_MEMINFO_SYSTEM,
>  #ifdef CONFIG_X86
>  	.umh_running_helpers	= ATOMIC_INIT(0),
> @@ -787,12 +798,15 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
>  	ve->fsync_enable = FSYNC_FILTERED;
>  
>  	atomic_set(&ve->netns_avail_nr, NETNS_MAX_NR_DEFAULT);
> +	atomic_set(&ve->netns_failcount, 0);
>  	ve->netns_max_nr = NETNS_MAX_NR_DEFAULT;
>  
>  	atomic_set(&ve->netif_avail_nr, NETIF_MAX_NR_DEFAULT);
> +	atomic_set(&ve->netif_failcount, 0);
>  	ve->netif_max_nr = NETIF_MAX_NR_DEFAULT;
>  
>  	atomic_set(&ve->bpf_prog_avail_nr, BPF_PROG_MAX_NR_DEFAULT);
> +	atomic_set(&ve->bpf_prog_failcount, 0);
>  	ve->bpf_prog_max_nr = BPF_PROG_MAX_NR_DEFAULT;
>  
>  	err = ve_log_init(ve);
> @@ -821,7 +835,9 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
>  
>  	atomic_set(&ve->arp_neigh_nr, 0);
>  	atomic_set(&ve->nd_neigh_nr, 0);
> +	atomic_set(&ve->neigh_tbl_failcount, 0);
>  	ve->mnt_max_nr = MNT_MAX_NR_DEFAULT;
> +	atomic_set(&ve->mnt_failcount, 0);
>  	atomic_set(&ve->mnt_avail_nr, MNT_MAX_NR_DEFAULT);
>  
>  #ifdef CONFIG_COREDUMP
> @@ -834,6 +850,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
>  	spin_lock_init(&ve->aio_nr_lock);
>  	ve->aio_nr = 0;
>  	ve->aio_max_nr = AIO_MAX_NR_DEFAULT;
> +	atomic_set(&ve->aio_failcount, 0);
>  #endif
>  
>  	return &ve->css;
> @@ -1074,6 +1091,53 @@ VE_RESOURCE(mnt);
>  VE_RESOURCE(netif);
>  VE_RESOURCE(bpf_prog);
>  
> +static const struct ve_failcount_entry {
> +	const char *name;
> +	size_t offset;
> +} ve_failcounts[] = {
> +#define VE_FC_ENTRY(name) { #name, offsetof(struct ve_struct, name##_failcount) }
> +	VE_FC_ENTRY(netns),
> +	VE_FC_ENTRY(mnt),
> +	VE_FC_ENTRY(netif),
> +	VE_FC_ENTRY(bpf_prog),
> +	VE_FC_ENTRY(neigh_tbl),
> +#ifdef CONFIG_AIO
> +	VE_FC_ENTRY(aio),
> +#endif
> +	{}
> +};
> +
> +static int ve_failcount_read(struct seq_file *sf, void *v)
> +{
> +	struct ve_struct *ve = css_to_ve(seq_css(sf));
> +	const struct ve_failcount_entry *entry;
> +	atomic_t *fc;
> +
> +	for (entry = ve_failcounts; entry->name; entry++) {
> +		fc = (void *)ve + entry->offset;
> +		seq_printf(sf, "%s: %d\n", entry->name, atomic_read(fc));
> +	}
> +	return 0;
> +}
> +
> +static ssize_t ve_failcount_write(struct kernfs_open_file *of, char *buf,
> +				  size_t nbytes, loff_t off)
> +{
> +	struct ve_struct *ve = css_to_ve(of_css(of));
> +	const struct ve_failcount_entry *entry;
> +	atomic_t *fc;
> +
> +	if (!ve_is_super(get_exec_env()) && !ve->is_pseudosuper)
> +		return -EPERM;
> +
> +	for (entry = ve_failcounts; entry->name; entry++) {
> +		fc = (void *)ve + entry->offset;
> +		atomic_set(fc, 0);
> +	}
> +
> +	return nbytes;
> +}
> +
>  static int ve_os_release_read(struct seq_file *sf, void *v)
>  {
>  	struct cgroup_subsys_state *css = seq_css(sf);
> @@ -1613,6 +1677,12 @@ static struct cftype ve_cftypes[] = {
>  		.flags			= CFTYPE_NOT_ON_ROOT,
>  		.write_u64		= ve_rpc_kill_write,
>  	},
> +	{
> +		.name			= "failcount",
> +		.flags			= CFTYPE_NOT_ON_ROOT,
> +		.seq_show		= ve_failcount_read,
> +		.write			= ve_failcount_write,
> +	},
>  	{ }
>  };
>  
> diff --git a/net/core/dev.c b/net/core/dev.c
> index c7dddb200489..7632ad1fd015 100644
> --- a/net/core/dev.c
> +++ b/net/core/dev.c
> @@ -10997,10 +10997,7 @@ int register_netdevice(struct net_device *dev)
>  
>  	ret = -ENOMEM;
>  	if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
> -		ve_pr_warn_ratelimited(VE_LOG_BOTH,
> -			"CT%s: hits max number of network devices, "
> -			"increase ve::netif_max_nr parameter\n",
> -			ve_name(net->owner_ve));
> +		ve_failcount_inc(net->owner_ve, netif);
>  		return ret;
>  	}
>  
> @@ -12211,10 +12208,7 @@ int __dev_change_net_namespace(struct net_device *dev, struct net *net,
>  
>  	err = -ENOMEM;
>  	if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
> -		ve_pr_warn_ratelimited(VE_LOG_BOTH,
> -			"CT%s: hits max number of network devices, "
> -			"increase ve::netif_max_nr parameter\n",
> -			ve_name(net->owner_ve));
> +		ve_failcount_inc(net->owner_ve, netif);
>  		goto out;
>  	}
>  	atomic_inc(&dev_net(dev)->owner_ve->netif_avail_nr);
> diff --git a/net/core/neighbour.c b/net/core/neighbour.c
> index f90deb17fb25..c7d5e6df50c2 100644
> --- a/net/core/neighbour.c
> +++ b/net/core/neighbour.c
> @@ -520,8 +520,7 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
>  	    (glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
>  	     time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
>  		if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
> -			net_info_ratelimited("%s: neighbor table overflow!\n",
> -					     tbl->id);
> +			ve_failcount_inc(ve, neigh_tbl);
>  			NEIGH_CACHE_STAT_INC(tbl, table_fulls);
>  			goto out_entries;
>  		}
> diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c
> index b3d54cad984a..328e1c96b6f4 100644
> --- a/net/core/net_namespace.c
> +++ b/net/core/net_namespace.c
> @@ -486,8 +486,10 @@ void net_drop_ns(void *p)
>  #ifdef CONFIG_VE
>  static int dec_netns_avail(struct ve_struct *ve)
>  {
> -	if (atomic_dec_if_positive(&ve->netns_avail_nr) < 0)
> +	if (atomic_dec_if_positive(&ve->netns_avail_nr) < 0) {
> +		ve_failcount_inc(ve, netns);
>  		return -ENOSPC;
> +	}
>  	return 0;
>  }
>  

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH hci-8.1 v10 02/13] ve/fs: count available mounts instead of used ones
  @ 2026-09-25 16:53  3% ` Konstantin Khorenko
  2026-09-25 16:53  4% ` [Devel] [PATCH hci-8.1 v10 07/13] ve: introduce per-VE failcounter infrastructure Konstantin Khorenko
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:53 UTC (permalink / raw)
  To: Vasileios Almpanis, Pavel Tikhomirov,
	cover.1789491989.git.vladimir.riabchun, Konstantin Khorenko,
	devel

From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

Previous approach with current mounts counter had an issue:
there was a gap between ve_mount_allowed check and ve_mount_nr_inc,
which could allow CT to have more mounts than expected.

Fix this by tracking the number of available mounts instead
of current ones. This also makes resources accounting
more consistent - we are using ***_avail_nr approach more.

One more issue with inconsistent ve value is fixed:
ve_mount_allowed always used ve from get_exec_env, but
ve_mount_nr_inc operated with owner_ve.
Now actual ve value is calculated in the beginning of alloc_vfsmnt.

To avoid incorrect accounting when is_pseudosuper is changed,
update avail_nr count without > 0 check if VE is ve0 or pseudosuper.
This also simplifies ve_mount_put, since increment is
now unconditional.

https://virtuozzo.atlassian.net/browse/VSTOR-135520

Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Reviewed-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 fs/namespace.c     | 78 ++++++++++++++++++++++++++++++++----------------------
 include/linux/ve.h |  2 +-
 kernel/ve/ve.c     | 12 ++++-----
 3 files changed, 54 insertions(+), 38 deletions(-)

diff --git a/fs/namespace.c b/fs/namespace.c
index 4adc1db84b4c0..1d6e66cd71745 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -317,19 +317,22 @@ int mnt_get_count(struct mount *mnt)
 #endif
 }
 
-static inline int ve_mount_allowed(void);
-static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve);
-static inline void ve_mount_nr_dec(struct mount *mnt);
+static inline int ve_try_reserve_mount(struct ve_struct *ve);
+static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve);
 static inline void ve_mount_reown(struct mount *mnt, struct ve_struct *ve);
 
 static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
 {
 	struct mount *mnt;
+	struct ve_struct *ve = owner_ve;
 
-	if (!ve_mount_allowed()) {
+	if (!ve)
+		ve = get_exec_env();
+
+	if (!ve_try_reserve_mount(ve)) {
 		pr_warn_ratelimited(
 			"CT#%s reached the limit on mounts.\n",
-			ve_name(get_exec_env()));
+			ve_name(ve));
 		return NULL;
 	}
 
@@ -337,6 +340,14 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
 	if (mnt) {
 		int err;
 
+#ifdef CONFIG_VE
+		/*
+		 * Got ve reference in ve_try_reserve_mount, set mnt ve data
+		 * here, so in case of error ve_mount_put sees correct info.
+		 */
+		mnt->ve_owner = ve;
+#endif
+
 		err = mnt_alloc_id(mnt);
 		if (err)
 			goto out_free_cache;
@@ -371,7 +382,8 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
 		INIT_LIST_HEAD(&mnt->mnt_umounting);
 		INIT_HLIST_HEAD(&mnt->mnt_stuck_children);
 		mnt->mnt.mnt_idmap = &nop_mnt_idmap;
-		ve_mount_nr_inc(mnt, owner_ve);
+	} else {
+		ve_mount_put(mnt, ve);
 	}
 	return mnt;
 
@@ -382,6 +394,8 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
 out_free_id:
 	mnt_free_id(mnt);
 out_free_cache:
+	/* Got ve reference in ve_try_reserve_mount */
+	ve_mount_put(mnt, ve);
 	kmem_cache_free(mnt_cache, mnt);
 	return NULL;
 }
@@ -751,7 +765,9 @@ int sb_prepare_remount_readonly(struct super_block *sb)
 static void free_vfsmnt(struct mount *mnt)
 {
 	mnt_idmap_put(mnt_idmap(&mnt->mnt));
-	ve_mount_nr_dec(mnt);
+#ifdef CONFIG_VE
+	ve_mount_put(mnt, mnt->ve_owner);
+#endif
 	kfree_const(mnt->mnt_devname);
 #ifdef CONFIG_SMP
 	free_percpu(mnt->mnt_pcp);
@@ -3214,7 +3230,7 @@ int ve_devmnt_process(struct ve_struct *ve, dev_t dev, void **data_pp, int remou
 		if (devmnt->dev == dev) {
 			err = ve_devmnt_check(data, devmnt->allowed_options);
 			/*
-			 * In case of @is_pseudouser set, ie restore procedure,
+			 * In case of @is_pseudosuper set, ie restore procedure,
 			 * we don't check for allowed options filtering, since
 			 * restore mode is special.
 			 */
@@ -3353,30 +3369,30 @@ int ve_devmnt_verify(struct ve_struct *ve, dev_t dev, char *opts, bool new_mount
 	return err;
 }
 
-static inline int ve_mount_allowed(void)
-{
-	struct ve_struct *ve = get_exec_env();
-
-	return ve_is_super(ve) || ve->is_pseudosuper ||
-		atomic_read(&ve->mnt_nr) < (int)sysctl_ve_mount_nr;
-}
-
-static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve)
+static inline int ve_try_reserve_mount(struct ve_struct *ve)
 {
-	if (!ve)
-		ve = get_exec_env();
+	int ret = ve_is_super(ve) || ve->is_pseudosuper;
+	/* Ignore limits in ve0 and pseudosuper cases, but still count. */
+	if (ret)
+		atomic_dec(&ve->mnt_avail_nr);
+	else
+		ret = atomic_dec_if_positive(&ve->mnt_avail_nr) >= 0;
 
-	mnt->ve_owner = get_ve(ve);
-	atomic_inc(&ve->mnt_nr);
+	if (ret)
+		get_ve(ve);
+	return ret;
 }
 
-static inline void ve_mount_nr_dec(struct mount *mnt)
+static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve)
 {
-	struct ve_struct *ve = mnt->ve_owner;
-
-	atomic_dec(&ve->mnt_nr);
+	/*
+	 * ve argument is needed to reuse this function in alloc_vfsmnt error path.
+	 * Other users should pass mnt->ve_owner value.
+	 */
+	atomic_inc(&ve->mnt_avail_nr);
 	put_ve(ve);
-	mnt->ve_owner = NULL;
+	if (mnt)
+		mnt->ve_owner = NULL;
 }
 
 /*
@@ -3397,9 +3413,9 @@ static inline void ve_mount_reown(struct mount *mnt, struct ve_struct *ve)
 	if (old == ve)
 		return;
 
-	atomic_inc(&ve->mnt_nr);
+	atomic_dec(&ve->mnt_avail_nr);
 	WRITE_ONCE(mnt->ve_owner, get_ve(ve));
-	atomic_dec(&old->mnt_nr);
+	atomic_inc(&old->mnt_avail_nr);
 	put_ve(old);
 }
 
@@ -3422,10 +3438,10 @@ bool is_sb_ve_accessible(struct ve_struct *ve, struct super_block *sb)
 
 #else /* CONFIG_VE */
 
-static inline int ve_mount_allowed(void) { return 1; }
-static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve) { }
-static inline void ve_mount_nr_dec(struct mount *mnt) { }
+static inline int ve_try_reserve_mount(struct ve_struct *ve) { return 1; }
+static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve) { }
 static inline void ve_mount_reown(struct mount *mnt, struct ve_struct *ve) { }
+
 #endif /* CONFIG_VE */
 
 /*
diff --git a/include/linux/ve.h b/include/linux/ve.h
index f946abe4d8596..65c058c0f34d4 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -96,7 +96,7 @@ struct ve_struct {
 	atomic_t		nd_neigh_nr;
 	unsigned long		meminfo_val;
 
-	atomic_t		mnt_nr; /* number of present VE mounts */
+	atomic_t		mnt_avail_nr; /* number of available VE mounts */
 
 #ifdef CONFIG_COREDUMP
 	char			core_pattern[CORENAME_MAX_SIZE];
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index e3ae2e5d9a86c..bed8a58af0853 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -82,7 +82,7 @@ struct ve_struct ve0 = {
 
 	.arp_neigh_nr		= ATOMIC_INIT(0),
 	.nd_neigh_nr		= ATOMIC_INIT(0),
-	.mnt_nr			= ATOMIC_INIT(0),
+	.mnt_avail_nr		= ATOMIC_INIT(INT_MAX),
 	.meminfo_val		= VE_MEMINFO_SYSTEM,
 	.umh_running_helpers	= ATOMIC_INIT(0),
 	.umh_helpers_waitq	= __WAIT_QUEUE_HEAD_INITIALIZER(ve0.umh_helpers_waitq),
@@ -787,7 +787,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
 
 	atomic_set(&ve->arp_neigh_nr, 0);
 	atomic_set(&ve->nd_neigh_nr, 0);
-	atomic_set(&ve->mnt_nr, 0);
+	atomic_set(&ve->mnt_avail_nr, sysctl_ve_mount_nr);
 
 #ifdef CONFIG_COREDUMP
 	strcpy(ve->core_pattern, "core");
@@ -1063,9 +1063,9 @@ static u64 ve_netns_avail_nr_read(struct cgroup_subsys_state *css, struct cftype
 	return atomic_read(&css_to_ve(css)->netns_avail_nr);
 }
 
-static u64 ve_mnt_nr_read(struct cgroup_subsys_state *css, struct cftype *cft)
+static s64 ve_mnt_avail_nr_read(struct cgroup_subsys_state *css, struct cftype *cft)
 {
-	return atomic_read(&css_to_ve(css)->mnt_nr);
+	return atomic_read(&css_to_ve(css)->mnt_avail_nr);
 }
 
 static u64 ve_netif_max_nr_read(struct cgroup_subsys_state *css, struct cftype *cft)
@@ -1627,8 +1627,8 @@ static struct cftype ve_cftypes[] = {
 		.read_u64		= ve_netns_avail_nr_read,
 	},
 	{
-		.name			= "mnt_nr",
-		.read_u64		= ve_mnt_nr_read,
+		.name			= "mnt_avail_nr",
+		.read_s64		= ve_mnt_avail_nr_read,
 	},
 	{
 		.name			= "netif_max_nr",

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH hci-8.1 v10 07/13] ve: introduce per-VE failcounter infrastructure
    2026-09-25 16:53  3% ` [Devel] [PATCH hci-8.1 v10 02/13] ve/fs: count available mounts instead of used ones Konstantin Khorenko
@ 2026-09-25 16:53  4% ` Konstantin Khorenko
  2026-09-25 16:53  4% ` [Devel] [PATCH hci-8.1 v10 09/13] ve: add mnt failcounter Konstantin Khorenko
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:53 UTC (permalink / raw)
  To: Vasileios Almpanis, Pavel Tikhomirov,
	cover.1789491989.git.vladimir.riabchun, Konstantin Khorenko,
	devel

From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

It may be useful to have a history of resource limit hits for every VE,
this may simplify debugging and provide some information about the
resources usage.

This information is provided by ve.failcount file, any write to it
resets all failcounts.

To add a new failcounter we need to create a new atomic_t field
name_failcount in ve structure and add a new VE_FC_ENTRY in
ve_failcounts array. The counters themselves come in the follow-up
patches, one per resource.

The neighbour table overflow will not be counted for now: neigh_alloc
can run in softirq context, where ve log must not be used, and
ve_failcount_inc writes to it. The neigh_tbl counter will be added
together with the ve_printk softirq fix (VSTOR-146839).

https://virtuozzo.atlassian.net/browse/VSTOR-135520

Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 include/linux/ve.h | 10 ++++++++++
 kernel/ve/ve.c     | 45 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 55 insertions(+)

diff --git a/include/linux/ve.h b/include/linux/ve.h
index fabc608e31f51..546060eddc765 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -151,6 +151,16 @@ extern int nr_ve;
 #define BPF_PROG_MAX_NR_DEFAULT	256	/* number of loaded BPF progs per-VE */
 #define MNT_MAX_NR_DEFAULT	4096	/* number of mounts per-VE */
 
+#define ve_failcount_inc(ve, name)					\
+do {									\
+	struct ve_struct *__ve = (ve);					\
+									\
+	atomic_inc(&__ve->name##_failcount);				\
+	ve_pr_warn_ratelimited(VE_LOG_BOTH, "CT%s: hits the " #name	\
+		" limit, see the '" #name "' counter in ve.failcount "	\
+		"of the container's ve cgroup\n", ve_name(__ve));	\
+} while (0)
+
 #ifdef CONFIG_VE
 static inline void ve_set_state(struct ve_struct *ve, int new_state)
 {
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index c4e5c000bc427..f915f86e7471e 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -1079,6 +1079,45 @@ VE_RESOURCE(mnt);
 VE_RESOURCE(netif);
 VE_RESOURCE(bpf_prog);
 
+static const struct ve_failcount_entry {
+	const char *name;
+	size_t offset;
+} ve_failcounts[] = {
+#define VE_FC_ENTRY(name) { #name, offsetof(struct ve_struct, name##_failcount) }
+	{}
+};
+
+static int ve_failcount_read(struct seq_file *sf, void *v)
+{
+	struct ve_struct *ve = css_to_ve(seq_css(sf));
+	const struct ve_failcount_entry *entry;
+	atomic_t *fc;
+
+	for (entry = ve_failcounts; entry->name; entry++) {
+		fc = (void *)ve + entry->offset;
+		seq_printf(sf, "%s: %d\n", entry->name, atomic_read(fc));
+	}
+	return 0;
+}
+
+static ssize_t ve_failcount_write(struct kernfs_open_file *of, char *buf,
+				  size_t nbytes, loff_t off)
+{
+	struct ve_struct *ve = css_to_ve(of_css(of));
+	const struct ve_failcount_entry *entry;
+	atomic_t *fc;
+
+	if (!ve_is_super(get_exec_env()) && !ve->is_pseudosuper)
+		return -EPERM;
+
+	for (entry = ve_failcounts; entry->name; entry++) {
+		fc = (void *)ve + entry->offset;
+		atomic_set(fc, 0);
+	}
+
+	return nbytes;
+}
+
 static int ve_os_release_read(struct seq_file *sf, void *v)
 {
 	struct cgroup_subsys_state *css = seq_css(sf);
@@ -1618,6 +1657,12 @@ static struct cftype ve_cftypes[] = {
 		.flags			= CFTYPE_NOT_ON_ROOT,
 		.write_u64		= ve_rpc_kill_write,
 	},
+	{
+		.name			= "failcount",
+		.flags			= CFTYPE_NOT_ON_ROOT,
+		.seq_show		= ve_failcount_read,
+		.write			= ve_failcount_write,
+	},
 	{ }
 };
 

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH hci-8.1 v10 09/13] ve: add mnt failcounter
    2026-09-25 16:53  3% ` [Devel] [PATCH hci-8.1 v10 02/13] ve/fs: count available mounts instead of used ones Konstantin Khorenko
  2026-09-25 16:53  4% ` [Devel] [PATCH hci-8.1 v10 07/13] ve: introduce per-VE failcounter infrastructure Konstantin Khorenko
@ 2026-09-25 16:53  4% ` Konstantin Khorenko
  2026-09-25 16:53  6% ` [Devel] [PATCH hci-8.1 v10 10/13] ve: add netif failcounter Konstantin Khorenko
  2026-09-25 16:53  6% ` [Devel] [PATCH hci-8.1 v10 13/13] selftests/ve: add mount accounting selftest Konstantin Khorenko
  4 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:53 UTC (permalink / raw)
  To: Vasileios Almpanis, Pavel Tikhomirov,
	cover.1789491989.git.vladimir.riabchun, Konstantin Khorenko,
	devel

From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

Count hits of the per-VE mount limit in ve.failcount.

The old warning in alloc_vfsmnt is dropped, otherwise every failed
mount would produce two ratelimited messages. The one printed by
ve_failcount_inc also names the counter to look at and goes to the
container log as well.

https://virtuozzo.atlassian.net/browse/VSTOR-135520

Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 fs/namespace.c     | 8 +++-----
 include/linux/ve.h | 1 +
 kernel/ve/ve.c     | 3 +++
 3 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/fs/namespace.c b/fs/namespace.c
index 1d6e66cd71745..cf77c3fb98407 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -329,12 +329,8 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
 	if (!ve)
 		ve = get_exec_env();
 
-	if (!ve_try_reserve_mount(ve)) {
-		pr_warn_ratelimited(
-			"CT#%s reached the limit on mounts.\n",
-			ve_name(ve));
+	if (!ve_try_reserve_mount(ve))
 		return NULL;
-	}
 
 	mnt = kmem_cache_zalloc(mnt_cache, GFP_KERNEL);
 	if (mnt) {
@@ -3380,6 +3376,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
 
 	if (ret)
 		get_ve(ve);
+	else
+		ve_failcount_inc(ve, mnt);
 	return ret;
 }
 
diff --git a/include/linux/ve.h b/include/linux/ve.h
index 969390bb511e7..0395e37208849 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -103,6 +103,7 @@ struct ve_struct {
 	 * other containers.
 	 */
 	atomic_t		mnt_avail_nr; /* number of available VE mounts */
+	atomic_t		mnt_failcount;
 	int			mnt_max_nr;
 
 #ifdef CONFIG_COREDUMP
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index 56936ef4cc92b..4c3acda6b135e 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -118,6 +118,7 @@ struct ve_struct ve0 = {
 	.nd_neigh_nr		= ATOMIC_INIT(0),
 	.mnt_avail_nr		= ATOMIC_INIT(INT_MAX),
 	.mnt_max_nr		= INT_MAX,
+	.mnt_failcount		= ATOMIC_INIT(0),
 #ifdef CONFIG_AIO
 	.aio_nr_lock		= __SPIN_LOCK_UNLOCKED(ve0.aio_nr_lock),
 	.aio_nr			= 0,
@@ -829,6 +830,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
 	atomic_set(&ve->arp_neigh_nr, 0);
 	atomic_set(&ve->nd_neigh_nr, 0);
 	ve->mnt_max_nr = MNT_MAX_NR_DEFAULT;
+	atomic_set(&ve->mnt_failcount, 0);
 	atomic_set(&ve->mnt_avail_nr, MNT_MAX_NR_DEFAULT);
 
 #ifdef CONFIG_COREDUMP
@@ -1087,6 +1089,7 @@ static const struct ve_failcount_entry {
 } ve_failcounts[] = {
 #define VE_FC_ENTRY(name) { #name, offsetof(struct ve_struct, name##_failcount) }
 	VE_FC_ENTRY(netns),
+	VE_FC_ENTRY(mnt),
 	{}
 };
 

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH hci-8.1 v10 13/13] selftests/ve: add mount accounting selftest
                     ` (3 preceding siblings ...)
  2026-09-25 16:53  6% ` [Devel] [PATCH hci-8.1 v10 10/13] ve: add netif failcounter Konstantin Khorenko
@ 2026-09-25 16:53  6% ` Konstantin Khorenko
  4 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:53 UTC (permalink / raw)
  To: Vasileios Almpanis, Pavel Tikhomirov,
	cover.1789491989.git.vladimir.riabchun, Konstantin Khorenko,
	devel

From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

There are 6 test cases, covered in the new test:
1. Simple mount accouting correctness, just mount/umount.
2. Verification of correct limit hits and changes, including
   negative values.
3. Partial mounts test, when mount limit is hit in the middle
   of creation.
4. Test that enabled pseudosuper allows overuse.
5. Test that pseudosuper doesn't affect mount accounting.
6. Failcount feature verification.

https://virtuozzo.atlassian.net/browse/VSTOR-135520

Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/selftests/ve/.gitignore              |   1 +
 tools/testing/selftests/ve/Makefile                |   1 +
 .../selftests/ve/ve_mount_accounting_test.c        | 554 +++++++++++++++++++++
 tools/testing/selftests/ve/ve_selftest.h           |  21 +
 4 files changed, 577 insertions(+)

diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index afa4c568c2c9b..3df4d05888dce 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,2 +1,3 @@
 ve_ns_owner_test
 ve_perms_test
+ve_mount_accounting_test
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index ec40cbc7b3a1b..c6efe7c4b4fba 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
 
 TEST_GEN_PROGS += ve_ns_owner_test
 TEST_GEN_PROGS += ve_perms_test
+TEST_GEN_PROGS += ve_mount_accounting_test
 
 include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_mount_accounting_test.c b/tools/testing/selftests/ve/ve_mount_accounting_test.c
new file mode 100644
index 0000000000000..e2a882137e11d
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
@@ -0,0 +1,554 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_mount_accounting selftests
+ *
+ * Tests to check the correctness of mount accounting.
+ */
+#define _GNU_SOURCE
+#include <asm/unistd.h>
+#include <linux/sched.h>
+#include <linux/limits.h>
+#include <sys/wait.h>
+#include <sys/syscall.h>
+#include <sys/stat.h>
+#include <sys/mount.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <time.h>
+
+#include "../kselftest_harness.h"
+#include "ve_selftest.h"
+
+#define TMP_DIR			"/ve-mnt-tmp/"
+#define VE_MOUNTS_MAX		128
+
+/*
+ * These values take a while to calculate, the first test will
+ * do this and store the values for those who come after.
+ */
+int *need_avail;
+
+FIXTURE(ve_mnt_acc)
+{
+	int cgv2_fd;
+	int ctid;
+	struct namespace_ctx ns_ctx;
+	int orig_have_mnt;
+};
+
+/*
+ * free_vfsmnt is called via call_rcu, so we need to ensure
+ * that all pending call_rcu tasks are finished.
+ * However, one barrier is not enough:
+ * mntput_no_expire
+ *   init_task_work(&mnt->mnt_rcu, __cleanup_mnt);
+ *   task_work_add(task, &mnt->mnt_rcu, TWA_RESUME)
+ *     OR
+ *   schedule_delayed_work(&delayed_mntput_work, 1)
+ *   # Both later call cleanup_mnt
+ *     cleanup_mnt
+ *       call_rcu(&mnt->mnt_rcu, delayed_free_vfsmnt);
+ *
+ * We can't enforce workqueue flush from userspace, so instead
+ * read several times until the value is the one we expect.
+ */
+static int wait_on_mnt_avail(FIXTURE_DATA(ve_mnt_acc) * self, int expected)
+{
+#define NSEC_PER_SEC		(1000ULL * 1000 * 1000)
+#define WAIT_TIMEOUT_NS		(4096ULL * 1000 * 1000)
+	char path_avail[64];
+	int mnt_avail_nr = 0;
+	unsigned long long delay_ns = 1000000;
+	struct timespec delay;
+
+	snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+	while (delay_ns <= WAIT_TIMEOUT_NS) {
+		if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr)) {
+			fprintf(stderr, "Failed to read from %s\n", path_avail);
+			return 1;
+		}
+		if (mnt_avail_nr == expected)
+			return 0;
+
+		do_rcu_barrier();
+		delay.tv_sec = delay_ns / NSEC_PER_SEC;
+		delay.tv_nsec = delay_ns % NSEC_PER_SEC;
+		if (nanosleep(&delay, NULL)) {
+			fprintf(stderr, "nanosleep failed: %s\n", strerror(errno));
+			return 1;
+		}
+		delay_ns *= 2;
+	}
+	fprintf(stderr, "Didn't get expected value at %s\n", path_avail);
+	fprintf(stderr, "Expected %d, last read %d\n", expected, mnt_avail_nr);
+	return -1;
+}
+
+static int set_pseudosuper(FIXTURE_DATA(ve_mnt_acc) * self, int value)
+{
+	char path[64];
+
+	snprintf(path, sizeof(path), "%d/ve.pseudosuper", self->ctid);
+	return write_u64_at(self->cgv2_fd, path, value);
+}
+
+static int _create_mount(void *id_ptr)
+{
+	char path[PATH_MAX];
+	int id = *(int *)id_ptr, ret;
+
+	snprintf(path, sizeof(path), TMP_DIR "%d", id);
+
+	if (mkdir(path, 0755) < 0) {
+		fprintf(stderr, "Failed to create directory %s: %s\n", path, strerror(errno));
+		return -1;
+	}
+	ret = mount("tmpfs", path, "tmpfs", 0, "size=1M");
+	if (!ret)
+		return 0;
+	fprintf(stderr, "Failed to mount tmpfs to %s: %s\n", path, strerror(errno));
+
+	rmdir(path);
+	return -1;
+}
+
+/* Create a mount and wait until mount counter is valid */
+static int create_mount(FIXTURE_DATA(ve_mnt_acc) * self, int id)
+{
+	int ret, avail_before;
+	char path[64];
+
+	snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+
+	if (read_s32_at(self->cgv2_fd, path, &avail_before)) {
+		fprintf(stderr, "Failed to read mnt_avail_nr in create_mount\n");
+		return 1;
+	}
+
+	ret = run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
+		CLONE_NEWVE | CLONE_NEWNS, _create_mount, &id);
+
+	if (ret)
+		wait_on_mnt_avail(self, avail_before);
+	else
+		ret = wait_on_mnt_avail(self, avail_before - 1);
+	return ret;
+}
+
+static int _destroy_mount(void *id_ptr)
+{
+	char path[PATH_MAX];
+	struct stat st;
+	int id = *(int *)id_ptr;
+
+	snprintf(path, sizeof(path), TMP_DIR "%d", id);
+
+	if (stat(path, &st))
+		return 1;
+	if (umount(path)) {
+		fprintf(stderr, "failed to umount directory %s: %s\n", path, strerror(errno));
+		return -1;
+	}
+	if (rmdir(path)) {
+		fprintf(stderr, "failed to remove directory %s: %s\n", path, strerror(errno));
+		return -1;
+	}
+	return 0;
+}
+
+/* Do umount and ensure that mnt_avail_nr is valid on exit(+1) */
+static int destroy_mount(FIXTURE_DATA(ve_mnt_acc) * self, int id)
+{
+	int ret, avail_before;
+	char path[64];
+
+	snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+
+	if (read_s32_at(self->cgv2_fd, path, &avail_before)) {
+		fprintf(stderr, "Failed to read mnt_avail_nr in create_mount\n");
+		return 1;
+	}
+
+	ret = run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
+		CLONE_NEWVE | CLONE_NEWNS, _destroy_mount, &id);
+
+	/* Cost must be exactly 1 */
+	if (!ret)
+		ret = wait_on_mnt_avail(self, avail_before + 1);
+
+	return ret;
+}
+
+#define MAX_MNT_ID 32
+
+static int get_free_mnt_id(void)
+{
+	int i;
+	struct stat st;
+	char path[PATH_MAX];
+
+	for (i = 0; i < MAX_MNT_ID; i++) {
+		snprintf(path, sizeof(path), TMP_DIR "%d", i);
+		if (stat(path, &st))
+			return i;
+	}
+	return -1;
+}
+
+/* Returns 0 if mount failed OR mount cost is not 1 */
+static int try_mount(FIXTURE_DATA(ve_mnt_acc) * self)
+{
+	int mnt_id = get_free_mnt_id();
+
+	if (mnt_id < 0 || create_mount(self, mnt_id))
+		return 0;
+
+	return destroy_mount(self, mnt_id) == 0;
+}
+
+/*
+ * Current kernel code first creates many mounts with ve_owner != ve0,
+ * so this VE needs more mounts than would be really consumed after commit_tree.
+ */
+static int get_required_avail_mnts(FIXTURE_DATA(ve_mnt_acc) * self)
+{
+	int l = 0, r = VE_MOUNTS_MAX + 1, mid;
+	char path_max_nr[64];
+
+	snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+	while (r - l > 1) {
+		mid = (l + r) / 2;
+		if (write_u64_at(self->cgv2_fd, path_max_nr, mid + self->orig_have_mnt))
+			return -1;
+
+		if (try_mount(self))
+			r = mid;
+		else
+			l = mid;
+	}
+	if (r == VE_MOUNTS_MAX + 1) {
+		fprintf(stderr, "%s: %d avail mounts is not enough\n", __func__, r);
+		return -1;
+	}
+	if (write_u64_at(self->cgv2_fd, path_max_nr, VE_MOUNTS_MAX))
+		return -1;
+	return r;
+}
+
+/* Expect mount success and return new avail value */
+static int mount_and_get_avail(struct __test_metadata *_metadata,
+			FIXTURE_DATA(ve_mnt_acc) * self, int mnt_id)
+{
+	char path_avail[64];
+	int mnt_avail_nr;
+
+	snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+	ASSERT_EQ(create_mount(self, mnt_id), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	return mnt_avail_nr;
+}
+
+/* Expect mount failure and ensure intact avail number */
+static int check_mount_fails(struct __test_metadata *_metadata,
+			FIXTURE_DATA(ve_mnt_acc) * self,
+			int mnt_id, int avail_count)
+{
+	char path_avail[64];
+	int mnt_avail_nr;
+
+	snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+	if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr))
+		return 1;
+	if (mnt_avail_nr != avail_count) {
+		fprintf(stderr, "%s: mnt_avail(%d) != avail_count(%d) before mount\n",
+			__func__, mnt_avail_nr, avail_count);
+		return -1;
+	}
+
+	if (create_mount(self, mnt_id) == 0) {
+		fprintf(stderr, "%s: successful mount\n", __func__);
+		return -1;
+	}
+
+	if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr))
+		return 1;
+	if (mnt_avail_nr != avail_count) {
+		fprintf(stderr, "%s: mnt_avail(%d) != avail_count(%d) after mount\n",
+			__func__, mnt_avail_nr, avail_count);
+		return -1;
+	}
+	return 0;
+}
+
+FIXTURE_SETUP(ve_mnt_acc)
+{
+	int initial_mnt_avail_nr;
+	char path[64];
+
+	self->cgv2_fd = mount_cg2_fd();
+	ASSERT_GE(self->cgv2_fd, 0);
+	mkdir(TMP_DIR, 0755);
+
+	ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
+		  VE_CONTROLLERS), 0);
+
+	self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
+	ASSERT_GE(self->ctid, 0);
+
+	snprintf(path, sizeof(path), "%d/ve.mnt_max_nr", self->ctid);
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path, VE_MOUNTS_MAX), 0);
+
+	/*
+	 * The new ve cgroup has not been entered by anything yet, so its
+	 * mnt_avail_nr counter should be VE_MOUNTS_MAX.
+	 */
+	snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
+	ASSERT_EQ(initial_mnt_avail_nr, VE_MOUNTS_MAX);
+
+	ASSERT_EQ(prepare_namespaces(&self->ns_ctx, CLONE_NEWNS | CLONE_NEWVE,
+				self->cgv2_fd, self->ctid), 0);
+
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
+	self->orig_have_mnt = VE_MOUNTS_MAX - initial_mnt_avail_nr;
+
+	/* Calculate once - it is time-consuming */
+	if (*need_avail == -1)
+		*need_avail = get_required_avail_mnts(self);
+
+	ASSERT_GE(*need_avail, 1);
+	/* try_mount ensures cost is 1 */
+	ASSERT_EQ(try_mount(self), 1);
+};
+
+FIXTURE_TEARDOWN(ve_mnt_acc)
+{
+	for (int i = 0; i < MAX_MNT_ID; i++)
+		_destroy_mount((void *)&i);
+
+	close_namespace_fds(&self->ns_ctx);
+	destroy_ve(self->cgv2_fd, self->ctid);
+	close(self->cgv2_fd);
+	rmdir(TMP_DIR);
+}
+
+/* Simple test to check mount/umount accounting correctness */
+TEST_F(ve_mnt_acc, mount_umount)
+{
+	int original_mnt_avail, mnt_avail_nr;
+	char path[64];
+
+	snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &original_mnt_avail), 0);
+
+	ASSERT_LT(mount_and_get_avail(_metadata, self, 0),
+		  original_mnt_avail);
+
+	ASSERT_EQ(destroy_mount(self, 0), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, original_mnt_avail);
+}
+
+/* Test mount limit hits */
+TEST_F(ve_mnt_acc, hit_limits)
+{
+	int mnt_avail_nr;
+	char path_avail[64], path_max_nr[64];
+
+	snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+	snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+	/* Step 1: reduce number of available mounts to *need_avail */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+		self->orig_have_mnt + 1 * (*need_avail)), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, 1 * *need_avail);
+
+	/* Step 2: do one mount, no mounts should be available */
+	ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), *need_avail - 1);
+
+	/* Step 3: check that one more mount fails */
+	ASSERT_EQ(check_mount_fails(_metadata, self, 1, (*need_avail) - 1), 0);
+
+	/* Step 4: increase by 1 and win now */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+		self->orig_have_mnt + 1 + *need_avail), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, *need_avail);
+
+	ASSERT_EQ(mount_and_get_avail(_metadata, self, 1), *need_avail - 1);
+
+	/* Step 5: reduce mnt_max_nr so we have more mounts than allowed */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+				self->orig_have_mnt + 1), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, -1);
+
+	/* Step 6: try to do mount when avail < 0, ensure number is intact */
+	ASSERT_EQ(check_mount_fails(_metadata, self, 2, -1), 0);
+
+	/* Step 7: remove one mount, check avail value update, mount should fail */
+	ASSERT_EQ(destroy_mount(self, 0), 0);
+	ASSERT_EQ(check_mount_fails(_metadata, self, 2, 0), 0);
+
+	/* Step 8: remove one more mount and check that new mount succeeds */
+	ASSERT_EQ(destroy_mount(self, 1), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, 1);
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + *need_avail), 0);
+	ASSERT_EQ(mount_and_get_avail(_metadata, self, 2), *need_avail - 1);
+
+	ASSERT_EQ(destroy_mount(self, 2), 0);
+}
+
+/*
+ * Mount propagation makes one mount cost more.
+ * This test checks that if we run out of mounts in the middle of creating
+ * a new one, everything is restored smoothly and nothing leaks.
+ */
+TEST_F(ve_mnt_acc, partial_mounts)
+{
+	char path_avail[64], path_max_nr[64];
+	int i;
+
+	snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+	snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+	if (*need_avail == 1)
+		SKIP(return, "mount cost is 1, no partial mounts possible");
+
+	for (i = 0; i < *need_avail; i++) {
+		ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + i), 0);
+		ASSERT_EQ(check_mount_fails(_metadata, self, 0, i), 0);
+	}
+}
+
+/* Test that pseudosuper allows negative avail with correct accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_allows_overuse)
+{
+	char path_max_nr[64];
+
+	snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + *need_avail), 0);
+	ASSERT_EQ(set_pseudosuper(self, 1), 0);
+
+	ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), *need_avail - 1);
+	ASSERT_EQ(mount_and_get_avail(_metadata, self, 1), *need_avail - 2);
+
+	/* Go below zero */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt), 0);
+	ASSERT_EQ(mount_and_get_avail(_metadata, self, 2), -3);
+
+	ASSERT_EQ(set_pseudosuper(self, 0), 0);
+
+	/* Below zero should fail */
+	ASSERT_EQ(check_mount_fails(_metadata, self, 3, -3), 0);
+	/* Now need 2 more to add a new mount */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+		self->orig_have_mnt + 3 + *need_avail - 2), 0);
+	ASSERT_EQ(check_mount_fails(_metadata, self, 3, *need_avail - 2), 0);
+
+	ASSERT_EQ(destroy_mount(self, 0), 0);
+	ASSERT_EQ(check_mount_fails(_metadata, self, 3, *need_avail - 1), 0);
+	ASSERT_EQ(destroy_mount(self, 2), 0);
+
+	ASSERT_EQ(try_mount(self), 1);
+}
+
+/* Test that pseudosuper doesn't disable accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_continues_accounting)
+{
+	int orig_mnt_avail, mnt_avail_nr;
+	char path_avail[64];
+
+	snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &orig_mnt_avail), 0);
+
+	ASSERT_EQ(set_pseudosuper(self, 0), 0);
+
+	/* mnt 0 - mounted without pseudosuper, umounted with it. */
+	ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), orig_mnt_avail - 1);
+	ASSERT_EQ(set_pseudosuper(self, 1), 0);
+
+	/* Cost is the same when mount/umount happen under pseudosuper. */
+	ASSERT_EQ(try_mount(self), 1);
+
+	/* mnt 1 - mounted with pseudosuper, umounted without it. */
+	ASSERT_EQ(mount_and_get_avail(_metadata, self, 1),
+		  orig_mnt_avail - 2);
+
+	ASSERT_EQ(destroy_mount(self, 0), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, orig_mnt_avail - 1);
+
+	ASSERT_EQ(set_pseudosuper(self, 0), 0);
+
+	ASSERT_EQ(destroy_mount(self, 1), 0);
+	ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+	ASSERT_EQ(mnt_avail_nr, orig_mnt_avail);
+}
+
+/* Test failcount feature */
+TEST_F(ve_mnt_acc, failcount)
+{
+	char path_fc[64], failcount_str[512], path_max_nr[64];
+
+	snprintf(path_fc, sizeof(path_fc), "%d/ve.failcount", self->ctid);
+	snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+	ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+			       failcount_str, sizeof(failcount_str)), 0);
+	ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
+
+	/* Check successful mount doesn't affect failcount */
+	mount_and_get_avail(_metadata, self, 0);
+	ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+			       failcount_str, sizeof(failcount_str)), 0);
+	ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
+	ASSERT_EQ(destroy_mount(self, 0), 0);
+
+	/* Check failcount update when mount fails */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, 0), 0);
+	ASSERT_LT(create_mount(self, 1), 0);
+	ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+			       failcount_str, sizeof(failcount_str)), 0);
+	ASSERT_TRUE(strstr(failcount_str, "mnt: 1\n") != NULL);
+
+	/* Check failcount flush */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_fc, 0), 0);
+	ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+			       failcount_str, sizeof(failcount_str)), 0);
+	ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
+
+	/* Check failcount update when mount fails again */
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, 0), 0);
+	ASSERT_LT(create_mount(self, 1), 0);
+	ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+			       failcount_str, sizeof(failcount_str)), 0);
+	ASSERT_TRUE(strstr(failcount_str, "mnt: 1\n") != NULL);
+}
+
+int main(int argc, char **argv)
+{
+	need_avail = mmap(NULL, sizeof(int),
+		PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+
+	if (need_avail == MAP_FAILED) {
+		fprintf(stderr, "Failed to mmap shared region: %s\n", strerror(errno));
+		return 1;
+	}
+	*need_avail = -1;
+
+	return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index 7b45f91c1d561..1fc3f8dcac180 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -416,4 +416,25 @@ static inline int is_mounted(const char *path)
 	return errno == EXDEV ? 1 : -1;
 }
 
+static inline int do_rcu_barrier(void)
+{
+	int fd;
+
+	errno = 0;
+	fd = open("/sys/module/rcutree/parameters/do_rcu_barrier", O_WRONLY);
+
+	if (fd < 0) {
+		fprintf(stderr, "Failed to open do_rcu_barrier: %s\n", strerror(errno));
+		return -1;
+	}
+	errno = 0;
+	if (write(fd, "1", 2) != 2) {
+		fprintf(stderr, "Failed to issue rcu_barrier: %s\n", strerror(errno));
+		close(fd);
+		return -1;
+	}
+	close(fd);
+	return 0;
+}
+
 #endif /* __SELFTESTS_VE_VE_SELFTEST_H */

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 6%]

* [Devel] [PATCH hci-8.1 v10 10/13] ve: add netif failcounter
                     ` (2 preceding siblings ...)
  2026-09-25 16:53  4% ` [Devel] [PATCH hci-8.1 v10 09/13] ve: add mnt failcounter Konstantin Khorenko
@ 2026-09-25 16:53  6% ` Konstantin Khorenko
  2026-09-25 16:53  6% ` [Devel] [PATCH hci-8.1 v10 13/13] selftests/ve: add mount accounting selftest Konstantin Khorenko
  4 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:53 UTC (permalink / raw)
  To: Vasileios Almpanis, Pavel Tikhomirov,
	cover.1789491989.git.vladimir.riabchun, Konstantin Khorenko,
	devel

From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

Count hits of the per-VE network interface limit in ve.failcount.
The per-site "increase ve::netif_max_nr" hints are replaced by the
unified message from ve_failcount_inc.

https://virtuozzo.atlassian.net/browse/VSTOR-135520

Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 include/linux/ve.h |  1 +
 kernel/ve/ve.c     |  3 +++
 net/core/dev.c     | 10 ++--------
 3 files changed, 6 insertions(+), 8 deletions(-)

diff --git a/include/linux/ve.h b/include/linux/ve.h
index 0395e37208849..5d94470692f72 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -84,6 +84,7 @@ struct ve_struct {
 	int			netns_max_nr;
 
 	atomic_t		netif_avail_nr;
+	atomic_t		netif_failcount;
 	int			netif_max_nr;
 
 	atomic_t		bpf_prog_avail_nr;
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index 4c3acda6b135e..8505c1dae4a18 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -103,6 +103,7 @@ struct ve_struct ve0 = {
 	.netns_failcount	= ATOMIC_INIT(0),
 	.netns_max_nr		= INT_MAX,
 	.netif_avail_nr		= ATOMIC_INIT(INT_MAX),
+	.netif_failcount	= ATOMIC_INIT(0),
 	.netif_max_nr		= INT_MAX,
 	.bpf_prog_avail_nr	= ATOMIC_INIT(INT_MAX),
 	.bpf_prog_max_nr	= INT_MAX,
@@ -798,6 +799,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
 	ve->netns_max_nr = NETNS_MAX_NR_DEFAULT;
 
 	atomic_set(&ve->netif_avail_nr, NETIF_MAX_NR_DEFAULT);
+	atomic_set(&ve->netif_failcount, 0);
 	ve->netif_max_nr = NETIF_MAX_NR_DEFAULT;
 
 	atomic_set(&ve->bpf_prog_avail_nr, BPF_PROG_MAX_NR_DEFAULT);
@@ -1090,6 +1092,7 @@ static const struct ve_failcount_entry {
 #define VE_FC_ENTRY(name) { #name, offsetof(struct ve_struct, name##_failcount) }
 	VE_FC_ENTRY(netns),
 	VE_FC_ENTRY(mnt),
+	VE_FC_ENTRY(netif),
 	{}
 };
 
diff --git a/net/core/dev.c b/net/core/dev.c
index c7dddb200489f..7632ad1fd0151 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -10997,10 +10997,7 @@ int register_netdevice(struct net_device *dev)
 
 	ret = -ENOMEM;
 	if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
-		ve_pr_warn_ratelimited(VE_LOG_BOTH,
-			"CT%s: hits max number of network devices, "
-			"increase ve::netif_max_nr parameter\n",
-			ve_name(net->owner_ve));
+		ve_failcount_inc(net->owner_ve, netif);
 		return ret;
 	}
 
@@ -12211,10 +12208,7 @@ int __dev_change_net_namespace(struct net_device *dev, struct net *net,
 
 	err = -ENOMEM;
 	if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
-		ve_pr_warn_ratelimited(VE_LOG_BOTH,
-			"CT%s: hits max number of network devices, "
-			"increase ve::netif_max_nr parameter\n",
-			ve_name(net->owner_ve));
+		ve_failcount_inc(net->owner_ve, netif);
 		goto out;
 	}
 	atomic_inc(&dev_net(dev)->owner_ve->netif_avail_nr);

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 6%]

* [QEMU HCI-7.4 PATCH] migration: set .vm_old_state in migration_stop_vm() #VSTOR-147066
@ 2026-09-28 13:23 30% Andrey Drobyshev
  2026-09-28 16:51  0% ` Denis V. Lunev
  0 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-09-28 13:23 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

Currently, when a postcopy migration starts, the .vm_old_state isn't
being recorded.  Thus it remains to be set to -1.  That might result into
a VM crash in case migration fails:

  migration_iteration_finish(s)
    runstate_set(s->vm_old_state=-1)
      assert(new_state < RUN_STATE__MAX)

To avoid that, let's just move saving the previous state directly into
the migration_stop_vm() helper.  Add 2nd function argument to
migration_stop_vm() to achieve that.

This code is mimicking what's already done in hci-8.0 and master.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 migration/migration.c | 21 ++++++++++-----------
 migration/migration.h |  2 +-
 2 files changed, 11 insertions(+), 12 deletions(-)

diff --git a/migration/migration.c b/migration/migration.c
index 00924b4a2a7..8f33181e00a 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -157,9 +157,14 @@ static gint page_request_addr_cmp(gconstpointer ap, gconstpointer bp)
     return (a > b) - (a < b);
 }
 
-int migration_stop_vm(RunState state)
+int migration_stop_vm(MigrationState *s, RunState state)
 {
-    int ret = vm_stop_force_state(state);
+    int ret;
+
+    s->vm_old_state = runstate_get();
+    global_state_store();
+
+    ret = vm_stop_force_state(state);
 
     trace_vmstate_downtime_checkpoint("src-vm-stopped");
 
@@ -2460,8 +2465,7 @@ static int postcopy_start(MigrationState *ms, Error **errp)
     migration_downtime_start(ms);
 
     qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
-    global_state_store();
-    ret = migration_stop_vm(RUN_STATE_FINISH_MIGRATE);
+    ret = migration_stop_vm(ms, RUN_STATE_FINISH_MIGRATE);
     if (ret < 0) {
         goto fail;
     }
@@ -2660,10 +2664,7 @@ static int migration_completion_precopy(MigrationState *s,
     migration_downtime_start(s);
     qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
 
-    s->vm_old_state = runstate_get();
-    global_state_store();
-
-    ret = migration_stop_vm(RUN_STATE_FINISH_MIGRATE);
+    ret = migration_stop_vm(s, RUN_STATE_FINISH_MIGRATE);
     trace_migration_completion_vm_stop(ret);
     if (ret < 0) {
         goto out_unlock;
@@ -3520,11 +3521,9 @@ static void *bg_migration_thread(void *opaque)
      * transition in vm_stop_force_state() we need to wakeup it up.
      */
     qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
-    s->vm_old_state = runstate_get();
 
-    global_state_store();
     /* Forcibly stop VM before saving state of vCPUs and devices */
-    if (migration_stop_vm(RUN_STATE_PAUSED)) {
+    if (migration_stop_vm(s, RUN_STATE_PAUSED)) {
         goto fail;
     }
     /*
diff --git a/migration/migration.h b/migration/migration.h
index f2704a0e3bd..fc33ee465ac 100644
--- a/migration/migration.h
+++ b/migration/migration.h
@@ -549,6 +549,6 @@ int migration_rp_wait(MigrationState *s);
  */
 void migration_rp_kick(MigrationState *s);
 
-int migration_stop_vm(RunState state);
+int migration_stop_vm(MigrationState *s, RunState state);
 
 #endif
-- 
2.47.1


^ permalink raw reply	[relevance 30%]

* [Devel] [PATCH hci-8.1 v4 1/2] selftests: ve: grant the directories the files sit in
  @ 2026-09-28 15:16 11% ` Eva Kurchatova
  2026-09-28 15:16  6% ` [Devel] [PATCH hci-8.1 v4 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
  1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-28 15:16 UTC (permalink / raw)
  To: khorenko; +Cc: devel

The sysfs variant names nested leaves, kernel/vmcoreinfo and
kernel/notes, while the proc one names files at the top of the
filesystem. Both enforcement tests grant the leaves and nothing else,
so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
cannot traverse, and every open answers ENOENT:

  not ok 14 ve_perms.sysfs.enforce_allows
  not ok 15 ve_perms.sysfs.enforce_denies

Grant x on each directory along the way, taken from the path the
variant already gives.

Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")

https://virtuozzo.atlassian.net/browse/VSTOR-144955

Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/ve/ve_perms_test.c | 34 ++++++++++++++++++++--
 1 file changed, 32 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 25ffbd42c380..8096b1fa3b43 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -114,9 +114,11 @@ static int ve_open_rel(int cgv2_fd, int ctid, const char *prefix,
 static int set_entry(int cgv2_fd, const char *path, const char *entry,
 		     const char *mask)
 {
-	char line[256];
+	char line[PATH_MAX];
 
-	snprintf(line, sizeof(line), "%s %s", entry, mask);
+	if (snprintf(line, sizeof(line), "%s %s", entry, mask) >=
+	    (int)sizeof(line))
+		return -1;
 	return write_file_at(cgv2_fd, path, line);
 }
 
@@ -343,6 +345,32 @@ TEST_F(ve_perms, write_parser)
 	EXPECT_LT(write_file_at(self->cgv2_fd, path, file), 0);
 }
 
+/* A leaf is reachable only once every directory above it can be traversed */
+static int allow_path(int cgv2_fd, const char *path, const char *file)
+{
+	char dir[PATH_MAX];
+	const char *sep;
+
+	for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
+		if (snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file),
+			     file) >= (int)sizeof(dir))
+			return -1;
+		if (set_entry(cgv2_fd, path, dir, "x") < 0)
+			return -1;
+	}
+	return 0;
+}
+
+static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
+{
+	int i;
+
+	for (i = 0; i < NR_FILES; i++)
+		if (allow_path(cgv2_fd, path, files[i]) < 0)
+			return -1;
+	return 0;
+}
+
 /*
  * Enforcement, allowed side: from inside a VE, granting r lets each file be
  * read, and granting x on the directory lets it be traversed (the lookup then
@@ -358,6 +386,7 @@ TEST_F(ve_perms, enforce_allows)
 
 	ve_file(path, sizeof(path), self->ctid_a, variant->fs);
 
+	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
 	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
 	for (i = 0; i < NR_FILES; i++)
 		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
@@ -400,6 +429,7 @@ TEST_F(ve_perms, enforce_denies)
 				      O_RDONLY), 0);
 
 	/* Granted w only: visible but not readable. */
+	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
 	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
 	for (i = 0; i < NR_FILES; i++)
 		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 11%]

* [Devel] [PATCH hci-8.1 v4 2/2] selftests: ve: introduce vtty pair teardown stress test
    2026-09-28 15:16 11% ` [Devel] [PATCH hci-8.1 v4 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
@ 2026-09-28 15:16  6% ` Eva Kurchatova
  1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-28 15:16 UTC (permalink / raw)
  To: khorenko; +Cc: devel

Either end of a vtty pair creates it, and the last close tears it down.

Race condition in older VZKernel vtty implementation allowed both sides
to decide they were the final one and call release_tty() twice, and a
master opening again while a teardown was in flight used to hand out a
reference to a tty_struct about to be freed. Either way, this caused
use-after-free. That issue was fixed in c5028cd61a69:
"ve/vtty: fix use-after-free on concurrent tty close and reopen"

Hammer both ends for ownership correctness & thread safety verification.

The original reproducer needed a few tens of minutes across a dozen
containers, where vzctl interaction with vtty subsystem was a small part
of the overall runtime, therefore it did not fire on every run. This test
opens and closes nothing but the pair, from both ends and without pause,
so the default stays in the tens of seconds and under the timeout of the
suite, and VE_VTTY_SECS or argv[1] allows to control this.

The test assumes the veid it picks is not in use by a running container:
ve_id_write() does not check that a veid is unique and the vtty map is
keyed by veid, so a clash would hand the test someone else's pair. The
108..200 range make_ve() uses, makes that unlikely on a test node.

https://virtuozzo.atlassian.net/browse/VSTOR-144771

Feature: ve selftest
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/ve/.gitignore       |   1 +
 tools/testing/selftests/ve/Makefile         |   1 +
 tools/testing/selftests/ve/ve_vtty_stress.c | 314 ++++++++++++++++++++
 3 files changed, 316 insertions(+)
 create mode 100644 tools/testing/selftests/ve/ve_vtty_stress.c

diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index afa4c568c2c9..9d8d86093827 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,2 +1,3 @@
 ve_ns_owner_test
 ve_perms_test
+ve_vtty_stress
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index ec40cbc7b3a1..6cd5823c4d93 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
 
 TEST_GEN_PROGS += ve_ns_owner_test
 TEST_GEN_PROGS += ve_perms_test
+TEST_GEN_PROGS += ve_vtty_stress
 
 include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
new file mode 100644
index 000000000000..85cb6fdb3fcc
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_vtty_stress.c
@@ -0,0 +1,314 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_vtty_stress - open and close a vtty pair from both ends at once.
+ *
+ * Either end of a vtty pair creates it, and the last close tears it down.
+ *
+ * Hammer both ends. The master comes from the vzctl ioctl, which does
+ * not look the VE up. The slave is reachable only from inside the VE, as
+ * TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
+ * vttys index minor - 1, so the test builds a VE with cgroup2, sets
+ * ve.veid to the cgroup it made, and runs the slave side there.
+ *
+ * A warning does not stop the host, so the run reads the kernel log too.
+ * One read() of /dev/kmsg is one record, so the test matches vtty driver
+ * splats, as well as generic kernel warning/oops/kasan reports.
+ *
+ * Usage: ve_vtty_stress [seconds] (default 30, or $VE_VTTY_SECS)
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <stdbool.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <linux/major.h>
+#include <sys/sysmacros.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "ve_selftest.h"
+#include "../kselftest.h"
+
+/* The ioctl argument; the trailing data[] the uapi struct has is unused. */
+struct vzctl_ve_configure {
+	unsigned int veid, key, val, size;
+};
+
+#define VZCTL_VE_CONFIGURE _IOW('.', 15, struct vzctl_ve_configure)
+#define VE_CONFIGURE_OPEN_TTY 5
+
+#define VTTY_IDX 0 /* The pair both ends work on */
+#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
+#define DEFAULT_SECS 30
+
+/*
+ * Expected open errno codes: the pair may be closing, or gone already,
+ * when an open lands on it.
+ * Anything else is an environment issue or a bug.
+ */
+static bool expected_errno(int err)
+{
+	return err == EBUSY || err == EIO || err == ENXIO;
+}
+
+static int open_master(int ctl_fd, unsigned int veid)
+{
+	struct vzctl_ve_configure c = {
+		.veid = veid,
+		.key = VE_CONFIGURE_OPEN_TTY,
+		.val = VTTY_IDX,
+	};
+
+	return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
+}
+
+/* The slave end, inside the VE. Returns the errno that stopped it, or 0. */
+static int hammer_slave(const char *node, time_t deadline,
+			unsigned long *opened)
+{
+	int err = 0;
+
+	while (time(NULL) < deadline) {
+		int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
+
+		if (fd >= 0) {
+			(*opened)++;
+			close(fd);
+		} else if (!expected_errno(errno)) {
+			err = errno;
+			break;
+		}
+	}
+
+	return err;
+}
+
+/* The kernel log, since a warning fails nothing by itself. */
+static int kmsg_open(void)
+{
+	int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+
+	if (fd >= 0)
+		lseek(fd, 0, SEEK_END);
+	return fd;
+}
+
+/*
+ * Check for kernel log warnings, one read() per record, hence
+ * the function name is not checked.
+ */
+static int kmsg_check(int fd)
+{
+	char buf[8192];
+	int found = 0;
+	ssize_t r;
+
+	if (fd < 0)
+		return 0;
+
+	for (;;) {
+		r = read(fd, buf, sizeof(buf) - 1);
+		if (r < 0) {
+			/* Ring overrun, continue with the oldest existing record */
+			if (errno == EPIPE)
+				continue;
+			break;
+		}
+		if (r == 0)
+			break;
+		buf[r] = '\0';
+		/* Known symptoms of a buggy kernel */
+		if (strstr(buf, "vtty") && (strstr(buf, "tty->count") ||
+					    strstr(buf, "bad slave count"))) {
+			ksft_print_msg("vtty mismatched count log: %s", buf);
+			found++;
+		}
+		if (strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") ||
+		    strstr(buf, ";BUG: ") || strstr(buf, "soft lockup - ") ||
+		    strstr(buf, "general protection fault")) {
+			ksft_print_msg("kernel log: %s", buf);
+			found++;
+		}
+	}
+	return found;
+}
+
+int main(int argc, char **argv)
+{
+	unsigned long master_opened = 0, slave_opened = 0;
+	int cg, ctid, ctl, kmsg, ready[2], count[2], status;
+	int master_err = 0, slave_err;
+	const char *env = getenv("VE_VTTY_SECS");
+	char path[PATH_MAX], val[16], c;
+	char dir[] = "/tmp/ve_vtty_XXXXXX";
+	char node[sizeof(dir) + 6];
+	time_t deadline;
+	long secs;
+	pid_t pid;
+
+	ksft_print_header();
+	secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
+	if (secs <= 0)
+		secs = DEFAULT_SECS;
+
+	ctl = open("/dev/vzctl", O_RDWR);
+	if (ctl < 0)
+		ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
+			       "(vzdev, vzmon) are needed\n",
+			       strerror(errno));
+
+	cg = mount_cg2_fd();
+	if (cg < 0)
+		ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
+	write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS);
+
+	ctid = make_ve(cg, CTID_MIN);
+	if (ctid < 0)
+		ksft_exit_skip("cannot make a VE cgroup\n");
+
+	/* The ve.veid must match between master / slave */
+	snprintf(path, sizeof(path), "%d/ve.veid", ctid);
+	snprintf(val, sizeof(val), "%d", ctid);
+	if (write_file_at(cg, path, val) < 0) {
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
+	}
+
+	/* Do not report a failure where the vtty pair cannot be had at all */
+	status = open_master(ctl, ctid);
+	if (status < 0 && errno != EBUSY) {
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("no vtty master: %s\n", strerror(errno));
+	}
+	if (status >= 0)
+		close(status);
+
+	/*
+	 * Use temp directory for the node the slave end opens.
+	 * Cleaned up by the master side instead of relying on the slave.
+	 */
+	if (!mkdtemp(dir)) {
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("mkdtemp: %s\n", strerror(errno));
+	}
+	snprintf(node, sizeof(node), "%s/slave", dir);
+	if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0) {
+		status = errno;
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("mknod: %s\n", strerror(status));
+	}
+
+	ksft_set_plan(1);
+	kmsg = kmsg_open();
+	if (kmsg < 0)
+		ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
+			       "be noticed\n",
+			       strerror(errno));
+
+	if (pipe(ready) || pipe(count)) {
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("pipe: %s\n", strerror(errno));
+	}
+
+	deadline = time(NULL) + secs;
+	ksft_print_msg("VE %d, pair %d, %ld seconds\n", ctid, VTTY_IDX, secs);
+
+	pid = fork();
+	if (pid == 0) {
+		close(ready[0]);
+		close(count[0]);
+		if (enter_cgroup(cg, ctid) < 0)
+			_exit(errno ? errno : 1);
+		/* CLONE_NEWVE puts the task in the VE of the cgroup */
+		if (unshare(CLONE_NEWVE | CLONE_NEWPID | CLONE_NEWNS) < 0)
+			_exit(errno ? errno : 1);
+		if (fork() == 0) {
+			unsigned long n = 0;
+			int err;
+
+			/*
+			 * Hammering on after the run has given up would hold
+			 * the cgroup open against destroy_ve().
+			 */
+			prctl(PR_SET_PDEATHSIG, SIGKILL);
+
+			c = 'r';
+			if (write(ready[1], &c, 1) != 1)
+				_exit(errno);
+			err = hammer_slave(node, deadline, &n);
+			/* Send the count either way: it says how far it got. */
+			if (write(count[1], &n, sizeof(n)) != sizeof(n))
+				_exit(errno);
+			_exit(err);
+		}
+		wait(&status);
+		_exit(WIFEXITED(status) ? WEXITSTATUS(status) : 1);
+	} else if (pid < 0) {
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("fork: %s\n", strerror(errno));
+	}
+	close(ready[1]);
+	close(count[1]);
+
+	if (read(ready[0], &c, 1) != 1) {
+		waitpid(pid, &status, 0);
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("the VE side never came up\n");
+	}
+
+	while (time(NULL) < deadline) {
+		int fd = open_master(ctl, ctid);
+
+		if (fd >= 0) {
+			master_opened++;
+			close(fd);
+		} else if (!expected_errno(errno)) {
+			master_err = errno;
+			break;
+		}
+	}
+
+	if (master_err) {
+		/* The other end runs to its own deadline, stop it */
+		kill(pid, SIGKILL);
+		waitpid(pid, &status, 0);
+		slave_err = 0;
+	} else {
+		if (read(count[0], &slave_opened, sizeof(slave_opened)) !=
+		    sizeof(slave_opened))
+			slave_opened = 0;
+		waitpid(pid, &status, 0);
+		slave_err = WIFEXITED(status) ? WEXITSTATUS(status) : EINTR;
+	}
+
+	ksft_print_msg("%lu master opens, %lu slave opens\n", master_opened,
+		       slave_opened);
+
+	status = kmsg_check(kmsg);
+	unlink(node);
+	rmdir(dir);
+	destroy_ve(cg, ctid);
+
+	if (master_err)
+		ksft_exit_fail_msg("the master end stopped: %s\n",
+				   strerror(master_err));
+	if (slave_err)
+		ksft_exit_fail_msg("the slave end stopped: %s\n",
+				   strerror(slave_err));
+	if (!master_opened)
+		ksft_exit_fail_msg("no master was ever opened\n");
+	if (!slave_opened)
+		ksft_exit_fail_msg("no slave was ever opened\n");
+	if (status)
+		ksft_exit_fail_msg("kernel complained %d time(s)\n", status);
+
+	ksft_test_result_pass("vtty pair closed from both ends\n");
+	ksft_finished();
+}
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 6%]

* [Devel] [PATCH hci-8.1 2/2] selftests/ve: check that host can go over container mount limit
  @ 2026-09-28 15:24  5% ` Pavel Tikhomirov
  0 siblings, 0 replies; 200+ results
From: Pavel Tikhomirov @ 2026-09-28 15:24 UTC (permalink / raw)
  To: Konstantin Khorenko, Vladimir Riabchun, Vasileios Almpanis; +Cc: devel

Like pseudosuper_allows_overuse, check that a bind mount done from ve0
in the container mount namespace succeeds when the container has no
mounts left, and is charged to the container.

https://virtuozzo.atlassian.net/browse/VSTOR-147661
Feature: per-ve failcounters
Signed-off-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
---
 .../selftests/ve/ve_mount_accounting_test.c        | 37 ++++++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/tools/testing/selftests/ve/ve_mount_accounting_test.c b/tools/testing/selftests/ve/ve_mount_accounting_test.c
index e2a882137e11d..8bb13e9615427 100644
--- a/tools/testing/selftests/ve/ve_mount_accounting_test.c
+++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
@@ -539,6 +539,43 @@ TEST_F(ve_mnt_acc, failcount)
 	ASSERT_TRUE(strstr(failcount_str, "mnt: 1\n") != NULL);
 }
 
+static int _create_bind_mount(void *id_ptr)
+{
+	char path[PATH_MAX];
+	int id = *(int *)id_ptr;
+
+	snprintf(path, sizeof(path), TMP_DIR "%d", id);
+
+	if (mkdir(path, 0755) < 0) {
+		fprintf(stderr, "Failed to create directory %s: %s\n", path, strerror(errno));
+		return -1;
+	}
+	if (!mount(path, path, NULL, MS_BIND, NULL))
+		return 0;
+	fprintf(stderr, "Failed to bind mount %s: %s\n", path, strerror(errno));
+
+	rmdir(path);
+	return -1;
+}
+
+/* Test that host can go over the limit in container mount namespace. */
+TEST_F(ve_mnt_acc, host_allows_overuse)
+{
+	char path_max_nr[64];
+	int id = 0;
+
+	snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+	ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt), 0);
+
+	/* Bind mount from ve0, charged to the container's mount namespace owner */
+	ASSERT_EQ(run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
+			    CLONE_NEWNS, _create_bind_mount, &id), 0);
+	ASSERT_EQ(wait_on_mnt_avail(self, -1), 0);
+
+	ASSERT_EQ(destroy_mount(self, 0), 0);
+}
+
 int main(int argc, char **argv)
 {
 	need_avail = mmap(NULL, sizeof(int),

-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 5%]

* Re: [QEMU HCI-7.4 PATCH] migration: set .vm_old_state in migration_stop_vm() #VSTOR-147066
  2026-09-28 13:23 30% [QEMU HCI-7.4 PATCH] migration: set .vm_old_state in migration_stop_vm() #VSTOR-147066 Andrey Drobyshev
@ 2026-09-28 16:51  0% ` Denis V. Lunev
  0 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-28 16:51 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: den

On 9/28/26 15:23, Andrey Drobyshev wrote:
> Currently, when a postcopy migration starts, the .vm_old_state isn't
> being recorded.  Thus it remains to be set to -1.  That might result into
> a VM crash in case migration fails:
>
>   migration_iteration_finish(s)
>     runstate_set(s->vm_old_state=-1)
>       assert(new_state < RUN_STATE__MAX)
>
> To avoid that, let's just move saving the previous state directly into
> the migration_stop_vm() helper.  Add 2nd function argument to
> migration_stop_vm() to achieve that.
>
> This code is mimicking what's already done in hci-8.0 and master.
>
> Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
> ---
>  migration/migration.c | 21 ++++++++++-----------
>  migration/migration.h |  2 +-
>  2 files changed, 11 insertions(+), 12 deletions(-)
>
> diff --git a/migration/migration.c b/migration/migration.c
> index 00924b4a2a7..8f33181e00a 100644
> --- a/migration/migration.c
> +++ b/migration/migration.c
> @@ -157,9 +157,14 @@ static gint page_request_addr_cmp(gconstpointer ap, gconstpointer bp)
>      return (a > b) - (a < b);
>  }
>  
> -int migration_stop_vm(RunState state)
> +int migration_stop_vm(MigrationState *s, RunState state)
>  {
> -    int ret = vm_stop_force_state(state);
> +    int ret;
> +
> +    s->vm_old_state = runstate_get();
> +    global_state_store();
> +
> +    ret = vm_stop_force_state(state);
>  
>      trace_vmstate_downtime_checkpoint("src-vm-stopped");
>  
> @@ -2460,8 +2465,7 @@ static int postcopy_start(MigrationState *ms, Error **errp)
>      migration_downtime_start(ms);
>  
>      qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
> -    global_state_store();
> -    ret = migration_stop_vm(RUN_STATE_FINISH_MIGRATE);
> +    ret = migration_stop_vm(ms, RUN_STATE_FINISH_MIGRATE);
>      if (ret < 0) {
>          goto fail;
>      }
> @@ -2660,10 +2664,7 @@ static int migration_completion_precopy(MigrationState *s,
>      migration_downtime_start(s);
>      qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
>  
> -    s->vm_old_state = runstate_get();
> -    global_state_store();
> -
> -    ret = migration_stop_vm(RUN_STATE_FINISH_MIGRATE);
> +    ret = migration_stop_vm(s, RUN_STATE_FINISH_MIGRATE);
>      trace_migration_completion_vm_stop(ret);
>      if (ret < 0) {
>          goto out_unlock;
> @@ -3520,11 +3521,9 @@ static void *bg_migration_thread(void *opaque)
>       * transition in vm_stop_force_state() we need to wakeup it up.
>       */
>      qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
> -    s->vm_old_state = runstate_get();
>  
> -    global_state_store();
>      /* Forcibly stop VM before saving state of vCPUs and devices */
> -    if (migration_stop_vm(RUN_STATE_PAUSED)) {
> +    if (migration_stop_vm(s, RUN_STATE_PAUSED)) {
>          goto fail;
>      }
>      /*
> diff --git a/migration/migration.h b/migration/migration.h
> index f2704a0e3bd..fc33ee465ac 100644
> --- a/migration/migration.h
> +++ b/migration/migration.h
> @@ -549,6 +549,6 @@ int migration_rp_wait(MigrationState *s);
>   */
>  void migration_rp_kick(MigrationState *s);
>  
> -int migration_stop_vm(RunState state);
> +int migration_stop_vm(MigrationState *s, RunState state);
>  
>  #endif
  Why it isn't needed for this ticket. In this incident the destination
crashed first, and the source then aborted on runstate_set(-1). With your
  destination fix, bitmap-only post-copy migration completes, so the
source failure path is never reached. The source bug now fires only when a
  post-copy migration fails for some other reason, such as the
destination dying or a stream error during completion. Your two commits
apply cleanly
  to origin/hci-7.4 without Andrey's patch.
 
  Why his patch alone is risky. Once vm_old_state is RUNNING,
migration_iteration_finish() on 7.4 handles any post-copy failure the
same way:
  bdrv_activate_all(), then vm_start() on the source. That also happens
after the device package has been sent and the destination has run the
  guest.
  - In this incident the destination ran the guest for about 4 seconds,
writing to shared NFS.
  - If the destination is dead, its image locks are gone, so activation
succeeds.
  - The source then resumes with RAM that is older than the disk. That
risks silent guest filesystem corruption.
  - Today's abort loses the VM but leaves the disk crash-consistent,
which is arguably the safer failure.
  
  Upstream says the same in 26f65c01edc (v10.2): once the destination
has started, "dest QEMU's RAM is the only version that is consistent with
  current status of the shared storage", so the source must not start
the VM. That commit only covers the case where the destination still
holds the
  locks.

  Why not take upstream:
  - 9867d4ddd04 is the commit that fixed vm_old_state upstream. It
conflicts on 7.4: it depends on the notifier rework series, about 8
commits that
    touch spice, virtio-net and vfio. It also changes CPR-reboot
behaviour by stopping the VM at migration start. That's too much for a
hotfix, and
    it brings the same restart-after-destination-ran behaviour.
  - 7b842fe354c (POSTCOPY_DEVICE, v10.2) is the real upstream answer. It
lets the source resume only while the destination is still loading device
    state and hasn't started the guest. It is too big a backport to qemu
8.2.

^ permalink raw reply	[relevance 0%]

* [Devel] [PATCH hci-8.1 v2 3/3] selftests: drv-net: read the channel count over netlink
  @ 2026-09-28 19:37  4% ` Eva Kurchatova
  0 siblings, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-28 19:37 UTC (permalink / raw)
  To: khorenko; +Cc: devel

napi_threaded.py asks ethtool for the channel count as JSON, which
ethtool 6.11 cannot print for -l:

  ethtool: bad command line argument(s)
  JSON output not available for this subcommand

  not ok 1 napi_threaded.napi_init

Read the count over ethtool netlink instead, so the test does not
depend on the ethtool version installed.

https://virtuozzo.atlassian.net/browse/VSTOR-139651
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/drivers/net/napi_threaded.py | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/drivers/net/napi_threaded.py b/tools/testing/selftests/drivers/net/napi_threaded.py
index ed66efa481b0..33cc3f8752d3 100755
--- a/tools/testing/selftests/drivers/net/napi_threaded.py
+++ b/tools/testing/selftests/drivers/net/napi_threaded.py
@@ -7,7 +7,7 @@ Test napi threaded states.
 
 from lib.py import ksft_run, ksft_exit
 from lib.py import ksft_eq, ksft_ne, ksft_ge
-from lib.py import NetDrvEnv, NetdevFamily
+from lib.py import EthtoolFamily, NetDrvEnv, NetdevFamily
 from lib.py import cmd, defer, ethtool
 
 
@@ -28,7 +28,9 @@ def _set_threaded_state(cfg, threaded) -> None:
 
 
 def _setup_deferred_cleanup(cfg) -> None:
-    combined = ethtool(f"-l {cfg.ifname}", json=True)[0].get("combined", 0)
+    # Not every ethtool prints -l as JSON, ask the kernel
+    chan = EthtoolFamily().channels_get({'header': {'dev-index': cfg.ifindex}})
+    combined = chan.get('combined-count', 0)
     ksft_ge(combined, 2)
     defer(ethtool, f"-L {cfg.ifname} combined {combined}")
 
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH hci-8.1] ms/selftests: net: reduce txtimestamp deschedule flakes
@ 2026-09-29 12:48  6% Eva Kurchatova
  2026-09-29 12:52  0% ` no-reply
  0 siblings, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-09-29 12:48 UTC (permalink / raw)
  To: khorenko; +Cc: devel

From: Willem de Bruijn <willemb@google.com>

This test occasionally fails due to exceeding timing bounds, as
run in continuous testing on netdev.bots:

  https://netdev.bots.linux.dev/contest.html?test=txtimestamp-sh

A common pattern is a single elevated delay between USR and SND.

    # 8.36 [+0.00] test SND
    # 8.36 [+0.00]     USR: 1767864384 s 240994 us (seq=0, len=0)
    # 8.44 [+0.08] ERROR: 18461 us expected between 10000 and 18000
    # 8.44 [+0.00]     SND: 1767864384 s 259455 us (seq=42, len=10)  (USR +18460 us)
    # 8.52 [+0.07]     SND: 1767864384 s 339523 us (seq=42, len=10)  (USR +10005 us)
    # 8.52 [+0.00]     USR: 1767864384 s 409580 us (seq=0, len=0)
    # 8.60 [+0.08]     SND: 1767864384 s 419586 us (seq=42, len=10)  (USR +10005 us)
    # 8.60 [+0.00]     USR: 1767864384 s 489645 us (seq=0, len=0)
    # 8.68 [+0.08]     SND: 1767864384 s 499651 us (seq=42, len=10)  (USR +10005 us)
    # 8.68 [+0.00]     USR-SND: count=4, avg=12119 us, min=10005 us, max=18460 us

(Note that other delays are nowhere near the large 8ms tolerance.)

One hypothesis is that the task is descheduled between taking the USR
timestamp and sending the packet. Possibly in printing.

Delay taking the timestamp closer to sendmsg, and delay printing until
after sendmsg.

With this change, failure rate is significantly lower in current runs.

Link: https://lore.kernel.org/netdev/20260107110521.1aab55e9@kernel.org/
Suggested-by: Jakub Kicinski <kuba@kernel.org>
Signed-off-by: Willem de Bruijn <willemb@google.com>
Link: https://patch.msgid.link/20260112163355.3510150-1-willemdebruijn.kernel@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
(cherry picked from commit c65182ef9df6bb96fd85b56a2bcdd18d64c4d3b5)

https://virtuozzo.atlassian.net/browse/VSTOR-147735
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/net/txtimestamp.c | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/tools/testing/selftests/net/txtimestamp.c b/tools/testing/selftests/net/txtimestamp.c
index dae91eb97d69..f13615654970 100644
--- a/tools/testing/selftests/net/txtimestamp.c
+++ b/tools/testing/selftests/net/txtimestamp.c
@@ -206,12 +206,10 @@ static void __print_timestamp(const char *name, struct timespec *cur,
 	fprintf(stderr, "\n");
 }
 
-static void print_timestamp_usr(void)
+static void record_timestamp_usr(void)
 {
 	if (clock_gettime(CLOCK_REALTIME, &ts_usr))
 		error(1, errno, "clock_gettime");
-
-	__print_timestamp("  USR", &ts_usr, 0, 0);
 }
 
 static void print_timestamp(struct scm_timestamping *tss, int tstype,
@@ -599,8 +597,6 @@ static void do_test(int family, unsigned int report_opt)
 			fill_header_udp(buf + off, family == PF_INET);
 		}
 
-		print_timestamp_usr();
-
 		iov.iov_base = buf;
 		iov.iov_len = total_len;
 
@@ -655,10 +651,14 @@ static void do_test(int family, unsigned int report_opt)
 
 		}
 
+		record_timestamp_usr();
+
 		val = sendmsg(fd, &msg, 0);
 		if (val != total_len)
 			error(1, errno, "send");
 
+		__print_timestamp("  USR", &ts_usr, 0, 0);
+
 		/* wait for all errors to be queued, else ACKs arrive OOO */
 		if (cfg_sleep_usec)
 			usleep(cfg_sleep_usec);
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 6%]

* Re: [Devel] [PATCH hci-8.1] ms/selftests: net: reduce txtimestamp deschedule flakes
  2026-09-29 12:48  6% [Devel] [PATCH hci-8.1] ms/selftests: net: reduce txtimestamp deschedule flakes Eva Kurchatova
@ 2026-09-29 12:52  0% ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-29 12:52 UTC (permalink / raw)
  To: Eva Kurchatova, khorenko; +Cc: devel

> From: Willem de Bruijn <willemb@google.com>
> 
> This test occasionally fails due to exceeding timing bounds, as
> run in continuous testing on netdev.bots:
> 
>   https://netdev.bots.linux.dev/contest.html?test=txtimestamp-sh
> 
> A common pattern is a single elevated delay between USR and SND.
> 
>     # 8.36 [+0.00] test SND
>     # 8.36 [+0.00]     USR: 1767864384 s 240994 us (seq=0, len=0)
>     # 8.44 [+0.08] ERROR: 18461 us expected between 10000 and 18000
>     # 8.44 [+0.00]     SND: 1767864384 s 259455 us (seq=42, len=10)  (USR +18460 us)
>     # 8.52 [+0.07]     SND: 1767864384 s 339523 us (seq=42, len=10)  (USR +10005 us)
>     # 8.52 [+0.00]     USR: 1767864384 s 409580 us (seq=0, len=0)
> [ ... ]
> (cherry picked from commit c65182ef9df6bb96fd85b56a2bcdd18d64c4d3b5)
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-147735
> Feature: fix selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260929124822.770587-1-eva.kurchatova@virtuozzo.com?part=1


_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* [Devel] [PATCH hci-8.1 3/3] fs/fuse: use wait_var_event to wait for fuse file references
  @ 2026-09-30  5:44  2% ` Liu Kui
  2026-09-30  5:51  0%   ` no-reply
  0 siblings, 1 reply; 200+ results
From: Liu Kui @ 2026-09-30  5:44 UTC (permalink / raw)
  To: Konstantin Khorenko; +Cc: azaitsev, devel, Liu Kui

On close_wait mounts, fuse_release_close_wait() needs to wait for all
asynchronous references to the fuse file to be dropped before sending
FUSE_RELEASE. This wait on fi->page_waitq was inherited from the past
implementation and is no longer appropriate as the wait condition
evolved:

 - fi->page_waitq is meant to serve page-under-writeback waiters and
   fuse_set_nowrite()'s writectr waiters; now it also has to serve the
   ff reference waiter, so every wakeup can wake unrelated waiters.

 - the wake is decoupled from the decrement, so every path dropping an
   ff reference must remember to wake fi->page_waitq, and because of
   this we grew two variants of the stock fuse_file_put():
   __fuse_file_put() and fuse_release_ff(). This is bug prone, and a
   misuse can leave the releaser blocked forever.

Instead of waiting on fi->page_waitq, a simple wait_var_event() on
&ff->count serves the purpose best: the matching wake_up_var() uses
the address purely as a hash key and never dereferences it, so the
wakeup can be folded into fuse_file_put() itself. With that, both
__fuse_file_put() and fuse_release_ff() become redundant and are
removed. This cleanup also makes the code less divergent from
mainline.

https://virtuozzo.atlassian.net/browse/VSTOR-147878

Signed-off-by: Liu Kui <kui.liu@virtuozzo.com>
---
 fs/fuse/file.c                     | 69 +++++++++++++++++++++-----------------
 fs/fuse/fuse_i.h                   |  2 +-
 fs/fuse/kio/pcs/pcs_fuse_kdirect.c |  3 +-
 3 files changed, 42 insertions(+), 32 deletions(-)

diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index d7c9ea8a7c45..74c8d925e2fd 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -24,6 +24,7 @@
 #include <linux/fiemap.h>
 #include <linux/jhash.h>
 #include <linux/file.h>
+#include <linux/wait_bit.h>
 
 unsigned int hash_seed_r;
 module_param(hash_seed_r, uint, 0644);
@@ -140,12 +141,23 @@ static void fuse_release_end(struct fuse_mount *fm, struct fuse_args *args,
 	kfree(ra);
 }
 
-static void fuse_file_put(struct fuse_file *ff, bool sync)
+void fuse_file_put(struct fuse_file *ff, bool sync)
 {
+	/* read before the drop: ff may not be touched after a non-final one */
+	bool close_wait = ff->fm->fc->close_wait;
+
 	if (refcount_dec_and_test(&ff->count)) {
 		struct fuse_release_args *ra = &ff->args->release_args;
 		struct fuse_args *args = (ra ? &ra->args : NULL);
 
+		/*
+		 * On close_wait the final put must come from a synchronous
+		 * caller: fuse_release() waits for ff->count == 1 while
+		 * holding its own reference, so an asynchronous completion
+		 * can never drop the last one.
+		 */
+		WARN_ON(!sync && close_wait);
+
 		if (ra && ra->inode)
 			fuse_file_io_release(ff, ra->inode);
 
@@ -165,14 +177,25 @@ static void fuse_file_put(struct fuse_file *ff, bool sync)
 				fuse_release_end(ff->fm, args, -ENOTCONN);
 		}
 		kfree(ff);
+		return;
 	}
-}
 
-static void __fuse_file_put(struct fuse_file *ff)
-{
-	if (refcount_dec_and_test(&ff->count))
-		BUG();
+	/*
+	 * ff may already be freed by a racing final put -- don't touch it
+	 * past this point.
+	 */
+	if (close_wait) {
+		/*
+		 * Pairs with wait_var_event() in fuse_release_close_wait():
+		 * the wake_up_var() contract requires a full barrier between
+		 * the counter update and the wakeup, and refcount_dec()
+		 * alone is only release-ordered.
+		 */
+		smp_mb__after_atomic();
+		wake_up_var(&ff->count);
+	}
 }
+EXPORT_SYMBOL_GPL(fuse_file_put);
 
 struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
 				 unsigned int open_flags, bool isdir)
@@ -557,9 +580,10 @@ static void fuse_release_close_wait(struct inode *inode, struct file *file)
 	/*
 	 * Wait until we are the last one holding a reference, so that all
 	 * in-flight requests on this file have finished and FUSE_RELEASE
-	 * is sent by the releaser itself.
+	 * is sent by the releaser itself. Woken by the wake_up_var() in
+	 * fuse_file_put().
 	 */
-	wait_event(fi->page_waitq, refcount_read(&ff->count) == 1);
+	wait_var_event(&ff->count, refcount_read(&ff->count) == 1);
 
 	inode_lock(inode);
 	fuse_sync_writes(inode);
@@ -1252,22 +1276,6 @@ static int fuse_read_folio(struct file *file, struct folio *folio)
 	return err;
 }
 
-void fuse_release_ff(struct inode *inode, struct fuse_file *ff)
-{
-	if (ff) {
-		if (ff->fm->fc->close_wait) {
-			struct fuse_inode *fi = get_fuse_inode(inode);
-			spin_lock(&fi->lock);
-			__fuse_file_put(ff);
-			wake_up(&fi->page_waitq);
-			spin_unlock(&fi->lock);
-		} else {
-			fuse_file_put(ff, false);
-		}
-	}
-}
-EXPORT_SYMBOL_GPL(fuse_release_ff);
-
 void fuse_revoke_readpages(struct fuse_file *ff)
 {
 	struct fuse_io_args *ia;
@@ -1330,7 +1338,7 @@ static void fuse_readpages_end(struct fuse_mount *fm, struct fuse_args *args,
 	fuse_invalidate_atime(inode);
 
 	if (ff)
-		fuse_release_ff(inode, ff);
+		fuse_file_put(ff, false);
 
 	fuse_io_free(ia);
 }
@@ -2189,7 +2197,7 @@ static void fuse_writepage_free(struct fuse_writepage_args *wpa)
 	for (i = 0; i < ap->num_pages; i++)
 		__free_page(ap->pages[i]);
 
-	fuse_release_ff(wpa->inode, wpa->ia.ff);
+	fuse_file_put(wpa->ia.ff, false);
 
 	kfree(ap->pages);
 	kfree(wpa);
@@ -2572,7 +2580,7 @@ static int fuse_writepage_locked(struct folio *folio)
 	return 0;
 
 err_writepage_args:
-	fuse_release_ff(inode, ff);
+	fuse_file_put(ff, false);
 err_nofile:
 	folio_put(tmp_folio);
 err:
@@ -2859,7 +2867,8 @@ static int fuse_writepages(struct address_space *mapping,
 		WARN_ON(!data.wpa->ia.ap.num_pages);
 		fuse_writepages_send(&data);
 	}
-	fuse_release_ff(inode, data.ff);
+	if (data.ff)
+		fuse_file_put(data.ff, false);
 
 	kfree(data.orig_pages);
 out:
@@ -3929,7 +3938,7 @@ static int fuse_request_fiemap(struct inode *inode, u32 cur_max,
 
 	ap.pages = fuse_pages_alloc(fc->max_pages, GFP_KERNEL, &ap.descs);
 	if (!ap.pages) {
-		fuse_release_ff(inode, ff);
+		fuse_file_put(ff, false);
 		return -ENOMEM;
 	}
 
@@ -4021,7 +4030,7 @@ static int fuse_request_fiemap(struct inode *inode, u32 cur_max,
 		ap.pages[allocated] = NULL;
 	}
 	kfree(ap.pages);
-	fuse_release_ff(inode, ff);
+	fuse_file_put(ff, false);
 	return err;
 }
 
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index f6da643b9c86..e94d40458af3 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1673,7 +1673,7 @@ int fuse_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo,
 
 struct fuse_file *__fuse_write_file_get(struct fuse_inode *fi);
 struct fuse_file *fuse_file_get(struct fuse_file *ff);
-void fuse_release_ff(struct inode *inode, struct fuse_file *ff);
+void fuse_file_put(struct fuse_file *ff, bool sync);
 struct fuse_req *fuse_dev_find_request(unsigned int fd, u64 unique);
 
 void fuse_revoke_readpages(struct fuse_file *ff);
diff --git a/fs/fuse/kio/pcs/pcs_fuse_kdirect.c b/fs/fuse/kio/pcs/pcs_fuse_kdirect.c
index efbab75654c8..5064198a7b4c 100644
--- a/fs/fuse/kio/pcs/pcs_fuse_kdirect.c
+++ b/fs/fuse/kio/pcs/pcs_fuse_kdirect.c
@@ -728,7 +728,8 @@ static int submit_size_grow(struct inode *inode, unsigned long long size)
 	args.out_args[0].value = &outarg;
 
 	err = fuse_simple_request(fm, &args);
-	fuse_release_ff(inode, ff);
+	if (ff)
+		fuse_file_put(ff, false);
 
 	return err;
 

-- 
Git-155)

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 2%]

* Re: [Devel] [PATCH hci-8.1 3/3] fs/fuse: use wait_var_event to wait for fuse file references
  2026-09-30  5:44  2% ` [Devel] [PATCH hci-8.1 3/3] fs/fuse: use wait_var_event to wait for fuse file references Liu Kui
@ 2026-09-30  5:51  0%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-30  5:51 UTC (permalink / raw)
  To: Konstantin Khorenko, Liu Kui; +Cc: devel, azaitsev

> On close_wait mounts, fuse_release_close_wait() needs to wait for all
> asynchronous references to the fuse file to be dropped before sending
> FUSE_RELEASE. This wait on fi->page_waitq was inherited from the past
> implementation and is no longer appropriate as the wait condition
> evolved:
> 
>  - fi->page_waitq is meant to serve page-under-writeback waiters and
>    fuse_set_nowrite()'s writectr waiters; now it also has to serve the
>    ff reference waiter, so every wakeup can wake unrelated waiters.
> 
>  - the wake is decoupled from the decrement, so every path dropping an
>    ff reference must remember to wake fi->page_waitq, and because of
>    this we grew two variants of the stock fuse_file_put():
>    __fuse_file_put() and fuse_release_ff(). This is bug prone, and a
>    misuse can leave the releaser blocked forever.
> 
> Instead of waiting on fi->page_waitq, a simple wait_var_event() on
> &ff->count serves the purpose best: the matching wake_up_var() uses
> the address purely as a hash key and never dereferences it, so the
> wakeup can be folded into fuse_file_put() itself. With that, both
> __fuse_file_put() and fuse_release_ff() become redundant and are
> removed. This cleanup also makes the code less divergent from
> mainline.
> 
> https://virtuozzo.atlassian.net/browse/VSTOR-147878
> 
> Signed-off-by: Liu Kui <kui.liu@virtuozzo.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930-close-wait-rework-v2-v1-0-f8062178544a@virtuozzo.com?part=3


_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH hci-8.1 2/3] fs/fuse kio: refactor transmit path in pcs_sockio_send()
  @ 2026-09-30  7:42  3%   ` no-reply
  2026-09-30 16:29  0%     ` Konstantin Khorenko
  0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-09-30  7:42 UTC (permalink / raw)
  To: Liu Kui, Konstantin Khorenko; +Cc: devel, azaitsev

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] fs/fuse kio: do_sockio_sendmsg() can spin forever on an empty get_iter() refill
- [Low] fs/fuse kio: iov_iter_sendpages_ok() under-checks pages of unaligned multi-page bvecs
--

commit 2b6fc40ff00b50732f7cf6408214e55943ba4135
Author: Liu Kui <kui.liu@virtuozzo.com>

fs/fuse kio: refactor transmit path in pcs_sockio_send()

This patch replaces do_send_one_seg() with do_sockio_sendmsg(). The new
function submits the whole iov_iter in one sock_sendmsg() call, and uses
MSG_SPLICE_PAGES when all pages qualify. It also clears PCS_MSG_BUSY in
the transport whenever IO on the msg ends.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-142167

> diff --git a/fs/fuse/kio/pcs/pcs_sock_io.c b/fs/fuse/kio/pcs/pcs_sock_io.c
> --- a/fs/fuse/kio/pcs/pcs_sock_io.c
> +++ b/fs/fuse/kio/pcs/pcs_sock_io.c

[ ... ]

> +static bool iov_iter_sendpages_ok(struct iov_iter *it)
> +{
> +	const struct bio_vec *bvec = it->bvec;
> +
> +	if (!iov_iter_is_bvec(it))
> +		return false;
> +
> +	while (bvec < (it->bvec + it->nr_segs)) {
> +		if (!sendpages_ok(bvec->bv_page, bvec->bv_len, bvec->bv_offset))
> +			return false;
> +		bvec++;
> +	}
> +
> +	return true;
> +}

[Severity: Low]
Can iov_iter_sendpages_ok() skip the last page of a multi-page bvec that
has an unaligned bv_offset?

sendpages_ok() starts at page + (offset >> PAGE_SHIFT) and steps
PAGE_SIZE for len bytes.  For example, with bv_offset=2048 and
bv_len=4096 the range covers two pages, but only one is checked with
sendpage_ok().

The bvec builder in pcs_krpc.c (umem branch) sets bv_offset to a
non-page-aligned value and bv_len up to the contiguous length, so such
bvecs look possible:

    bvec->bv_offset = offset;
    ...
    bvec->bv_len = len < contig ? len : contig;

If that happens, MSG_SPLICE_PAGES could be set in do_sockio_sendmsg()
while the unchecked page is a slab page or has a zero refcount.  That
page would then be spliced into the socket by reference instead of being
copied.  Is the page check meant to cover the pages that the byte range
actually touches?

> +static int do_sockio_sendmsg(struct pcs_sockio *sio, struct pcs_msg *msg)
> +{

[ ... ]

> +	while (sio->write_offset < msg->size) {
> +		size_t left = msg->size - sio->write_offset;
> +		struct msghdr sockmsg = {.msg_flags = flags};
> +
> +		TRACE("sio(%px) offset:%d msg:%px left:%zu, it->len:%zu\n", sio, sio->write_offset, msg, left, iov_iter_count(it));
> +
> +		if (!iov_iter_count(it)) {
> +			/* Current iter is exhausted, init new one */
> +			msg->get_iter(msg, sio->write_offset, it, WRITE);
> +		}
> +
> +		BUG_ON(iov_iter_count(it) > left);

[ ... ]

> +		while (iov_iter_count(it)) {

[Severity: Medium]
Can do_sockio_sendmsg() spin forever here if msg->get_iter() returns an
iterator with a zero count while sio->write_offset < msg->size?

In that case the BUG_ON(iov_iter_count(it) > left) check passes, the
inner while (iov_iter_count(it)) loop is skipped, and sock_sendmsg() is
never called.  WARN_ON(msg_data_left(&sockmsg)) sees zero and does not
fire.  write_offset does not change, so the outer loop repeats with the
same state and no exit, which would be a soft lockup.

The old do_send_one_seg() returned 0 for an empty segment, and
pcs_sockio_send() turned that into a WARN_ON and sio_abort(). I did not
find a get_iter() callback that returns an empty iterator for an
in-range offset (krpc_msg_get_data() and pcs_get_iter_inline() look
fine), so this looks latent.  Would it make sense to add a check that
fails the message when get_iter() returns an empty iterator?

> +			int ret;

[ ... ]

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930-sockio-refactor-8-1-v1-0-806348b9eb26@virtuozzo.com?part=2

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* [PATCH hci-8.0 1/9] block/commit: pass BDRV_WANT_PRECISE to block-status #VSTOR-134085
  @ 2026-09-30 11:17 21% ` Denis V. Lunev
  2026-09-30 11:17  3% ` [PATCH hci-8.0 2/9] iotests/040: cover large and fragmented commit runs #VSTOR-134085 Denis V. Lunev
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

From: Denis V. Lunev <den@openvz.org>

c33159dec790 replaced the want_zero bool with a bitmask of BDRV_WANT_*
flags. block/commit.c was missed and still passes "true", which is
BDRV_BLOCK_DATA, not BDRV_WANT_PRECISE. Convert it like the other
callers.

Fixes: c33159dec790 ("block: Expand block status mode from bool to flags")
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
CC: Eric Blake <eblake@redhat.com>
---
 block/commit.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/block/commit.c b/block/commit.c
index 0d9e1a16d7a..a2e807288b2 100644
--- a/block/commit.c
+++ b/block/commit.c
@@ -139,8 +139,8 @@ static int commit_iteration(CommitBlockJob *s, int64_t offset,
     /* Copy if allocated above the base */
     WITH_GRAPH_RDLOCK_GUARD() {
         ret = bdrv_co_common_block_status_above(blk_bs(s->top),
-            s->base_overlay, true, true, offset, COMMIT_BUFFER_SIZE,
-            &bytes, NULL, NULL, NULL);
+            s->base_overlay, true, BDRV_WANT_PRECISE, offset,
+            COMMIT_BUFFER_SIZE, &bytes, NULL, NULL, NULL);
     }
 
     trace_commit_one_iteration(s, offset, bytes, ret);
-- 
2.53.0


^ permalink raw reply	[relevance 21%]

* [PATCH hci-8.0 3/9] block/commit: batch block-status queries #VSTOR-134085
    2026-09-30 11:17 21% ` [PATCH hci-8.0 1/9] block/commit: pass BDRV_WANT_PRECISE to block-status #VSTOR-134085 Denis V. Lunev
  2026-09-30 11:17  3% ` [PATCH hci-8.0 2/9] iotests/040: cover large and fragmented commit runs #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:17 18% ` Denis V. Lunev
  2026-09-30 11:21  3%   ` no-reply
                     ` (5 subsequent siblings)
  8 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

From: Denis V. Lunev <den@openvz.org>

commit_iteration() asks for block status COMMIT_BUFFER_SIZE (512K) at a
time, so a long run above base pays one query per 512K for an answer
the whole run shares.

Query the remainder of the image instead and keep the answer in a
CommitStatus owned by commit_run(). No device or export writes top
above base_overlay while the job runs: they ask for
BLK_PERM_CONSISTENT_READ along with BLK_PERM_WRITE, and the job
withholds CONSISTENT_READ there, having to share WRITE or it would
block its own writes to base through the backing chain. A job target
asks for WRITE alone and is refused by the op blocker instead. From
filtered_base downwards CONSISTENT_READ is shared again, and there only
the job writes.

Copying is still bounded by the read buffer. Zeroes need no buffer, so
COMMIT_ZERO_CHUNK bounds them, keeping a cancel from waiting on a huge
write-zeroes, and an unallocated span is crossed in one step.

Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 block/commit.c | 59 ++++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 48 insertions(+), 11 deletions(-)

diff --git a/block/commit.c b/block/commit.c
index a2e807288b2..80b89e6269d 100644
--- a/block/commit.c
+++ b/block/commit.c
@@ -31,8 +31,23 @@ enum {
      * contiguous regions of the image is efficient.
      */
     COMMIT_BUFFER_SIZE = 512 * 1024, /* in bytes */
+
+    /*
+     * Zeroes need no buffer, so they are bounded by this instead. It stays
+     * well under BDRV_REQUEST_MAX_BYTES and keeps a cancel from waiting on
+     * a multi-gigabyte write.
+     */
+    COMMIT_ZERO_CHUNK = 256 * 1024 * 1024, /* in bytes */
 };
 
+/* Last block-status answer, covering [offset, end). Empty when equal. */
+typedef struct CommitStatus {
+    int64_t len;
+    int64_t offset;
+    int64_t end;
+    int ret;
+} CommitStatus;
+
 typedef struct CommitBlockJob {
     BlockJob common;
     BlockDriverState *commit_top_bs;
@@ -129,26 +144,46 @@ static void commit_clean(Job *job)
 }
 
 static int commit_iteration(CommitBlockJob *s, int64_t offset,
-                            int64_t *requested_bytes, void *buf)
+                            int64_t *requested_bytes, void *buf,
+                            CommitStatus *st)
 {
     BlockErrorAction action;
-    int64_t bytes = *requested_bytes;
+    int64_t bytes;
     int ret = 0;
     bool error_in_source = true;
 
-    /* Copy if allocated above the base */
-    WITH_GRAPH_RDLOCK_GUARD() {
-        ret = bdrv_co_common_block_status_above(blk_bs(s->top),
-            s->base_overlay, true, BDRV_WANT_PRECISE, offset,
-            COMMIT_BUFFER_SIZE, &bytes, NULL, NULL, NULL);
+    assert(offset >= st->offset);
+
+    if (offset >= st->end) {
+        /* Copy if allocated above the base */
+        WITH_GRAPH_RDLOCK_GUARD() {
+            ret = bdrv_co_common_block_status_above(blk_bs(s->top),
+                s->base_overlay, true, BDRV_WANT_PRECISE, offset,
+                st->len - offset, &bytes, NULL, NULL, NULL);
+        }
+
+        if (ret < 0) {
+            trace_commit_one_iteration(s, offset, 0, ret);
+            goto fail;
+        }
+
+        st->offset = offset;
+        st->end = offset + bytes;
+        st->ret = ret;
     }
 
-    trace_commit_one_iteration(s, offset, bytes, ret);
+    ret = st->ret;
+    bytes = st->end - offset;
 
-    if (ret < 0) {
-        goto fail;
+    /* An unallocated span costs no I/O, so it is crossed in one step. */
+    if (ret & BDRV_BLOCK_ZERO) {
+        bytes = MIN(bytes, COMMIT_ZERO_CHUNK);
+    } else if (ret & BDRV_BLOCK_ALLOCATED) {
+        bytes = MIN(bytes, COMMIT_BUFFER_SIZE);
     }
 
+    trace_commit_one_iteration(s, offset, bytes, ret);
+
     if (ret & BDRV_BLOCK_ALLOCATED) {
         if (ret & BDRV_BLOCK_ZERO) {
             /*
@@ -214,6 +249,7 @@ static int coroutine_fn commit_run(Job *job, Error **errp)
     int64_t n = 0; /* bytes */
     QEMU_AUTO_VFREE void *buf = NULL;
     int64_t len, base_len;
+    CommitStatus st = { 0 };
 
     len = blk_co_getlength(s->top);
     if (len < 0) {
@@ -234,6 +270,7 @@ static int coroutine_fn commit_run(Job *job, Error **errp)
     }
 
     buf = blk_blockalign(s->top, COMMIT_BUFFER_SIZE);
+    st.len = len;
 
     for (offset = 0; offset < len; offset += n) {
         /* Note that even when no rate limit is applied we need to yield
@@ -244,7 +281,7 @@ static int coroutine_fn commit_run(Job *job, Error **errp)
             break;
         }
 
-        ret = commit_iteration(s, offset, &n, buf);
+        ret = commit_iteration(s, offset, &n, buf, &st);
 
         if (ret < 0) {
             return ret;
-- 
2.53.0


^ permalink raw reply	[relevance 18%]

* [PATCH hci-8.0 6/9] block/block-copy: extract block_copy_set_task_method() #VSTOR-134085
                     ` (4 preceding siblings ...)
  2026-09-30 11:17 25% ` [PATCH hci-8.0 5/9] block/block-copy: don't reserve memory for zero tasks #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:17 23% ` Denis V. Lunev
  2026-09-30 11:17 33% ` [PATCH hci-8.0 7/9] block/block-copy: track known-zero source clusters #VSTOR-134085 Denis V. Lunev
                   ` (2 subsequent siblings)
  8 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

From: Denis V. Lunev <den@openvz.org>

Move the per-task block-status query and its skip_unallocated and
BDRV_BLOCK_ZERO handling out of block_copy_dirty_clusters() into
block_copy_set_task_method(). No behavior change. The next patch
changes that decision, and having it in one place keeps that change to
just the new logic.

The block-status result no longer lands in the caller's @ret, so drop
the note saying @ret may be positive at the out: label.

Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 block/block-copy.c | 46 ++++++++++++++++++++++++++++++----------------
 1 file changed, 30 insertions(+), 16 deletions(-)

diff --git a/block/block-copy.c b/block/block-copy.c
index aecbe0c16ab..e210688b1df 100644
--- a/block/block-copy.c
+++ b/block/block-copy.c
@@ -780,6 +780,35 @@ int64_t coroutine_fn block_copy_reset_unallocated(BlockCopyState *s,
     return ret;
 }
 
+/*
+ * Decide how @task is copied: COPY_WRITE_ZEROES if it reads as zero. May
+ * shrink @task. Returns false if @task is to be skipped (already ended,
+ * not freed).
+ */
+static bool coroutine_fn GRAPH_RDLOCK
+block_copy_set_task_method(BlockCopyState *s, BlockCopyTask *task)
+{
+    int ret;
+    int64_t status_bytes;
+
+    ret = block_copy_block_status(s, task->req.offset, task->req.bytes,
+                                  &status_bytes);
+    assert(ret >= 0); /* never fail */
+    if (status_bytes < task->req.bytes) {
+        block_copy_task_shrink(task, status_bytes);
+    }
+    if (qatomic_read(&s->skip_unallocated) && !(ret & BDRV_BLOCK_ALLOCATED)) {
+        block_copy_task_end(task, 0);
+        trace_block_copy_skip_range(s, task->req.offset, task->req.bytes);
+        return false;
+    }
+    if (ret & BDRV_BLOCK_ZERO) {
+        task->method = COPY_WRITE_ZEROES;
+    }
+
+    return true;
+}
+
 /*
  * block_copy_dirty_clusters
  *
@@ -812,7 +841,6 @@ block_copy_dirty_clusters(BlockCopyCallState *call_state)
     while (bytes && aio_task_pool_status(aio) == 0 &&
            !qatomic_read(&call_state->cancelled)) {
         BlockCopyTask *task;
-        int64_t status_bytes;
 
         task = block_copy_task_create(s, call_state, offset, bytes);
         if (!task) {
@@ -826,24 +854,12 @@ block_copy_dirty_clusters(BlockCopyCallState *call_state)
 
         found_dirty = true;
 
-        ret = block_copy_block_status(s, task->req.offset, task->req.bytes,
-                                      &status_bytes);
-        assert(ret >= 0); /* never fail */
-        if (status_bytes < task->req.bytes) {
-            block_copy_task_shrink(task, status_bytes);
-        }
-        if (qatomic_read(&s->skip_unallocated) &&
-            !(ret & BDRV_BLOCK_ALLOCATED)) {
-            block_copy_task_end(task, 0);
-            trace_block_copy_skip_range(s, task->req.offset, task->req.bytes);
+        if (!block_copy_set_task_method(s, task)) {
             offset = task_end(task);
             bytes = end - offset;
             g_free(task);
             continue;
         }
-        if (ret & BDRV_BLOCK_ZERO) {
-            task->method = COPY_WRITE_ZEROES;
-        }
 
         if (!call_state->ignore_ratelimit) {
             uint64_t ns = ratelimit_calculate_delay(&s->rate_limit, 0);
@@ -884,8 +900,6 @@ out:
          * block_copy_task_run. If it fails, it means some task already failed
          * for real reason, let's return first failure.
          * Still, assert that we don't rewrite failure by success.
-         *
-         * Note: ret may be positive here because of block-status result.
          */
         assert(ret >= 0 || aio_task_pool_status(aio) < 0);
         ret = aio_task_pool_status(aio);
-- 
2.53.0


^ permalink raw reply	[relevance 23%]

* [PATCH hci-8.0 5/9] block/block-copy: don't reserve memory for zero tasks #VSTOR-134085
                     ` (3 preceding siblings ...)
  @ 2026-09-30 11:17 25% ` Denis V. Lunev
  2026-09-30 11:17 23% ` [PATCH hci-8.0 6/9] block/block-copy: extract block_copy_set_task_method() #VSTOR-134085 Denis V. Lunev
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

From: Denis V. Lunev <den@openvz.org>

block_copy_dirty_clusters() charges task->req.bytes to the shared memory
pool for every task, but a COPY_WRITE_ZEROES task allocates no bounce
buffer, so there is nothing to account for. Add
block_copy_task_shres_bytes() and use it at all three call sites.

Zero tasks lose the throttling the pool gave them incidentally and are
bounded only by BLOCK_COPY_MAX_WORKERS (64).

Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 block/block-copy.c | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/block/block-copy.c b/block/block-copy.c
index e68d3d8b141..aecbe0c16ab 100644
--- a/block/block-copy.c
+++ b/block/block-copy.c
@@ -470,6 +470,11 @@ void block_copy_set_progress_meter(BlockCopyState *s, ProgressMeter *pm)
     s->progress = pm;
 }
 
+static uint64_t block_copy_task_shres_bytes(BlockCopyTask *task)
+{
+    return task->method == COPY_WRITE_ZEROES ? 0 : task->req.bytes;
+}
+
 /*
  * Takes ownership of @task
  *
@@ -491,7 +496,7 @@ static coroutine_fn int block_copy_task_run(AioTaskPool *pool,
 
     aio_task_pool_wait_slot(pool);
     if (aio_task_pool_status(pool) < 0) {
-        co_put_to_shres(task->s->mem, task->req.bytes);
+        co_put_to_shres(task->s->mem, block_copy_task_shres_bytes(task));
         block_copy_task_end(task, -ECANCELED);
         g_free(task);
         return -ECANCELED;
@@ -636,7 +641,7 @@ static coroutine_fn int block_copy_task_entry(AioTask *task)
             progress_work_done(s->progress, t->req.bytes);
         }
     }
-    co_put_to_shres(s->mem, t->req.bytes);
+    co_put_to_shres(s->mem, block_copy_task_shres_bytes(t));
     block_copy_task_end(t, ret);
 
     if (s->discard_source && ret == 0) {
@@ -855,7 +860,7 @@ block_copy_dirty_clusters(BlockCopyCallState *call_state)
 
         trace_block_copy_process(s, task->req.offset);
 
-        co_get_from_shres(s->mem, task->req.bytes);
+        co_get_from_shres(s->mem, block_copy_task_shres_bytes(task));
 
         offset = task_end(task);
         bytes = end - offset;
-- 
2.53.0


^ permalink raw reply	[relevance 25%]

* [PATCH hci-8.0 7/9] block/block-copy: track known-zero source clusters #VSTOR-134085
                     ` (5 preceding siblings ...)
  2026-09-30 11:17 23% ` [PATCH hci-8.0 6/9] block/block-copy: extract block_copy_set_task_method() #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:17 33% ` Denis V. Lunev
  2026-09-30 11:30  6%   ` no-reply
  2026-09-30 11:17 25% ` [PATCH hci-8.0 8/9] block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085 Denis V. Lunev
  2026-09-30 11:17 26% ` [PATCH hci-8.0 9/9] block/block-copy: coalesce write-zeroes tasks #VSTOR-134085 Denis V. Lunev
  8 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

From: Denis V. Lunev <den@openvz.org>

block_copy_reset_unallocated()'s up-front scan already queries the
source, and the same query reports zero-ness. The copy loop re-queries
per task and, having no answer up front, can only size a task by the
copy buffer, so a mostly-zero image is cut into thousands of pieces
that each turn out to read as zero.

Cache the answer in zero_bitmap, published by zero_bitmap_valid once
the scan is done. block_copy_task_create() then picks the method from
the bitmap and ends the task where the answer changes, so a zero task
never reaches into data and a copy task never swallows a zero run. The
per-task query stays as the fallback until the flag is set, since CBW
intercepts guest writes while the scan is still running.

The scan and the query it replaces must resolve BDRV_BLOCK_ZERO against
the same part of the chain, so the scan moves from bdrv_co_is_allocated()
to bdrv_co_block_status_above() and the base selection is factored out
into block_copy_status_base().

zero_bitmap is a plain HBitmap, not a BdrvDirtyBitmap: an internal cache
has no business in query-named-block-nodes. One writer and readers gated
by the flag need no mutex, but the publish needs ordering, hence
store-release and load-acquire. It is allocated on demand, so sync=none
and a standalone copy-before-write filter do not pay for it.

hci-8.0: 6b1a4754713 ("backup: skip copying unallocated for full
mode") scans for sync=full too and leaves skip_unallocated set after
the scan, choosing the base from top_mode instead. So
block_copy_status_base() keys on top_mode, and replaces the base
block_copy_is_cluster_allocated() computed on its own.

Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 block/backup.c             |   1 +
 block/block-copy.c         | 117 ++++++++++++++++++++++++++++++-------
 include/block/block-copy.h |   1 +
 3 files changed, 98 insertions(+), 21 deletions(-)

diff --git a/block/backup.c b/block/backup.c
index f7d8421812a..03ad1ad350b 100644
--- a/block/backup.c
+++ b/block/backup.c
@@ -284,6 +284,7 @@ static int coroutine_fn backup_run(Job *job, Error **errp)
 
             offset += count;
         }
+        block_copy_set_zero_bitmap_valid(s->bcs);
     }
 
     if (s->sync_mode == MIRROR_SYNC_MODE_NONE) {
diff --git a/block/block-copy.c b/block/block-copy.c
index e210688b1df..27a5d1a41b2 100644
--- a/block/block-copy.c
+++ b/block/block-copy.c
@@ -20,6 +20,8 @@
 #include "block/block_int-io.h"
 #include "block/dirty-bitmap.h"
 #include "block/reqlist.h"
+#include "qemu/hbitmap.h"
+#include "qemu/host-utils.h"
 #include "system/block-backend.h"
 #include "qemu/units.h"
 #include "qemu/co-shared-resource.h"
@@ -159,7 +161,10 @@ typedef struct BlockCopyState {
     /* State fields that use a thread-safe API */
     BdrvDirtyBitmap *copy_bitmap;
     bool top_mode;
-
+    /* Clusters reading as zero; allocated on demand, frozen once valid. */
+    HBitmap *zero_bitmap;
+    /* Published only once the scan is done. */
+    bool zero_bitmap_valid; /* atomic, store-release/load-acquire */
     ProgressMeter *progress;
     SharedResource *mem;
     RateLimit rate_limit;
@@ -193,6 +198,7 @@ block_copy_task_create(BlockCopyState *s, BlockCopyCallState *call_state,
                        int64_t offset, int64_t bytes)
 {
     BlockCopyTask *task;
+    BlockCopyMethod method;
     int64_t max_chunk;
 
     QEMU_LOCK_GUARD(&s->lock);
@@ -204,6 +210,27 @@ block_copy_task_create(BlockCopyState *s, BlockCopyCallState *call_state,
         return NULL;
     }
 
+    method = s->method;
+
+    /*
+     * The scan already knows how this range reads: pick the method here and
+     * stop the task where the answer changes.
+     */
+    if (qatomic_load_acquire(&s->zero_bitmap_valid)) {
+        int64_t boundary;
+
+        if (hbitmap_get(s->zero_bitmap, offset)) {
+            method = COPY_WRITE_ZEROES;
+            boundary = hbitmap_next_zero(s->zero_bitmap, offset, bytes);
+        } else {
+            boundary = hbitmap_next_dirty(s->zero_bitmap, offset, bytes);
+        }
+
+        if (boundary >= 0) {
+            bytes = boundary - offset;
+        }
+    }
+
     assert(QEMU_IS_ALIGNED(offset, s->cluster_size));
     bytes = QEMU_ALIGN_UP(bytes, s->cluster_size);
 
@@ -218,7 +245,7 @@ block_copy_task_create(BlockCopyState *s, BlockCopyCallState *call_state,
         .task.func = block_copy_task_entry,
         .s = s,
         .call_state = call_state,
-        .method = s->method,
+        .method = method,
     };
     reqlist_init_req(&s->reqs, &task->req, offset, bytes);
 
@@ -274,6 +301,9 @@ void block_copy_state_free(BlockCopyState *s)
 
     ratelimit_destroy(&s->rate_limit);
     bdrv_release_dirty_bitmap(s->copy_bitmap);
+    if (s->zero_bitmap) {
+        hbitmap_free(s->zero_bitmap);
+    }
     shres_destroy(s->mem);
     g_free(s);
 }
@@ -655,20 +685,24 @@ static coroutine_fn int block_copy_task_entry(AioTask *task)
     return ret;
 }
 
+/* The scan and the per-task query must resolve BDRV_BLOCK_ZERO alike. */
+static GRAPH_RDLOCK BlockDriverState *block_copy_status_base(BlockCopyState *s)
+{
+    if (qatomic_read(&s->top_mode)) {
+        return bdrv_backing_chain_next(s->source->bs);
+    }
+
+    return NULL;
+}
+
 static coroutine_fn GRAPH_RDLOCK
 int block_copy_block_status(BlockCopyState *s, int64_t offset, int64_t bytes,
                             int64_t *pnum)
 {
     int64_t num;
-    BlockDriverState *base;
+    BlockDriverState *base = block_copy_status_base(s);
     int ret;
 
-    if (qatomic_read(&s->top_mode)) {
-        base = bdrv_backing_chain_next(s->source->bs);
-    } else {
-        base = NULL;
-    }
-
     ret = bdrv_co_block_status_above(s->source->bs, base, offset, bytes, &num,
                                      NULL, NULL);
     if (ret < 0 || num < s->cluster_size) {
@@ -688,50 +722,79 @@ int block_copy_block_status(BlockCopyState *s, int64_t offset, int64_t bytes,
     return ret;
 }
 
+/* Only the scan allocates, and it runs before zero_bitmap_valid. */
+static HBitmap *block_copy_zero_bitmap(BlockCopyState *s)
+{
+    if (!s->zero_bitmap) {
+        s->zero_bitmap = hbitmap_alloc(s->len, ctz32(s->cluster_size));
+    }
+
+    return s->zero_bitmap;
+}
+
+static void block_copy_mark_zero_prefix(BlockCopyState *s, int64_t offset,
+                                        int64_t zero_count)
+{
+    int64_t zero_bytes = QEMU_ALIGN_DOWN(zero_count, s->cluster_size);
+
+    if (zero_bytes > 0) {
+        hbitmap_set(block_copy_zero_bitmap(s), offset, zero_bytes);
+    }
+}
+
 /*
  * Check if the cluster starting at offset is allocated or not.
  * return via pnum the number of contiguous clusters sharing this allocation.
+ * Also marks the zero prefix of the range in zero_bitmap.
  */
 static int coroutine_fn GRAPH_RDLOCK
 block_copy_is_cluster_allocated(BlockCopyState *s, int64_t offset,
                                 int64_t *pnum)
 {
     BlockDriverState *bs = s->source->bs;
-    BlockDriverState *base;
+    BlockDriverState *base = block_copy_status_base(s);
+    int64_t orig_offset = offset;
     int64_t count, total_count = 0;
+    int64_t zero_count = 0;
+    bool zero_broken = false;
     int64_t bytes = s->len - offset;
     int ret;
 
-    if (s->top_mode && s->source->bs->backing) {
-        base = s->source->bs->backing->bs;
-    } else {
-        base = NULL;
-    }
-
     assert(QEMU_IS_ALIGNED(offset, s->cluster_size));
 
     while (true) {
         /* protected in backup_run() */
-        ret = bdrv_co_is_allocated_above(bs, base, false, offset, bytes,
-                                         &count);
+        ret = bdrv_co_block_status_above(bs, base, offset, bytes, &count,
+                                         NULL, NULL);
         if (ret < 0) {
             return ret;
         }
 
+        if (!zero_broken) {
+            if (ret & BDRV_BLOCK_ZERO) {
+                zero_count += count;
+            } else {
+                zero_broken = true;
+            }
+        }
+
         total_count += count;
 
-        if (ret || count == 0) {
+        if ((ret & BDRV_BLOCK_ALLOCATED) || count == 0) {
             /*
-             * ret: partial segment(s) are considered allocated.
+             * BDRV_BLOCK_ALLOCATED: partial segment(s) are considered
+             * allocated.
              * otherwise: unallocated tail is treated as an entire segment.
              */
             *pnum = DIV_ROUND_UP(total_count, s->cluster_size);
-            return ret;
+            block_copy_mark_zero_prefix(s, orig_offset, zero_count);
+            return !!(ret & BDRV_BLOCK_ALLOCATED);
         }
 
         /* Unallocated segment(s) with uncertain following segment(s) */
         if (total_count >= s->cluster_size) {
             *pnum = total_count / s->cluster_size;
+            block_copy_mark_zero_prefix(s, orig_offset, zero_count);
             return 0;
         }
 
@@ -791,6 +854,12 @@ block_copy_set_task_method(BlockCopyState *s, BlockCopyTask *task)
     int ret;
     int64_t status_bytes;
 
+    /* block_copy_task_create() already decided, from zero_bitmap. */
+    if (qatomic_load_acquire(&s->zero_bitmap_valid)) {
+        return true;
+    }
+
+    /* CBW filter could call this early. */
     ret = block_copy_block_status(s, task->req.offset, task->req.bytes,
                                   &status_bytes);
     assert(ret >= 0); /* never fail */
@@ -1122,6 +1191,12 @@ void block_copy_set_options(BlockCopyState *s, bool skip_unallocated,
     qatomic_set(&s->top_mode, top_mode);
 }
 
+void block_copy_set_zero_bitmap_valid(BlockCopyState *s)
+{
+    block_copy_zero_bitmap(s);
+    qatomic_store_release(&s->zero_bitmap_valid, true);
+}
+
 void block_copy_set_speed(BlockCopyState *s, uint64_t speed)
 {
     ratelimit_set_speed(&s->rate_limit, speed, BLOCK_COPY_SLICE_TIME);
diff --git a/include/block/block-copy.h b/include/block/block-copy.h
index d9304d1cf56..57e13f82f78 100644
--- a/include/block/block-copy.h
+++ b/include/block/block-copy.h
@@ -102,5 +102,6 @@ int64_t block_copy_cluster_size(BlockCopyState *s);
 void block_copy_set_options(BlockCopyState *s, bool skip_unallocated,
                             bool top_mode);
 void block_copy_set_global_error(BlockCopyState *s, int error);
+void block_copy_set_zero_bitmap_valid(BlockCopyState *s);
 
 #endif /* BLOCK_COPY_H */
-- 
2.53.0


^ permalink raw reply	[relevance 33%]

* [PATCH hci-8.0 8/9] block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085
                     ` (6 preceding siblings ...)
  2026-09-30 11:17 33% ` [PATCH hci-8.0 7/9] block/block-copy: track known-zero source clusters #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:17 25% ` Denis V. Lunev
  2026-09-30 11:33  5%   ` no-reply
  2026-09-30 11:17 26% ` [PATCH hci-8.0 9/9] block/block-copy: coalesce write-zeroes tasks #VSTOR-134085 Denis V. Lunev
  8 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

From: Denis V. Lunev <den@openvz.org>

Only sync=top runs an up-front scan, so the previous patch did nothing
for the other modes. FULL and BITMAP know their complete copy_bitmap
before copying starts, and the same copy-before-write guarantee holds:
a cluster still dirty when the copy loop reaches it has not been written
since the scan.

Add block_copy_calculate_zero_bitmap(), driven one query at a time from
backup_scan_source() so the graph rdlock and the pause point are taken
per query, and query each confirmed span once however many dirty areas
it covers. The scan only fills zero_bitmap and never clears copy_bitmap,
so sync=full still writes zeroes for clusters the source never
allocated; it just stops re-querying to find that out. Unlike the
per-task queries it replaces the scan is serial, so a sparsely dirtied
job against a slow source may spend more time here than it saves. The
sync=top loop had the same shape, so both fold into
backup_scan_source().

sync=none keeps the per-task query. Its bitmap says "anything may be
copied", not "this will be copied", and it only ever copies what the
guest writes during the fleecing window, so scanning the whole image at
attach is the wrong trade.

hci-8.0: sync=full already runs block_copy_reset_unallocated() since
6b1a4754713, so it stays on that path and only sync=bitmap moves to
block_copy_calculate_zero_bitmap(). Unallocated clusters are not
written for sync=full here.

Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 block/backup.c             | 95 +++++++++++++++++++++++---------------
 block/block-copy.c         | 41 ++++++++++++++++
 include/block/block-copy.h |  4 ++
 3 files changed, 104 insertions(+), 36 deletions(-)

diff --git a/block/backup.c b/block/backup.c
index 03ad1ad350b..eef4512b8bb 100644
--- a/block/backup.c
+++ b/block/backup.c
@@ -250,60 +250,83 @@ static void backup_init_bcs_bitmap(BackupBlockJob *job)
     job_progress_set_remaining(&job->common.job, estimate);
 }
 
-static int coroutine_fn backup_run(Job *job, Error **errp)
+/*
+ * Walk the source before copying starts: fill zero_bitmap and, for sync=top
+ * and sync=full, drop the clusters that are not allocated.
+ */
+static int coroutine_fn backup_scan_source(BackupBlockJob *s)
 {
-    BackupBlockJob *s = container_of(job, BackupBlockJob, common.job);
-    int ret;
-
-    backup_init_bcs_bitmap(s);
-
-    if (s->sync_mode == MIRROR_SYNC_MODE_TOP ||
-        s->sync_mode == MIRROR_SYNC_MODE_FULL)
-    {
-        int64_t offset = 0;
-        int64_t count;
+    Job *job = &s->common.job;
+    bool skip = s->sync_mode == MIRROR_SYNC_MODE_TOP ||
+                s->sync_mode == MIRROR_SYNC_MODE_FULL;
+    int64_t offset, cached_end = 0, count;
+    int ret = 0;
 
-        for (offset = 0; offset < s->len; ) {
-            if (job_is_cancelled(job)) {
-                return -ECANCELED;
-            }
+    for (offset = 0; offset < s->len; offset += count) {
+        if (job_is_cancelled(job)) {
+            return -ECANCELED;
+        }
 
-            job_pause_point(job);
+        job_pause_point(job);
 
-            if (job_is_cancelled(job)) {
-                return -ECANCELED;
-            }
+        if (job_is_cancelled(job)) {
+            return -ECANCELED;
+        }
 
-            /* rdlock protects the subsequent call to bdrv_is_allocated() */
-            bdrv_graph_co_rdlock();
+        /* rdlock protects the block-status queries below */
+        bdrv_graph_co_rdlock();
+        if (skip) {
             ret = block_copy_reset_unallocated(s->bcs, offset, &count);
-            bdrv_graph_co_rdunlock();
-            if (ret < 0) {
-                return ret;
-            }
+        } else {
+            block_copy_calculate_zero_bitmap(s->bcs, offset, &cached_end,
+                                             &count);
+        }
+        bdrv_graph_co_rdunlock();
 
-            offset += count;
+        if (ret < 0) {
+            return ret;
         }
-        block_copy_set_zero_bitmap_valid(s->bcs);
     }
 
-    if (s->sync_mode == MIRROR_SYNC_MODE_NONE) {
+    block_copy_set_zero_bitmap_valid(s->bcs);
+
+    return 0;
+}
+
+static int coroutine_fn backup_run(Job *job, Error **errp)
+{
+    BackupBlockJob *s = container_of(job, BackupBlockJob, common.job);
+    int ret;
+
+    backup_init_bcs_bitmap(s);
+
+    switch (s->sync_mode) {
+    case MIRROR_SYNC_MODE_TOP:
+    case MIRROR_SYNC_MODE_FULL:
+    case MIRROR_SYNC_MODE_BITMAP:
+        ret = backup_scan_source(s);
+        if (ret < 0) {
+            return ret;
+        }
+        break;
+
+    case MIRROR_SYNC_MODE_NONE:
         /*
          * All bits are set in bcs bitmap to allow any cluster to be copied.
-         * This does not actually require them to be copied.
+         * This does not actually require them to be copied. Yield until the
+         * job is cancelled and let the before_write notify callback service
+         * CoW requests.
          */
         while (!job_is_cancelled(job)) {
-            /*
-             * Yield until the job is cancelled.  We just let our before_write
-             * notify callback service CoW requests.
-             */
             job_yield(job);
         }
-    } else {
-        return backup_loop(s);
+        return 0;
+
+    default:
+        g_assert_not_reached();
     }
 
-    return 0;
+    return backup_loop(s);
 }
 
 static void coroutine_fn backup_pause(Job *job)
diff --git a/block/block-copy.c b/block/block-copy.c
index 27a5d1a41b2..a35fba45183 100644
--- a/block/block-copy.c
+++ b/block/block-copy.c
@@ -843,6 +843,47 @@ int64_t coroutine_fn block_copy_reset_unallocated(BlockCopyState *s,
     return ret;
 }
 
+/*
+ * One scan step, like block_copy_reset_unallocated(); @cached_end
+ * tracks progress across calls, @count how far @offset should move.
+ */
+void coroutine_fn GRAPH_RDLOCK
+block_copy_calculate_zero_bitmap(BlockCopyState *s, int64_t offset,
+                                 int64_t *cached_end, int64_t *count)
+{
+    int64_t dirty_offset, dirty_bytes, dirty_end;
+    bool found;
+
+    /* CBW mutates copy_bitmap from its own AioContext meanwhile. */
+    WITH_QEMU_LOCK_GUARD(&s->lock) {
+        found = bdrv_dirty_bitmap_next_dirty_area(s->copy_bitmap, offset,
+                                                  s->len, INT64_MAX,
+                                                  &dirty_offset, &dirty_bytes);
+    }
+
+    if (!found) {
+        *count = s->len - offset;
+        return;
+    }
+    dirty_end = dirty_offset + dirty_bytes;
+
+    if (*cached_end < dirty_end) {
+        int64_t clusters;
+        int64_t query_offset = MAX(dirty_offset, *cached_end);
+        int ret = block_copy_is_cluster_allocated(s, query_offset, &clusters);
+
+        if (ret >= 0) {
+            *cached_end = query_offset + clusters * s->cluster_size;
+        } else {
+            /* Best-effort: leave this area unmarked; it copies as data. */
+            *cached_end = dirty_end;
+        }
+    }
+
+    /* 0 if the query above didn't yet reach dirty_end: try again next call. */
+    *count = *cached_end >= dirty_end ? dirty_end - offset : 0;
+}
+
 /*
  * Decide how @task is copied: COPY_WRITE_ZEROES if it reads as zero. May
  * shrink @task. Returns false if @task is to be skipped (already ended,
diff --git a/include/block/block-copy.h b/include/block/block-copy.h
index 57e13f82f78..40dd5432735 100644
--- a/include/block/block-copy.h
+++ b/include/block/block-copy.h
@@ -43,6 +43,10 @@ void block_copy_reset(BlockCopyState *s, int64_t offset, int64_t bytes);
 int64_t coroutine_fn GRAPH_RDLOCK
 block_copy_reset_unallocated(BlockCopyState *s, int64_t offset, int64_t *count);
 
+void coroutine_fn GRAPH_RDLOCK
+block_copy_calculate_zero_bitmap(BlockCopyState *s, int64_t offset,
+                                 int64_t *cached_end, int64_t *count);
+
 int coroutine_fn block_copy(BlockCopyState *s, int64_t offset, int64_t bytes,
                             bool ignore_ratelimit, uint64_t timeout_ns,
                             BlockCopyAsyncCallbackFunc cb,
-- 
2.53.0


^ permalink raw reply	[relevance 25%]

* [PATCH hci-8.0 2/9] iotests/040: cover large and fragmented commit runs #VSTOR-134085
    2026-09-30 11:17 21% ` [PATCH hci-8.0 1/9] block/commit: pass BDRV_WANT_PRECISE to block-status #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:17  3% ` Denis V. Lunev
  2026-09-30 11:17 18% ` [PATCH hci-8.0 3/9] block/commit: batch block-status queries #VSTOR-134085 Denis V. Lunev
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

From: Denis V. Lunev <den@openvz.org>

040 never commits runs long enough to span more than one block-status
query, so the answer that commit_iteration() is about to start caching
goes untested.

Add two cases on a base <- mid <- active chain, committing mid so the
job takes the regular commit path rather than active commit, and compare
base against a snapshot of mid taken before the commit. One case mixes
multi-megabyte data, zero and hole runs, the other fragments them down
to single clusters with every transition off the 512K boundary.

Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 tests/qemu-iotests/040     | 102 ++++++++++++++++++++++++++++++++++++-
 tests/qemu-iotests/040.out |   4 +-
 2 files changed, 103 insertions(+), 3 deletions(-)

diff --git a/tests/qemu-iotests/040 b/tests/qemu-iotests/040
index 5c18e413ec9..e01c9385bae 100755
--- a/tests/qemu-iotests/040
+++ b/tests/qemu-iotests/040
@@ -25,13 +25,14 @@
 import time
 import os
 import iotests
-from iotests import qemu_img, qemu_io
+from iotests import qemu_img, qemu_img_create, qemu_io, compare_images
 import struct
 import errno
 
 backing_img = os.path.join(iotests.test_dir, 'backing.img')
 mid_img = os.path.join(iotests.test_dir, 'mid.img')
 test_img = os.path.join(iotests.test_dir, 'test.img')
+reference_img = os.path.join(iotests.test_dir, 'reference.img')
 
 class ImageCommitTestCase(iotests.QMPTestCase):
     '''Abstract base class for image commit test cases'''
@@ -951,6 +952,105 @@ class TestCommitWithOverriddenBacking(iotests.QMPTestCase):
         self.vm.qmp('block-job-complete', device='commit')
         self.vm.event_wait('BLOCK_JOB_COMPLETED')
 
+class TestCommitLargeRuns(iotests.QMPTestCase):
+    """Commit runs long enough to cross commit_iteration()'s cached span."""
+
+    MB = 1024 * 1024
+    CLUSTER = 64 * 1024
+
+    # Runs several COMMIT_BUFFER_SIZE (512K) chunks long, of every kind.
+    SIZE = 32 * MB
+    LAYOUT = [
+        (0, 4 * MB, 'data'),
+        (4 * MB, 6 * MB, 'hole'),
+        (10 * MB, 4 * MB, 'data'),
+        (14 * MB, 6 * MB, 'zero'),
+        (20 * MB, 4 * MB, 'data'),
+        (24 * MB, 8 * MB, 'hole'),
+    ]
+
+    # The same, with every transition off the 512K boundary.
+    SIZE_FRAGMENTED = 384 * CLUSTER  # 24M
+    LAYOUT_FRAGMENTED = [
+        (0, 45 * CLUSTER, 'data'),
+        (45 * CLUSTER, 55 * CLUSTER, 'hole'),
+        (100 * CLUSTER, CLUSTER, 'data'),
+        (101 * CLUSTER, 49 * CLUSTER, 'zero'),
+        (150 * CLUSTER, 80 * CLUSTER, 'data'),
+        (230 * CLUSTER, CLUSTER, 'hole'),
+        (231 * CLUSTER, 69 * CLUSTER, 'data'),
+        (300 * CLUSTER, 83 * CLUSTER, 'zero'),
+        (383 * CLUSTER, CLUSTER, 'hole'),
+    ]
+
+    def setUp(self):
+        self.vm = iotests.VM()
+        self.vm.launch()
+
+    def tearDown(self):
+        self.vm.shutdown()
+        for img in (backing_img, mid_img, test_img, reference_img):
+            if os.path.exists(img):
+                os.remove(img)
+
+    def build_images(self, layout, size):
+        # A pattern of its own in base, so a misplaced cluster shows up.
+        qemu_img_create('-f', iotests.imgfmt, backing_img, str(size))
+        qemu_io('-c', f'write -P 0x11 0 {size}', backing_img)
+
+        qemu_img_create('-f', iotests.imgfmt, '-b', backing_img, '-F',
+                        iotests.imgfmt, mid_img, str(size))
+        for offset, length, kind in layout:
+            if kind == 'data':
+                qemu_io('-c', f'write -P 0x22 {offset} {length}', mid_img)
+            elif kind == 'zero':
+                qemu_io('-c', f'write -z {offset} {length}', mid_img)
+
+        # What base must equal once mid is committed into it.
+        qemu_img('convert', '-f', iotests.imgfmt, '-O', iotests.imgfmt,
+                 mid_img, reference_img)
+
+        # An empty layer above mid, so top_node=mid is not the active one.
+        qemu_img_create('-f', iotests.imgfmt, '-b', mid_img, '-F',
+                        iotests.imgfmt, test_img, str(size))
+
+        self.vm.cmd('blockdev-add', {
+            'node-name': 'base',
+            'driver': iotests.imgfmt,
+            'file': {'driver': 'file', 'filename': backing_img},
+        })
+        self.vm.cmd('blockdev-add', {
+            'node-name': 'mid',
+            'driver': iotests.imgfmt,
+            'file': {'driver': 'file', 'filename': mid_img},
+            'backing': 'base',
+        })
+        self.vm.cmd('blockdev-add', {
+            'node-name': 'active',
+            'driver': iotests.imgfmt,
+            'file': {'driver': 'file', 'filename': test_img},
+            'backing': 'mid',
+        })
+
+    def commit_and_verify(self):
+        self.vm.cmd('block-commit', job_id='commit0', device='active',
+                    top_node='mid', base_node='base')
+        self.wait_until_completed(drive='commit0')
+
+        self.vm.cmd('blockdev-del', node_name='active')
+        self.vm.cmd('blockdev-del', node_name='mid')
+        self.vm.cmd('blockdev-del', node_name='base')
+        self.assertTrue(compare_images(reference_img, backing_img))
+
+    def test_commit_large_runs(self):
+        self.build_images(self.LAYOUT, self.SIZE)
+        self.commit_and_verify()
+
+    def test_commit_fragmented_runs(self):
+        self.build_images(self.LAYOUT_FRAGMENTED, self.SIZE_FRAGMENTED)
+        self.commit_and_verify()
+
+
 if __name__ == '__main__':
     iotests.main(supported_fmts=['qcow2', 'qed'],
                  supported_protocols=['file'])
diff --git a/tests/qemu-iotests/040.out b/tests/qemu-iotests/040.out
index 1bb1dc5f0ea..d2e2a2d98f6 100644
--- a/tests/qemu-iotests/040.out
+++ b/tests/qemu-iotests/040.out
@@ -1,5 +1,5 @@
-.................................................................
+...................................................................
 ----------------------------------------------------------------------
-Ran 65 tests
+Ran 67 tests
 
 OK
-- 
2.53.0


^ permalink raw reply	[relevance 3%]

* [PATCH hci-8.0 9/9] block/block-copy: coalesce write-zeroes tasks #VSTOR-134085
                     ` (7 preceding siblings ...)
  2026-09-30 11:17 25% ` [PATCH hci-8.0 8/9] block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:17 26% ` Denis V. Lunev
  2026-09-30 11:34  3%   ` no-reply
  8 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

From: Denis V. Lunev <den@openvz.org>

Task size was capped to block_copy_chunk_size() regardless of method,
which sizes a task by the copy buffer. A write-zeroes task carries no
buffer, so that cap only splits one long known-zero run into a crowd of
small COPY_WRITE_ZEROES tasks, each with its own request against the
target.

block_copy_task_create() already decides from zero_bitmap how far a task
may run, so add block_copy_widen_zero_area() to that decision: for a
write-zeroes task, re-search the dirty area under BDRV_REQUEST_MAX_BYTES
instead of the buffer chunk size, and let the existing clamp cut it back
to where the zero run ends. That bound is INT_MAX rounded down to a
sector, so nothing has aligned it to cluster_size and it is aligned down
at the use site. Widening is safe: an overlapping caller waits on the
task's BlockReq via reqlist_wait_one() rather than observing it
mid-flight.

A request that large is only cheap where the target zeroes by metadata.
BDRV_REQ_NO_FALLBACK in supported_zero_flags rules out the targets which
cannot, qcow2 v2 and iscsi among them, but it is optimistic for the rest:
file-posix advertises it at open and only learns from a failing
fallocate. So a write-zeroes request asks for it while the target is
believed to oblige, which costs nothing when it does. A refusal ends the
widening, and the range is written in buffer-sized chunks, as is any
task widened before the refusal came. -ENOTSUP says the fast path is
unavailable, not that the target wrote nothing, so the range is
rewritten whole: zeroes over zeroes change nothing.

hci-8.0: 6b1a4754713 made the 056 copy-before-write source read as
zeroes. One widened write-zeroes task now covers all of it, and the
rate limit only charges a task after it ran, so speed=1 no longer
keeps the job alive until the test cancels it. Cap the task with
x-perf max-chunk there.

Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 block/block-copy.c     | 94 ++++++++++++++++++++++++++++++++++++++----
 tests/qemu-iotests/056 |  3 +-
 2 files changed, 89 insertions(+), 8 deletions(-)

diff --git a/block/block-copy.c b/block/block-copy.c
index a35fba45183..082257da7be 100644
--- a/block/block-copy.c
+++ b/block/block-copy.c
@@ -161,6 +161,8 @@ typedef struct BlockCopyState {
     /* State fields that use a thread-safe API */
     BdrvDirtyBitmap *copy_bitmap;
     bool top_mode;
+    /* Whether a write-zeroes task may widen; see block_copy_write_zeroes(). */
+    bool zero_widen; /* atomic */
     /* Clusters reading as zero; allocated on demand, frozen once valid. */
     HBitmap *zero_bitmap;
     /* Published only once the scan is done. */
@@ -189,6 +191,34 @@ static int64_t block_copy_chunk_size(BlockCopyState *s)
     }
 }
 
+/*
+ * A write-zeroes task carries no buffer, so it may cover far more than
+ * block_copy_chunk_size(). Return how far it may run; the caller clamps it
+ * to where the zero run ends.
+ */
+static int64_t block_copy_widen_zero_area(BlockCopyState *s,
+                                          BlockCopyCallState *call_state,
+                                          int64_t offset, int64_t search_end,
+                                          int64_t bytes)
+{
+    int64_t aligned = QEMU_ALIGN_DOWN(BDRV_REQUEST_MAX_BYTES,
+                                      s->cluster_size);
+    int64_t zero_chunk = MIN_NON_ZERO(MAX(aligned, s->cluster_size),
+                                      call_state->max_chunk);
+    int64_t wide_offset, wide_bytes;
+
+    if (!bdrv_dirty_bitmap_next_dirty_area(s->copy_bitmap, offset, search_end,
+                                           zero_chunk, &wide_offset,
+                                           &wide_bytes)) {
+        return bytes;
+    }
+
+    /* @offset is dirty, so the search cannot have moved past it. */
+    assert(wide_offset == offset);
+
+    return wide_bytes;
+}
+
 /*
  * Search for the first dirty area in offset/bytes range and create task at
  * the beginning of it.
@@ -200,6 +230,7 @@ block_copy_task_create(BlockCopyState *s, BlockCopyCallState *call_state,
     BlockCopyTask *task;
     BlockCopyMethod method;
     int64_t max_chunk;
+    int64_t search_end = offset + bytes;
 
     QEMU_LOCK_GUARD(&s->lock);
     max_chunk = MIN_NON_ZERO(block_copy_chunk_size(s), call_state->max_chunk);
@@ -221,6 +252,10 @@ block_copy_task_create(BlockCopyState *s, BlockCopyCallState *call_state,
 
         if (hbitmap_get(s->zero_bitmap, offset)) {
             method = COPY_WRITE_ZEROES;
+            if (qatomic_read(&s->zero_widen)) {
+                bytes = block_copy_widen_zero_area(s, call_state, offset,
+                                                   search_end, bytes);
+            }
             boundary = hbitmap_next_zero(s->zero_bitmap, offset, bytes);
         } else {
             boundary = hbitmap_next_dirty(s->zero_bitmap, offset, bytes);
@@ -481,6 +516,7 @@ BlockCopyState *block_copy_state_new(BdrvChild *source, BdrvChild *target,
         .max_transfer = QEMU_ALIGN_DOWN(
                                     block_copy_max_transfer(source, target),
                                     cluster_size),
+        .zero_widen = target->bs->supported_zero_flags & BDRV_REQ_NO_FALLBACK,
     };
 
     s->discard_source = discard_source;
@@ -537,6 +573,56 @@ static coroutine_fn int block_copy_task_run(AioTaskPool *pool,
     return 0;
 }
 
+/*
+ * Widening a write-zeroes request only pays off where the target zeroes by
+ * metadata, so while the target is believed to oblige, a request asks to fail
+ * instead of falling back to writing the zeroes out. That costs nothing when
+ * it does oblige, and a refusal ends the widening for the rest of the run.
+ */
+static int coroutine_fn GRAPH_RDLOCK
+block_copy_write_zeroes(BlockCopyState *s, int64_t offset, int64_t bytes,
+                        bool *error_is_read)
+{
+    BdrvRequestFlags flags = s->write_flags & ~BDRV_REQ_WRITE_COMPRESSED;
+    int64_t chunk;
+    int ret = 0;
+
+    if (qatomic_read(&s->zero_widen)) {
+        ret = bdrv_co_pwrite_zeroes(s->target, offset, bytes,
+                                    flags | BDRV_REQ_NO_FALLBACK);
+        if (ret != -ENOTSUP) {
+            goto out;
+        }
+
+        /* Redoing the range is safe: zeroes over zeroes change nothing. */
+        qatomic_set(&s->zero_widen, false);
+    }
+
+    WITH_QEMU_LOCK_GUARD(&s->lock) {
+        chunk = block_copy_chunk_size(s);
+    }
+
+    while (bytes) {
+        int64_t n = MIN(bytes, chunk);
+
+        ret = bdrv_co_pwrite_zeroes(s->target, offset, n, flags);
+        if (ret < 0) {
+            break;
+        }
+
+        offset += n;
+        bytes -= n;
+    }
+
+out:
+    if (ret < 0) {
+        trace_block_copy_write_zeroes_fail(s, offset, ret);
+        *error_is_read = false;
+    }
+
+    return ret;
+}
+
 /*
  * block_copy_do_copy
  *
@@ -568,13 +654,7 @@ block_copy_do_copy(BlockCopyState *s, int64_t offset, int64_t bytes,
 
     switch (*method) {
     case COPY_WRITE_ZEROES:
-        ret = bdrv_co_pwrite_zeroes(s->target, offset, nbytes, s->write_flags &
-                                    ~BDRV_REQ_WRITE_COMPRESSED);
-        if (ret < 0) {
-            trace_block_copy_write_zeroes_fail(s, offset, ret);
-            *error_is_read = false;
-        }
-        return ret;
+        return block_copy_write_zeroes(s, offset, nbytes, error_is_read);
 
     case COPY_RANGE_SMALL:
     case COPY_RANGE_FULL:
diff --git a/tests/qemu-iotests/056 b/tests/qemu-iotests/056
index e006d1296d9..72ea28365ea 100755
--- a/tests/qemu-iotests/056
+++ b/tests/qemu-iotests/056
@@ -117,7 +117,8 @@ class TestCopyBeforeWrite(iotests.QMPTestCase):
         self.vm.pause_drive("drive0")
         self.vm.cmd('drive-backup', device='drive0',
                     sync='full', target=target_img,
-                    format="file", speed=1)
+                    format="file", speed=1,
+                    x_perf={'max-chunk': 65536})
         self.vm.cmd('block-job-pause', device="drive0")
         # Speed is low enough that this must be an uncopied range, which will
         # trigger the before write notifier
-- 
2.53.0


^ permalink raw reply	[relevance 26%]

* Re: [PATCH hci-8.0 3/9] block/commit: batch block-status queries #VSTOR-134085
  2026-09-30 11:17 18% ` [PATCH hci-8.0 3/9] block/commit: batch block-status queries #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:21  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-30 11:21 UTC (permalink / raw)
  To: svt-core, Denis V. Lunev; +Cc: andrey.drobyshev, den

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] block/commit: unallocated zero span is still capped at COMMIT_ZERO_CHUNK despite comment
--

commit 1c010ab0f99a89e08cf4922150238581339cd7aa
Author: Denis V. Lunev <den@openvz.org>

block/commit: batch block-status queries #VSTOR-134085

This patch changes commit_iteration() to query block status for the whole
remainder of the image and cache the answer in a CommitStatus owned by
commit_run(). Copying of allocated data stays bounded by
COMMIT_BUFFER_SIZE. Zero spans are bounded by the new COMMIT_ZERO_CHUNK.

> diff --git a/block/commit.c b/block/commit.c
> index a2e807288b23..80b89e6269d8 100644
> --- a/block/commit.c
> +++ b/block/commit.c

[ ... ]

> @@ -129,26 +144,46 @@ static void commit_clean(Job *job)

[ ... ]

> +    ret = st->ret;
> +    bytes = st->end - offset;
>  
> +    /* An unallocated span costs no I/O, so it is crossed in one step. */
> +    if (ret & BDRV_BLOCK_ZERO) {
> +        bytes = MIN(bytes, COMMIT_ZERO_CHUNK);
> +    } else if (ret & BDRV_BLOCK_ALLOCATED) {
> +        bytes = MIN(bytes, COMMIT_BUFFER_SIZE);
>      }

[Severity: Low]
This isn't a bug, but the comment and the commit message don't match what
the code does. The comment says "An unallocated span costs no I/O, so it is
crossed in one step", and the commit message says "an unallocated span is
crossed in one step".

Can a span that reports BDRV_BLOCK_ZERO without BDRV_BLOCK_ALLOCATED still
be capped at COMMIT_ZERO_CHUNK here?

bdrv_co_do_block_status() in block/io.c returns BDRV_BLOCK_ZERO without
BDRV_BLOCK_ALLOCATED in two cases:

- A supports_backing layer that has no cow_bs.
- BDRV_WANT_PRECISE mode with offset at or beyond the backing file size.
  This can happen when top is larger than the backing of base_overlay.

For these spans, commit_iteration() takes the first branch:

    if (ret & BDRV_BLOCK_ZERO) {
        bytes = MIN(bytes, COMMIT_ZERO_CHUNK);

The later "if (ret & BDRV_BLOCK_ALLOCATED)" test is false, so no I/O is
done, but the span is still split into 256M steps. Each extra step goes
through block_job_ratelimit_sleep(), the job_is_cancelled() check and
job_progress_update().

Only the fall-through case, where neither flag is set, is crossed in one
step. Should the cap apply only when both BDRV_BLOCK_ZERO and
BDRV_BLOCK_ALLOCATED are set, or should the comment and commit message be
adjusted to match the current behaviour?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930111739.3866427-1-den@openvz.org?part=3

^ permalink raw reply	[relevance 3%]

* Re: [PATCH hci-8.0 7/9] block/block-copy: track known-zero source clusters #VSTOR-134085
  2026-09-30 11:17 33% ` [PATCH hci-8.0 7/9] block/block-copy: track known-zero source clusters #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:30  6%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-30 11:30 UTC (permalink / raw)
  To: svt-core, Denis V. Lunev; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Low] block-copy: ctz32() on 64-bit cluster_size gives wrong zero_bitmap granularity
- [Low] block-copy: unaligned EOF tail cluster never marked zero in zero_bitmap
- [Low] block-copy: skip_unallocated doc stale; early return relies on implicit complete-scan invariant
--

commit 00cb83b3deb7d878b4202ae80e855cacc98ffc19
Author: Denis V. Lunev <den@openvz.org>

block/block-copy: track known-zero source clusters #VSTOR-134085

This patch caches the source's zero-ness in a new zero_bitmap, filled by
the block_copy_reset_unallocated() scan. The bitmap is published through
zero_bitmap_valid once the scan is done. After that,
block_copy_task_create() picks the copy method from the bitmap and ends
each task where the answer changes.

> diff --git a/block/block-copy.c b/block/block-copy.c
> --- a/block/block-copy.c
> +++ b/block/block-copy.c

[ ... ]

> @@ -688,50 +722,79 @@ int block_copy_block_status(BlockCopyState *s, int64_t offset, int64_t bytes,
>      return ret;
>  }
>  
> +/* Only the scan allocates, and it runs before zero_bitmap_valid. */
> +static HBitmap *block_copy_zero_bitmap(BlockCopyState *s)
> +{
> +    if (!s->zero_bitmap) {
> +        s->zero_bitmap = hbitmap_alloc(s->len, ctz32(s->cluster_size));

[Severity: Low]
Is ctz32() the right helper here? s->cluster_size is an int64_t, and
block_copy_state_new() accepts any power-of-two min_cluster_size up to
INT64_MAX.

For a cluster_size of 2^33 or more, the low 32 bits are zero. In that
case ctz32() returns 32, so hbitmap_alloc() gets granularity 32. The
zero_bitmap then describes different cluster boundaries than copy_bitmap.

Those boundaries would feed hbitmap_get() and hbitmap_next_zero() in
block_copy_task_create(). Would ctz64() be more appropriate? This only
matters for a very large x-perf min-cluster-size, so it may be a niche
case.

> +    }
> +
> +    return s->zero_bitmap;
> +}
> +
> +static void block_copy_mark_zero_prefix(BlockCopyState *s, int64_t offset,
> +                                        int64_t zero_count)
> +{
> +    int64_t zero_bytes = QEMU_ALIGN_DOWN(zero_count, s->cluster_size);
> +
> +    if (zero_bytes > 0) {
> +        hbitmap_set(block_copy_zero_bitmap(s), offset, zero_bytes);
> +    }
> +}

[Severity: Low]
When s->len is not a multiple of cluster_size and the tail reads as
zero, QEMU_ALIGN_DOWN(zero_count, s->cluster_size) drops the tail
cluster. Is that intended?

The old per-task path went through block_copy_block_status(). It aligned
the final chunk up to a full cluster and could return BDRV_BLOCK_ZERO for
it.

Once zero_bitmap_valid is set, block_copy_set_task_method() returns early
and never re-queries. The tail is then read and written as data instead of
using COPY_WRITE_ZEROES. This is only a missed optimization of one
cluster-sized copy per job.

[ ... ]

> @@ -791,6 +854,12 @@ block_copy_set_task_method(BlockCopyState *s, BlockCopyTask *task)
>      int ret;
>      int64_t status_bytes;
>  
> +    /* block_copy_task_create() already decided, from zero_bitmap. */
> +    if (qatomic_load_acquire(&s->zero_bitmap_valid)) {
> +        return true;
> +    }

[Severity: Low]
This isn't a bug, but the early return skips the skip_unallocated check
and the block_copy_task_shrink() call. That is correct only because
backup_run() sets the flag after the full scan. The scan has already
cleared every unallocated cluster from copy_bitmap.

The comment on BlockCopyState.skip_unallocated still says:

     * In this case, block_copy() will query the source's allocation status,
     * skip unallocated regions, clear them in the copy_bitmap, and invoke
     * block_copy_reset_unallocated() every time it does.

That comment was not updated for the zero_bitmap short-circuit.

Should the complete-scan requirement be documented on
block_copy_set_zero_bitmap_valid(), or asserted? Otherwise a later caller
could publish the flag without a full scan. Unallocated clusters would
then be copied silently.

> +
> +    /* CBW filter could call this early. */
>      ret = block_copy_block_status(s, task->req.offset, task->req.bytes,
>                                    &status_bytes);

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930111739.3866427-1-den@openvz.org?part=7

^ permalink raw reply	[relevance 6%]

* Re: [PATCH hci-8.0 4/9] iotests/124: cover backup of zero clusters and holes #VSTOR-134085
  @ 2026-09-30 11:31  5%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-30 11:31 UTC (permalink / raw)
  To: svt-core, Denis V. Lunev; +Cc: andrey.drobyshev, den

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] iotests: 124 TAIL_ZERO tests expect behaviour that only appears in later patches
--

commit 147936f45e8ec1c124ea1a5b3dbf035c9a679b89
Author: Denis V. Lunev <den@openvz.org>

iotests/124: cover backup of zero clusters and holes #VSTOR-134085

This commit adds iotest 124 cases that back up sources mixing data,
write-zero and holes with sync=full, sync=top and sync=bitmap. It checks
the target layout with qemu-img map. It covers boundary, large-zero,
tail-cluster and mixed-cluster cases.

> diff --git a/tests/qemu-iotests/124 b/tests/qemu-iotests/124
> --- a/tests/qemu-iotests/124
> +++ b/tests/qemu-iotests/124

[ ... ]

> +# A zero run past the old block_copy_chunk_size() 16M cap.
> +LARGE_ZERO = Case(48 * MB, [
> +    (0, MB, 'data'),
> +    (4 * MB, 32 * MB, 'zero'),
> +    (40 * MB, MB, 'data'),
> +])

[Severity: Medium]
The comment about the old block_copy_chunk_size() 16M cap refers to a
chunk-cap removal that only lands in the later patch "block/block-copy:
coalesce write-zeroes tasks". Is this comment accurate at this commit?

> +# Image size not a multiple of the cluster size: a partial tail cluster.
> +TAIL_SIZE = 4 * MB + 4096
> +TAIL_CLUSTER = (TAIL_SIZE // CLUSTER) * CLUSTER

[ ... ]

> +# The partial tail cluster falls out of zero_bitmap, so it copies as data.
> +TAIL_ZERO = Case(TAIL_SIZE, [
> +    (0, MB, 'data'),
> +    (2 * MB, TAIL_SIZE - 2 * MB, 'zero'),
> +], [
> +    (0, MB, 'data'),
> +    (2 * MB, TAIL_CLUSTER - 2 * MB, 'zero'),
> +    (TAIL_CLUSTER, TAIL_SIZE - TAIL_CLUSTER, 'data'),
> +])

[Severity: Medium]
Can test_tail_zero and test_tail_zero_bitmap pass at this commit? The
expected 'data' extent at TAIL_CLUSTER appears to hold only after later
patches in the series.

block/block-copy.c has no zero_bitmap at this point, and git grep finds it
only in block/mirror.c. The zero_bitmap is added by the later patch
"block/block-copy: track known-zero source clusters", and
block_copy_mark_zero_prefix() there marks only
QEMU_ALIGN_DOWN(zero_count, cluster_size). The tail cluster then falls
outside the bitmap and is copied as data.

Here is what seems to happen at this commit. TAIL_ZERO writes zeroes from
2M to EOF on a 4M+4096 source. qcow2_co_pwrite_zeroes() skips tail handling
when offset+bytes equals the image end, so the whole partial last cluster
becomes a zero cluster on the source.

block_copy_block_status() then reports ZERO for the run and rounds it up to
the cluster size, because offset+num == s->len. After that,
block_copy_dirty_clusters() selects the write-zeroes method:

block/block-copy.c:block_copy_dirty_clusters() {
    ...
    if (ret & BDRV_BLOCK_ZERO) {
        task->method = COPY_WRITE_ZEROES;
    }
    ...
}

block_copy_do_copy() calls bdrv_co_pwrite_zeroes() with nbytes clipped to
s->len. The request ends at the target EOF, so qcow2 again skips tail
handling.

The target would therefore report a zero extent up to EOF, while
assert_map() expects a 4096-byte 'data' extent at TAIL_CLUSTER. The
assert_map() comparison should fail at this commit and pass only at the end
of the series. That breaks bisectability.

The later patches that make the expectation true are "block/block-copy:
track known-zero source clusters" and "block/backup: pre-fill zero_bitmap
for full/bitmap". This was found by static reading and the tests were not
run.

Also, the commit message says the tests pin how backup treats zero clusters
and holes. Is that accurate for the TAIL_ZERO and LARGE_ZERO cases, which
depend on behaviour added later in the series?

Could these cases, or at least their comments and expectations, move into
the patches that introduce the behaviour?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930111739.3866427-1-den@openvz.org?part=4

^ permalink raw reply	[relevance 5%]

* Re: [PATCH hci-8.0 8/9] block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085
  2026-09-30 11:17 25% ` [PATCH hci-8.0 8/9] block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:33  5%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-30 11:33 UTC (permalink / raw)
  To: Denis V. Lunev, svt-core; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] block: block_copy_calculate_zero_bitmap() can return count 0 forever, hanging the scan
- [Low] block/backup: commit message says full/bitmap but only sync=bitmap uses the new scan
--

commit 252a85c484652c4a95f8a77f49b08e8150b81585
Author: Denis V. Lunev <den@openvz.org>

block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085

This commit adds block_copy_calculate_zero_bitmap() and folds the
up-front source scan into a new backup_scan_source(). The scan now
pre-fills zero_bitmap for sync=bitmap as well as sync=top. sync=full
stays on block_copy_reset_unallocated(), and sync=none keeps the
per-task query.

> block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085

This isn't a code bug, but the subject line and body say that both FULL
and BITMAP get the new up-front scan. Two quoted portions of the commit
message read this way:

  "Add block_copy_calculate_zero_bitmap(), driven one query at a time
  from backup_scan_source() ..."
  "FULL and BITMAP know their complete copy_bitmap before copying
  starts"

In the code, sync=full still takes the block_copy_reset_unallocated()
path, and only sync=bitmap calls block_copy_calculate_zero_bitmap(). The
trailing paragraph of the message says so itself: "sync=full already
runs block_copy_reset_unallocated() ... so it stays on that path and only
sync=bitmap moves to block_copy_calculate_zero_bitmap()".

That leaves the message contradicting itself. It also carries the
downstream "hci-8.0:" tag. Could the subject and body be reworded to
match the code?

[Severity: Low]

> diff --git a/block/backup.c b/block/backup.c
> --- a/block/backup.c
> +++ b/block/backup.c

[ ... ]

> +static int coroutine_fn backup_scan_source(BackupBlockJob *s)
>  {
[ ... ]
> +    for (offset = 0; offset < s->len; offset += count) {
> +        if (job_is_cancelled(job)) {
> +            return -ECANCELED;
> +        }
> +
> +        job_pause_point(job);
[ ... ]
> +        bdrv_graph_co_rdlock();
> +        if (skip) {
> +            ret = block_copy_reset_unallocated(s->bcs, offset, &count);
> +        } else {
> +            block_copy_calculate_zero_bitmap(s->bcs, offset, &cached_end,
> +                                             &count);
> +        }
> +        bdrv_graph_co_rdunlock();

> diff --git a/block/block-copy.c b/block/block-copy.c
> --- a/block/block-copy.c
> +++ b/block/block-copy.c

[ ... ]

> +    if (*cached_end < dirty_end) {
> +        int64_t clusters;
> +        int64_t query_offset = MAX(dirty_offset, *cached_end);
> +        int ret = block_copy_is_cluster_allocated(s, query_offset, &clusters);
> +
> +        if (ret >= 0) {
> +            *cached_end = query_offset + clusters * s->cluster_size;
> +        } else {
> +            /* Best-effort: leave this area unmarked; it copies as data. */
> +            *cached_end = dirty_end;
> +        }
> +    }
> +
> +    /* 0 if the query above didn't yet reach dirty_end: try again next call. */
> +    *count = *cached_end >= dirty_end ? dirty_end - offset : 0;
> +}

Can block_copy_calculate_zero_bitmap() return *count == 0 forever for
sync=bitmap?

block_copy_is_cluster_allocated() sets *pnum = DIV_ROUND_UP(total_count,
cluster_size). That is 0 when bdrv_co_block_status_above() returns
count == 0, for example at or beyond the source EOF. The function still
returns ret >= 0 in that case, since it has an explicit "count == 0"
branch.

If that happens, the ret >= 0 branch above sets
  *cached_end = query_offset + 0 * s->cluster_size;
which is still below dirty_end. *count then stays 0.

In backup_scan_source(), "offset += count" does not advance, so the next
iteration repeats the identical query. job_pause_point() and
job_is_cancelled() do not yield unless the job is paused or cancelled.
That would leave the scan spinning without yielding in the job's
AioContext.

I could not confirm that s->len can exceed the real source length, for
example if the source shrinks during the job, so this may be hard to
reach.

The same "offset += count" shape already exists in
block_copy_reset_unallocated() for top/full. However, the sync=bitmap
path through block_copy_calculate_zero_bitmap() is new with this patch.
The later patch "block/block-copy: coalesce write-zeroes tasks" does not
touch this scan.

Would it make sense to force progress, for example by treating
clusters == 0 like the error path (cached_end = dirty_end), or by
clamping clusters to at least 1?

[Severity: Medium]

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930111739.3866427-1-den@openvz.org?part=8

^ permalink raw reply	[relevance 5%]

* Re: [PATCH hci-8.0 9/9] block/block-copy: coalesce write-zeroes tasks #VSTOR-134085
  2026-09-30 11:17 26% ` [PATCH hci-8.0 9/9] block/block-copy: coalesce write-zeroes tasks #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:34  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-30 11:34 UTC (permalink / raw)
  To: Denis V. Lunev, svt-core; +Cc: andrey.drobyshev, den

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] block-copy: widened write-zeroes task blocks overlapping writers and rate limit for ~2 GiB
- [Low] block-copy: write_zeroes_fail trace reports advanced offset in chunked fallback
--

commit f1b3fcf0ac0c96a51feac391dbd24c727baced51
Author: Denis V. Lunev <den@openvz.org>

block/block-copy: coalesce write-zeroes tasks #VSTOR-134085

This patch lets a write-zeroes task in block-copy grow to about
BDRV_REQUEST_MAX_BYTES instead of being capped at block_copy_chunk_size().
It adds block_copy_widen_zero_area() and a new block_copy_write_zeroes().
The new function first asks the target for BDRV_REQ_NO_FALLBACK and falls
back to chunked writes on -ENOTSUP. The patch also caps the chunk size in
qemu-iotests 056.

> diff --git a/block/block-copy.c b/block/block-copy.c
> --- a/block/block-copy.c
> +++ b/block/block-copy.c

[ ... ]

> +static int64_t block_copy_widen_zero_area(BlockCopyState *s,
> +                                          BlockCopyCallState *call_state,
> +                                          int64_t offset, int64_t search_end,
> +                                          int64_t bytes)
> +{
> +    int64_t aligned = QEMU_ALIGN_DOWN(BDRV_REQUEST_MAX_BYTES,
> +                                      s->cluster_size);
> +    int64_t zero_chunk = MIN_NON_ZERO(MAX(aligned, s->cluster_size),
> +                                      call_state->max_chunk);

[Severity: Medium]
Can one widened task now hold its BlockReq for too long?

With this limit, block_copy_task_create() can register a single BlockReq of
up to about 2 GiB through reqlist_init_req().

A copy-before-write guest write that overlaps the range waits in
reqlist_wait_one() until one bdrv_co_pwrite_zeroes() of that whole size
completes.

The job rate limit only charges a task after it has run. Pause and cancel
also take effect only between tasks, so they wait for the whole request too.

The only mitigation left is the existing max-chunk option. The commit
message admits this effect for test 056, but the limit is changed for all
users.

Is there a reason not to bound the task size by something smaller by
default?

[ ... ]

> +static int coroutine_fn GRAPH_RDLOCK
> +block_copy_write_zeroes(BlockCopyState *s, int64_t offset, int64_t bytes,
> +                        bool *error_is_read)
> +{
> +    BdrvRequestFlags flags = s->write_flags & ~BDRV_REQ_WRITE_COMPRESSED;
> +    int64_t chunk;
> +    int ret = 0;
> +
> +    if (qatomic_read(&s->zero_widen)) {
> +        ret = bdrv_co_pwrite_zeroes(s->target, offset, bytes,
> +                                    flags | BDRV_REQ_NO_FALLBACK);
> +        if (ret != -ENOTSUP) {
> +            goto out;
> +        }

[Severity: Medium]
Is this a problem for slow targets?

The first request covers the whole widened range in one call. If the
target refuses the fast path, for example file-posix on a failing
fallocate, the range is then rewritten in chunks by the while loop below.

Overlapping writers stay blocked on the task's BlockReq during both
passes. NO_FALLBACK is also only optimistic, as the commit message says,
so slow zeroing paths such as a block device or a network filesystem can
still be reached.

[ ... ]

> +        offset += n;
> +        bytes -= n;
> +    }
> +
> +out:
> +    if (ret < 0) {
> +        trace_block_copy_write_zeroes_fail(s, offset, ret);

[Severity: Low]
This isn't a bug, but the offset passed to trace_block_copy_write_zeroes_fail()
differs between paths.

In the chunked fallback loop, offset is advanced by n after each
successful chunk. If a later chunk fails, the trace reports the advanced
offset. If the NO_FALLBACK request fails, it reports the task start.

The old code always reported the task offset. Would it be better to report
the same value on both paths, for example by using a separate variable for
the loop position?

[ ... ]

> diff --git a/tests/qemu-iotests/056 b/tests/qemu-iotests/056
> --- a/tests/qemu-iotests/056
> +++ b/tests/qemu-iotests/056

[ ... ]

> @@ -117,7 +117,8 @@ class TestCopyBeforeWrite(iotests.QMPTestCase):
>          self.vm.pause_drive("drive0")
>          self.vm.cmd('drive-backup', device='drive0',
>                      sync='full', target=target_img,
> -                    format="file", speed=1)
> +                    format="file", speed=1,
> +                    x_perf={'max-chunk': 65536})

[Severity: Medium]
The test has to cap the chunk size by hand to keep the job rate-limited.
Does this show that the throttling behavior changed for real users, with
the workaround applied only in the test?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930111739.3866427-1-den@openvz.org?part=9

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 2/3] fs/fuse kio: refactor transmit path in pcs_sockio_send()
  2026-09-30  7:42  3%   ` no-reply
@ 2026-09-30 16:29  0%     ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-30 16:29 UTC (permalink / raw)
  To: Liu Kui, Alexey Kuznetsov; +Cc: devel, azaitsev

i have commited this patch, but this review looks valid, is you agree,
please consider sending an incremental patch like

  diff --git a/fs/fuse/kio/pcs/pcs_sock_io.c b/fs/fuse/kio/pcs/pcs_sock_io.c
  index 2da6d51c898b0..3a2228088540a 100644
  --- a/fs/fuse/kio/pcs/pcs_sock_io.c
  +++ b/fs/fuse/kio/pcs/pcs_sock_io.c
  @@ -307,6 +307,12 @@ static int do_sockio_sendmsg(struct pcs_sockio *sio, struct pcs_msg *msg)
                if (!iov_iter_count(it)) {
                        /* Current iter is exhausted, init new one */
                        msg->get_iter(msg, sio->write_offset, it, WRITE);
  +
  +                     /* An empty refill would spin this loop forever */
  +                     if (WARN_ON(!iov_iter_count(it))) {
  +                             clear_and_wake_up_bit(PCS_MSG_BUSY, &msg->flags);
  +                             return -PCS_ERR_NET_ABORT;
  +                     }
                }

                BUG_ON(iov_iter_count(it) > left);

--
Best regards,

Konstantin Khorenko,
Virtuozzo Linux Kernel Team

On 9/30/26 09:42, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> - [Medium] fs/fuse kio: do_sockio_sendmsg() can spin forever on an empty get_iter() refill
> - [Low] fs/fuse kio: iov_iter_sendpages_ok() under-checks pages of unaligned multi-page bvecs
> --
> 
> commit 2b6fc40ff00b50732f7cf6408214e55943ba4135
> Author: Liu Kui <kui.liu@virtuozzo.com>
> 
> fs/fuse kio: refactor transmit path in pcs_sockio_send()
> 
> This patch replaces do_send_one_seg() with do_sockio_sendmsg(). The new
> function submits the whole iov_iter in one sock_sendmsg() call, and uses
> MSG_SPLICE_PAGES when all pages qualify. It also clears PCS_MSG_BUSY in
> the transport whenever IO on the msg ends.
> 
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-142167
> 
>> diff --git a/fs/fuse/kio/pcs/pcs_sock_io.c b/fs/fuse/kio/pcs/pcs_sock_io.c
>> --- a/fs/fuse/kio/pcs/pcs_sock_io.c
>> +++ b/fs/fuse/kio/pcs/pcs_sock_io.c
> [ ... ]
> 
>> +static bool iov_iter_sendpages_ok(struct iov_iter *it)
>> +{
>> +	const struct bio_vec *bvec = it->bvec;
>> +
>> +	if (!iov_iter_is_bvec(it))
>> +		return false;
>> +
>> +	while (bvec < (it->bvec + it->nr_segs)) {
>> +		if (!sendpages_ok(bvec->bv_page, bvec->bv_len, bvec->bv_offset))
>> +			return false;
>> +		bvec++;
>> +	}
>> +
>> +	return true;
>> +}
> [Severity: Low]
> Can iov_iter_sendpages_ok() skip the last page of a multi-page bvec that
> has an unaligned bv_offset?
> 
> sendpages_ok() starts at page + (offset >> PAGE_SHIFT) and steps
> PAGE_SIZE for len bytes.  For example, with bv_offset=2048 and
> bv_len=4096 the range covers two pages, but only one is checked with
> sendpage_ok().
> 
> The bvec builder in pcs_krpc.c (umem branch) sets bv_offset to a
> non-page-aligned value and bv_len up to the contiguous length, so such
> bvecs look possible:
> 
>     bvec->bv_offset = offset;
>     ...
>     bvec->bv_len = len < contig ? len : contig;
> 
> If that happens, MSG_SPLICE_PAGES could be set in do_sockio_sendmsg()
> while the unchecked page is a slab page or has a zero refcount.  That
> page would then be spliced into the socket by reference instead of being
> copied.  Is the page check meant to cover the pages that the byte range
> actually touches?
> 
>> +static int do_sockio_sendmsg(struct pcs_sockio *sio, struct pcs_msg *msg)
>> +{
> [ ... ]
> 
>> +	while (sio->write_offset < msg->size) {
>> +		size_t left = msg->size - sio->write_offset;
>> +		struct msghdr sockmsg = {.msg_flags = flags};
>> +
>> +		TRACE("sio(%px) offset:%d msg:%px left:%zu, it->len:%zu\n", sio, sio->write_offset, msg, left, iov_iter_count(it));
>> +
>> +		if (!iov_iter_count(it)) {
>> +			/* Current iter is exhausted, init new one */
>> +			msg->get_iter(msg, sio->write_offset, it, WRITE);
>> +		}
>> +
>> +		BUG_ON(iov_iter_count(it) > left);
> [ ... ]
> 
>> +		while (iov_iter_count(it)) {
> [Severity: Medium]
> Can do_sockio_sendmsg() spin forever here if msg->get_iter() returns an
> iterator with a zero count while sio->write_offset < msg->size?
> 
> In that case the BUG_ON(iov_iter_count(it) > left) check passes, the
> inner while (iov_iter_count(it)) loop is skipped, and sock_sendmsg() is
> never called.  WARN_ON(msg_data_left(&sockmsg)) sees zero and does not
> fire.  write_offset does not change, so the outer loop repeats with the
> same state and no exit, which would be a soft lockup.
> 
> The old do_send_one_seg() returned 0 for an empty segment, and
> pcs_sockio_send() turned that into a WARN_ON and sio_abort(). I did not
> find a get_iter() callback that returns an empty iterator for an
> in-range offset (krpc_msg_get_data() and pcs_get_iter_inline() look
> fine), so this looks latent.  Would it make sense to add a check that
> fails the message when get_iter() returns an empty iterator?
> 
>> +			int ret;
> [ ... ]
> 
> -- Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930-sockio-refactor-8-1-v1-0-806348b9eb26@virtuozzo.com?part=2
> 

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* [Devel] [PATCH hci-8.1 v5 1/2] selftests: ve: grant the directories the files sit in
  @ 2026-10-01  3:24 11% ` Eva Kurchatova
  2026-10-01  3:24  6% ` [Devel] [PATCH hci-8.1 v5 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
  1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-10-01  3:24 UTC (permalink / raw)
  To: khorenko; +Cc: devel

The sysfs variant names nested leaves, kernel/vmcoreinfo and
kernel/notes, while the proc one names files at the top of the
filesystem. Both enforcement tests grant the leaves and nothing else,
so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
cannot traverse, and every open answers ENOENT.

Grant x on each directory along the way, taken from the path the
variant already gives.

Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")

https://virtuozzo.atlassian.net/browse/VSTOR-144955

Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/ve/ve_perms_test.c | 34 ++++++++++++++++++++--
 1 file changed, 32 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 25ffbd42c380..8096b1fa3b43 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -114,9 +114,11 @@ static int ve_open_rel(int cgv2_fd, int ctid, const char *prefix,
 static int set_entry(int cgv2_fd, const char *path, const char *entry,
 		     const char *mask)
 {
-	char line[256];
+	char line[PATH_MAX];
 
-	snprintf(line, sizeof(line), "%s %s", entry, mask);
+	if (snprintf(line, sizeof(line), "%s %s", entry, mask) >=
+	    (int)sizeof(line))
+		return -1;
 	return write_file_at(cgv2_fd, path, line);
 }
 
@@ -343,6 +345,32 @@ TEST_F(ve_perms, write_parser)
 	EXPECT_LT(write_file_at(self->cgv2_fd, path, file), 0);
 }
 
+/* A leaf is reachable only once every directory above it can be traversed */
+static int allow_path(int cgv2_fd, const char *path, const char *file)
+{
+	char dir[PATH_MAX];
+	const char *sep;
+
+	for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
+		if (snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file),
+			     file) >= (int)sizeof(dir))
+			return -1;
+		if (set_entry(cgv2_fd, path, dir, "x") < 0)
+			return -1;
+	}
+	return 0;
+}
+
+static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
+{
+	int i;
+
+	for (i = 0; i < NR_FILES; i++)
+		if (allow_path(cgv2_fd, path, files[i]) < 0)
+			return -1;
+	return 0;
+}
+
 /*
  * Enforcement, allowed side: from inside a VE, granting r lets each file be
  * read, and granting x on the directory lets it be traversed (the lookup then
@@ -358,6 +386,7 @@ TEST_F(ve_perms, enforce_allows)
 
 	ve_file(path, sizeof(path), self->ctid_a, variant->fs);
 
+	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
 	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
 	for (i = 0; i < NR_FILES; i++)
 		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
@@ -400,6 +429,7 @@ TEST_F(ve_perms, enforce_denies)
 				      O_RDONLY), 0);
 
 	/* Granted w only: visible but not readable. */
+	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
 	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
 	for (i = 0; i < NR_FILES; i++)
 		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 11%]

* [Devel] [PATCH hci-8.1 v5 2/2] selftests: ve: introduce vtty pair teardown stress test
    2026-10-01  3:24 11% ` [Devel] [PATCH hci-8.1 v5 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
@ 2026-10-01  3:24  6% ` Eva Kurchatova
  2026-10-01  3:34  5%   ` no-reply
  1 sibling, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-10-01  3:24 UTC (permalink / raw)
  To: khorenko; +Cc: devel

Either end of a vtty pair creates it, and the last close tears it down.

Race condition in older VZKernel vtty implementation allowed both sides
to decide they were the final one and call release_tty() twice, and a
master opening again while a teardown was in flight used to hand out a
reference to a tty_struct about to be freed. Either way, this caused
use-after-free. That issue was fixed in c5028cd61a69:
"ve/vtty: fix use-after-free on concurrent tty close and reopen"

Hammer both ends for ownership correctness & thread safety verification.

The original reproducer needed a few tens of minutes across a dozen
containers, where vzctl interaction with vtty subsystem was a small part
of the overall runtime, therefore it did not fire on every run. This test
opens and closes nothing but the pair, from both ends and without pause,
so the default stays in the tens of seconds and under the timeout of the
suite, and VE_VTTY_SECS or argv[1] allows to control this.

The test assumes the veid it picks is not in use by a running container:
ve_id_write() does not check that a veid is unique and the vtty map is
keyed by veid, so a clash would hand the test someone else's pair. The
108..200 range make_ve() uses, makes that unlikely on a test node.

https://virtuozzo.atlassian.net/browse/VSTOR-144771

Feature: ve selftest
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/ve/.gitignore       |   1 +
 tools/testing/selftests/ve/Makefile         |   3 +-
 tools/testing/selftests/ve/ve_vtty_stress.c | 317 ++++++++++++++++++++
 3 files changed, 320 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/ve/ve_vtty_stress.c

diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index 3df4d05888dc..d8eb5c346c6e 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,3 +1,4 @@
 ve_ns_owner_test
 ve_perms_test
 ve_mount_accounting_test
+ve_vtty_stress
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index c6efe7c4b4fb..d287a73c5e97 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -1,9 +1,10 @@
 # SPDX-License-Identifier: GPL-2.0
 # Makefile for ve selftests.
-CFLAGS += -g -Wall -O2
+CFLAGS += -g -Wall -O2 $(KHDR_INCLUDES)
 
 TEST_GEN_PROGS += ve_ns_owner_test
 TEST_GEN_PROGS += ve_perms_test
 TEST_GEN_PROGS += ve_mount_accounting_test
+TEST_GEN_PROGS += ve_vtty_stress
 
 include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
new file mode 100644
index 000000000000..56355bb8dbe4
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_vtty_stress.c
@@ -0,0 +1,317 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_vtty_stress - open and close a vtty pair from both ends at once.
+ *
+ * Either end of a vtty pair creates it, and the last close tears it down.
+ *
+ * Hammer both ends. The master comes from the vzctl ioctl, which does
+ * not look the VE up. The slave is reachable only from inside the VE, as
+ * TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
+ * vttys index minor - 1, so the test builds a VE with cgroup2, sets
+ * ve.veid to the cgroup it made, and runs the slave side there.
+ *
+ * A warning does not stop the host, so the run reads the kernel log too.
+ * One read() of /dev/kmsg is one record, so the test matches vtty driver
+ * splats, as well as generic kernel warning/oops/kasan reports.
+ *
+ * Usage: ve_vtty_stress [seconds] (default 30, or $VE_VTTY_SECS)
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <stdbool.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <linux/major.h>
+#include <linux/vzcalluser.h>
+#include <sys/sysmacros.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "ve_selftest.h"
+#include "../kselftest.h"
+
+#define VTTY_IDX 0 /* The pair both ends work on */
+#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
+#define DEFAULT_SECS 30
+
+/*
+ * Expected open errno codes: the pair may be closing, or gone already,
+ * when an open lands on it.
+ * Anything else is an environment issue or a bug.
+ */
+static bool expected_errno(int err)
+{
+	return err == EBUSY || err == EIO || err == ENXIO;
+}
+
+static int open_master(int ctl_fd, unsigned int veid)
+{
+	struct vzctl_ve_configure c = {
+		.veid = veid,
+		.key = VE_CONFIGURE_OPEN_TTY,
+		.val = VTTY_IDX,
+	};
+
+	return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
+}
+
+/* The slave end, inside the VE. Returns the errno that stopped it, or 0. */
+static int hammer_slave(const char *node, time_t deadline,
+			unsigned long *opened)
+{
+	int err = 0;
+
+	while (time(NULL) < deadline) {
+		int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
+
+		if (fd >= 0) {
+			(*opened)++;
+			close(fd);
+		} else if (!expected_errno(errno)) {
+			err = errno;
+			break;
+		}
+	}
+
+	return err;
+}
+
+/* The kernel log, since a warning fails nothing by itself. */
+static int kmsg_open(void)
+{
+	int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+
+	if (fd >= 0)
+		lseek(fd, 0, SEEK_END);
+	return fd;
+}
+
+/*
+ * Check for kernel log warnings, one read() per record, hence
+ * the function name is not checked.
+ */
+static int kmsg_check(int fd)
+{
+	char buf[8192];
+	int found = 0;
+	ssize_t r;
+
+	if (fd < 0)
+		return 0;
+
+	for (;;) {
+		r = read(fd, buf, sizeof(buf) - 1);
+		if (r < 0) {
+			/* Ring overrun, continue with the oldest existing record */
+			if (errno == EPIPE)
+				continue;
+			break;
+		}
+		if (r == 0)
+			break;
+		buf[r] = '\0';
+		/* Known symptoms of a buggy kernel */
+		if (strstr(buf, "vtty") && (strstr(buf, "tty->count") ||
+					    strstr(buf, "bad slave count"))) {
+			ksft_print_msg("vtty mismatched count log: %s", buf);
+			found++;
+		}
+		if (strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") ||
+		    strstr(buf, ";BUG: ") || strstr(buf, "soft lockup - ") ||
+		    strstr(buf, "general protection fault")) {
+			ksft_print_msg("kernel log: %s", buf);
+			found++;
+		}
+	}
+	return found;
+}
+
+int main(int argc, char **argv)
+{
+	unsigned long master_opened = 0, slave_opened = 0;
+	int cg, ctid, ctl, kmsg, ready[2], count[2], status;
+	int master_err = 0, slave_err;
+	const char *env = getenv("VE_VTTY_SECS");
+	char path[PATH_MAX], val[16], c;
+	char dir[] = "/tmp/ve_vtty_XXXXXX";
+	char node[sizeof(dir) + 6];
+	struct namespace_ctx ns;
+	time_t deadline;
+	long secs;
+	pid_t pid;
+
+	ksft_print_header();
+	secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
+	if (secs <= 0)
+		secs = DEFAULT_SECS;
+
+	ctl = open("/dev/vzctl", O_RDWR);
+	if (ctl < 0)
+		ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
+			       "(vzdev, vzmon) are needed\n",
+			       strerror(errno));
+
+	cg = mount_cg2_fd();
+	if (cg < 0)
+		ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
+	if (write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS) < 0)
+		ksft_exit_skip("cannot enable the ve controller: %s\n",
+			       strerror(errno));
+
+	ctid = make_ve(cg, CTID_MIN);
+	if (ctid < 0)
+		ksft_exit_skip("cannot make a VE cgroup\n");
+
+	/* The ve.veid must match between master / slave */
+	snprintf(path, sizeof(path), "%d/ve.veid", ctid);
+	snprintf(val, sizeof(val), "%d", ctid);
+	if (write_file_at(cg, path, val) < 0) {
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
+	}
+
+	/* Do not report a failure where the vtty pair cannot be had at all */
+	status = open_master(ctl, ctid);
+	if (status < 0 && errno != EBUSY) {
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("no vtty master: %s\n", strerror(errno));
+	}
+	if (status >= 0)
+		close(status);
+
+	/*
+	 * Use temp directory for the node the slave end opens.
+	 * Cleaned up by the master side instead of relying on the slave.
+	 */
+	if (!mkdtemp(dir)) {
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("mkdtemp: %s\n", strerror(errno));
+	}
+	snprintf(node, sizeof(node), "%s/slave", dir);
+	if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0) {
+		status = errno;
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("mknod: %s\n", strerror(status));
+	}
+
+	ksft_set_plan(1);
+	kmsg = kmsg_open();
+	if (kmsg < 0)
+		ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
+			       "be noticed\n",
+			       strerror(errno));
+
+	if (pipe(ready) || pipe(count)) {
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("pipe: %s\n", strerror(errno));
+	}
+
+	/* The VE of the cgroup, which the slave side enters */
+	if (prepare_namespaces(&ns, CLONE_NEWVE, cg, ctid) < 0) {
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("cannot prepare the VE namespace\n");
+	}
+
+	deadline = time(NULL) + secs;
+	ksft_print_msg("VE %d, pair %d, %ld seconds\n", ctid, VTTY_IDX, secs);
+
+	pid = fork();
+	if (pid == 0) {
+		unsigned long n = 0;
+		int err;
+
+		close(ready[0]);
+		close(count[0]);
+		/*
+		 * Hammering on after the run has given up would hold
+		 * the cgroup open against destroy_ve().
+		 */
+		prctl(PR_SET_PDEATHSIG, SIGKILL);
+		if (enter_cgroup(cg, ctid) < 0 ||
+		    enter_namespaces(&ns, CLONE_NEWVE) < 0)
+			_exit(errno);
+
+		c = 'r';
+		if (write(ready[1], &c, 1) != 1)
+			_exit(errno);
+		err = hammer_slave(node, deadline, &n);
+		/* Send the count either way: it says how far it got. */
+		if (write(count[1], &n, sizeof(n)) != sizeof(n))
+			_exit(errno);
+		_exit(err);
+	} else if (pid < 0) {
+		close_namespace_fds(&ns);
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("fork: %s\n", strerror(errno));
+	}
+	close(ready[1]);
+	close(count[1]);
+
+	if (read(ready[0], &c, 1) != 1) {
+		waitpid(pid, &status, 0);
+		close_namespace_fds(&ns);
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("the VE side never came up: %s\n",
+				   strerror(WIFEXITED(status) ?
+					    WEXITSTATUS(status) : EINTR));
+	}
+
+	while (time(NULL) < deadline) {
+		int fd = open_master(ctl, ctid);
+
+		if (fd >= 0) {
+			master_opened++;
+			close(fd);
+		} else if (!expected_errno(errno)) {
+			master_err = errno;
+			break;
+		}
+	}
+
+	if (master_err) {
+		/* The other end runs to its own deadline, stop it */
+		kill(pid, SIGKILL);
+		waitpid(pid, &status, 0);
+		slave_err = 0;
+	} else {
+		if (read(count[0], &slave_opened, sizeof(slave_opened)) !=
+		    sizeof(slave_opened))
+			slave_opened = 0;
+		waitpid(pid, &status, 0);
+		slave_err = WIFEXITED(status) ? WEXITSTATUS(status) : EINTR;
+	}
+
+	ksft_print_msg("%lu master opens, %lu slave opens\n", master_opened,
+		       slave_opened);
+
+	status = kmsg_check(kmsg);
+	close_namespace_fds(&ns);
+	unlink(node);
+	rmdir(dir);
+	destroy_ve(cg, ctid);
+
+	if (master_err)
+		ksft_exit_fail_msg("the master end stopped: %s\n",
+				   strerror(master_err));
+	if (slave_err)
+		ksft_exit_fail_msg("the slave end stopped: %s\n",
+				   strerror(slave_err));
+	if (!master_opened)
+		ksft_exit_fail_msg("no master was ever opened\n");
+	if (!slave_opened)
+		ksft_exit_fail_msg("no slave was ever opened\n");
+	if (status)
+		ksft_exit_fail_msg("kernel complained %d time(s)\n", status);
+
+	ksft_test_result_pass("vtty pair closed from both ends\n");
+	ksft_finished();
+}
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 6%]

* Re: [Devel] [PATCH hci-8.1 v5 2/2] selftests: ve: introduce vtty pair teardown stress test
  2026-10-01  3:24  6% ` [Devel] [PATCH hci-8.1 v5 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
@ 2026-10-01  3:34  5%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-01  3:34 UTC (permalink / raw)
  To: Eva Kurchatova, khorenko; +Cc: devel

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Low] selftests/ve: vtty stress test can hijack a live container's vtty pair via veid clash
- [Low] selftests/ve: kmsg_check() fails the test on any unrelated kernel warning
- [Low] selftests/ve: slave node on possibly-nodev /tmp gives hard failure instead of skip
--

commit 58d6472da1964666674d9430e2db654718c0e4a4
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>

selftests: ve: introduce vtty pair teardown stress test

This commit adds ve_vtty_stress, a selftest that opens and closes a vtty
pair from the master and slave ends at the same time. It checks the
kernel log for vtty count mismatches and for generic warnings, oopses and
lockups. The test is wired into the ve selftests Makefile and .gitignore.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-144771

> diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
> new file mode 100644
> index 000000000000..56355bb8dbe4
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_vtty_stress.c

[ ... ]

> +/* The slave end, inside the VE. Returns the errno that stopped it, or 0. */
> +static int hammer_slave(const char *node, time_t deadline,
> +			unsigned long *opened)
> +{
> +	int err = 0;
> +
> +	while (time(NULL) < deadline) {
> +		int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
> +
> +		if (fd >= 0) {
> +			(*opened)++;
> +			close(fd);
> +		} else if (!expected_errno(errno)) {
> +			err = errno;
> +			break;
> +		}
> +	}
> +
> +	return err;
> +}

[ ... ]

> +static int kmsg_check(int fd)
> +{

[ ... ]

> +		if (strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") ||
> +		    strstr(buf, ";BUG: ") || strstr(buf, "soft lockup - ") ||
> +		    strstr(buf, "general protection fault")) {
> +			ksft_print_msg("kernel log: %s", buf);
> +			found++;
> +		}
> +	}
> +	return found;
> +}

[Severity: Low]
Can an unrelated kernel warning fail this test?

kmsg_open() seeks to the end of /dev/kmsg, and kmsg_check() then scans
every record logged anywhere in the kernel during the run. Only the
"tty->count" and "bad slave count" match is restricted to vtty.

The second condition is global:

    strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") || ...

Any warning from another subsystem (a lockdep splat or a driver WARN, for
example) increments found. main() then reaches:

    ksft_exit_fail_msg("kernel complained %d time(s)\n", status);

On a busy node this gives a failure that has nothing to do with vtty.
Would it make sense to limit these matches to records that mention vtty,
or to the test's own tasks?

> +int main(int argc, char **argv)
> +{

[ ... ]

> +	ctid = make_ve(cg, CTID_MIN);
> +	if (ctid < 0)
> +		ksft_exit_skip("cannot make a VE cgroup\n");
> +
> +	/* The ve.veid must match between master / slave */
> +	snprintf(path, sizeof(path), "%d/ve.veid", ctid);
> +	snprintf(val, sizeof(val), "%d", ctid);
> +	if (write_file_at(cg, path, val) < 0) {
> +		destroy_ve(cg, ctid);
> +		ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
> +	}

[Severity: Low]
Can this test pick up a live container's vtty pair?

make_ve() picks the first cgroup directory name in the 108..199 range
that does not exist. That is a check on cgroup names, not on veids.

The chosen number is then written to ve.veid. ve_id_write() in
kernel/ve/ve.c has this comment and does no uniqueness check:

    /* FIXME: check veid is uniqul */

The vtty map is keyed by veid. A running container with the same veid
but a different cgroup name, such as a UUID-named cgroup, would
therefore be reached by open_master(). The test could then take over or
tear down that container's console pair and report spurious failures.

The commit message acknowledges this and says the range makes it
unlikely. Is there a way to check that the veid is unused before
writing it to ve.veid, or to skip the test when other containers are
running?

[ ... ]

> +	snprintf(node, sizeof(node), "%s/slave", dir);
> +	if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0) {
> +		status = errno;
> +		rmdir(dir);
> +		destroy_ve(cg, ctid);
> +		ksft_exit_fail_msg("mknod: %s\n", strerror(status));
> +	}

[Severity: Low]
Can a nodev /tmp turn this into a failure instead of a skip?

The node is created with mknod() under a mkdtemp() directory in /tmp.
On a host where /tmp is mounted nodev (systemd tmp.mount with
nosuid,nodev, for example), mknod() succeeds. The node is never
probe-opened before the fork.

In the child, hammer_slave() then gets EACCES from open(). Since
expected_errno() accepts only EBUSY, EIO and ENXIO, hammer_slave()
returns at once, and main() ends with:

    ksft_exit_fail_msg("the slave end stopped: %s\n", ...);

The test skips for the other environment problems, such as a missing
master. Should the node be probe-opened before the fork, with a skip on
EACCES, or be created on a filesystem that allows device nodes?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261001032531.1307025-1-eva.kurchatova@virtuozzo.com?part=2

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 5%]

* [Devel] [PATCH hci-8.1 v6 1/2] selftests: ve: grant the directories the files sit in
  @ 2026-10-02  4:43 11% ` Eva Kurchatova
  2026-10-02  4:43  6% ` [Devel] [PATCH hci-8.1 v6 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
  1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-10-02  4:43 UTC (permalink / raw)
  To: khorenko; +Cc: devel

The sysfs variant names nested leaves, kernel/vmcoreinfo and
kernel/notes, while the proc one names files at the top of the
filesystem. Both enforcement tests grant the leaves and nothing else,
so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
cannot traverse, and every open answers ENOENT.

Grant x on each directory along the way, taken from the path the
variant already gives.

Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")

https://virtuozzo.atlassian.net/browse/VSTOR-144955

Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/ve/ve_perms_test.c | 34 ++++++++++++++++++++--
 1 file changed, 32 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 25ffbd42c380..8096b1fa3b43 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -114,9 +114,11 @@ static int ve_open_rel(int cgv2_fd, int ctid, const char *prefix,
 static int set_entry(int cgv2_fd, const char *path, const char *entry,
 		     const char *mask)
 {
-	char line[256];
+	char line[PATH_MAX];
 
-	snprintf(line, sizeof(line), "%s %s", entry, mask);
+	if (snprintf(line, sizeof(line), "%s %s", entry, mask) >=
+	    (int)sizeof(line))
+		return -1;
 	return write_file_at(cgv2_fd, path, line);
 }
 
@@ -343,6 +345,32 @@ TEST_F(ve_perms, write_parser)
 	EXPECT_LT(write_file_at(self->cgv2_fd, path, file), 0);
 }
 
+/* A leaf is reachable only once every directory above it can be traversed */
+static int allow_path(int cgv2_fd, const char *path, const char *file)
+{
+	char dir[PATH_MAX];
+	const char *sep;
+
+	for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
+		if (snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file),
+			     file) >= (int)sizeof(dir))
+			return -1;
+		if (set_entry(cgv2_fd, path, dir, "x") < 0)
+			return -1;
+	}
+	return 0;
+}
+
+static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
+{
+	int i;
+
+	for (i = 0; i < NR_FILES; i++)
+		if (allow_path(cgv2_fd, path, files[i]) < 0)
+			return -1;
+	return 0;
+}
+
 /*
  * Enforcement, allowed side: from inside a VE, granting r lets each file be
  * read, and granting x on the directory lets it be traversed (the lookup then
@@ -358,6 +386,7 @@ TEST_F(ve_perms, enforce_allows)
 
 	ve_file(path, sizeof(path), self->ctid_a, variant->fs);
 
+	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
 	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
 	for (i = 0; i < NR_FILES; i++)
 		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
@@ -400,6 +429,7 @@ TEST_F(ve_perms, enforce_denies)
 				      O_RDONLY), 0);
 
 	/* Granted w only: visible but not readable. */
+	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
 	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
 	for (i = 0; i < NR_FILES; i++)
 		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 11%]

* [Devel] [PATCH hci-8.1 v6 2/2] selftests: ve: introduce vtty pair teardown stress test
    2026-10-02  4:43 11% ` [Devel] [PATCH hci-8.1 v6 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
@ 2026-10-02  4:43  6% ` Eva Kurchatova
  2026-10-02  4:54  4%   ` no-reply
  1 sibling, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-10-02  4:43 UTC (permalink / raw)
  To: khorenko; +Cc: devel

Either end of a vtty pair creates it, and the last close tears it down.

Race condition in older VZKernel vtty implementation allowed both sides
to decide they were the final one and call release_tty() twice, and a
master opening again while a teardown was in flight used to hand out a
reference to a tty_struct about to be freed. Either way, this caused
use-after-free. That issue was fixed in c5028cd61a69:
"ve/vtty: fix use-after-free on concurrent tty close and reopen"

Hammer both ends for ownership correctness & thread safety verification.

The original reproducer needed a few tens of minutes across a dozen
containers, where vzctl interaction with vtty subsystem was a small part
of the overall runtime, therefore it did not fire on every run. This test
opens and closes nothing but the pair, from both ends and without pause,
so the default stays in the tens of seconds and under the timeout of the
suite, and VE_VTTY_SECS or argv[1] allows to control this.

ve_id_write() does not check that a veid is unique and the vtty map is
keyed by veid, so a clash would hand the test a running container's
pair. Before writing ve.veid the test asks the kernel, through the
vzctl ioctl, whether the veid is taken, and skips when it is. It also
skips where /tmp is mounted nodev: mknod() succeeds there and every
open of the slave node fails.

https://virtuozzo.atlassian.net/browse/VSTOR-144771

Feature: ve selftest
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/ve/.gitignore       |   1 +
 tools/testing/selftests/ve/Makefile         |   3 +-
 tools/testing/selftests/ve/ve_vtty_stress.c | 343 ++++++++++++++++++++
 3 files changed, 346 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/ve/ve_vtty_stress.c

diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index 3df4d05888dc..d8eb5c346c6e 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,3 +1,4 @@
 ve_ns_owner_test
 ve_perms_test
 ve_mount_accounting_test
+ve_vtty_stress
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index c6efe7c4b4fb..d287a73c5e97 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -1,9 +1,10 @@
 # SPDX-License-Identifier: GPL-2.0
 # Makefile for ve selftests.
-CFLAGS += -g -Wall -O2
+CFLAGS += -g -Wall -O2 $(KHDR_INCLUDES)
 
 TEST_GEN_PROGS += ve_ns_owner_test
 TEST_GEN_PROGS += ve_perms_test
 TEST_GEN_PROGS += ve_mount_accounting_test
+TEST_GEN_PROGS += ve_vtty_stress
 
 include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
new file mode 100644
index 000000000000..8be6955d1dbc
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_vtty_stress.c
@@ -0,0 +1,343 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_vtty_stress - open and close a vtty pair from both ends at once.
+ *
+ * Either end of a vtty pair creates it, and the last close tears it down.
+ *
+ * Hammer both ends. The master comes from the vzctl ioctl, which does
+ * not look the VE up. The slave is reachable only from inside the VE, as
+ * TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
+ * vttys index minor - 1, so the test builds a VE with cgroup2, sets
+ * ve.veid to the cgroup it made, and runs the slave side there.
+ *
+ * A warning does not stop the host, so the run reads the kernel log too.
+ * One read() of /dev/kmsg is one record, so the test matches vtty driver
+ * splats, as well as generic kernel warning/oops/kasan reports.
+ *
+ * Usage: ve_vtty_stress [seconds] (default 30, or $VE_VTTY_SECS)
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <stdbool.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <sys/statvfs.h>
+#include <linux/major.h>
+#include <linux/vzcalluser.h>
+#include <sys/sysmacros.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "ve_selftest.h"
+#include "../kselftest.h"
+
+#define VTTY_IDX 0 /* The pair both ends work on */
+#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
+#define DEFAULT_SECS 30
+
+/*
+ * Expected open errno codes: the pair may be closing, or gone already,
+ * when an open lands on it.
+ * Anything else is an environment issue or a bug.
+ */
+static bool expected_errno(int err)
+{
+	return err == EBUSY || err == EIO || err == ENXIO;
+}
+
+static int open_master(int ctl_fd, unsigned int veid)
+{
+	struct vzctl_ve_configure c = {
+		.veid = veid,
+		.key = VE_CONFIGURE_OPEN_TTY,
+		.val = VTTY_IDX,
+	};
+
+	return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
+}
+
+/* Any key but the tty one makes ve_configure() look a running VE up */
+static bool veid_in_use(int ctl_fd, unsigned int veid)
+{
+	struct vzctl_ve_configure c = {
+		.veid = veid,
+		.key = VE_CONFIGURE_OS_RELEASE,
+	};
+
+	return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c) < 0 && errno == ENOKEY;
+}
+
+/* The slave end, inside the VE. Returns the errno that stopped it, or 0. */
+static int hammer_slave(const char *node, time_t deadline,
+			unsigned long *opened)
+{
+	int err = 0;
+
+	while (time(NULL) < deadline) {
+		int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
+
+		if (fd >= 0) {
+			(*opened)++;
+			close(fd);
+		} else if (!expected_errno(errno)) {
+			err = errno;
+			break;
+		}
+	}
+
+	return err;
+}
+
+/* The kernel log, since a warning fails nothing by itself. */
+static int kmsg_open(void)
+{
+	int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+
+	if (fd >= 0)
+		lseek(fd, 0, SEEK_END);
+	return fd;
+}
+
+/*
+ * Check for kernel log warnings, one read() per record, hence
+ * the function name is not checked.
+ */
+static int kmsg_check(int fd)
+{
+	char buf[8192];
+	int found = 0;
+	ssize_t r;
+
+	if (fd < 0)
+		return 0;
+
+	for (;;) {
+		r = read(fd, buf, sizeof(buf) - 1);
+		if (r < 0) {
+			/* Ring overrun, continue with the oldest existing record */
+			if (errno == EPIPE)
+				continue;
+			break;
+		}
+		if (r == 0)
+			break;
+		buf[r] = '\0';
+		/* Known symptoms of a buggy kernel */
+		if (strstr(buf, "vtty") && (strstr(buf, "tty->count") ||
+					    strstr(buf, "bad slave count"))) {
+			ksft_print_msg("vtty mismatched count log: %s", buf);
+			found++;
+		}
+		if (strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") ||
+		    strstr(buf, ";BUG: ") || strstr(buf, "soft lockup - ") ||
+		    strstr(buf, "general protection fault")) {
+			ksft_print_msg("kernel log: %s", buf);
+			found++;
+		}
+	}
+	return found;
+}
+
+int main(int argc, char **argv)
+{
+	unsigned long master_opened = 0, slave_opened = 0;
+	int cg, ctid, ctl, kmsg, ready[2], count[2], status;
+	int master_err = 0, slave_err;
+	const char *env = getenv("VE_VTTY_SECS");
+	char path[PATH_MAX], val[16], c;
+	char dir[] = "/tmp/ve_vtty_XXXXXX";
+	char node[sizeof(dir) + 6];
+	struct namespace_ctx ns;
+	struct statvfs st;
+	time_t deadline;
+	long secs;
+	pid_t pid;
+
+	ksft_print_header();
+	secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
+	if (secs <= 0)
+		secs = DEFAULT_SECS;
+
+	ctl = open("/dev/vzctl", O_RDWR);
+	if (ctl < 0)
+		ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
+			       "(vzdev, vzmon) are needed\n",
+			       strerror(errno));
+
+	cg = mount_cg2_fd();
+	if (cg < 0)
+		ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
+	if (write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS) < 0)
+		ksft_exit_skip("cannot enable the ve controller: %s\n",
+			       strerror(errno));
+
+	ctid = make_ve(cg, CTID_MIN);
+	if (ctid < 0)
+		ksft_exit_skip("cannot make a VE cgroup\n");
+
+	/* The vtty map is keyed by veid, and ve.veid takes a taken one */
+	if (veid_in_use(ctl, ctid)) {
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("veid %d is in use by a running container\n",
+			       ctid);
+	}
+
+	/* The ve.veid must match between master / slave */
+	snprintf(path, sizeof(path), "%d/ve.veid", ctid);
+	snprintf(val, sizeof(val), "%d", ctid);
+	if (write_file_at(cg, path, val) < 0) {
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
+	}
+
+	/* Do not report a failure where the vtty pair cannot be had at all */
+	status = open_master(ctl, ctid);
+	if (status < 0 && errno != EBUSY) {
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("no vtty master: %s\n", strerror(errno));
+	}
+	if (status >= 0)
+		close(status);
+
+	/*
+	 * Use temp directory for the node the slave end opens.
+	 * Cleaned up by the master side instead of relying on the slave.
+	 */
+	if (!mkdtemp(dir)) {
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("mkdtemp: %s\n", strerror(errno));
+	}
+	/* A nodev mount takes the mknod() and refuses every open */
+	if (statvfs(dir, &st) == 0 && (st.f_flag & ST_NODEV)) {
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("%s is on a nodev filesystem\n", dir);
+	}
+	snprintf(node, sizeof(node), "%s/slave", dir);
+	if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0) {
+		status = errno;
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("mknod: %s\n", strerror(status));
+	}
+
+	ksft_set_plan(1);
+	kmsg = kmsg_open();
+	if (kmsg < 0)
+		ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
+			       "be noticed\n",
+			       strerror(errno));
+
+	if (pipe(ready) || pipe(count)) {
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("pipe: %s\n", strerror(errno));
+	}
+
+	/* The VE of the cgroup, which the slave side enters */
+	if (prepare_namespaces(&ns, CLONE_NEWVE, cg, ctid) < 0) {
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("cannot prepare the VE namespace\n");
+	}
+
+	deadline = time(NULL) + secs;
+	ksft_print_msg("VE %d, pair %d, %ld seconds\n", ctid, VTTY_IDX, secs);
+
+	pid = fork();
+	if (pid == 0) {
+		unsigned long n = 0;
+		int err;
+
+		close(ready[0]);
+		close(count[0]);
+		/*
+		 * Hammering on after the run has given up would hold
+		 * the cgroup open against destroy_ve().
+		 */
+		prctl(PR_SET_PDEATHSIG, SIGKILL);
+		if (enter_cgroup(cg, ctid) < 0 ||
+		    enter_namespaces(&ns, CLONE_NEWVE) < 0)
+			_exit(errno);
+
+		c = 'r';
+		if (write(ready[1], &c, 1) != 1)
+			_exit(errno);
+		err = hammer_slave(node, deadline, &n);
+		/* Send the count either way: it says how far it got. */
+		if (write(count[1], &n, sizeof(n)) != sizeof(n))
+			_exit(errno);
+		_exit(err);
+	} else if (pid < 0) {
+		close_namespace_fds(&ns);
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("fork: %s\n", strerror(errno));
+	}
+	close(ready[1]);
+	close(count[1]);
+
+	if (read(ready[0], &c, 1) != 1) {
+		waitpid(pid, &status, 0);
+		close_namespace_fds(&ns);
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("the VE side never came up: %s\n",
+				   strerror(WIFEXITED(status) ?
+					    WEXITSTATUS(status) : EINTR));
+	}
+
+	while (time(NULL) < deadline) {
+		int fd = open_master(ctl, ctid);
+
+		if (fd >= 0) {
+			master_opened++;
+			close(fd);
+		} else if (!expected_errno(errno)) {
+			master_err = errno;
+			break;
+		}
+	}
+
+	if (master_err) {
+		/* The other end runs to its own deadline, stop it */
+		kill(pid, SIGKILL);
+		waitpid(pid, &status, 0);
+		slave_err = 0;
+	} else {
+		if (read(count[0], &slave_opened, sizeof(slave_opened)) !=
+		    sizeof(slave_opened))
+			slave_opened = 0;
+		waitpid(pid, &status, 0);
+		slave_err = WIFEXITED(status) ? WEXITSTATUS(status) : EINTR;
+	}
+
+	ksft_print_msg("%lu master opens, %lu slave opens\n", master_opened,
+		       slave_opened);
+
+	status = kmsg_check(kmsg);
+	close_namespace_fds(&ns);
+	unlink(node);
+	rmdir(dir);
+	destroy_ve(cg, ctid);
+
+	if (master_err)
+		ksft_exit_fail_msg("the master end stopped: %s\n",
+				   strerror(master_err));
+	if (slave_err)
+		ksft_exit_fail_msg("the slave end stopped: %s\n",
+				   strerror(slave_err));
+	if (!master_opened)
+		ksft_exit_fail_msg("no master was ever opened\n");
+	if (!slave_opened)
+		ksft_exit_fail_msg("no slave was ever opened\n");
+	if (status)
+		ksft_exit_fail_msg("kernel complained %d time(s)\n", status);
+
+	ksft_test_result_pass("vtty pair closed from both ends\n");
+	ksft_finished();
+}
-- 
2.55.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 6%]

* Re: [Devel] [PATCH hci-8.1 v6 2/2] selftests: ve: introduce vtty pair teardown stress test
  2026-10-02  4:43  6% ` [Devel] [PATCH hci-8.1 v6 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
@ 2026-10-02  4:54  4%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-02  4:54 UTC (permalink / raw)
  To: Eva Kurchatova, khorenko; +Cc: devel

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] selftests/ve: kmsg_check() ignores EPIPE ring overrun and can falsely PASS
- [Low] selftests/ve: kmsg_check() fails on any unrelated kernel WARNING/Oops/BUG
- [Low] selftests/ve: veid uniqueness check is a TOCTOU, so the stress may hit a live container
--

commit e116830e8cd9eabf2049859ae877c3c8c485e837
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>

selftests: ve: introduce vtty pair teardown stress test

This adds a selftest that opens and closes a vtty pair from both the
master end (vzctl ioctl) and the slave end (inside a VE) for a
configurable time. It then scans /dev/kmsg for vtty count mismatches and
generic kernel warnings, to catch the use-after-free fixed in c5028cd61a69.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-144771

> diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
> new file mode 100644
> index 000000000000..8be6955d1dbc
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_vtty_stress.c

[ ... ]

> +static int kmsg_check(int fd)
> +{
> +	char buf[8192];
> +	int found = 0;
> +	ssize_t r;
> +
> +	if (fd < 0)
> +		return 0;
> +
> +	for (;;) {
> +		r = read(fd, buf, sizeof(buf) - 1);
> +		if (r < 0) {
> +			/* Ring overrun, continue with the oldest existing record */
> +			if (errno == EPIPE)
> +				continue;
> +			break;
> +		}

[Severity: Medium]
Can this test report PASS even though the kernel logged the bug it is
meant to detect?

kmsg_check() is called only once, after the whole run, so /dev/kmsg is
drained after up to 30 seconds of logging. If the log ring wraps during
that time, the next read() returns EPIPE, and the branch below silently
resumes from the oldest surviving record:

    if (errno == EPIPE)
            continue;

Records emitted early in the run, such as a "bad slave count" message or
a WARNING from a double release_tty(), would be dropped without being
counted. Nothing tells the user that records were lost.

Should the EPIPE case at least print a message that records were lost, or
make the test fail or skip?

> +		if (r == 0)
> +			break;
> +		buf[r] = '\0';
> +		/* Known symptoms of a buggy kernel */
> +		if (strstr(buf, "vtty") && (strstr(buf, "tty->count") ||
> +					    strstr(buf, "bad slave count"))) {
> +			ksft_print_msg("vtty mismatched count log: %s", buf);
> +			found++;
> +		}
> +		if (strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") ||
> +		    strstr(buf, ";BUG: ") || strstr(buf, "soft lockup - ") ||
> +		    strstr(buf, "general protection fault")) {

[Severity: Low]
Can an unrelated kernel message fail this test?

kmsg_open() seeks to the end of /dev/kmsg, and kmsg_check() then reads
every record logged until the end of the run. Any record from another
subsystem that contains "WARNING: ", "Oops: ", ";BUG: ", "soft lockup - "
or "general protection fault" is counted in found.

main() then calls ksft_exit_fail_msg("kernel complained %d time(s)\n", ...).
Only the "tty->count" and "bad slave count" checks are restricted to vtty.

On a busy host, wouldn't this make the result depend on unrelated activity?
Would it be possible to filter by something related to the vtty or tty
code, such as the call trace?

> +			ksft_print_msg("kernel log: %s", buf);
> +			found++;
> +		}
> +	}
> +	return found;
> +}

[ ... ]

> +	ctid = make_ve(cg, CTID_MIN);
> +	if (ctid < 0)
> +		ksft_exit_skip("cannot make a VE cgroup\n");
> +
> +	/* The vtty map is keyed by veid, and ve.veid takes a taken one */
> +	if (veid_in_use(ctl, ctid)) {
> +		destroy_ve(cg, ctid);
> +		ksft_exit_skip("veid %d is in use by a running container\n",
> +			       ctid);
> +	}
> +
> +	/* The ve.veid must match between master / slave */
> +	snprintf(path, sizeof(path), "%d/ve.veid", ctid);
> +	snprintf(val, sizeof(val), "%d", ctid);
> +	if (write_file_at(cg, path, val) < 0) {

[Severity: Low]
Is the veid uniqueness check in main() racy?

veid_in_use() queries only running VEs, and ve.veid is written afterwards.
ve_id_write() in kernel/ve/ve.c carries "/* FIXME: check veid is uniqul */"
and does not reserve the id. The test's own VE enters the idr only later,
after the namespace switch.

If a container with the same veid is started between veid_in_use() and the
write of ve.veid, would the test not detect it? It could then drive that
container's vtty pair 0 from both ends.

The commit message says "Before writing ve.veid the test asks the kernel
... whether the veid is taken, and skips when it is." This describes the
check as a full safeguard, but it only narrows the window. Could the
commit message or a code comment say that the check is best-effort?

[ ... ]

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002044431.1632888-1-eva.kurchatova@virtuozzo.com?part=2

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* [PATCH hci-8.0 1/1] hw/scsi: avoid deadlock upon TMF request cancelling with VirtIO #VSTOR-148264
@ 2026-10-02 10:58  4% Denis V. Lunev
  2026-10-02 11:14  0% ` Andrey Drobyshev
  0 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-10-02 10:58 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

From: Fiona Ebner <f.ebner@proxmox.com>

When scsi_req_dequeue() is reached via
scsi_req_cancel_async()
virtio_scsi_tmf_cancel_req()
virtio_scsi_do_tmf_aio_context(),
there is a deadlock when trying to acquire the SCSI device's requests
lock, because it was already acquired in
virtio_scsi_do_tmf_aio_context().

In particular, the issue happens with a FreeBSD guest (13, 14, 15,
maybe more), when it cancels SCSI requests, because of timeout.

This is a regression caused by commit da6eebb33b ("virtio-scsi:
perform TMFs in appropriate AioContexts") and the introduction of the
requests_lock earlier.

To fix the issue, only cancel the requests after releasing the
requests_lock. For this, the SCSI device's requests are iterated while
holding the requests_lock and the requests to be cancelled are
collected in a list. Then, the collected requests are cancelled
one by one while not holding the requests_lock. This is safe, because
only requests from the current AioContext are collected and acted
upon.

Originally reported by Proxmox VE users:
https://bugzilla.proxmox.com/show_bug.cgi?id=6810
https://forum.proxmox.com/threads/173914/

Fixes: da6eebb33b ("virtio-scsi: perform TMFs in appropriate AioContexts")
Suggested-by: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
Message-id: 20251017094518.328905-1-f.ebner@proxmox.com
[Changed g_list_append() to g_list_prepend() to avoid traversing the
list each time.
--Stefan]
Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
(cherry picked from commit 6910f04aa646f63a0257f77201ad8ea15992b816)
Signed-off-by: Michael Tokarev <mjt@tls.msk.ru>
(cherry picked from commit dfd597681092ba72c2c050cd0aa1d80a87822804)
---
 hw/scsi/virtio-scsi.c | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/hw/scsi/virtio-scsi.c b/hw/scsi/virtio-scsi.c
index 34ae14f7bf9..3b635053b56 100644
--- a/hw/scsi/virtio-scsi.c
+++ b/hw/scsi/virtio-scsi.c
@@ -343,6 +343,7 @@ static void virtio_scsi_do_tmf_aio_context(void *opaque)
     SCSIDevice *d = virtio_scsi_device_get(s, tmf->req.tmf.lun);
     SCSIRequest *r;
     bool match_tag;
+    g_autoptr(GList) reqs = NULL;
 
     if (!d) {
         tmf->resp.tmf.response = VIRTIO_SCSI_S_BAD_TARGET;
@@ -378,10 +379,21 @@ static void virtio_scsi_do_tmf_aio_context(void *opaque)
             if (match_tag && cmd_req->req.cmd.tag != tmf->req.tmf.tag) {
                 continue;
             }
-            virtio_scsi_tmf_cancel_req(tmf, r);
+            /*
+             * Cannot cancel directly, because scsi_req_dequeue() would deadlock
+             * when attempting to acquire the request_lock a second time. Taking
+             * a reference here is paired with an unref after cancelling below.
+             */
+            scsi_req_ref(r);
+            reqs = g_list_prepend(reqs, r);
         }
     }
 
+    for (GList *elem = g_list_first(reqs); elem; elem = g_list_next(elem)) {
+        virtio_scsi_tmf_cancel_req(tmf, elem->data);
+        scsi_req_unref(elem->data);
+    }
+
     /* Incremented by virtio_scsi_do_tmf() */
     virtio_scsi_tmf_dec_remaining(tmf);
 
-- 
2.53.0


^ permalink raw reply	[relevance 4%]

* Re: [PATCH hci-8.0 1/1] hw/scsi: avoid deadlock upon TMF request cancelling with VirtIO #VSTOR-148264
  2026-10-02 10:58  4% [PATCH hci-8.0 1/1] hw/scsi: avoid deadlock upon TMF request cancelling with VirtIO #VSTOR-148264 Denis V. Lunev
@ 2026-10-02 11:14  0% ` Andrey Drobyshev
  0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-02 11:14 UTC (permalink / raw)
  To: Denis V. Lunev, svt-core; +Cc: den

Ack
Applied to hci-8.0

On 10/2/26 1:58 PM, Denis V. Lunev wrote:
> From: Fiona Ebner <f.ebner@proxmox.com>
> 
> When scsi_req_dequeue() is reached via
> scsi_req_cancel_async()
> virtio_scsi_tmf_cancel_req()
> virtio_scsi_do_tmf_aio_context(),
> there is a deadlock when trying to acquire the SCSI device's requests
> lock, because it was already acquired in
> virtio_scsi_do_tmf_aio_context().
> 
> In particular, the issue happens with a FreeBSD guest (13, 14, 15,
> maybe more), when it cancels SCSI requests, because of timeout.
> 
> This is a regression caused by commit da6eebb33b ("virtio-scsi:
> perform TMFs in appropriate AioContexts") and the introduction of the
> requests_lock earlier.
> 
> To fix the issue, only cancel the requests after releasing the
> requests_lock. For this, the SCSI device's requests are iterated while
> holding the requests_lock and the requests to be cancelled are
> collected in a list. Then, the collected requests are cancelled
> one by one while not holding the requests_lock. This is safe, because
> only requests from the current AioContext are collected and acted
> upon.
> 
> Originally reported by Proxmox VE users:
> https://bugzilla.proxmox.com/show_bug.cgi?id=6810
> https://forum.proxmox.com/threads/173914/
> 
> Fixes: da6eebb33b ("virtio-scsi: perform TMFs in appropriate AioContexts")
> Suggested-by: Stefan Hajnoczi <stefanha@redhat.com>
> Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
> Message-id: 20251017094518.328905-1-f.ebner@proxmox.com
> [Changed g_list_append() to g_list_prepend() to avoid traversing the
> list each time.
> --Stefan]
> Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
> (cherry picked from commit 6910f04aa646f63a0257f77201ad8ea15992b816)
> Signed-off-by: Michael Tokarev <mjt@tls.msk.ru>
> (cherry picked from commit dfd597681092ba72c2c050cd0aa1d80a87822804)
> ---
>  hw/scsi/virtio-scsi.c | 14 +++++++++++++-
>  1 file changed, 13 insertions(+), 1 deletion(-)
> 
> diff --git a/hw/scsi/virtio-scsi.c b/hw/scsi/virtio-scsi.c
> index 34ae14f7bf9..3b635053b56 100644
> --- a/hw/scsi/virtio-scsi.c
> +++ b/hw/scsi/virtio-scsi.c
> @@ -343,6 +343,7 @@ static void virtio_scsi_do_tmf_aio_context(void *opaque)
>      SCSIDevice *d = virtio_scsi_device_get(s, tmf->req.tmf.lun);
>      SCSIRequest *r;
>      bool match_tag;
> +    g_autoptr(GList) reqs = NULL;
>  
>      if (!d) {
>          tmf->resp.tmf.response = VIRTIO_SCSI_S_BAD_TARGET;
> @@ -378,10 +379,21 @@ static void virtio_scsi_do_tmf_aio_context(void *opaque)
>              if (match_tag && cmd_req->req.cmd.tag != tmf->req.tmf.tag) {
>                  continue;
>              }
> -            virtio_scsi_tmf_cancel_req(tmf, r);
> +            /*
> +             * Cannot cancel directly, because scsi_req_dequeue() would deadlock
> +             * when attempting to acquire the request_lock a second time. Taking
> +             * a reference here is paired with an unref after cancelling below.
> +             */
> +            scsi_req_ref(r);
> +            reqs = g_list_prepend(reqs, r);
>          }
>      }
>  
> +    for (GList *elem = g_list_first(reqs); elem; elem = g_list_next(elem)) {
> +        virtio_scsi_tmf_cancel_req(tmf, elem->data);
> +        scsi_req_unref(elem->data);
> +    }
> +
>      /* Incremented by virtio_scsi_do_tmf() */
>      virtio_scsi_tmf_dec_remaining(tmf);
>  


^ permalink raw reply	[relevance 0%]

* [PATCH 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304
    2026-10-02 16:48  7% ` [PATCH 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304 Denis V. Lunev
@ 2026-10-02 16:48  3% ` Denis V. Lunev
  2026-10-02 16:54  3%   ` no-reply
    2 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-10-02 16:48 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

From: Denis V. Lunev <den@openvz.org>

A chardev with logfile= opens the log in qemu_char_open() on every
start, including the start of the new QEMU after cpr-exec. The backend
fds are taken from the cpr fd registry there, the log fd is not, so the
new process reopens the path itself.

libvirt normally opens the log through virtlogd and passes a pipe in an
fdset. The qemu-update command line cannot carry that fdset, so it
names the log file path. The exec'd QEMU, running as the qemu user,
then fails to create a log it has no write access to, e.g. a nova
console.log on node-local storage:

  qemu-kvm: -chardev pty,id=charserial0,logfile=.../console.log,
  logappend=on: Could not create '.../console.log': Permission denied

and the VM is destroyed. Where the open succeeds, the new QEMU writes
the file directly, behind virtlogd's back.

Save the log fd in the cpr registry under the chardev label and take it
from there on cpr-exec incoming, as the backends do. The logfile path
is not opened at all then. If the source did not save it, run without
a log rather than reopen the path. A failed chardev-change re-claims
the log through the base class cpr_claim_fds(), since the replacement's
log has displaced the entry.

Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 chardev/char.c          | 30 +++++++++++++++++++++++++-----
 include/migration/cpr.h |  1 +
 2 files changed, 26 insertions(+), 5 deletions(-)

diff --git a/chardev/char.c b/chardev/char.c
index b64767f574e..c037d2e7730 100644
--- a/chardev/char.c
+++ b/chardev/char.c
@@ -40,6 +40,7 @@
 #include "qemu/id.h"
 #include "qemu/coroutine.h"
 #include "qemu/yank.h"
+#include "migration/cpr.h"
 
 #include "chardev-internal.h"
 
@@ -261,9 +262,21 @@ static void qemu_char_open(Chardev *chr, ChardevBackend *backend,
         } else {
             flags |= O_TRUNC;
         }
-        chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
-        if (chr->logfd < 0) {
-            return;
+        if (cpr_exec_is_incoming()) {
+            chr->logfd = cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID);
+            if (chr->logfd < 0) {
+                warn_report("chardev '%s': log file was not preserved, "
+                            "continuing without it", chr->label);
+            }
+        } else {
+            chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
+            if (chr->logfd < 0) {
+                return;
+            }
+            if (!cpr_is_incoming()) {
+                cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
+                cpr_save_fd(chr->label, CPR_CHARDEV_LOG_ID, chr->logfd);
+            }
         }
     }
 
@@ -297,6 +310,10 @@ static int null_chr_write(Chardev *chr, const uint8_t *buf, int len)
 
 static void char_cpr_claim_fds(Chardev *chr)
 {
+    if (chr->logfd >= 0) {
+        cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
+        cpr_save_fd(chr->label, CPR_CHARDEV_LOG_ID, chr->logfd);
+    }
 }
 
 static void char_class_init(ObjectClass *oc, const void *data)
@@ -315,11 +332,14 @@ static void char_finalize(Object *obj)
     if (chr->be) {
         chr->be->chr = NULL;
     }
-    g_free(chr->filename);
-    g_free(chr->label);
     if (chr->logfd != -1) {
+        if (cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID) == chr->logfd) {
+            cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
+        }
         close(chr->logfd);
     }
+    g_free(chr->filename);
+    g_free(chr->label);
     qemu_mutex_destroy(&chr->chr_write_lock);
 }
 
diff --git a/include/migration/cpr.h b/include/migration/cpr.h
index d74ff1ea602..8fb79eb0a28 100644
--- a/include/migration/cpr.h
+++ b/include/migration/cpr.h
@@ -15,6 +15,7 @@
 
 #define CPR_LISTENSOCK_ID       0
 #define CPR_CONNSOCK_ID         1
+#define CPR_CHARDEV_LOG_ID      2
 
 #define QEMU_CPR_FILE_MAGIC     0x51435052
 #define QEMU_CPR_FILE_VERSION   0x00000001
-- 
2.53.0


^ permalink raw reply	[relevance 3%]

* [PATCH 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304
  @ 2026-10-02 16:48  7% ` Denis V. Lunev
  2026-10-02 16:48  3% ` [PATCH 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
    2 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-10-02 16:48 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

From: Denis V. Lunev <den@openvz.org>

Give TYPE_CHARDEV an empty cpr_claim_fds() and make the file, pty and
socket backends chain to their parent's. Every chardev class then has
the hook, so qmp_chardev_change() calls it without a NULL check, and
state owned by the base class can be re-claimed there as well.

No functional change.

Signed-off-by: Denis V. Lunev <den@openvz.org>
---
 chardev/char-file.c   |  3 +++
 chardev/char-pty.c    |  3 +++
 chardev/char-socket.c |  3 +++
 chardev/char.c        | 13 +++++++------
 4 files changed, 16 insertions(+), 6 deletions(-)

diff --git a/chardev/char-file.c b/chardev/char-file.c
index a90f4a77e13..89011f52e3e 100644
--- a/chardev/char-file.c
+++ b/chardev/char-file.c
@@ -124,7 +124,10 @@ out_close:
 static void char_file_cpr_claim_fds(Chardev *chr)
 {
     FDChardev *s = FD_CHARDEV(chr);
+    ChardevClass *pc = CHARDEV_CLASS(
+        object_class_get_parent(object_class_by_name(TYPE_CHARDEV_FILE)));
 
+    pc->cpr_claim_fds(chr);
     if (s->ioc_out) {
         cpr_delete_fd(chr->label, 0);
         cpr_save_fd(chr->label, 0, QIO_CHANNEL_FILE(s->ioc_out)->fd);
diff --git a/chardev/char-pty.c b/chardev/char-pty.c
index a0090e4c3d1..8058da92bb4 100644
--- a/chardev/char-pty.c
+++ b/chardev/char-pty.c
@@ -403,7 +403,10 @@ static void char_pty_open(Chardev *chr,
 static void char_pty_cpr_claim_fds(Chardev *chr)
 {
     PtyChardev *s = PTY_CHARDEV(chr);
+    ChardevClass *pc = CHARDEV_CLASS(
+        object_class_get_parent(object_class_by_name(TYPE_CHARDEV_PTY)));
 
+    pc->cpr_claim_fds(chr);
     if (s->ioc) {
         cpr_delete_fd(chr->label, 0);
         cpr_save_fd(chr->label, 0, QIO_CHANNEL_FILE(s->ioc)->fd);
diff --git a/chardev/char-socket.c b/chardev/char-socket.c
index d20aea764f9..c73a3dd0f7a 100644
--- a/chardev/char-socket.c
+++ b/chardev/char-socket.c
@@ -1681,7 +1681,10 @@ char_socket_get_connected(Object *obj, Error **errp)
 static void char_socket_cpr_claim_fds(Chardev *chr)
 {
     SocketChardev *s = SOCKET_CHARDEV(chr);
+    ChardevClass *pc = CHARDEV_CLASS(
+        object_class_get_parent(object_class_by_name(TYPE_CHARDEV_SOCKET)));
 
+    pc->cpr_claim_fds(chr);
     if (s->sioc) {
         cpr_delete_fd(chr->label, CPR_CONNSOCK_ID);
         cpr_save_fd(chr->label, CPR_CONNSOCK_ID, s->sioc->fd);
diff --git a/chardev/char.c b/chardev/char.c
index 87f40d0137e..b64767f574e 100644
--- a/chardev/char.c
+++ b/chardev/char.c
@@ -295,12 +295,17 @@ static int null_chr_write(Chardev *chr, const uint8_t *buf, int len)
     return len;
 }
 
+static void char_cpr_claim_fds(Chardev *chr)
+{
+}
+
 static void char_class_init(ObjectClass *oc, const void *data)
 {
     ChardevClass *cc = CHARDEV_CLASS(oc);
 
     cc->chr_write = null_chr_write;
     cc->chr_be_event = chr_be_event;
+    cc->cpr_claim_fds = char_cpr_claim_fds;
 }
 
 static void char_finalize(Object *obj)
@@ -1164,9 +1169,7 @@ ChardevReturn *qmp_chardev_change(const char *id, ChardevBackend *backend,
                           backend, chr->gcontext, handover_yank_instance, errp);
     if (!chr_new) {
         /* the failed replacement's open() may have displaced our cpr fds */
-        if (cc->cpr_claim_fds) {
-            cc->cpr_claim_fds(chr);
-        }
+        cc->cpr_claim_fds(chr);
         return NULL;
     }
 
@@ -1186,9 +1189,7 @@ ChardevReturn *qmp_chardev_change(const char *id, ChardevBackend *backend,
             qemu_chr_be_event(chr, CHR_EVENT_OPENED);
         }
         /* chr_new's open() may have displaced our cpr fds; re-claim them */
-        if (cc->cpr_claim_fds) {
-            cc->cpr_claim_fds(chr);
-        }
+        cc->cpr_claim_fds(chr);
         object_unref(OBJECT(chr_new));
         return NULL;
     }
-- 
2.53.0


^ permalink raw reply	[relevance 7%]

* Re: [PATCH 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304
  2026-10-02 16:48  3% ` [PATCH 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
@ 2026-10-02 16:54  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-02 16:54 UTC (permalink / raw)
  To: svt-core, Denis V. Lunev; +Cc: andrey.drobyshev, den

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] chardev: logfd saved in all CPR modes but claimed only on cpr-exec incoming
--

commit 05df918f015920ae0431d59bdf9bb6ff8741e4fc
Author: Denis V. Lunev <den@openvz.org>

chardev: preserve the logfile fd across cpr-exec #VSTOR-148304

This patch saves the chardev log fd in the cpr fd registry under the chardev label. On cpr-exec incoming, it takes the fd from the registry instead of reopening the logfile path. If the source did not save the fd, the chardev runs without a log.

> diff --git a/chardev/char.c b/chardev/char.c
> --- a/chardev/char.c
> +++ b/chardev/char.c

[ ... ]

> @@ -261,9 +262,21 @@ static void qemu_char_open(Chardev *chr, ChardevBackend *backend,
>          } else {
>              flags |= O_TRUNC;
>          }
> -        chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
> -        if (chr->logfd < 0) {
> -            return;
> +        if (cpr_exec_is_incoming()) {
> +            chr->logfd = cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID);
> +            if (chr->logfd < 0) {
> +                warn_report("chardev '%s': log file was not preserved, "
> +                            "continuing without it", chr->label);
> +            }
> +        } else {
> +            chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
> +            if (chr->logfd < 0) {
> +                return;
> +            }
> +            if (!cpr_is_incoming()) {
> +                cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
> +                cpr_save_fd(chr->label, CPR_CHARDEV_LOG_ID, chr->logfd);
> +            }
>          }
>      }

[Severity: Low]
Is the log fd handled consistently across cpr modes here? The fd is saved
to the registry for every start where cpr_is_incoming() is false, but it
is only claimed when cpr_exec_is_incoming() is true.

In cpr-transfer, the target takes the else branch in qemu_char_open():

    chr->logfd = qemu_create(common->logfile, flags, 0666, errp);

so it reopens the logfile path itself. The save is then skipped because
cpr_is_incoming() is true. Can this leave the transferred fd unclaimed in
the registry, and also hit the same permission and virtlogd-bypass problem
the commit message describes for cpr-exec?

This follows the exec-only pattern in char-file.c and char-pty.c. I did not
verify whether cpr-transfer sends every registry fd. If it does not, the
leak does not occur, but the path reopen remains.

[ ... ]

> @@ -315,11 +332,14 @@ static void char_finalize(Object *obj)
>      if (chr->be) {
>          chr->be->chr = NULL;
>      }
> -    g_free(chr->filename);
> -    g_free(chr->label);
>      if (chr->logfd != -1) {
> +        if (cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID) == chr->logfd) {
> +            cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
> +        }
>          close(chr->logfd);
>      }
> +    g_free(chr->filename);
> +    g_free(chr->label);
>      qemu_mutex_destroy(&chr->chr_write_lock);
>  }

[Severity: Low]
In the cpr-transfer target, can the registry entry outlive the chardev
here? cpr_find_fd() in char_finalize() returns the transferred fd, which
differs from chr->logfd, so cpr_delete_fd() is not called.

Would the stale registry entry and its open fd then stay until process
exit?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002164853.2130337-1-den@openvz.org?part=2

^ permalink raw reply	[relevance 3%]

* Re: [PATCH 3/3] iotests/migrate-cpr-exec: cover chardevs with logfile= #VSTOR-148304
  @ 2026-10-02 16:55  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-02 16:55 UTC (permalink / raw)
  To: Denis V. Lunev, svt-core; +Cc: andrey.drobyshev, den

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Low] iotests: socket logfile test leaves stale path=sock1 in exec command line
- [Low] iotests: assertFalse on BAD_LOG parent dir cannot detect an attempted logfile open
- [Low] iotests: _open_files() compares /proc readlink results to non-canonical paths
- [Low] iotests: pre_cpr rewrite drops logappend=on from the console chardev argument
--

commit 8a854fc4d41646d884346abb08e5af9be8f46b16
Author: Denis V. Lunev <den@openvz.org>

iotests/migrate-cpr-exec: cover chardevs with logfile= #VSTOR-148304

This adds four tests to migrate-cpr-exec. They cover a pty console, a
guest agent socket and a second QMP monitor, each with a log, through
cpr-exec with logfile paths that cannot be created. They also cover
chardev-change to a new log for pty and socket, and a failed
chardev-change whose rollback must keep the old log.

> diff --git a/tests/qemu-iotests/tests/migrate-cpr-exec b/tests/qemu-iotests/tests/migrate-cpr-exec
> index 51e6cbcd6593..e8a6c502c840 100755
> --- a/tests/qemu-iotests/tests/migrate-cpr-exec
> +++ b/tests/qemu-iotests/tests/migrate-cpr-exec

[ ... ]

> +    def _open_files(self) -> set:
> +        fd_dir = f'/proc/{self.vm.get_pid()}/fd'
> +        return {os.readlink(os.path.join(fd_dir, fd))
> +                for fd in os.listdir(fd_dir)}

[Severity: Low]
Can this comparison fail when TEST_DIR is not canonical?

os.readlink() on /proc/<pid>/fd/N returns the kernel's resolved path. The
expected paths come from _log_path() as os.path.join(iotests.test_dir, name),
and they are not passed through os.path.realpath().

If TEST_DIR is reached through a symlink, this assertion fails even though
the fds were preserved:

    self.assertLessEqual({agent_log, mon_log}, self._open_files())

In the same case, this assertion in _test_chardev_change_logfile() passes
vacuously, so it no longer checks that the old log fd was dropped:

    self.assertNotIn(log_old, self._open_files())

I did not confirm how iotests normalizes test_dir. Would applying
os.path.realpath() to the expected paths avoid the mismatch?

[ ... ]

> +        def pre_cpr():
> +            self._serial_write('before\n')
> +            for c in (console, agent, mon):
> +                self._replace_arg(c, c.replace(c[c.index('logfile='):],
> +                                               f'logfile={self.BAD_LOG}'))

[Severity: Low]
Does this rewrite drop logappend=on from the console chardev argument?

For the console chardev, c[c.index('logfile='):] is
'logfile=/dev/fdset/1,logappend=on'. The whole tail is replaced, so the new
argument becomes:

    pty,id=charserial0,logfile=<BAD_LOG>

The agent and mon arguments have logfile= last, so they are unaffected.

The combination of logappend=on with an fd-backed preserved log is
therefore not exercised after the exec. This is the combination the commit
message says libvirt sends. Today qemu_char_open() does not use the
computed flags when cpr_exec_is_incoming() is true, so the test passes. A
later change that consults logappend on the restore path would not be
caught.

Would it be better to replace only the path value and keep ",logappend=on"?

> +            args = list(self.vm._qemu_full_args)
> +            i = args.index('-add-fd')
> +            self.vm._qemu_full_args = tuple(args[:i] + args[i + 2:])
> +
> +        def post_cpr():
> +            self._serial_write('after\n')
> +            self.assertEqual(self._read(console_log), 'before\nafter\n')
> +            self.assertLessEqual({agent_log, mon_log}, self._open_files())
> +            self.assertFalse(os.path.exists(os.path.dirname(self.BAD_LOG)))

[Severity: Low]
Can this assertion ever fail?

BAD_LOG is <test_dir>/nonexistent/chr.log, and nothing creates the
"nonexistent" directory. qemu_create() called from qemu_char_open() does
not create parent directories. If a regressed QEMU tried to open the path,
it would get ENOENT and the directory would stay absent, so
assertFalse(os.path.exists(os.path.dirname(self.BAD_LOG))) always holds.

The real evidence that the path is not opened is that the new QEMU starts
at all. The docstring ("the new logfile= paths are never opened") and the
commit message suggest this assertion adds coverage that it does not.

Could the assertion be dropped, or replaced with a check that can actually
fail?

[ ... ]

> +    def test_chardev_change_logfile_socket(self) -> None:
> +        sock1 = iotests.file_path('chr-log-sock1')
> +        sock2 = iotests.file_path('chr-log-sock2')
> +        self._test_chardev_change_logfile(
> +            'socket',
> +            {'addr': {'type': 'unix', 'data': {'path': sock2}},
> +             'server': True, 'wait': False},
> +            f'path={sock1},server=on,wait=off,')

[Severity: Low]
This isn't a bug, but the rewritten exec command line for the socket case
looks inconsistent with the live chardev.

In _test_chardev_change_logfile(), change() calls:

    self._replace_arg(arg, f'{backend},id=char0,{opts_old}'
                           f'logfile={self.BAD_LOG}')

For the socket case opts_old is 'path=<sock1>,server=on,wait=off,'. After
chardev-change the live chardev listens on sock2, so the new command line
still names sock1, which is no longer in use.

The existing test_chardev_change_socket_to_socket updates the path to
sock2. The test passes here because chr_socket_cpr_restore_addr_fd() in
chardev/char-socket.c uses the inherited fd and not the path. As written,
the test does not model what a management layer would send. It would also
not catch a regression that used the path instead of the fd.

Should the rewritten argument use sock2 for the path?

[ ... ]

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002164853.2130337-1-den@openvz.org?part=3

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH hci-8.1 3/6] ve/printk: Specify ve in printks
      @ 2026-10-02 21:26 15% ` Vladimir Riabchun
  2026-10-02 21:36  4%   ` no-reply
  2026-10-02 21:26  3% ` [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
                   ` (2 subsequent siblings)
  5 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-02 21:26 UTC (permalink / raw)
  To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel

Introduce ve_printk helpers that accept VE structure,
so we can provide correct VE where get_exec_env is unreliable.

This is true for ve_printk_deferred functions, and their users.
Now VE ratelimiting requires specified VE, and this requirement
is further forwarded to ve_net_ratelimited kind.

https://virtuozzo.atlassian.net/browse/VSTOR-146839

Feature: ve printk
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
 include/linux/net.h                 | 16 ++++----
 include/linux/printk.h              | 23 ++++++++++--
 include/linux/ratelimit_types.h     |  6 ++-
 kernel/printk/printk.c              | 74 +++++++++++++++++++++++++++++++------
 lib/ratelimit.c                     |  7 ++--
 net/core/utils.c                    |  4 +-
 net/netfilter/nf_conntrack_core.c   |  2 +-
 net/netfilter/nf_conntrack_expect.c |  2 +-
 net/netfilter/xt_TCPMSS.c           |  4 +-
 9 files changed, 104 insertions(+), 34 deletions(-)

diff --git a/include/linux/net.h b/include/linux/net.h
index 3f3bbe9f149d..4c34b9a0386f 100644
--- a/include/linux/net.h
+++ b/include/linux/net.h
@@ -265,7 +265,7 @@ struct socket *sockfd_lookup(int fd, int *err);
 struct socket *sock_from_file(struct file *file);
 #define		     sockfd_put(sock) fput(sock->file)
 int net_ratelimit(void);
-int ve_net_ratelimit(int dst);
+int ve_net_ratelimit(int dst, struct ve_struct *ve);
 
 #define net_ratelimited_function(function, ...)			\
 do {								\
@@ -273,10 +273,10 @@ do {								\
 		function(__VA_ARGS__);				\
 } while (0)
 
-#define ve_net_ratelimited_function(function, dst, ...)		\
+#define ve_net_ratelimited_function(function, dst, ve, ...)	\
 do {								\
-	if (ve_net_ratelimit(dst))				\
-		function(dst, __VA_ARGS__);			\
+	if (ve_net_ratelimit(dst, ve))				\
+		function(dst, ve, __VA_ARGS__);			\
 } while (0)
 
 #define net_emerg_ratelimited(fmt, ...)				\
@@ -311,10 +311,10 @@ do {									\
 	no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
 #endif
 
-#define net_velog_ratelimited(fmt, ...)                                \
-	ve_net_ratelimited_function(ve_printk, VE_LOG, fmt, ##__VA_ARGS__)
-#define net_veboth_ratelimited(fmt, ...)                               \
-	ve_net_ratelimited_function(ve_printk, VE_LOG_BOTH, fmt, ##__VA_ARGS__)
+#define net_velog_ratelimited(ve, fmt, ...)						\
+	ve_net_ratelimited_function(__ve_printk, VE_LOG, ve, fmt, ##__VA_ARGS__)
+#define net_veboth_ratelimited(ve, fmt, ...)						\
+	ve_net_ratelimited_function(__ve_printk, VE_LOG_BOTH, ve, fmt, ##__VA_ARGS__)
 
 #define net_get_random_once(buf, nbytes)			\
 	get_random_once((buf), (nbytes))
diff --git a/include/linux/printk.h b/include/linux/printk.h
index 3b0af0deb6c3..1478e47dc582 100644
--- a/include/linux/printk.h
+++ b/include/linux/printk.h
@@ -76,6 +76,7 @@ extern int console_printk[];
 #define VE0_LOG		1
 #define VE_LOG		2
 #define VE_LOG_BOTH	(VE0_LOG | VE_LOG)
+struct ve_struct;
 
 extern void console_verbose(void);
 
@@ -160,6 +161,11 @@ int ve_vprintk_emit(int facility, int level,
 		    const struct dev_printk_info *dev_info,
 		    const char *fmt, va_list args);
 
+asmlinkage __printf(5, 0)
+int __ve_vprintk_emit(struct ve_struct *ve, int facility, int level,
+		    const struct dev_printk_info *dev_info,
+		    const char *fmt, va_list args);
+
 asmlinkage __printf(1, 0)
 int vprintk(const char *fmt, va_list args);
 __printf(1, 0)
@@ -171,8 +177,11 @@ int _printk(const char *fmt, ...);
 asmlinkage __printf(2, 3) __cold
 int ve_printk(int dst, const char *fmt, ...);
 
-asmlinkage __printf(2, 3) __cold
-int ve_printk_deferred(int dst, const char *fmt, ...);
+asmlinkage __printf(3, 4) __cold
+int __ve_printk(int dst, struct ve_struct *ve, const char *fmt, ...);
+
+asmlinkage __printf(3, 4) __cold
+int ve_printk_deferred(int dst, struct ve_struct *ve, const char *fmt, ...);
 
 struct ve_struct;
 int ve_log_init(struct ve_struct *ve);
@@ -248,8 +257,13 @@ int ve_printk(int dst, const char *s, ...)
 {
 	return 0;
 }
+static inline __printf(3, 4) __cold
+int __ve_printk(int dst, struct ve_struct *ve, const char *s, ...)
+{
+	return 0;
+}
 static inline __printf(2, 3) __cold
-int ve_printk_deferred(int dst, const char *s, ...)
+int ve_printk_deferred(int dst, struct ve_struct *ve, const char *s, ...)
 {
 	return 0;
 }
@@ -747,7 +761,8 @@ struct pi_entry {
 				      DEFAULT_RATELIMIT_INTERVAL,	\
 				      DEFAULT_RATELIMIT_BURST);		\
 									\
-	if (ve_ratelimit(dst, &_rs))					\
+	WARN_ON_ONCE((dst & VE_LOG) && !in_task());			\
+	if (ve_ratelimit(dst, get_exec_env(), &_rs))			\
 		ve_printk(dst, fmt, ##__VA_ARGS__);			\
 })
 #else
diff --git a/include/linux/ratelimit_types.h b/include/linux/ratelimit_types.h
index 3902066f0fee..aa9eee2a2455 100644
--- a/include/linux/ratelimit_types.h
+++ b/include/linux/ratelimit_types.h
@@ -44,7 +44,9 @@ struct ratelimit_state {
 extern int ___ratelimit(struct ratelimit_state *rs, const char *func);
 #define __ratelimit(state) ___ratelimit(state, __func__)
 
-extern int __ve_ratelimit(int dst, struct ratelimit_state *rs, const char *func);
-#define ve_ratelimit(dst, state) __ve_ratelimit(dst, state, __func__)
+struct ve_struct;
+extern int __ve_ratelimit(int dst, struct ve_struct *ve,
+	struct ratelimit_state *rs, const char *func);
+#define ve_ratelimit(dst, ve, state) __ve_ratelimit(dst, ve, state, __func__)
 
 #endif /* _LINUX_RATELIMIT_TYPES_H */
diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
index b06fc835d7c7..df7cf83fb4af 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -567,6 +567,16 @@ struct log_state *ve_log_state(void)
 	return log;
 }
 
+struct log_state *__ve_log_state(struct ve_struct *ve)
+{
+	struct log_state *log = &init_log_state;
+#ifdef CONFIG_VE
+	if (ve->log_state)
+		log = ve->log_state;
+#endif
+	return log;
+}
+
 void log_poll_wait(struct file *filp, poll_table *p)
 {
 	poll_wait(filp, &ve_log_state()->wait, p);
@@ -2525,26 +2535,45 @@ asmlinkage int ve_vprintk_emit(int facility, int level,
 }
 EXPORT_SYMBOL(ve_vprintk_emit);
 
+asmlinkage int __ve_vprintk_emit(struct ve_struct *ve, int facility, int level,
+			       const struct dev_printk_info *dev_info,
+			       const char *fmt, va_list args)
+{
+	return vprintk_emit_log(__ve_log_state(ve),
+				facility, level, dev_info,
+				fmt, args);
+}
+EXPORT_SYMBOL(__ve_vprintk_emit);
+
 int vprintk_default(const char *fmt, va_list args)
 {
 	return vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, fmt, args);
 }
 EXPORT_SYMBOL_GPL(vprintk_default);
 
-static asmlinkage int ve_vprintk(int dst, const char *fmt, va_list args)
+static asmlinkage int ve_vprintk(int dst, struct ve_struct *ve,
+				const char *fmt, va_list args)
 {
 	va_list args2;
-	int r = 0;
+	int r = 0, ve_provided = (ve != NULL);
 
 	WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
 
+	if (!ve)
+		ve = get_exec_env();
+
 	va_copy(args2, args);
 	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
 		r = vprintk(fmt, args);
 
 	/* Report IRQ usage here - printing to VE requires explicit ve reference */
-	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
-		r = ve_vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, fmt, args2);
+	if ((dst & VE_LOG)) {
+		if (!ve_provided && WARN_ON_ONCE(!in_task()))
+			goto done;
+		if (!ve_is_super(ve))
+			r = __ve_vprintk_emit(ve, 0, LOGLEVEL_DEFAULT, NULL, fmt, args2);
+	}
+done:
 	va_end(args2);
 
 	return r;
@@ -2560,39 +2589,62 @@ asmlinkage int ve_printk(int dst, const char *fmt, ...)
 	int r;
 
 	va_start(args, fmt);
-	r = ve_vprintk(dst, fmt, args);
+	r = ve_vprintk(dst, NULL, fmt, args);
 	va_end(args);
 
 	return r;
 }
 EXPORT_SYMBOL(ve_printk);
 
-static asmlinkage int ve_vprintk_deferred(int dst, const char *fmt, va_list args)
+asmlinkage int __ve_printk(int dst, struct ve_struct *ve, const char *fmt, ...)
+{
+	va_list args;
+	int r;
+
+	va_start(args, fmt);
+	r = ve_vprintk(dst, ve, fmt, args);
+	va_end(args);
+
+	return r;
+}
+EXPORT_SYMBOL(__ve_printk);
+
+static asmlinkage int ve_vprintk_deferred(int dst, struct ve_struct *ve, 
+					const char *fmt, va_list args)
 {
 	va_list args2;
-	int r = 0;
+	int r = 0, ve_provided = (ve != NULL);
 
 	WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
 
+	if (!ve)
+		ve = get_exec_env();
+
 	va_copy(args2, args);
 	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
 		r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
 
 	/* Report IRQ usage here - printing to VE requires explicit ve reference */
-	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
-		r = ve_vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args2);
+	if ((dst & VE_LOG)) {
+		if (!ve_provided && WARN_ON_ONCE(!in_task()))
+			goto done;
+		if (!ve_is_super(ve))
+			r = __ve_vprintk_emit(ve, 0, LOGLEVEL_SCHED, NULL, fmt, args2);
+	}
+done:
 	va_end(args2);
 
 	return r;
 }
 
-asmlinkage int ve_printk_deferred(int dst, const char *fmt, ...)
+asmlinkage int ve_printk_deferred(int dst, struct ve_struct *ve,
+	const char *fmt, ...)
 {
 	va_list args;
 	int r;
 
 	va_start(args, fmt);
-	r = ve_vprintk_deferred(dst, fmt, args);
+	r = ve_vprintk_deferred(dst, ve, fmt, args);
 	va_end(args);
 
 	return r;
diff --git a/lib/ratelimit.c b/lib/ratelimit.c
index 98409e870eea..98c45bb19e50 100644
--- a/lib/ratelimit.c
+++ b/lib/ratelimit.c
@@ -11,6 +11,7 @@
 #include <linux/ratelimit.h>
 #include <linux/jiffies.h>
 #include <linux/export.h>
+#include <linux/ve_proto.h>
 
 /*
  * __ratelimit - rate limiting
@@ -24,7 +25,7 @@
  * 0 means callbacks will be suppressed.
  * 1 means go ahead and do it.
  */
-int __ve_ratelimit(int dst, struct ratelimit_state *rs, const char *func)
+int __ve_ratelimit(int dst, struct ve_struct *ve, struct ratelimit_state *rs, const char *func)
 {
 	/* Paired with WRITE_ONCE() in .proc_handler().
 	 * Changing two values seperately could be inconsistent
@@ -55,7 +56,7 @@ int __ve_ratelimit(int dst, struct ratelimit_state *rs, const char *func)
 
 		if (m) {
 			if (!(rs->flags & RATELIMIT_MSG_ON_RELEASE)) {
-				ve_printk_deferred(dst, KERN_WARNING
+				ve_printk_deferred(dst, ve, KERN_WARNING
 						"%s: %d callbacks suppressed\n", func, m);
 			}
 		}
@@ -77,6 +78,6 @@ EXPORT_SYMBOL(__ve_ratelimit);
 
 int ___ratelimit(struct ratelimit_state *rs, const char *func)
 {
-	return __ve_ratelimit(VE0_LOG, rs, func);
+	return __ve_ratelimit(VE0_LOG, get_ve0(), rs, func);
 }
 EXPORT_SYMBOL(___ratelimit);
diff --git a/net/core/utils.c b/net/core/utils.c
index cffc3bc44baa..bf4e53205a8e 100644
--- a/net/core/utils.c
+++ b/net/core/utils.c
@@ -41,9 +41,9 @@ int net_ratelimit(void)
 }
 EXPORT_SYMBOL(net_ratelimit);
 
-int ve_net_ratelimit(int dst)
+int ve_net_ratelimit(int dst, struct ve_struct *ve)
 {
-	return ve_ratelimit(dst, &net_ratelimit_state);
+	return ve_ratelimit(dst, ve, &net_ratelimit_state);
 }
 EXPORT_SYMBOL(ve_net_ratelimit);
 
diff --git a/net/netfilter/nf_conntrack_core.c b/net/netfilter/nf_conntrack_core.c
index f078136edddd..8900a41b48b6 100644
--- a/net/netfilter/nf_conntrack_core.c
+++ b/net/netfilter/nf_conntrack_core.c
@@ -1701,7 +1701,7 @@ __nf_conntrack_alloc(struct net *net,
 			if (!conntrack_gc_work.early_drop)
 				conntrack_gc_work.early_drop = true;
 			atomic_dec(&cnet->count);
-			net_veboth_ratelimited(KERN_WARNING "VE%s: "
+			net_veboth_ratelimited(net->owner_ve, KERN_WARNING "VE%s: "
 					       "nf_conntrack table full in netns %u, dropping packet\n",
 					       ve_name(net->owner_ve), net->ns.inum);
 			return ERR_PTR(-ENOMEM);
diff --git a/net/netfilter/nf_conntrack_expect.c b/net/netfilter/nf_conntrack_expect.c
index 4b077b2cb432..23e78957bb79 100644
--- a/net/netfilter/nf_conntrack_expect.c
+++ b/net/netfilter/nf_conntrack_expect.c
@@ -508,7 +508,7 @@ static inline int __nf_ct_expect_check(struct nf_conntrack_expect *expect,
 
 	cnet = nf_ct_pernet(net);
 	if (cnet->expect_count >= cnet->expect_max) {
-		net_veboth_ratelimited(KERN_WARNING "VE%s: "
+		net_veboth_ratelimited(net->owner_ve, KERN_WARNING "VE%s: "
 					"nf_conntrack: expectation table full in netns %u\n",
 					ve_name(net->owner_ve), net->ns.inum);
 		ret = -EMFILE;
diff --git a/net/netfilter/xt_TCPMSS.c b/net/netfilter/xt_TCPMSS.c
index 822b3f71570e..0b9ccc8987e2 100644
--- a/net/netfilter/xt_TCPMSS.c
+++ b/net/netfilter/xt_TCPMSS.c
@@ -105,8 +105,8 @@ tcpmss_mangle_packet(struct sk_buff *skb,
 		unsigned int min_mtu = min(dst_mtu(skb_dst(skb)), in_mtu);
 
 		if (min_mtu <= minlen) {
-			net_velog_ratelimited("unknown or invalid path-MTU (%u)\n",
-					    min_mtu);
+			net_velog_ratelimited(net->owner_ve, "unknown or invalid path-MTU (%u)\n",
+					      min_mtu);
 			return -1;
 		}
 		newmss = min_mtu - minlen;

-- 
2.52.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 15%]

* [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization
                     ` (2 preceding siblings ...)
  2026-10-02 21:26 15% ` [Devel] [PATCH hci-8.1 3/6] ve/printk: Specify ve in printks Vladimir Riabchun
@ 2026-10-02 21:26  3% ` Vladimir Riabchun
  2026-10-02 21:37  3%   ` no-reply
  2026-10-02 21:26  8% ` [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
  2026-10-02 21:26  4% ` [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
  5 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-02 21:26 UTC (permalink / raw)
  To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel

The usage of get_exec_env() in ve_timens_add_boottime_ns can lead
to time travels - a printk from IRQ might use container time for
printing to host dmesg and vice versa.

Fix this by adding timens_offsets info to printk_info entry. This way
correct time is provided by the original printer of message, rather than
printing job, that may be delayed and called from a different context.
It seems complicated to keep time namespace in log_state, because
VE creation and namespace creation happen at different times and it
seems that one VE can have different time namespaces if it was stopped
and then started again.
So instead of tracking time namespaces, let's put actual time offsets
into log state.

https://virtuozzo.atlassian.net/browse/VSTOR-146839

Feature: ve printk
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
 kernel/printk/internal.h          |  6 +++++
 kernel/printk/printk.c            | 51 ++++++++++++++++++++++++---------------
 kernel/printk/printk_ringbuffer.h |  2 ++
 3 files changed, 39 insertions(+), 20 deletions(-)

diff --git a/kernel/printk/internal.h b/kernel/printk/internal.h
index d00ac40d8401..d0096aba5af7 100644
--- a/kernel/printk/internal.h
+++ b/kernel/printk/internal.h
@@ -4,6 +4,7 @@
  */
 #include <linux/console.h>
 #include <linux/percpu.h>
+#include <linux/time_namespace.h>
 #include <linux/types.h>
 
 #if defined(CONFIG_PRINTK) && defined(CONFIG_SYSCTL)
@@ -354,11 +355,16 @@ struct log_state {
 	 */
 	struct latched_seq clear_seq;
 	struct printk_ringbuffer *prb;
+	/* Updated on every log_state request */
+	struct timens_offsets time_offset;
 };
 
+#define GET_VE_LOG(ve) ((struct log_state *) ve->log_state)
+
 extern struct log_state init_log_state;
 
 struct log_state *ve_log_state(void);
+struct log_state *__ve_log_state(struct ve_struct *ve);
 
 bool printk_get_next_message(struct log_state *log,
 			     struct printk_message *pmsg, u64 seq,
diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
index df7cf83fb4af..575bd3590198 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -556,13 +556,28 @@ DEFINE_STRUCT_MEMBER_ALIAS(clear_seq, init_log_state, clear_seq);
 DEFINE_STRUCT_MEMBER_ALIAS(prb, init_log_state, prb);
 #undef DEFINE_STRUCT_MEMBER_ALIAS
 
+static void update_log_time(struct ve_struct *ve)
+{
+	struct time_namespace *timens = ve_get_time_ns(ve);
+
+	if (timens)
+		GET_VE_LOG(ve)->time_offset = timens->offsets;
+	else
+		GET_VE_LOG(ve)->time_offset = init_time_ns.offsets;
+
+	if (timens)
+		put_time_ns(timens);
+}
+
 struct log_state *ve_log_state(void)
 {
 	struct log_state *log = &init_log_state;
 #ifdef CONFIG_VE
 	/* get_exec_env() is reliable only in task context */
-	if (!WARN_ON_ONCE(!in_task()) && get_exec_env()->log_state)
+	if (!WARN_ON_ONCE(!in_task()) && get_exec_env()->log_state) {
+		update_log_time(get_exec_env());
 		log = get_exec_env()->log_state;
+	}
 #endif
 	return log;
 }
@@ -571,8 +586,10 @@ struct log_state *__ve_log_state(struct ve_struct *ve)
 {
 	struct log_state *log = &init_log_state;
 #ifdef CONFIG_VE
-	if (ve->log_state)
+	if (ve->log_state) {
+		update_log_time(ve);
 		log = ve->log_state;
+	}
 #endif
 	return log;
 }
@@ -698,21 +715,10 @@ static void append_char(char **pp, char *e, char c)
 		*(*pp)++ = c;
 }
 
-static inline u64 ve_timens_add_boottime_ns(u64 nsec)
+static inline u64 ve_timens_add_boottime_ns(u64 nsec, const struct timens_offsets *off)
 {
-	struct time_namespace *ve_time_ns;
-	struct timens_offsets *ns_offsets;
-
-	ve_time_ns = ve_get_time_ns(get_exec_env());
-	if (unlikely(!ve_time_ns)) {
-		/* container not yet started */
-		return nsec;
-	}
-
-	ns_offsets = &ve_time_ns->offsets;
-	nsec += ns_offsets->boottime.tv_sec * NSEC_PER_SEC;
-	nsec += ns_offsets->boottime.tv_nsec;
-	put_time_ns(ve_time_ns);
+	nsec += off->boottime.tv_sec * NSEC_PER_SEC;
+	nsec += off->boottime.tv_nsec;
 
 	return nsec;
 }
@@ -732,7 +738,7 @@ static ssize_t info_print_ext_header(char *buf, size_t size,
 #endif
 
 	/* shift the timestamp on the Container uptime value */
-	ts_usec = ve_timens_add_boottime_ns(ts_usec);
+	ts_usec = ve_timens_add_boottime_ns(ts_usec, &info->time_offset);
 	do_div(ts_usec, 1000);
 
 	return scnprintf(buf, size, "%u,%llu,%llu,%c%s;",
@@ -1108,6 +1114,7 @@ void log_buf_vmcoreinfo_setup(void)
 	VMCOREINFO_OFFSET(printk_info, text_len);
 	VMCOREINFO_OFFSET(printk_info, caller_id);
 	VMCOREINFO_OFFSET(printk_info, dev_info);
+	VMCOREINFO_OFFSET(printk_info, time_offset);
 
 	VMCOREINFO_STRUCT_SIZE(dev_printk_info);
 	VMCOREINFO_OFFSET(dev_printk_info, subsystem);
@@ -1219,6 +1226,7 @@ static unsigned int __init add_to_rb(struct printk_ringbuffer *rb,
 	dest_r.info->ts_nsec = r->info->ts_nsec;
 	dest_r.info->caller_id = r->info->caller_id;
 	memcpy(&dest_r.info->dev_info, &r->info->dev_info, sizeof(dest_r.info->dev_info));
+	dest_r.info->time_offset = r->info->time_offset;
 
 	prb_final_commit(&e);
 
@@ -1427,12 +1435,13 @@ static size_t print_syslog(unsigned int level, char *buf)
 	return sprintf(buf, "<%u>", level);
 }
 
-static size_t print_time(u64 ts, char *buf)
+static size_t print_time(const struct printk_info *info, char *buf)
 {
 	unsigned long rem_nsec;
+	u64 ts;
 
 	/* shift the timestamp on the Container uptime value */
-	ts = ve_timens_add_boottime_ns(ts);
+	ts = ve_timens_add_boottime_ns(info->ts_nsec, &info->time_offset);
 
 	rem_nsec = do_div(ts, 1000000000);
 
@@ -1462,7 +1471,7 @@ static size_t info_print_prefix(const struct printk_info  *info, bool syslog,
 		len = print_syslog((info->facility << 3) | info->level, buf);
 
 	if (time)
-		len += print_time(info->ts_nsec, buf + len);
+		len += print_time(info, buf + len);
 
 	len += print_caller(info->caller_id, buf + len);
 
@@ -2402,6 +2411,8 @@ static int vprintk_store_log(struct log_state *log, int facility, int level,
 	if (dev_info)
 		memcpy(&r.info->dev_info, dev_info, sizeof(r.info->dev_info));
 
+	r.info->time_offset = log->time_offset;
+
 	/* A message without a trailing newline can be continued. */
 	if (!(flags & LOG_NEWLINE))
 		prb_commit(&e);
diff --git a/kernel/printk/printk_ringbuffer.h b/kernel/printk/printk_ringbuffer.h
index 4ef81349d9fb..19052ba005fc 100644
--- a/kernel/printk/printk_ringbuffer.h
+++ b/kernel/printk/printk_ringbuffer.h
@@ -7,6 +7,7 @@
 #include <linux/bits.h>
 #include <linux/dev_printk.h>
 #include <linux/stddef.h>
+#include <linux/time_namespace.h>
 #include <linux/types.h>
 
 /*
@@ -25,6 +26,7 @@ struct printk_info {
 	u32	caller_id;	/* thread id or processor id */
 
 	struct dev_printk_info	dev_info;
+	struct timens_offsets	time_offset;	/* Author timens offsets */
 };
 
 /*

-- 
2.52.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve
                     ` (3 preceding siblings ...)
  2026-10-02 21:26  3% ` [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
@ 2026-10-02 21:26  8% ` Vladimir Riabchun
  2026-10-02 21:38  5%   ` no-reply
  2026-10-02 21:26  4% ` [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
  5 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-02 21:26 UTC (permalink / raw)
  To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel

ve_failcount_inc could be called from IRQ context, and we want the message
to go to the right place. Use new  __ve_printk helpers for this.

https://virtuozzo.atlassian.net/browse/VSTOR-146839

Feature: ve printk
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
 include/linux/printk.h | 18 ++++++++++++++----
 include/linux/ve.h     | 16 ++++++++--------
 2 files changed, 22 insertions(+), 12 deletions(-)

diff --git a/include/linux/printk.h b/include/linux/printk.h
index 1478e47dc582..ce82623fb138 100644
--- a/include/linux/printk.h
+++ b/include/linux/printk.h
@@ -755,21 +755,28 @@ struct pi_entry {
 		printk(fmt, ##__VA_ARGS__);				\
 })
 
-#define ve_printk_ratelimited(dst, fmt, ...)				\
+#define __ve_printk_ratelimited(dst, ve, fmt, ...)			\
 ({									\
 	static DEFINE_RATELIMIT_STATE(_rs,				\
 				      DEFAULT_RATELIMIT_INTERVAL,	\
 				      DEFAULT_RATELIMIT_BURST);		\
 									\
-	WARN_ON_ONCE((dst & VE_LOG) && !in_task());			\
-	if (ve_ratelimit(dst, get_exec_env(), &_rs))			\
-		ve_printk(dst, fmt, ##__VA_ARGS__);			\
+	if (ve_ratelimit(dst, ve, &_rs))				\
+		__ve_printk(dst, ve, fmt, ##__VA_ARGS__);		\
+})
+
+#define ve_printk_ratelimited(dst, fmt, ...)					\
+({										\
+	WARN_ON_ONCE((dst & VE_LOG) && !in_task());				\
+	__ve_printk_ratelimited(dst, get_exec_env(), fmt, ##__VA_ARGS__);	\
 })
 #else
 #define printk_ratelimited(fmt, ...)					\
 	no_printk(fmt, ##__VA_ARGS__)
 #define ve_printk_ratelimited(dst, fmt, ...)				\
 	no_printk(fmt, ##__VA_ARGS__)
+#define __ve_printk_ratelimited(dst, fmt, ...)				\
+	no_printk(fmt, ##__VA_ARGS__)
 #endif
 
 #define pr_emerg_ratelimited(fmt, ...)					\
@@ -787,6 +794,7 @@ struct pi_entry {
 #define pr_info_ratelimited(fmt, ...)					\
 	printk_ratelimited(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
 /* no pr_cont_ratelimited, don't do that... */
+
 #define ve_pr_emerg_ratelimited(dst, fmt, ...)					\
 	ve_printk_ratelimited(dst, KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)
 #define ve_pr_alert_ratelimited(dst, fmt, ...)					\
@@ -797,6 +805,8 @@ struct pi_entry {
 	ve_printk_ratelimited(dst, KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
 #define ve_pr_warn_ratelimited(dst, fmt, ...)					\
 	ve_printk_ratelimited(dst, KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
+#define __ve_pr_warn_ratelimited(dst, ve, fmt, ...)				\
+	__ve_printk_ratelimited(dst, ve, KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
 #define ve_pr_notice_ratelimited(dst, fmt, ...)					\
 	ve_printk_ratelimited(dst, KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
 #define ve_pr_info_ratelimited(dst, fmt, ...)					\
diff --git a/include/linux/ve.h b/include/linux/ve.h
index a400d21593b2..946df4eebe89 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -156,14 +156,14 @@ extern int nr_ve;
 #define BPF_PROG_MAX_NR_DEFAULT	256	/* number of loaded BPF progs per-VE */
 #define MNT_MAX_NR_DEFAULT	4096	/* number of mounts per-VE */
 
-#define ve_failcount_inc(ve, name)					\
-do {									\
-	struct ve_struct *__ve = (ve);					\
-									\
-	atomic_inc(&__ve->name##_failcount);				\
-	ve_pr_warn_ratelimited(VE_LOG_BOTH, "CT%s: hits the " #name	\
-		" limit, see the '" #name "' counter in ve.failcount "	\
-		"of the container's ve cgroup\n", ve_name(__ve));	\
+#define ve_failcount_inc(ve, name)						\
+do {										\
+	struct ve_struct *__ve = (ve);						\
+										\
+	atomic_inc(&__ve->name##_failcount);					\
+	__ve_pr_warn_ratelimited(VE_LOG_BOTH, __ve, "CT%s: hits the " #name	\
+		" limit, see the '" #name "' counter in ve.failcount "		\
+		"of the container's ve cgroup\n", ve_name(__ve));		\
 } while (0)
 
 #ifdef CONFIG_VE

-- 
2.52.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 8%]

* [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter
                     ` (4 preceding siblings ...)
  2026-10-02 21:26  8% ` [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
@ 2026-10-02 21:26  4% ` Vladimir Riabchun
  2026-10-02 21:40  3%   ` no-reply
  5 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-02 21:26 UTC (permalink / raw)
  To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel

Count hits of the per-VE neigh_tbl limit in ve.failcount.

This one was delayed due to a high risk of IRQ-context hits,
when destination VE can't be obtained from get_exec_env.

https://virtuozzo.atlassian.net/browse/VSTOR-135520
https://virtuozzo.atlassian.net/browse/VSTOR-146839

Feature: per-ve failcounters
Feature: ve printk
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
 include/linux/ve.h   | 1 +
 kernel/ve/ve.c       | 3 +++
 net/core/neighbour.c | 3 +--
 3 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/include/linux/ve.h b/include/linux/ve.h
index 946df4eebe89..44e04dc75823 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -97,6 +97,7 @@ struct ve_struct {
 
 	atomic_t		arp_neigh_nr;
 	atomic_t		nd_neigh_nr;
+	atomic_t		neigh_tbl_failcount;
 	unsigned long		meminfo_val;
 
 	/*
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index 51cc635be2e2..35fad9658149 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -118,6 +118,7 @@ struct ve_struct ve0 = {
 
 	.arp_neigh_nr		= ATOMIC_INIT(0),
 	.nd_neigh_nr		= ATOMIC_INIT(0),
+	.neigh_tbl_failcount	= ATOMIC_INIT(0),
 	.mnt_avail_nr		= ATOMIC_INIT(INT_MAX),
 	.mnt_max_nr		= INT_MAX,
 	.mnt_failcount		= ATOMIC_INIT(0),
@@ -834,6 +835,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
 
 	atomic_set(&ve->arp_neigh_nr, 0);
 	atomic_set(&ve->nd_neigh_nr, 0);
+	atomic_set(&ve->neigh_tbl_failcount, 0);
 	ve->mnt_max_nr = MNT_MAX_NR_DEFAULT;
 	atomic_set(&ve->mnt_failcount, 0);
 	atomic_set(&ve->mnt_avail_nr, MNT_MAX_NR_DEFAULT);
@@ -1098,6 +1100,7 @@ static const struct ve_failcount_entry {
 	VE_FC_ENTRY(mnt),
 	VE_FC_ENTRY(netif),
 	VE_FC_ENTRY(bpf_prog),
+	VE_FC_ENTRY(neigh_tbl),
 #ifdef CONFIG_AIO
 	VE_FC_ENTRY(aio),
 #endif
diff --git a/net/core/neighbour.c b/net/core/neighbour.c
index f90deb17fb25..c7d5e6df50c2 100644
--- a/net/core/neighbour.c
+++ b/net/core/neighbour.c
@@ -520,8 +520,7 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
 	    (glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
 	     time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
 		if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
-			net_info_ratelimited("%s: neighbor table overflow!\n",
-					     tbl->id);
+			ve_failcount_inc(ve, neigh_tbl);
 			NEIGH_CACHE_STAT_INC(tbl, table_fulls);
 			goto out_entries;
 		}

-- 
2.52.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH hci-8.1 1/6] ve/printk: Add sanity checks in ve_printk functions
  @ 2026-10-02 21:31  4%   ` no-reply
  2026-10-03 12:26  0%     ` Vladimir Riabchun
  0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-02 21:31 UTC (permalink / raw)
  To: vasileios.almpanis, Vladimir Riabchun, Konstantin Khorenko; +Cc: devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] printk: WARN_ON_ONCE() in ve_vprintk_deferred() uses non-deferred printk path
--

commit 2b22fc2da5243bd2b5c695100f66ab107b28c158
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

ve/printk: Add sanity checks in ve_printk functions

This patch adds a sanity check on the dst argument in the ve_printk
helpers. It also reorders the VE_LOG test so the dst bit is checked
before ve_is_super().

Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839

> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c

[ ... ]

> @@ -2567,10 +2569,12 @@ static asmlinkage int ve_vprintk_deferred(int dst, const char *fmt, va_list args
>  	va_list args2;
>  	int r = 0;
>  
> +	WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
> +
>  	va_copy(args2, args);
>  	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>  		r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);

[Severity: Low]
This isn't a bug in the normal case, but is the new WARN_ON_ONCE() safe
in ve_vprintk_deferred()?

This function exists for contexts where the regular printk path is
unsafe, which is why it emits at LOGLEVEL_SCHED through vprintk_emit().
WARN_ON_ONCE() reports through the normal printk and console path. It
can also panic if panic_on_warn is set.

If a caller passes dst == 0 or a dst with bits outside VE_LOG_BOTH from
such a context, could the warning recurse into scheduler or console
locks that the deferred variant is meant to avoid?

I did not find a caller that passes an invalid dst. The trigger would
need a buggy caller, and the warning fires only once.

Would a BUILD_BUG_ON() for constant arguments be more suitable here?
Alternatively, the report could go through a deferred-safe mechanism
such as printk_deferred().

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002-ve-printk-irq-v1-0-cb55896d455c@virtuozzo.com?part=1

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH hci-8.1 2/6] ve/printk: Report potentially leaking usage of ve_printk functions
  @ 2026-10-02 21:32  3%   ` no-reply
  2026-10-03 12:26  0%     ` Vladimir Riabchun
  0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-02 21:32 UTC (permalink / raw)
  To: vasileios.almpanis, Vladimir Riabchun, Konstantin Khorenko; +Cc: devel

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] printk: WARN_ON_ONCE() added to ve_vprintk_deferred() and ve_log_state() on printk-sensitive paths
- [Low] ve: ve_dev_printk_emit() returns uninitialized r when VE_LOG is rejected in non-task context
- [Low] printk: in_task() WARN is evaluated before ve_is_super(), so host-context calls warn spuriously
- [Low] printk: host-log decision still uses get_exec_env() in non-task context
--

commit b1f1fbc536b0ab55f70c0064fc9579c59cbe13a9
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

ve/printk: Report potentially leaking usage of ve_printk functions

This patch adds WARN_ON_ONCE(!in_task()) checks to the ve printk helpers
and to ve_log_state(). The goal is to avoid relying on get_exec_env() in
non-task context, and either reject VE printing or fall back to the host
log state.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839

> diff --git a/drivers/base/core.c b/drivers/base/core.c
> --- a/drivers/base/core.c
> +++ b/drivers/base/core.c

[ ... ]

> @@ -4952,7 +4952,9 @@ int ve_dev_printk_emit(int dst, int level, const struct device *dev, const char
>  
>  	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>  		r = dev_vprintk_emit(level, dev, fmt, args);
> -	if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
> +
> +	/* Report IRQ usage here - printing to VE requires explicit ve reference */
> +	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
>  		r = ve_dev_vprintk_emit(level, dev, fmt, args2);

[Severity: Low]
Can ve_dev_printk_emit() return an uninitialized value here?

The local variable is declared as "int r;" with no initializer. If
ve_dev_printk_emit(VE_LOG, ...) is called from IRQ, softirq or NMI
context while the interrupted task is in a non-super container, the
first branch is not taken because ve_is_super() is false and VE0_LOG
is not set. The second branch is now skipped because
WARN_ON_ONCE(!in_task()) is true. The function then does "return r"
with r never assigned.

Before this patch every valid dst assigned r in at least one branch.
The sibling functions ve_vprintk() and ve_vprintk_deferred() use
"int r = 0". Should r be initialized the same way here?

[Severity: Low]
Is it intended that WARN_ON_ONCE(!in_task()) is evaluated before
ve_is_super() in this condition?

With the current ordering, ve_dev_printk_emit(VE_LOG, ...) called from
IRQ context while the interrupted task is in the host (VE0) triggers
the warning, even though a host caller cannot leak into a container.
Because the warning is once per site, an early harmless hit could also
suppress a later hit that really would leak. The same ordering appears
in ve_vprintk() and ve_vprintk_deferred() below. Would testing
!ve_is_super(get_exec_env()) first limit the warning to the
potentially leaking case?

[Severity: Low]
Is the host-log decision in the preceding branch still based on
get_exec_env() in non-task context?

	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
		r = dev_vprintk_emit(level, dev, fmt, args);

If ve_dev_printk_emit(VE_LOG, ...) is called from IRQ and the
interrupted task is in the host, the message goes to the host log. If
the interrupted task is in a container, the VE branch is rejected by
the WARN and the message is dropped. The commit message says printing
is either rejected or the host log state is returned, but this branch
still depends on whichever task was interrupted. The same pattern
exists in ve_vprintk() and ve_vprintk_deferred(). Is that routing
intended?

> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c
> @@ -560,7 +560,8 @@ struct log_state *ve_log_state(void)
>  {
>  	struct log_state *log = &init_log_state;
>  #ifdef CONFIG_VE
> -	if (get_exec_env()->log_state)
> +	/* get_exec_env() is reliable only in task context */
> +	if (!WARN_ON_ONCE(!in_task()) && get_exec_env()->log_state)
>  		log = get_exec_env()->log_state;
>  #endif
>  	return log;

[Severity: Medium]
Can this WARN_ON_ONCE() recurse into printk?

ve_log_state() is reached from ve_vprintk_emit() and then
vprintk_emit_log(), so the WARN fires in the middle of the printk emit
path. A WARN prints a message and backtrace through the regular
non-deferred printk, and it can panic under panic_on_warn.

[ ... ]

> @@ -2540,7 +2541,9 @@ static asmlinkage int ve_vprintk(int dst, const char *fmt, va_list args)
>  	va_copy(args2, args);
>  	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>  		r = vprintk(fmt, args);
> -	if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
> +
> +	/* Report IRQ usage here - printing to VE requires explicit ve reference */
> +	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
>  		r = ve_vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, fmt, args2);
>  	va_end(args2);

[ ... ]

> @@ -2574,7 +2577,9 @@ static asmlinkage int ve_vprintk_deferred(int dst, const char *fmt, va_list args
>  	va_copy(args2, args);
>  	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>  		r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
> -	if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
> +
> +	/* Report IRQ usage here - printing to VE requires explicit ve reference */
> +	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
>  		r = ve_vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args2);
>  	va_end(args2);

[Severity: Medium]
Is a synchronous WARN_ON_ONCE() safe inside ve_vprintk_deferred()?

This helper exists for contexts where regular printk is unsafe, such as
scheduler code or code holding rq or timekeeping locks. If a caller
uses ve_printk_deferred(VE_LOG, ...) from hardirq, softirq or
scheduler-related context, the WARN fires inside
ve_vprintk_deferred() and prints through the non-deferred printk with
a backtrace. Can this deadlock or recurse into printk, or panic under
panic_on_warn?

The WARN added in ve_log_state() has the same concern on the
ve_vprintk_emit() path used here.

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002-ve-printk-irq-v1-0-cb55896d455c@virtuozzo.com?part=2

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 3/6] ve/printk: Specify ve in printks
  2026-10-02 21:26 15% ` [Devel] [PATCH hci-8.1 3/6] ve/printk: Specify ve in printks Vladimir Riabchun
@ 2026-10-02 21:36  4%   ` no-reply
  2026-10-03 13:49  0%     ` Vladimir Riabchun
  0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-02 21:36 UTC (permalink / raw)
  To: Konstantin Khorenko, Vladimir Riabchun, vasileios.almpanis; +Cc: devel

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] printk: host-log decision in ve_vprintk() uses get_exec_env() instead of the explicit ve
- [Low] printk: ve_printk_ratelimited() passes get_exec_env() as explicit ve, bypassing in_task() guard
- [Low] printk: !CONFIG_PRINTK stub ve_printk_deferred() keeps stale __printf(2, 3) attribute
--

commit 59948e2d94e2f343f89f9252383a061f24bb4a33
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

ve/printk: Specify ve in printks

This commit adds ve_printk helpers that take an explicit VE, so callers
where get_exec_env() is unreliable can name the VE to log to. The
ve_ratelimit() and ve_net_ratelimit() helpers now require a VE argument,
and the netfilter callers pass net->owner_ve.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839

> diff --git a/include/linux/printk.h b/include/linux/printk.h
> --- a/include/linux/printk.h
> +++ b/include/linux/printk.h

[ ... ]

>  static inline __printf(3, 4) __cold
> +int __ve_printk(int dst, struct ve_struct *ve, const char *s, ...)
> +{
> +	return 0;
> +}
>  static inline __printf(2, 3) __cold
> -int ve_printk_deferred(int dst, const char *s, ...)
> +int ve_printk_deferred(int dst, struct ve_struct *ve, const char *s, ...)
>  {
>  	return 0;
>  }

[Severity: Low]
The stub ve_printk_deferred() gained the struct ve_struct *ve parameter,
so the format string is now argument 3. It still carries
__printf(2, 3), while the real prototype was updated to __printf(3, 4).

With the old attribute, GCC treats the ve pointer as the format string.
Could this produce a -Wformat warning (an error under -Werror) at call
sites such as __ve_ratelimit() in lib/ratelimit.c when building with
CONFIG_PRINTK=n?

Should the stub use __printf(3, 4) to match?

[ ... ]

> @@ -747,7 +761,8 @@ struct pi_entry {
>  				      DEFAULT_RATELIMIT_BURST);		\
>  									\
> -	if (ve_ratelimit(dst, &_rs))					\
> +	WARN_ON_ONCE((dst & VE_LOG) && !in_task());			\
> +	if (ve_ratelimit(dst, get_exec_env(), &_rs))			\
>  		ve_printk(dst, fmt, ##__VA_ARGS__);			\
>  })

[Severity: Low]
Here get_exec_env() is passed as an explicit VE. When
ve_printk_ratelimited(VE_LOG, ...) runs from softirq, the interval-reset
path in __ve_ratelimit() calls:

__ve_ratelimit()
  ve_printk_deferred(dst, ve, KERN_WARNING "%s: %d callbacks suppressed\n", ...)

With a non-NULL ve, ve_vprintk_deferred() computes ve_provided as true,
so this check is skipped:

	if (!ve_provided && WARN_ON_ONCE(!in_task()))
		goto done;

Can the "callbacks suppressed" message then be written to the log of
whichever VE belongs to the interrupted task? The in_task() guard no
longer protects this path.

Would it be better to keep passing NULL here, or to use a variant that
keeps the guard?

> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c

[ ... ]

> -static asmlinkage int ve_vprintk(int dst, const char *fmt, va_list args)
> +static asmlinkage int ve_vprintk(int dst, struct ve_struct *ve,
> +				const char *fmt, va_list args)
>  {
>  	va_list args2;
> -	int r = 0;
> +	int r = 0, ve_provided = (ve != NULL);
>  
>  	WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
>  
> +	if (!ve)
> +		ve = get_exec_env();
> +
>  	va_copy(args2, args);
>  	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>  		r = vprintk(fmt, args);

[Severity: Medium]
The host-log decision here still uses ve_is_super(get_exec_env()), while
the per-VE branch below uses ve_is_super(ve). When an explicit ve is
passed, can the two destinations disagree?

Take net_velog_ratelimited(net->owner_ve, ...) called from a netfilter
softirq path such as xt_TCPMSS or nf_conntrack allocation:

- If a container-owned VE_LOG-only message is emitted while the softirq
  interrupted a VE0 task, ve_is_super(get_exec_env()) is true. The
  message is then also printed to the host log.

- If a VE0-owned message is emitted while the softirq interrupted a
  container task, ve_is_super(get_exec_env()) is false. If dst has no
  VE0_LOG, the host branch is skipped. The per-VE branch is skipped too,
  since ve_is_super(ve) is true. The message is lost.

Should this test use ve_is_super(ve) instead of get_exec_env()?

[ ... ]

> +static asmlinkage int ve_vprintk_deferred(int dst, struct ve_struct *ve, 
> +					const char *fmt, va_list args)
>  {
>  	va_list args2;
> -	int r = 0;
> +	int r = 0, ve_provided = (ve != NULL);
>  
>  	WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
>  
> +	if (!ve)
> +		ve = get_exec_env();
> +
>  	va_copy(args2, args);
>  	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>  		r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);

[Severity: Medium]
The deferred variant has the same issue. __ve_ratelimit() calls
ve_printk_deferred() from arbitrary contexts with an explicit ve, but the
host-log condition still looks at get_exec_env().

Can the "callbacks suppressed" message go to the host log, or be dropped,
based on the interrupted task's VE rather than the ve passed in?

Should this use ve_is_super(ve) as well?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002-ve-printk-irq-v1-0-cb55896d455c@virtuozzo.com?part=3

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization
  2026-10-02 21:26  3% ` [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
@ 2026-10-02 21:37  3%   ` no-reply
  2026-10-05  8:45  0%     ` Vladimir Riabchun
  0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-02 21:37 UTC (permalink / raw)
  To: Konstantin Khorenko, Vladimir Riabchun, vasileios.almpanis; +Cc: devel

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] printk: ve_log_printk() path stamps records with zero or stale time_offset
- [Medium] printk: unsynchronized log_state->time_offset used to pass author offsets
- [Medium] printk: time_namespace get/put in update_log_time() on atomic printk paths
- [Low] printk: 32-byte timens_offsets embedded in every printk_info grows ringbuffers and vmcoreinfo
--

commit eea8f8e8aad7cad7bcb35ee878f7d51794c27e83
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

ve/printk: Fix time virtualization

This patch stores timens offsets in each printk_info record, so the
timestamp is shifted using the offsets of the context that emitted the
message. The offsets are copied into log_state on each log_state lookup
and then into the record in vprintk_store_log().

Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839

> diff --git a/kernel/printk/internal.h b/kernel/printk/internal.h
> --- a/kernel/printk/internal.h
> +++ b/kernel/printk/internal.h

[ ... ]

> @@ -354,11 +355,16 @@ struct log_state {
>  	 */
>  	struct latched_seq clear_seq;
>  	struct printk_ringbuffer *prb;
> +	/* Updated on every log_state request */
> +	struct timens_offsets time_offset;
>  };

[Severity: Medium]
Can passing the author offsets through shared log_state storage produce
torn or stale values?

update_log_time() does a plain multi-word struct copy into
GET_VE_LOG(ve)->time_offset on every ve_log_state() or __ve_log_state()
call. That includes read-only callers such as devkmsg_poll(),
devkmsg_read() and do_syslog(), and they can run concurrently on other
CPUs or in IRQ context.

vprintk_store_log() later reads the field with another plain copy, after
vsnprintf and prb_reserve. Nothing orders the two accesses, since there
is no lock, no seqcount and no READ_ONCE()/WRITE_ONCE().

The offsets are normally the same for a given VE. They can differ across a
VE stop and start, which the commit message says can produce a new time
namespace. In that case a mixed or stale value could be stored in the
ring buffer, and KCSAN would likely flag the race.

Would it be possible to compute the offsets locally in the writer and
pass them down to vprintk_emit_log() by value or by pointer?

> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c
> @@ -556,13 +556,28 @@ DEFINE_STRUCT_MEMBER_ALIAS(clear_seq, init_log_state, clear_seq);
>  DEFINE_STRUCT_MEMBER_ALIAS(prb, init_log_state, prb);
>  #undef DEFINE_STRUCT_MEMBER_ALIAS
>  
> +static void update_log_time(struct ve_struct *ve)
> +{
> +	struct time_namespace *timens = ve_get_time_ns(ve);
> +
> +	if (timens)
> +		GET_VE_LOG(ve)->time_offset = timens->offsets;
> +	else
> +		GET_VE_LOG(ve)->time_offset = init_time_ns.offsets;
> +
> +	if (timens)
> +		put_time_ns(timens);
> +}

[Severity: Medium]
Can the get/put pair here run from atomic context?

__ve_vprintk_emit() calls __ve_log_state(ve), which has no in_task()
guard, so it can be reached from IRQ or other atomic context:

__ve_vprintk_emit()
  __ve_log_state()
    update_log_time()
      ve_get_time_ns()
      put_time_ns()

If VE teardown clears ve_nsproxy and drops its reference between the get
and the put, the put_time_ns() here would be the last one. free_time_ns()
would then run from atomic or scheduler-locked printk context. I could not
confirm that this race is reachable, so this part is speculative.

Is the reference needed at all? An rcu_read_lock() section that copies the
offsets looks like it would be enough. The old code only took the reference
when formatting output, whereas now every log_state lookup pays for the
refcount inc/dec and the struct copy, including the read-only syslog and
devkmsg paths.

>  struct log_state *ve_log_state(void)
>  {
>  	struct log_state *log = &init_log_state;
>  #ifdef CONFIG_VE
>  	/* get_exec_env() is reliable only in task context */
> -	if (!WARN_ON_ONCE(!in_task()) && get_exec_env()->log_state)
> +	if (!WARN_ON_ONCE(!in_task()) && get_exec_env()->log_state) {
> +		update_log_time(get_exec_env());
>  		log = get_exec_env()->log_state;
> +	}
>  #endif
>  	return log;
>  }

[ ... ]

> @@ -1108,6 +1114,7 @@ void log_buf_vmcoreinfo_setup(void)
>  	VMCOREINFO_OFFSET(printk_info, text_len);
>  	VMCOREINFO_OFFSET(printk_info, caller_id);
>  	VMCOREINFO_OFFSET(printk_info, dev_info);
> +	VMCOREINFO_OFFSET(printk_info, time_offset);

[Severity: Low]
Is the size of the new field intended? struct printk_info now embeds a
32-byte struct timens_offsets (two timespec64) in every descriptor, but
only the boottime offset is used.

That grows the static host __log_buf infos array and every per-VE
ringbuffer infos array. SIZE(printk_info) also changes and a new
OFFSET(printk_info.time_offset) is exported, so dump tools such as crash
and makedumpfile would need updating.

Would a per-log value, or a 16-byte boottime delta, be enough? The commit
message does not mention the size or vmcoreinfo change.

[ ... ]

> @@ -2402,6 +2411,8 @@ static int vprintk_store_log(struct log_state *log, int facility, int level,
>  	if (dev_info)
>  		memcpy(&r.info->dev_info, dev_info, sizeof(r.info->dev_info));
>  
> +	r.info->time_offset = log->time_offset;
> +

[Severity: Medium]
Can this store a zero or stale time_offset for records coming from
ve_log_printk()?

ve_log_printk() calls ve_log_vprintk(), which for a non-super VE calls
vprintk_emit_log() directly:

r = vprintk_emit_log(ve->log_state, 0, LOGLEVEL_DEFAULT, NULL,
		     fmt, args);

That path never goes through ve_log_state() or __ve_log_state(), which
are the only callers of update_log_time(). vprintk_store_log() then
copies whatever is in log->time_offset.

The field is zero from the kzalloc in ve_log_init() unless some other
lookup has updated it. The record would then be stored with an unshifted
host uptime, which is the leak this patch is meant to prevent.

Should the offsets be refreshed on this path as well?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002-ve-printk-irq-v1-0-cb55896d455c@virtuozzo.com?part=4

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve
  2026-10-02 21:26  8% ` [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
@ 2026-10-02 21:38  5%   ` no-reply
  2026-10-05  9:56  0%     ` Vladimir Riabchun
  0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-02 21:38 UTC (permalink / raw)
  To: Vladimir Riabchun, Konstantin Khorenko, vasileios.almpanis; +Cc: devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] printk: !CONFIG_PRINTK __ve_printk_ratelimited() stub lacks the ve parameter
--

commit 446bb8b8f19e9083361fc5d794bba0859020d3a4
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

ve/failcount: Use ve_printk with specified ve

This patch adds __ve_printk_ratelimited() and __ve_pr_warn_ratelimited()
helpers that take an explicit ve argument. ve_failcount_inc() now uses them
so the message goes to the right VE when called from IRQ context.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839

> diff --git a/include/linux/printk.h b/include/linux/printk.h
> --- a/include/linux/printk.h
> +++ b/include/linux/printk.h

[ ... ]

>  #else
>  #define printk_ratelimited(fmt, ...)					\
>  	no_printk(fmt, ##__VA_ARGS__)
>  #define ve_printk_ratelimited(dst, fmt, ...)				\
>  	no_printk(fmt, ##__VA_ARGS__)
> +#define __ve_printk_ratelimited(dst, fmt, ...)				\
> +	no_printk(fmt, ##__VA_ARGS__)
>  #endif

[Severity: Low]
Is the parameter list of this !CONFIG_PRINTK stub correct? The
CONFIG_PRINTK version of __ve_printk_ratelimited() takes
(dst, ve, fmt, ...), but this stub takes only (dst, fmt, ...).

[ ... ]

> +#define __ve_pr_warn_ratelimited(dst, ve, fmt, ...)				\
> +	__ve_printk_ratelimited(dst, ve, KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)

This macro is defined outside the #ifdef and always passes ve as the second
argument. With CONFIG_VE=y and CONFIG_PRINTK=n, it expands into the
mismatched stub, so ve is bound to fmt.

> diff --git a/include/linux/ve.h b/include/linux/ve.h
> --- a/include/linux/ve.h
> +++ b/include/linux/ve.h

[ ... ]

> +	__ve_pr_warn_ratelimited(VE_LOG_BOTH, __ve, "CT%s: hits the " #name	\
> +		" limit, see the '" #name "' counter in ve.failcount "		\
> +		"of the container's ve cgroup\n", ve_name(__ve));		\

With CONFIG_PRINTK=n, ve_failcount_inc() expands like this:

ve_failcount_inc()
  __ve_pr_warn_ratelimited(VE_LOG_BOTH, __ve, "CT%s: ...", ve_name(__ve))
    __ve_printk_ratelimited(dst, ve, KERN_WARNING pr_fmt(fmt), ...)
      no_printk(<struct ve_struct *>, <real format>, ve_name(__ve))

no_printk() then receives a struct ve_struct pointer as its const char *
format argument. The real format string and ve_name(__ve) end up in the
varargs.

Can this produce an incompatible-pointer diagnostic, which is an error with
-Werror or newer compilers? The format string also stops being checked.

Should the stub be changed to take (dst, ve, fmt, ...)? As written, this
can break the build when CONFIG_VE=y and CONFIG_PRINTK=n.

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002-ve-printk-irq-v1-0-cb55896d455c@virtuozzo.com?part=5

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 5%]

* Re: [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter
  2026-10-02 21:26  4% ` [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
@ 2026-10-02 21:40  3%   ` no-reply
  2026-10-05 10:02  0%     ` Vladimir Riabchun
  0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-02 21:40 UTC (permalink / raw)
  To: vasileios.almpanis, Konstantin Khorenko, Vladimir Riabchun; +Cc: devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] net: neigh_alloc() drops table-specific overflow log; ARP and ND share one failcount
--

commit 83435d15f32b1a7fcb7f3aac09fb30774f63d147
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

ve: add neigh_tbl failcounter

This commit adds a per-VE neigh_tbl_failcount and exposes it in
ve.failcount. In neigh_alloc(), the "neighbor table overflow!" message
is replaced by a call to ve_failcount_inc() when the table limit is hit.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-135520
Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839

> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
> --- a/kernel/ve/ve.c
> +++ b/kernel/ve/ve.c

[ ... ]

> @@ -834,6 +835,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
>  
>  	atomic_set(&ve->arp_neigh_nr, 0);
>  	atomic_set(&ve->nd_neigh_nr, 0);
> +	atomic_set(&ve->neigh_tbl_failcount, 0);

[Severity: Medium]
ARP and ND have separate per-VE counters and limits (arp_neigh_nr and
nd_neigh_nr, selected by get_perve_tbl_entries_counter()), but both now
feed the single neigh_tbl_failcount.

Can the ve.failcount output tell an operator whether the ARP table or the
ND table hit its limit?

[ ... ]

> diff --git a/net/core/neighbour.c b/net/core/neighbour.c
> --- a/net/core/neighbour.c
> +++ b/net/core/neighbour.c

[ ... ]

> @@ -520,8 +520,7 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
>  	    (glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
>  	     time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
>  		if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
> -			net_info_ratelimited("%s: neighbor table overflow!\n",
> -					     tbl->id);
> +			ve_failcount_inc(ve, neigh_tbl);
>  			NEIGH_CACHE_STAT_INC(tbl, table_fulls);
>  			goto out_entries;
>  		}

[Severity: Medium]
The removed net_info_ratelimited() message named the table through
tbl->id. The replacement message from ve_failcount_inc() is fixed text
and has no table or family information:

"CT<name>: hits the neigh_tbl limit, see the 'neigh_tbl' counter in
ve.failcount of the container's ve cgroup"

Is it intended to drop the arp_tbl versus nd_tbl distinction and the
host-level overflow message? The commit message does not mention that the
original message is being removed.

Also, this path runs for ve0 as well. Would the host then log "CT0: hits
the neigh_tbl limit ... of the container's ve cgroup", which seems
misleading wording for the host?

Finally, the ratelimit state in ve_failcount_inc() comes from
__ve_pr_warn_ratelimited(), which is a single static state at the macro
call site. Is it a concern that all VEs share it, so one container that
overflows its table often could suppress the message for other containers?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002-ve-printk-irq-v1-0-cb55896d455c@virtuozzo.com?part=6

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 1/6] ve/printk: Add sanity checks in ve_printk functions
  2026-10-02 21:31  4%   ` no-reply
@ 2026-10-03 12:26  0%     ` Vladimir Riabchun
  0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-03 12:26 UTC (permalink / raw)
  To: vasileios.almpanis, Konstantin Khorenko; +Cc: devel



On 10/2/26 23:31, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Low] printk: WARN_ON_ONCE() in ve_vprintk_deferred() uses non-deferred printk path
> --
> 
> commit 2b22fc2da5243bd2b5c695100f66ab107b28c158
> Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> 
> ve/printk: Add sanity checks in ve_printk functions
> 
> This patch adds a sanity check on the dst argument in the ve_printk
> helpers. It also reorders the VE_LOG test so the dst bit is checked
> before ve_is_super().
> 
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
> 
>> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
>> --- a/kernel/printk/printk.c
>> +++ b/kernel/printk/printk.c
> 
> [ ... ]
> 
>> @@ -2567,10 +2569,12 @@ static asmlinkage int ve_vprintk_deferred(int dst, const char *fmt, va_list args
>>   	va_list args2;
>>   	int r = 0;
>>   
>> +	WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
>> +
>>   	va_copy(args2, args);
>>   	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>>   		r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
> 
> [Severity: Low]
> This isn't a bug in the normal case, but is the new WARN_ON_ONCE() safe
> in ve_vprintk_deferred()?
> 
> This function exists for contexts where the regular printk path is
> unsafe, which is why it emits at LOGLEVEL_SCHED through vprintk_emit().
> WARN_ON_ONCE() reports through the normal printk and console path. It
> can also panic if panic_on_warn is set.
> 
> If a caller passes dst == 0 or a dst with bits outside VE_LOG_BOTH from
> such a context, could the warning recurse into scheduler or console
> locks that the deferred variant is meant to avoid?
> 
> I did not find a caller that passes an invalid dst. The trigger would
> need a buggy caller, and the warning fires only once.
> 
> Would a BUILD_BUG_ON() for constant arguments be more suitable here?
> Alternatively, the report could go through a deferred-safe mechanism
> such as printk_deferred().

We can't know passed values when compiling this file.
Also, WARN_ON doesn't print anything itself, instead it executes UD2,
leaving all the job to the exception handler.

And kernel does use WARNs in sched code: check 
force_compatible_cpus_allowed_ptr,
it has both WARN_ON and printk_deferred.

> 

-- 
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH hci-8.1 2/6] ve/printk: Report potentially leaking usage of ve_printk functions
  2026-10-02 21:32  3%   ` no-reply
@ 2026-10-03 12:26  0%     ` Vladimir Riabchun
  0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-03 12:26 UTC (permalink / raw)
  To: vasileios.almpanis, Konstantin Khorenko; +Cc: devel



On 10/2/26 23:32, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
> - [Medium] printk: WARN_ON_ONCE() added to ve_vprintk_deferred() and ve_log_state() on printk-sensitive paths
> - [Low] ve: ve_dev_printk_emit() returns uninitialized r when VE_LOG is rejected in non-task context
> - [Low] printk: in_task() WARN is evaluated before ve_is_super(), so host-context calls warn spuriously
> - [Low] printk: host-log decision still uses get_exec_env() in non-task context
> --
> 
> commit b1f1fbc536b0ab55f70c0064fc9579c59cbe13a9
> Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> 
> ve/printk: Report potentially leaking usage of ve_printk functions
> 
> This patch adds WARN_ON_ONCE(!in_task()) checks to the ve printk helpers
> and to ve_log_state(). The goal is to avoid relying on get_exec_env() in
> non-task context, and either reject VE printing or fall back to the host
> log state.
> 
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
> 
>> diff --git a/drivers/base/core.c b/drivers/base/core.c
>> --- a/drivers/base/core.c
>> +++ b/drivers/base/core.c
> 
> [ ... ]
> 
>> @@ -4952,7 +4952,9 @@ int ve_dev_printk_emit(int dst, int level, const struct device *dev, const char
>>   
>>   	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>>   		r = dev_vprintk_emit(level, dev, fmt, args);
>> -	if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
>> +
>> +	/* Report IRQ usage here - printing to VE requires explicit ve reference */
>> +	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
>>   		r = ve_dev_vprintk_emit(level, dev, fmt, args2);
> 
> [Severity: Low]
> Can ve_dev_printk_emit() return an uninitialized value here?
> 
> The local variable is declared as "int r;" with no initializer. If
> ve_dev_printk_emit(VE_LOG, ...) is called from IRQ, softirq or NMI
> context while the interrupted task is in a non-super container, the
> first branch is not taken because ve_is_super() is false and VE0_LOG
> is not set. The second branch is now skipped because
> WARN_ON_ONCE(!in_task()) is true. The function then does "return r"
> with r never assigned.
> 
> Before this patch every valid dst assigned r in at least one branch.
> The sibling functions ve_vprintk() and ve_vprintk_deferred() use
> "int r = 0". Should r be initialized the same way here?

True, will fix in v2.

> 
> [Severity: Low]
> Is it intended that WARN_ON_ONCE(!in_task()) is evaluated before
> ve_is_super() in this condition?
> 
> With the current ordering, ve_dev_printk_emit(VE_LOG, ...) called from
> IRQ context while the interrupted task is in the host (VE0) triggers
> the warning, even though a host caller cannot leak into a container.
> Because the warning is once per site, an early harmless hit could also
> suppress a later hit that really would leak. The same ordering appears
> in ve_vprintk() and ve_vprintk_deferred() below. Would testing
> !ve_is_super(get_exec_env()) first limit the warning to the
> potentially leaking case?

It doesn't matter that this hit was harmless. We report problematic
places, not problematic situations - interrupted task is a random value.

> 
> [Severity: Low]
> Is the host-log decision in the preceding branch still based on
> get_exec_env() in non-task context?
> 
> 	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
> 		r = dev_vprintk_emit(level, dev, fmt, args);
> 
> If ve_dev_printk_emit(VE_LOG, ...) is called from IRQ and the
> interrupted task is in the host, the message goes to the host log. If
> the interrupted task is in a container, the VE branch is rejected by
> the WARN and the message is dropped. The commit message says printing
> is either rejected or the host log state is returned, but this branch
> still depends on whichever task was interrupted. The same pattern
> exists in ve_vprintk() and ve_vprintk_deferred(). Is that routing
> intended?

We might miss some logs in the described case, and these logs might
go to the host log instead of container's one.
Both cases are fine IMO and commit allows both. This is a sort of
best-effort printing.

> 
>> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
>> --- a/kernel/printk/printk.c
>> +++ b/kernel/printk/printk.c
>> @@ -560,7 +560,8 @@ struct log_state *ve_log_state(void)
>>   {
>>   	struct log_state *log = &init_log_state;
>>   #ifdef CONFIG_VE
>> -	if (get_exec_env()->log_state)
>> +	/* get_exec_env() is reliable only in task context */
>> +	if (!WARN_ON_ONCE(!in_task()) && get_exec_env()->log_state)
>>   		log = get_exec_env()->log_state;
>>   #endif
>>   	return log;
> 
> [Severity: Medium]
> Can this WARN_ON_ONCE() recurse into printk?

It is fine.

> 
> ve_log_state() is reached from ve_vprintk_emit() and then
> vprintk_emit_log(), so the WARN fires in the middle of the printk emit
> path. A WARN prints a message and backtrace through the regular
> non-deferred printk, and it can panic under panic_on_warn.
> 
> [ ... ]
> 
>> @@ -2540,7 +2541,9 @@ static asmlinkage int ve_vprintk(int dst, const char *fmt, va_list args)
>>   	va_copy(args2, args);
>>   	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>>   		r = vprintk(fmt, args);
>> -	if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
>> +
>> +	/* Report IRQ usage here - printing to VE requires explicit ve reference */
>> +	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
>>   		r = ve_vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, fmt, args2);
>>   	va_end(args2);
> 
> [ ... ]
> 
>> @@ -2574,7 +2577,9 @@ static asmlinkage int ve_vprintk_deferred(int dst, const char *fmt, va_list args
>>   	va_copy(args2, args);
>>   	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>>   		r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
>> -	if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
>> +
>> +	/* Report IRQ usage here - printing to VE requires explicit ve reference */
>> +	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
>>   		r = ve_vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args2);
>>   	va_end(args2);
> 
> [Severity: Medium]
> Is a synchronous WARN_ON_ONCE() safe inside ve_vprintk_deferred()?
> 
> This helper exists for contexts where regular printk is unsafe, such as
> scheduler code or code holding rq or timekeeping locks. If a caller
> uses ve_printk_deferred(VE_LOG, ...) from hardirq, softirq or
> scheduler-related context, the WARN fires inside
> ve_vprintk_deferred() and prints through the non-deferred printk with
> a backtrace. Can this deadlock or recurse into printk, or panic under
> panic_on_warn?
> 
> The WARN added in ve_log_state() has the same concern on the
> ve_vprintk_emit() path used here.

It should be fine, WARNs are used in sched code. See reply to the
previous patch.

> 

-- 
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH hci-8.1 3/6] ve/printk: Specify ve in printks
  2026-10-02 21:36  4%   ` no-reply
@ 2026-10-03 13:49  0%     ` Vladimir Riabchun
  0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-03 13:49 UTC (permalink / raw)
  To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel



On 10/2/26 23:36, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
> - [Medium] printk: host-log decision in ve_vprintk() uses get_exec_env() instead of the explicit ve
> - [Low] printk: ve_printk_ratelimited() passes get_exec_env() as explicit ve, bypassing in_task() guard
> - [Low] printk: !CONFIG_PRINTK stub ve_printk_deferred() keeps stale __printf(2, 3) attribute
> --
> 
> commit 59948e2d94e2f343f89f9252383a061f24bb4a33
> Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> 
> ve/printk: Specify ve in printks
> 
> This commit adds ve_printk helpers that take an explicit VE, so callers
> where get_exec_env() is unreliable can name the VE to log to. The
> ve_ratelimit() and ve_net_ratelimit() helpers now require a VE argument,
> and the netfilter callers pass net->owner_ve.
> 
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
> 
>> diff --git a/include/linux/printk.h b/include/linux/printk.h
>> --- a/include/linux/printk.h
>> +++ b/include/linux/printk.h
> 
> [ ... ]
> 
>>   static inline __printf(3, 4) __cold
>> +int __ve_printk(int dst, struct ve_struct *ve, const char *s, ...)
>> +{
>> +	return 0;
>> +}
>>   static inline __printf(2, 3) __cold
>> -int ve_printk_deferred(int dst, const char *s, ...)
>> +int ve_printk_deferred(int dst, struct ve_struct *ve, const char *s, ...)
>>   {
>>   	return 0;
>>   }
> 
> [Severity: Low]
> The stub ve_printk_deferred() gained the struct ve_struct *ve parameter,
> so the format string is now argument 3. It still carries
> __printf(2, 3), while the real prototype was updated to __printf(3, 4).
> 
> With the old attribute, GCC treats the ve pointer as the format string.
> Could this produce a -Wformat warning (an error under -Werror) at call
> sites such as __ve_ratelimit() in lib/ratelimit.c when building with
> CONFIG_PRINTK=n?
> 
> Should the stub use __printf(3, 4) to match?

Yes, correct. Will fix in v2.

> 
> [ ... ]
> 
>> @@ -747,7 +761,8 @@ struct pi_entry {
>>   				      DEFAULT_RATELIMIT_BURST);		\
>>   									\
>> -	if (ve_ratelimit(dst, &_rs))					\
>> +	WARN_ON_ONCE((dst & VE_LOG) && !in_task());			\
>> +	if (ve_ratelimit(dst, get_exec_env(), &_rs))			\
>>   		ve_printk(dst, fmt, ##__VA_ARGS__);			\
>>   })
> 
> [Severity: Low]
> Here get_exec_env() is passed as an explicit VE. When
> ve_printk_ratelimited(VE_LOG, ...) runs from softirq, the interval-reset
> path in __ve_ratelimit() calls:
> 
> __ve_ratelimit()
>    ve_printk_deferred(dst, ve, KERN_WARNING "%s: %d callbacks suppressed\n", ...)
> 
> With a non-NULL ve, ve_vprintk_deferred() computes ve_provided as true,
> so this check is skipped:
> 
> 	if (!ve_provided && WARN_ON_ONCE(!in_task()))
> 		goto done;
> 
> Can the "callbacks suppressed" message then be written to the log of
> whichever VE belongs to the interrupted task? The in_task() guard no
> longer protects this path.

True. Will fix in v2 by passing ve0 value here in IRQ case.

> 
> Would it be better to keep passing NULL here, or to use a variant that
> keeps the guard?
> 
>> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
>> --- a/kernel/printk/printk.c
>> +++ b/kernel/printk/printk.c
> 
> [ ... ]
> 
>> -static asmlinkage int ve_vprintk(int dst, const char *fmt, va_list args)
>> +static asmlinkage int ve_vprintk(int dst, struct ve_struct *ve,
>> +				const char *fmt, va_list args)
>>   {
>>   	va_list args2;
>> -	int r = 0;
>> +	int r = 0, ve_provided = (ve != NULL);
>>   
>>   	WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
>>   
>> +	if (!ve)
>> +		ve = get_exec_env();
>> +
>>   	va_copy(args2, args);
>>   	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>>   		r = vprintk(fmt, args);
> 
> [Severity: Medium]
> The host-log decision here still uses ve_is_super(get_exec_env()), while
> the per-VE branch below uses ve_is_super(ve). When an explicit ve is
> passed, can the two destinations disagree?
> 
> Take net_velog_ratelimited(net->owner_ve, ...) called from a netfilter
> softirq path such as xt_TCPMSS or nf_conntrack allocation:
> 
> - If a container-owned VE_LOG-only message is emitted while the softirq
>    interrupted a VE0 task, ve_is_super(get_exec_env()) is true. The
>    message is then also printed to the host log.
> 
> - If a VE0-owned message is emitted while the softirq interrupted a
>    container task, ve_is_super(get_exec_env()) is false. If dst has no
>    VE0_LOG, the host branch is skipped. The per-VE branch is skipped too,
>    since ve_is_super(ve) is true. The message is lost.
> 
> Should this test use ve_is_super(ve) instead of get_exec_env()?

True, missed it. Will fix in v2. Next one as well.

> 
> [ ... ]
> 
>> +static asmlinkage int ve_vprintk_deferred(int dst, struct ve_struct *ve,
>> +					const char *fmt, va_list args)
>>   {
>>   	va_list args2;
>> -	int r = 0;
>> +	int r = 0, ve_provided = (ve != NULL);
>>   
>>   	WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
>>   
>> +	if (!ve)
>> +		ve = get_exec_env();
>> +
>>   	va_copy(args2, args);
>>   	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>>   		r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
> 
> [Severity: Medium]
> The deferred variant has the same issue. __ve_ratelimit() calls
> ve_printk_deferred() from arbitrary contexts with an explicit ve, but the
> host-log condition still looks at get_exec_env().
> 
> Can the "callbacks suppressed" message go to the host log, or be dropped,
> based on the interrupted task's VE rather than the ve passed in?
> 
> Should this use ve_is_super(ve) as well?
> 

-- 
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization
  2026-10-02 21:37  3%   ` no-reply
@ 2026-10-05  8:45  0%     ` Vladimir Riabchun
  0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-05  8:45 UTC (permalink / raw)
  To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel



On 10/2/26 23:37, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
> - [Medium] printk: ve_log_printk() path stamps records with zero or stale time_offset
> - [Medium] printk: unsynchronized log_state->time_offset used to pass author offsets
> - [Medium] printk: time_namespace get/put in update_log_time() on atomic printk paths
> - [Low] printk: 32-byte timens_offsets embedded in every printk_info grows ringbuffers and vmcoreinfo
> --

All of these are true. Two biggest issues seem to be printk_info
growth and synchronization troubles with log_state->time_offset.
The patch will be reworked in v2.

> 
> commit eea8f8e8aad7cad7bcb35ee878f7d51794c27e83
> Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> 
> ve/printk: Fix time virtualization
> 
> ...
> 

-- 
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* [PATCH hci-8.0 v2 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304
  @ 2026-10-05  9:26  7% ` Denis V. Lunev
  2026-10-05  9:26  3% ` [PATCH hci-8.0 v2 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
  1 sibling, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-10-05  9:26 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

From: Denis V. Lunev <den@openvz.org>

Give TYPE_CHARDEV an empty cpr_claim_fds() and make the file, pty and
socket backends chain to their parent's. Every chardev class then has
the hook, so qmp_chardev_change() calls it without a NULL check, and
state owned by the base class can be re-claimed there as well.

No functional change.

Signed-off-by: Denis V. Lunev <den@openvz.org>
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 chardev/char-file.c    |  3 +++
 chardev/char-pty.c     |  3 +++
 chardev/char-socket.c  |  3 +++
 chardev/char.c         | 13 +++++++------
 include/chardev/char.h |  5 ++++-
 5 files changed, 20 insertions(+), 7 deletions(-)

diff --git a/chardev/char-file.c b/chardev/char-file.c
index a90f4a77e13..89011f52e3e 100644
--- a/chardev/char-file.c
+++ b/chardev/char-file.c
@@ -124,7 +124,10 @@ out_close:
 static void char_file_cpr_claim_fds(Chardev *chr)
 {
     FDChardev *s = FD_CHARDEV(chr);
+    ChardevClass *pc = CHARDEV_CLASS(
+        object_class_get_parent(object_class_by_name(TYPE_CHARDEV_FILE)));
 
+    pc->cpr_claim_fds(chr);
     if (s->ioc_out) {
         cpr_delete_fd(chr->label, 0);
         cpr_save_fd(chr->label, 0, QIO_CHANNEL_FILE(s->ioc_out)->fd);
diff --git a/chardev/char-pty.c b/chardev/char-pty.c
index a0090e4c3d1..8058da92bb4 100644
--- a/chardev/char-pty.c
+++ b/chardev/char-pty.c
@@ -403,7 +403,10 @@ static void char_pty_open(Chardev *chr,
 static void char_pty_cpr_claim_fds(Chardev *chr)
 {
     PtyChardev *s = PTY_CHARDEV(chr);
+    ChardevClass *pc = CHARDEV_CLASS(
+        object_class_get_parent(object_class_by_name(TYPE_CHARDEV_PTY)));
 
+    pc->cpr_claim_fds(chr);
     if (s->ioc) {
         cpr_delete_fd(chr->label, 0);
         cpr_save_fd(chr->label, 0, QIO_CHANNEL_FILE(s->ioc)->fd);
diff --git a/chardev/char-socket.c b/chardev/char-socket.c
index d20aea764f9..c73a3dd0f7a 100644
--- a/chardev/char-socket.c
+++ b/chardev/char-socket.c
@@ -1681,7 +1681,10 @@ char_socket_get_connected(Object *obj, Error **errp)
 static void char_socket_cpr_claim_fds(Chardev *chr)
 {
     SocketChardev *s = SOCKET_CHARDEV(chr);
+    ChardevClass *pc = CHARDEV_CLASS(
+        object_class_get_parent(object_class_by_name(TYPE_CHARDEV_SOCKET)));
 
+    pc->cpr_claim_fds(chr);
     if (s->sioc) {
         cpr_delete_fd(chr->label, CPR_CONNSOCK_ID);
         cpr_save_fd(chr->label, CPR_CONNSOCK_ID, s->sioc->fd);
diff --git a/chardev/char.c b/chardev/char.c
index 87f40d0137e..b64767f574e 100644
--- a/chardev/char.c
+++ b/chardev/char.c
@@ -295,12 +295,17 @@ static int null_chr_write(Chardev *chr, const uint8_t *buf, int len)
     return len;
 }
 
+static void char_cpr_claim_fds(Chardev *chr)
+{
+}
+
 static void char_class_init(ObjectClass *oc, const void *data)
 {
     ChardevClass *cc = CHARDEV_CLASS(oc);
 
     cc->chr_write = null_chr_write;
     cc->chr_be_event = chr_be_event;
+    cc->cpr_claim_fds = char_cpr_claim_fds;
 }
 
 static void char_finalize(Object *obj)
@@ -1164,9 +1169,7 @@ ChardevReturn *qmp_chardev_change(const char *id, ChardevBackend *backend,
                           backend, chr->gcontext, handover_yank_instance, errp);
     if (!chr_new) {
         /* the failed replacement's open() may have displaced our cpr fds */
-        if (cc->cpr_claim_fds) {
-            cc->cpr_claim_fds(chr);
-        }
+        cc->cpr_claim_fds(chr);
         return NULL;
     }
 
@@ -1186,9 +1189,7 @@ ChardevReturn *qmp_chardev_change(const char *id, ChardevBackend *backend,
             qemu_chr_be_event(chr, CHR_EVENT_OPENED);
         }
         /* chr_new's open() may have displaced our cpr fds; re-claim them */
-        if (cc->cpr_claim_fds) {
-            cc->cpr_claim_fds(chr);
-        }
+        cc->cpr_claim_fds(chr);
         object_unref(OBJECT(chr_new));
         return NULL;
     }
diff --git a/include/chardev/char.h b/include/chardev/char.h
index 431d07ad3c6..968766fd5d5 100644
--- a/include/chardev/char.h
+++ b/include/chardev/char.h
@@ -305,7 +305,10 @@ struct ChardevClass {
     /* notify the backend of frontend open state */
     void (*chr_set_fe_open)(Chardev *chr, int fe_open);
 
-    /* (re-)register the backend's CPR fds, e.g. on a failed chardev-change */
+    /*
+     * (re-)register CPR fds, e.g. on a failed chardev-change; an override
+     * must call the parent's hook first
+     */
     void (*cpr_claim_fds)(Chardev *chr);
 
     /* handle various events */
-- 
2.53.0


^ permalink raw reply	[relevance 7%]

* [PATCH hci-8.0 v2 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304
    2026-10-05  9:26  7% ` [PATCH hci-8.0 v2 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304 Denis V. Lunev
@ 2026-10-05  9:26  3% ` Denis V. Lunev
  2026-10-05  9:30  3%   ` no-reply
  1 sibling, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-10-05  9:26 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

From: Denis V. Lunev <den@openvz.org>

A chardev with logfile= opens the log in qemu_char_open() on every
start, including the start of the new QEMU after cpr-exec. The backend
fds are taken from the cpr fd registry there, the log fd is not, so the
new process reopens the path itself.

libvirt normally opens the log through virtlogd and passes a pipe in an
fdset. The qemu-update command line cannot carry that fdset, so it
names the log file path. The exec'd QEMU, running as the qemu user,
then fails to create a log it has no write access to, e.g. a nova
console.log on node-local storage:

  qemu-kvm: -chardev pty,id=charserial0,logfile=.../console.log,
  logappend=on: Could not create '.../console.log': Permission denied

and the VM is destroyed. Where the open succeeds, the new QEMU writes
the file directly, behind virtlogd's back.

Save the log fd in the cpr registry under the chardev label and take it
from there on cpr-exec incoming, as the backends do. The logfile path
is not opened at all then. If the source did not save it, run without
a log rather than reopen the path. A failed chardev-change re-claims
the log through the base class cpr_claim_fds(), since the replacement's
log has displaced the entry.

Signed-off-by: Denis V. Lunev <den@openvz.org>
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 chardev/char.c          | 30 +++++++++++++++++++++++++-----
 include/migration/cpr.h |  1 +
 2 files changed, 26 insertions(+), 5 deletions(-)

diff --git a/chardev/char.c b/chardev/char.c
index b64767f574e..c037d2e7730 100644
--- a/chardev/char.c
+++ b/chardev/char.c
@@ -40,6 +40,7 @@
 #include "qemu/id.h"
 #include "qemu/coroutine.h"
 #include "qemu/yank.h"
+#include "migration/cpr.h"
 
 #include "chardev-internal.h"
 
@@ -261,9 +262,21 @@ static void qemu_char_open(Chardev *chr, ChardevBackend *backend,
         } else {
             flags |= O_TRUNC;
         }
-        chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
-        if (chr->logfd < 0) {
-            return;
+        if (cpr_exec_is_incoming()) {
+            chr->logfd = cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID);
+            if (chr->logfd < 0) {
+                warn_report("chardev '%s': log file was not preserved, "
+                            "continuing without it", chr->label);
+            }
+        } else {
+            chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
+            if (chr->logfd < 0) {
+                return;
+            }
+            if (!cpr_is_incoming()) {
+                cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
+                cpr_save_fd(chr->label, CPR_CHARDEV_LOG_ID, chr->logfd);
+            }
         }
     }
 
@@ -297,6 +310,10 @@ static int null_chr_write(Chardev *chr, const uint8_t *buf, int len)
 
 static void char_cpr_claim_fds(Chardev *chr)
 {
+    if (chr->logfd >= 0) {
+        cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
+        cpr_save_fd(chr->label, CPR_CHARDEV_LOG_ID, chr->logfd);
+    }
 }
 
 static void char_class_init(ObjectClass *oc, const void *data)
@@ -315,11 +332,14 @@ static void char_finalize(Object *obj)
     if (chr->be) {
         chr->be->chr = NULL;
     }
-    g_free(chr->filename);
-    g_free(chr->label);
     if (chr->logfd != -1) {
+        if (cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID) == chr->logfd) {
+            cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
+        }
         close(chr->logfd);
     }
+    g_free(chr->filename);
+    g_free(chr->label);
     qemu_mutex_destroy(&chr->chr_write_lock);
 }
 
diff --git a/include/migration/cpr.h b/include/migration/cpr.h
index d74ff1ea602..8fb79eb0a28 100644
--- a/include/migration/cpr.h
+++ b/include/migration/cpr.h
@@ -15,6 +15,7 @@
 
 #define CPR_LISTENSOCK_ID       0
 #define CPR_CONNSOCK_ID         1
+#define CPR_CHARDEV_LOG_ID      2
 
 #define QEMU_CPR_FILE_MAGIC     0x51435052
 #define QEMU_CPR_FILE_VERSION   0x00000001
-- 
2.53.0


^ permalink raw reply	[relevance 3%]

* Re: [PATCH hci-8.0 v2 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304
  2026-10-05  9:26  3% ` [PATCH hci-8.0 v2 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
@ 2026-10-05  9:30  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-05  9:30 UTC (permalink / raw)
  To: Denis V. Lunev, svt-core; +Cc: andrey.drobyshev, den

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] chardev: log fd left stale in CPR registry on cpr-transfer target
--

commit 9181fad86181ebb4fb1b78687516f5494301eab0
Author: Denis V. Lunev <den@openvz.org>

chardev: preserve the logfile fd across cpr-exec #VSTOR-148304

This patch saves the chardev log fd in the CPR fd registry under the
chardev label. On cpr-exec incoming, qemu_char_open() takes the fd from
the registry instead of reopening the logfile path. char_finalize() and
char_cpr_claim_fds() are updated to keep the registry entry consistent.

> diff --git a/chardev/char.c b/chardev/char.c
> --- a/chardev/char.c
> +++ b/chardev/char.c

[ ... ]

> @@ -261,9 +262,21 @@ static void qemu_char_open(Chardev *chr, ChardevBackend *backend,
>          } else {
>              flags |= O_TRUNC;
>          }
> -        chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
> -        if (chr->logfd < 0) {
> -            return;
> +        if (cpr_exec_is_incoming()) {
> +            chr->logfd = cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID);
> +            if (chr->logfd < 0) {
> +                warn_report("chardev '%s': log file was not preserved, "
> +                            "continuing without it", chr->label);
> +            }
> +        } else {
> +            chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
> +            if (chr->logfd < 0) {
> +                return;
> +            }
> +            if (!cpr_is_incoming()) {
> +                cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
> +                cpr_save_fd(chr->label, CPR_CHARDEV_LOG_ID, chr->logfd);
> +            }
>          }
>      }

[Severity: Low]
On a cpr-transfer target, cpr_is_incoming() is true but
cpr_exec_is_incoming() is false. Does qemu_char_open() then take the
else branch and reopen the path with qemu_create()?

The log fd that the source saved with cpr_save_fd() is part of the
registry that cpr_state_save() serializes for MIG_MODE_CPR_TRANSFER as
well. On the target, that fd is neither used nor replaced, because the
cpr_save_fd() call is skipped by the !cpr_is_incoming() check.

Could this leave an unused log fd open and registered under
(label, CPR_CHARDEV_LOG_ID) until process exit? If the target later does
a CPR save, the stale entry would be serialized too.

Would the Permission denied failure described in the commit message also
still occur in cpr-transfer mode, since the path is reopened there?

[ ... ]

> @@ -315,11 +332,14 @@ static void char_finalize(Object *obj)
>      if (chr->be) {
>          chr->be->chr = NULL;
>      }
> -    g_free(chr->filename);
> -    g_free(chr->label);
>      if (chr->logfd != -1) {
> +        if (cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID) == chr->logfd) {
> +            cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
> +        }
>          close(chr->logfd);
>      }

Related to the above: in char_finalize(), the registry entry is removed
only when cpr_find_fd() equals chr->logfd. On a cpr-transfer target, the
stale entry holds the fd received from the source, which differs from the
freshly opened chr->logfd. Does that entry then never get removed, so its
fd stays open?

I did not find any code that clears it, and the follow-up patch only adds
an iotest.

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261005092635.3564485-1-den@openvz.org?part=2

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve
  2026-10-02 21:38  5%   ` no-reply
@ 2026-10-05  9:56  0%     ` Vladimir Riabchun
  0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-05  9:56 UTC (permalink / raw)
  To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel



On 10/2/26 23:38, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Low] printk: !CONFIG_PRINTK __ve_printk_ratelimited() stub lacks the ve parameter
> --
> 
> commit 446bb8b8f19e9083361fc5d794bba0859020d3a4
> Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> 
> ve/failcount: Use ve_printk with specified ve
> 
> This patch adds __ve_printk_ratelimited() and __ve_pr_warn_ratelimited()
> helpers that take an explicit ve argument. ve_failcount_inc() now uses them
> so the message goes to the right VE when called from IRQ context.
> 
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
> 
>> diff --git a/include/linux/printk.h b/include/linux/printk.h
>> --- a/include/linux/printk.h
>> +++ b/include/linux/printk.h
> 
> [ ... ]
> 
>>   #else
>>   #define printk_ratelimited(fmt, ...)					\
>>   	no_printk(fmt, ##__VA_ARGS__)
>>   #define ve_printk_ratelimited(dst, fmt, ...)				\
>>   	no_printk(fmt, ##__VA_ARGS__)
>> +#define __ve_printk_ratelimited(dst, fmt, ...)				\
>> +	no_printk(fmt, ##__VA_ARGS__)
>>   #endif
> 
> [Severity: Low]
> Is the parameter list of this !CONFIG_PRINTK stub correct? The
> CONFIG_PRINTK version of __ve_printk_ratelimited() takes
> (dst, ve, fmt, ...), but this stub takes only (dst, fmt, ...).

No, it is not. Will fix in v2.

> 
> [ ... ]
> 
>> +#define __ve_pr_warn_ratelimited(dst, ve, fmt, ...)				\
>> +	__ve_printk_ratelimited(dst, ve, KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
> 
> This macro is defined outside the #ifdef and always passes ve as the second
> argument. With CONFIG_VE=y and CONFIG_PRINTK=n, it expands into the
> mismatched stub, so ve is bound to fmt.
> 
>> diff --git a/include/linux/ve.h b/include/linux/ve.h
>> --- a/include/linux/ve.h
>> +++ b/include/linux/ve.h
> 
> [ ... ]
> 
>> +	__ve_pr_warn_ratelimited(VE_LOG_BOTH, __ve, "CT%s: hits the " #name	\
>> +		" limit, see the '" #name "' counter in ve.failcount "		\
>> +		"of the container's ve cgroup\n", ve_name(__ve));		\
> 
> With CONFIG_PRINTK=n, ve_failcount_inc() expands like this:
> 
> ve_failcount_inc()
>    __ve_pr_warn_ratelimited(VE_LOG_BOTH, __ve, "CT%s: ...", ve_name(__ve))
>      __ve_printk_ratelimited(dst, ve, KERN_WARNING pr_fmt(fmt), ...)
>        no_printk(<struct ve_struct *>, <real format>, ve_name(__ve))
> 
> no_printk() then receives a struct ve_struct pointer as its const char *
> format argument. The real format string and ve_name(__ve) end up in the
> varargs.
> 
> Can this produce an incompatible-pointer diagnostic, which is an error with
> -Werror or newer compilers? The format string also stops being checked.
> 
> Should the stub be changed to take (dst, ve, fmt, ...)? As written, this
> can break the build when CONFIG_VE=y and CONFIG_PRINTK=n.
> 

-- 
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter
  2026-10-02 21:40  3%   ` no-reply
@ 2026-10-05 10:02  0%     ` Vladimir Riabchun
  0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-05 10:02 UTC (permalink / raw)
  To: vasileios.almpanis, Konstantin Khorenko; +Cc: devel



On 10/2/26 23:40, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Medium] net: neigh_alloc() drops table-specific overflow log; ARP and ND share one failcount
> --
> 
> commit 83435d15f32b1a7fcb7f3aac09fb30774f63d147
> Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> 
> ve: add neigh_tbl failcounter
> 
> This commit adds a per-VE neigh_tbl_failcount and exposes it in
> ve.failcount. In neigh_alloc(), the "neighbor table overflow!" message
> is replaced by a call to ve_failcount_inc() when the table limit is hit.
> 
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-135520
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
> 
>> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
>> --- a/kernel/ve/ve.c
>> +++ b/kernel/ve/ve.c
> 
> [ ... ]
> 
>> @@ -834,6 +835,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
>>   
>>   	atomic_set(&ve->arp_neigh_nr, 0);
>>   	atomic_set(&ve->nd_neigh_nr, 0);
>> +	atomic_set(&ve->neigh_tbl_failcount, 0);
> 
> [Severity: Medium]
> ARP and ND have separate per-VE counters and limits (arp_neigh_nr and
> nd_neigh_nr, selected by get_perve_tbl_entries_counter()), but both now
> feed the single neigh_tbl_failcount.

This is true, but IMO it won't add much value.
I'll return net_info_ratelimited message with table id and ve name
for debugging purposes. It will go to the host only.

> 
> Can the ve.failcount output tell an operator whether the ARP table or the
> ND table hit its limit?
> 
> [ ... ]
> 
>> diff --git a/net/core/neighbour.c b/net/core/neighbour.c
>> --- a/net/core/neighbour.c
>> +++ b/net/core/neighbour.c
> 
> [ ... ]
> 
>> @@ -520,8 +520,7 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
>>   	    (glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
>>   	     time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
>>   		if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
>> -			net_info_ratelimited("%s: neighbor table overflow!\n",
>> -					     tbl->id);
>> +			ve_failcount_inc(ve, neigh_tbl);
>>   			NEIGH_CACHE_STAT_INC(tbl, table_fulls);
>>   			goto out_entries;
>>   		}
> 
> [Severity: Medium]
> The removed net_info_ratelimited() message named the table through
> tbl->id. The replacement message from ve_failcount_inc() is fixed text
> and has no table or family information:
> 
> "CT<name>: hits the neigh_tbl limit, see the 'neigh_tbl' counter in
> ve.failcount of the container's ve cgroup"
> 
> Is it intended to drop the arp_tbl versus nd_tbl distinction and the
> host-level overflow message? The commit message does not mention that the
> original message is being removed.
> 
> Also, this path runs for ve0 as well. Would the host then log "CT0: hits
> the neigh_tbl limit ... of the container's ve cgroup", which seems
> misleading wording for the host?
> 
> Finally, the ratelimit state in ve_failcount_inc() comes from
> __ve_pr_warn_ratelimited(), which is a single static state at the macro
> call site. Is it a concern that all VEs share it, so one container that
> overflows its table often could suppress the message for other containers?
> 

-- 
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH hci-8.1 v4 1/1] selftests: net: skip the veth GRO checks where they cannot run
  @ 2026-10-05 11:29  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-05 11:29 UTC (permalink / raw)
  To: khorenko, Eva Kurchatova; +Cc: devel

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] selftests: veth.sh udp_encap_in_use() probes only some UDP encap users
- [Low] selftests: veth.sh ss port regex is not anchored to local encap sockets
- [Low] selftests: veth.sh UDP_ENCAP flag is inverted and skips are not reported as skips
--

commit f9959728bd7c821cbe897be9079c649853692e5a
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>

selftests: net: skip the veth GRO checks where they cannot run

This patch adds udp_encap_in_use() to veth.sh. It detects UDP
encapsulation users such as vxlan, geneve, bareudp, fou, l2tp and sockets
on ports 4789 and 6081. A new chk_gro_fwd() wrapper then skips the three
rx-udp-gro-forwarding aggregation checks when any of them is found.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-139651

> diff --git a/tools/testing/selftests/net/veth.sh b/tools/testing/selftests/net/veth.sh
> --- a/tools/testing/selftests/net/veth.sh
> +++ b/tools/testing/selftests/net/veth.sh

[ ... ]

> +udp_encap_in_use() {
> +	local ns cmd t
> +
> +	for ns in "" `ip netns list 2>/dev/null | awk '{print $1}'`; do

[Severity: Medium]
udp_encap_enable() does static_branch_inc(&udp_encap_needed_key), and
udp_gro_receive() tests that global key. The set of callers is much wider
than what this function probes. WireGuard (drivers/net/wireguard/socket.c),
GTP (drivers/net/gtp.c) and the UDP_ENCAP setsockopt path, which covers
ESP-in-UDP and NAT-T sockets, all reach it.

Consider a host with only one of those. None of the vxlan, geneve,
bareudp, fou, l2tp or port 4789/6081 probes match, so udp_encap_in_use()
returns 1. chk_gro_fwd() then calls chk_gro(), and the check still fails
with 10 packets where 1 is expected. Doesn't that leave the failure this
patch is meant to remove?

Only named namespaces from "ip netns list" are visited. Anonymous and
container-runtime namespaces are missed, even though the key is global.
Can the commit message wording "in any namespace" hold with this approach?

Errors from "ip fou show", "ip l2tp show tunnel" and "ss" are discarded
with 2>/dev/null. Doesn't a missing tool or an unloaded module then read
as "no encapsulation"?

The probe also runs only once, before the tests start. Tunnels created
later are not seen.

The commit message says "Any UDP encapsulation socket on the machine".
The comment above chk_gro_fwd() says "Only meaningful where nothing else
has turned udp_encap_needed_key on". Could both be reworded to match the
subset of users that is actually detected?

> +		$cmd ss -uapn 2>/dev/null | \
> +			grep -qE ':(4789|6081)([^0-9]|$)' && return 0

[Severity: Low]
ss -uapn prints both the local and the peer address:port columns. This
regex is not anchored to the local column. Can a plain UDP socket bound to
4789 or 6081, or a client connected to a peer on those ports, match here?
Such sockets do not enable udp_encap_needed_key, so the checks would be
skipped on hosts where they could have run.

Conversely, encap sockets on non-default ports are only caught through the
link type checks.

[ ... ]

> +# Only meaningful where nothing else has turned udp_encap_needed_key on
> +chk_gro_fwd() {
> +	local msg="$1"
> +	local expected=$2
> +
> +	if [ $UDP_ENCAP -eq 0 ]; then
> +		printf "%-60s" "$msg"
> +		echo " skip - UDP encapsulation in use"
> +		return
> +	fi

[Severity: Low]
This isn't a bug, but UDP_ENCAP=0 meaning "encapsulation in use" comes
from storing the exit status of udp_encap_in_use() with UDP_ENCAP=$?. It
is easy to misread.

The skip path only prints text. It does not set the kselftest skip exit
code (4). Can the script then exit 0 when all three gated checks were
skipped?

> +
> +	chk_gro "$msg" $expected
> +}

[ ... ]

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261005112332.1989594-1-eva.kurchatova@virtuozzo.com?part=1

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* [PATCH QEMU HCI-8.0 v6 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
  @ 2026-10-06 10:34 31% ` Andrey Drobyshev
  2026-10-06 10:37  7%   ` no-reply
  2026-10-06 10:34 13% ` [PATCH QEMU HCI-8.0 v6 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
                   ` (10 subsequent siblings)
  11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

The FD passed into usb_host_open() for a wrapped device is only stored in
s->hostfd after libusb_wrap_sys_device() succeeds, and the failure path
doesn't close it.  if wrapping fails, or a later step of the open sequence
fails (e.g. usb_device_attach), the FD is leaked, as usb_host_close() only
runs for fully opened devices, and the realize error path doesn't clean it
up either.

Store the FD in s->hostfd before wrapping and close it on the failure
path.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b74670ae256..83645fbe0ce 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -954,6 +954,11 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
     int rc;
     Error *local_err = NULL;
 
+    /* Make sure failure path frees caller-owned FD */
+    if (!dev) {
+        s->hostfd = hostfd;
+    }
+
     if (s->bh_postld_pending) {
         return -1;
     }
@@ -978,7 +983,6 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
         if (rc != 0) {
             goto fail;
         }
-        s->hostfd  = hostfd;
         dev = libusb_get_device(s->dh);
         bus_num = libusb_get_bus_number(dev);
         addr = libusb_get_device_address(dev);
@@ -1066,6 +1070,10 @@ fail:
         s->dh = NULL;
         s->dev = NULL;
     }
+    if (s->hostfd != -1) {
+        close(s->hostfd);
+        s->hostfd = -1;
+    }
     return -1;
 }
 

-- 
2.47.1


^ permalink raw reply	[relevance 31%]

* [PATCH QEMU HCI-8.0 v6 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800
    2026-10-06 10:34 31% ` [PATCH QEMU HCI-8.0 v6 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 13% ` Andrey Drobyshev
  2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

We're about to add support for CPR migration to usb-host in the upcoming
commits.  Right now CPR is crashing with usb-host devices.  Let's add
migration blocker upfront, for both cpr-exec and cpr-transfer.  The
blocker is added unconditionally, so that we refuse CPR migration instead
of crashing on it.  That is for the sake of bisectability of the upcoming
commits.  It is going to be lifted (conditioned) in a following patch
once CPR support is added.  Also, blocker is added at the end of
usb_host_realize(), so we have to cleanup all the side effects done
earlier in .realize() in case migrate_add_blocker_modes() fails.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 83645fbe0ce..770bdaca4c8 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -46,6 +46,7 @@
 #endif
 
 #include "qapi/error.h"
+#include "migration/blocker.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -118,6 +119,9 @@ struct USBHostDevice {
     bool                             bh_postld_pending;
     Notifier                         exit;
 
+    /* CPR migration */
+    Error                            *cpr_blocker;
+
     /* request queues */
     QTAILQ_HEAD(, USBHostRequest)    requests;
     QTAILQ_HEAD(, USBHostIsoRing)    isorings;
@@ -1256,6 +1260,18 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
         usb_host_auto_check(NULL);
     }
 
+    error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+               DEVICE(s)->id ?: "(anonymous)");
+    if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+                                  MIG_MODE_CPR_TRANSFER,
+                                  MIG_MODE_CPR_EXEC, -1) < 0) {
+        if (s->needs_autoscan) {
+            QTAILQ_REMOVE(&hostdevs, s, next);
+        }
+        usb_host_close(s);
+        return;
+    }
+
     s->exit.notify = usb_host_exit_notifier;
     qemu_add_exit_notifier(&s->exit);
 }
@@ -1279,6 +1295,7 @@ static void usb_host_unrealize(USBDevice *udev)
         QTAILQ_REMOVE(&hostdevs, s, next);
     }
     usb_host_close(s);
+    migrate_del_blocker(&s->cpr_blocker);
 }
 
 static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)

-- 
2.47.1


^ permalink raw reply	[relevance 13%]

* [PATCH QEMU HCI-8.0 v6 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800
    2026-10-06 10:34 31% ` [PATCH QEMU HCI-8.0 v6 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
  2026-10-06 10:34 13% ` [PATCH QEMU HCI-8.0 v6 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 26% ` Andrey Drobyshev
  2026-10-06 10:34 25% ` [PATCH QEMU HCI-8.0 v6 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

In .realize(), save freshly opened host device FD into the CPR registry,
and then reuse it on CPR target.  Also, add deletion of that FD from
registry to usb_host_open() / usb_host_close() cleanup paths.

For this to work, we also need to skip the .post-load() code which
closes, detaches the device, and then rescans the host bus to reopen
matching hostdevs.  For the CPR-case migration we don't want any of that
as the hostdev FD stays preserved, and the guest shouldn't notice the
switchover.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 33 ++++++++++++++++++++++++++++++++-
 1 file changed, 32 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 770bdaca4c8..f601b85b4d9 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -47,6 +47,7 @@
 
 #include "qapi/error.h"
 #include "migration/blocker.h"
+#include "migration/cpr.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -949,6 +950,21 @@ static void usb_host_ep_update(USBHostDevice *s)
     libusb_free_config_descriptor(conf);
 }
 
+static char *usb_host_cpr_fd_name(USBHostDevice *s)
+{
+    /* Prefix the key so it can't clash with another CPR label */
+    return DEVICE(s)->id ? g_strdup_printf("usb-host/%s", DEVICE(s)->id)
+                         : NULL;
+}
+
+static void usb_host_cpr_delete_fd(USBHostDevice *s)
+{
+    g_autofree char *name = usb_host_cpr_fd_name(s);
+    if (name) {
+        cpr_delete_fd(name, 0);
+    }
+}
+
 static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
 {
     USBDevice *udev = USB_DEVICE(s);
@@ -1075,6 +1091,7 @@ fail:
         s->dev = NULL;
     }
     if (s->hostfd != -1) {
+        usb_host_cpr_delete_fd(s);
         close(s->hostfd);
         s->hostfd = -1;
     }
@@ -1136,6 +1153,7 @@ static int usb_host_close(USBHostDevice *s)
     s->dev = NULL;
 
     if (s->hostfd != -1) {
+        usb_host_cpr_delete_fd(s);
         close(s->hostfd);
         s->hostfd = -1;
     }
@@ -1222,9 +1240,12 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
 
 #if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
     if (s->hostdevice) {
+        g_autofree char *name = usb_host_cpr_fd_name(s);
         int fd;
+
         s->needs_autoscan = false;
-        fd = qemu_open(s->hostdevice, O_RDWR, errp);
+        fd = name ? cpr_open_fd(s->hostdevice, O_RDWR, name, 0, errp)
+                  : qemu_open(s->hostdevice, O_RDWR, errp);
         if (fd < 0) {
             return;
         }
@@ -1763,6 +1784,16 @@ static int usb_host_post_load(void *opaque, int version_id)
 {
     USBHostDevice *dev = opaque;
 
+    /*
+     * For CPR migration, device wasn't released/reset, and the guest
+     * is unaware of the switchover.  The detach/rescan performed in
+     * usb_host_post_load_bh() only exists for cross-host migration.
+     * Skip it for CPR.
+     */
+    if (cpr_is_incoming()) {
+        return 0;
+    }
+
     if (!dev->bh_postld) {
         dev->bh_postld = qemu_bh_new_guarded(usb_host_post_load_bh, dev,
                                              &DEVICE(dev)->mem_reentrancy_guard);

-- 
2.47.1


^ permalink raw reply	[relevance 26%]

* [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800
                     ` (3 preceding siblings ...)
  2026-10-06 10:34 25% ` [PATCH QEMU HCI-8.0 v6 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 26% ` Andrey Drobyshev
  2026-10-06 10:48  5%   ` no-reply
  2026-10-06 10:34 23% ` [PATCH QEMU HCI-8.0 v6 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
                   ` (6 subsequent siblings)
  11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

URBs submitted by the old QEMU must not outlive it across a CPR
switchover: the kernel would complete them into buffers of an address
space that is gone after exec, and the new libusb context would reap URB
pointers it never submitted.

Register a vm_change_state handler that, when the VM stops for CPR
(RUN_STATE_FINISH_MIGRATE), cancels and reaps every pending transfer.
It runs before the vCPU/device snapshot, so a completion raised during
the drain lands in the still-live interrupt controller and is migrated
too.  A transfer that already completed in the kernel is delivered
normally; one cancelled after moving some data is delivered as a short
transfer; one that moved nothing is suppressed, and the target replays
it from the start of the TD via usb_xhci_post_load().  A VM already
paused when migration starts never hits that transition, so .pre_save()
drains as a fallback, suppressing every URB for the target to replay.

Isochronous URBs live on their own rings; cancel and reap them too -- the
target reconstructs the stream from the guest's transfer ring.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 220 ++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 193 insertions(+), 27 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 5dc3308770e..d68fe0df03e 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -48,6 +48,7 @@
 #include "qapi/error.h"
 #include "migration/blocker.h"
 #include "migration/cpr.h"
+#include "migration/misc.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -122,6 +123,9 @@ struct USBHostDevice {
 
     /* CPR migration */
     Error                            *cpr_blocker;
+    VMChangeStateEntry               *cpr_vmse;
+    bool                             cpr_drained;
+    bool                             cpr_replay_all;
 
     /* request queues */
     QTAILQ_HEAD(, USBHostRequest)    requests;
@@ -405,7 +409,10 @@ static void usb_host_req_free(USBHostRequest *r)
 #ifdef CONFIG_WIN32
     request_count--;
 #endif
-    QTAILQ_REMOVE(&r->host->requests, r, next);
+    /* Skip if a timed-out reap already unlinked it (avoid a double remove) */
+    if (QTAILQ_IN_USE(r, next)) {
+        QTAILQ_REMOVE(&r->host->requests, r, next);
+    }
     libusb_free_transfer(r->xfer);
     g_free(r->buffer);
     g_free(r);
@@ -423,6 +430,25 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
     return NULL;
 }
 
+/*
+ * Classify a reap during the CPR drain (r->p is still set).  One that moved no
+ * data is suppressed for the target to replay; one that moved some is kept as a
+ * short transfer.  Returns true if suppressed, so the caller frees the request.
+ */
+static bool usb_host_req_cpr_replay(USBHostRequest *r,
+                                    struct libusb_transfer *xfer)
+{
+    if (xfer->status != LIBUSB_TRANSFER_CANCELLED) {
+        return false;
+    }
+    if (xfer->actual_length != 0 && !r->host->cpr_replay_all) {
+        /* deliver as a short transfer */
+        xfer->status = LIBUSB_TRANSFER_COMPLETED;
+        return false;
+    }
+    return true;
+}
+
 static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
 {
     USBHostRequest *r = xfer->user_data;
@@ -432,6 +458,9 @@ static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
     if (r->p == NULL) {
         goto out; /* request was canceled */
     }
+    if (usb_host_req_cpr_replay(r, xfer)) {
+        goto out; /* CPR drain: the target will replay this packet */
+    }
 
     r->p->status = status_map[xfer->status];
     r->p->actual_length = xfer->actual_length;
@@ -482,6 +511,9 @@ static void LIBUSB_CALL usb_host_req_complete_data(struct libusb_transfer *xfer)
     if (r->p == NULL) {
         goto out; /* request was canceled */
     }
+    if (usb_host_req_cpr_replay(r, xfer)) {
+        goto out; /* CPR drain: the target will replay this packet */
+    }
 
     r->p->status = status_map[xfer->status];
     if (r->in && xfer->actual_length) {
@@ -502,22 +534,29 @@ out:
     }
 }
 
-static void usb_host_req_abort(USBHostRequest *r)
+/* Fail r's in-flight packet with an error status and detach it from r. */
+static void usb_host_req_complete_err(USBHostRequest *r, int status)
 {
-    USBHostDevice  *s = r->host;
-    bool inflight = (r->p && r->p->state == USB_PACKET_ASYNC);
+    USBHostDevice *s = r->host;
 
-    if (inflight) {
-        r->p->status = USB_RET_NODEV;
-        trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
-                                    r->p->status, r->p->actual_length);
-        if (r->p->ep->nr == 0) {
-            usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
-        } else {
-            usb_packet_complete(USB_DEVICE(s), r->p);
-        }
-        r->p = NULL;
+    if (!r->p || r->p->state != USB_PACKET_ASYNC) {
+        return;
+    }
+    r->p->status = status;
+    trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
+                                r->p->status, r->p->actual_length);
+    if (r->p->ep->nr == 0) {
+        usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
+    } else {
+        usb_packet_complete(USB_DEVICE(s), r->p);
+    }
+    r->p = NULL;
+}
 
+static void usb_host_req_abort(USBHostRequest *r)
+{
+    if (r->p && r->p->state == USB_PACKET_ASYNC) {
+        usb_host_req_complete_err(r, USB_RET_NODEV);
         libusb_cancel_transfer(r->xfer);
     }
 }
@@ -1098,9 +1137,18 @@ fail:
     return -1;
 }
 
-static void usb_host_reap_xfers(USBHostDevice *s)
+/* libusb still owns cancelled-but-unreaped transfers: unlink, don't free. */
+static void usb_host_drop_requests(USBHostDevice *s)
 {
     USBHostRequest *r, *rtmp;
+
+    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+        QTAILQ_REMOVE(&s->requests, r, next);
+    }
+}
+
+static void usb_host_reap_xfers(USBHostDevice *s)
+{
     int limit = 100;
 
     while (QTAILQ_FIRST(&s->requests) != NULL) {
@@ -1110,15 +1158,10 @@ static void usb_host_reap_xfers(USBHostDevice *s)
         libusb_handle_events_timeout(ctx, &tv);
         if (--limit == 0) {
             /*
-             * Don't wait forever for libusb calling the complete
-             * callback (which will unlink and free the request).
-             *
-             * Leaking memory here, to make sure libusb will not
-             * access memory which we have released already.
+             * Don't wait forever for libusb to call the complete callback
+             * (which unlinks and frees the request).
              */
-            QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
-                QTAILQ_REMOVE(&s->requests, r, next);
-            }
+            usb_host_drop_requests(s);
             return;
         }
     }
@@ -1134,6 +1177,105 @@ static void usb_host_abort_xfers(USBHostDevice *s)
     usb_host_reap_xfers(s);
 }
 
+static bool usb_host_iso_inflight(USBHostDevice *s)
+{
+    USBHostIsoRing *ring;
+
+    QTAILQ_FOREACH(ring, &s->isorings, next) {
+        if (!QTAILQ_EMPTY(&ring->inflight)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/*
+ * Quiesce the device for a CPR switchover.  Cancel and reap all URBs,
+ * so that no URB submitted by this process can outlive it.
+ */
+static int usb_host_cpr_drain_xfers(USBHostDevice *s)
+{
+    USBHostRequest *r, *rtmp;
+    USBHostIsoRing *ring;
+    USBHostIsoXfer *xfer;
+    int limit = 100;
+
+    s->cpr_drained = true;
+
+    /*
+     * Cancel and reap every URB.  Completing a reaped URB can make xHCI queue
+     * the next TD, so re-cancel whatever reappears until the queue stays empty.
+     */
+    while (!QTAILQ_EMPTY(&s->requests)) {
+        struct timeval tv = { .tv_usec = 2500 };
+
+        QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+            libusb_cancel_transfer(r->xfer);
+        }
+        libusb_handle_events_timeout(ctx, &tv);
+        if (--limit == 0) {
+            /*
+             * The device won't release these URBs.  Error the packets out so
+             * the resumed guest retries them, leak the requests (libusb still
+             * owns their transfers) and abort the migration.
+             */
+            QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+                usb_host_req_complete_err(r, USB_RET_IOERROR);
+            }
+            usb_host_drop_requests(s);
+            return -1;
+        }
+    }
+
+    /*
+     * Iso URBs are on the rings, not s->requests.  Cancel them: one in
+     * flight when the guest stops may never complete on its own, and none
+     * may be left on the preserved fd.  The target replays from the ring.
+     */
+    QTAILQ_FOREACH(ring, &s->isorings, next) {
+        QTAILQ_FOREACH(xfer, &ring->inflight, next) {
+            libusb_cancel_transfer(xfer->xfer);
+        }
+    }
+
+    /*
+     * Cap the reap at 2x a full ring:
+     * iso_urb_count URBs x iso_urb_frames packets each
+     */
+    limit = 2 * s->iso_urb_count * s->iso_urb_frames;
+    while (usb_host_iso_inflight(s)) {
+        struct timeval tv = { .tv_usec = 1000 };
+        libusb_handle_events_timeout(ctx, &tv);
+        if (limit-- == 0) {
+            return -1;
+        }
+    }
+    usb_host_iso_free_all(s);
+    return 0;
+}
+
+/*
+ * Quiesce the URB data plane around a CPR switchover: drain when the VM stops
+ * for CPR -- before the device/irqchip snapshot, so completions raised during
+ * the drain are migrated too.
+ */
+static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
+{
+    USBHostDevice *s = opaque;
+    MigMode mode;
+
+    if (running || !s->dh || state != RUN_STATE_FINISH_MIGRATE) {
+        return;
+    }
+    mode = migrate_mode();
+    if (mode != MIG_MODE_CPR_EXEC && mode != MIG_MODE_CPR_TRANSFER) {
+        return;
+    }
+    if (usb_host_cpr_drain_xfers(s) < 0) {
+        migration_file_set_error(-EIO, NULL);
+    }
+}
+
 static int usb_host_close(USBHostDevice *s)
 {
     USBDevice *udev = USB_DEVICE(s);
@@ -1301,6 +1443,8 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
 
     s->exit.notify = usb_host_exit_notifier;
     qemu_add_exit_notifier(&s->exit);
+
+    s->cpr_vmse = qemu_add_vm_change_state_handler(usb_host_cpr_vm_state, s);
 }
 
 static void usb_host_instance_init(Object *obj)
@@ -1323,6 +1467,9 @@ static void usb_host_unrealize(USBDevice *udev)
     }
     usb_host_close(s);
     migrate_del_blocker(&s->cpr_blocker);
+    if (s->cpr_vmse) {
+        qemu_del_vm_change_state_handler(s->cpr_vmse);
+    }
 }
 
 static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1791,10 +1938,8 @@ static int usb_host_post_load(void *opaque, int version_id)
     USBHostDevice *dev = opaque;
 
     /*
-     * For CPR migration, device wasn't released/reset, and the guest
-     * is unaware of the switchover.  The detach/rescan performed in
-     * usb_host_post_load_bh() only exists for cross-host migration.
-     * Skip it for CPR.
+     * CPR keeps the device open and the guest unaware, so skip the detach and
+     * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
      */
     if (cpr_is_incoming()) {
         return 0;
@@ -1809,10 +1954,31 @@ static int usb_host_post_load(void *opaque, int version_id)
     return 0;
 }
 
+static int usb_host_pre_save(void *opaque)
+{
+    USBHostDevice *s = opaque;
+    MigMode mode = migrate_mode();
+
+    /*
+     * The vm-state handler drains on the stop-for-CPR transition, but a VM
+     * already paused never hits it.  Drain here instead; the stopped guest
+     * can't take completions, so suppress every URB and let the target replay.
+     */
+    if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
+        s->dh && !s->cpr_drained) {
+        s->cpr_replay_all = true;
+        if (usb_host_cpr_drain_xfers(s) < 0) {
+            return -1;
+        }
+    }
+    return 0;
+}
+
 static const VMStateDescription vmstate_usb_host = {
     .name = "usb-host",
     .version_id = 1,
     .minimum_version_id = 1,
+    .pre_save = usb_host_pre_save,
     .post_load = usb_host_post_load,
     .fields = (const VMStateField[]) {
         VMSTATE_USB_DEVICE(parent_obj, USBHostDevice),

-- 
2.47.1


^ permalink raw reply	[relevance 26%]

* [PATCH QEMU HCI-8.0 v6 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800
                     ` (2 preceding siblings ...)
  2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 25% ` Andrey Drobyshev
  2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
                   ` (7 subsequent siblings)
  11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

usb_host_abort_xfers() cancels every pending request and then pumps
libusb events until all of them are reaped, with a bounded wait.  Split
that reap-wait loop out into usb_host_reap_xfers().  No functional
change.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index f601b85b4d9..5dc3308770e 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1098,15 +1098,11 @@ fail:
     return -1;
 }
 
-static void usb_host_abort_xfers(USBHostDevice *s)
+static void usb_host_reap_xfers(USBHostDevice *s)
 {
     USBHostRequest *r, *rtmp;
     int limit = 100;
 
-    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
-        usb_host_req_abort(r);
-    }
-
     while (QTAILQ_FIRST(&s->requests) != NULL) {
         struct timeval tv;
         memset(&tv, 0, sizeof(tv));
@@ -1128,6 +1124,16 @@ static void usb_host_abort_xfers(USBHostDevice *s)
     }
 }
 
+static void usb_host_abort_xfers(USBHostDevice *s)
+{
+    USBHostRequest *r, *rtmp;
+
+    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+        usb_host_req_abort(r);
+    }
+    usb_host_reap_xfers(s);
+}
+
 static int usb_host_close(USBHostDevice *s)
 {
     USBDevice *udev = USB_DEVICE(s);

-- 
2.47.1


^ permalink raw reply	[relevance 25%]

* [PATCH QEMU HCI-8.0 v6 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
                     ` (4 preceding siblings ...)
  2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 23% ` Andrey Drobyshev
  2026-10-06 10:46  6%   ` no-reply
  2026-10-06 10:34 22% ` [PATCH QEMU HCI-8.0 v6 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
                   ` (5 subsequent siblings)
  11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

If CPR fails, the source VM resumes with the drained packets still owned
by the host controller as in-flight, and nothing left to complete them.

Remember the packets suppressed during the drain on a list, and re-issue
them on the preserved fd from the vm-state handler's resume branch -- it
runs on any resume but only acts once we have actually drained
(cpr_drained).  Control transfers cannot be reconstructed from the packet
alone, so complete those as errors and let the guest driver retry.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 66 ++++++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 62 insertions(+), 4 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index d68fe0df03e..85509dc2d83 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -124,6 +124,7 @@ struct USBHostDevice {
     /* CPR migration */
     Error                            *cpr_blocker;
     VMChangeStateEntry               *cpr_vmse;
+    GSList                           *cpr_inflight;
     bool                             cpr_drained;
     bool                             cpr_replay_all;
 
@@ -432,8 +433,9 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
 
 /*
  * Classify a reap during the CPR drain (r->p is still set).  One that moved no
- * data is suppressed for the target to replay; one that moved some is kept as a
- * short transfer.  Returns true if suppressed, so the caller frees the request.
+ * data is suppressed and queued on cpr_inflight for the target to replay; one
+ * that moved some is kept as a short transfer.  Returns true if suppressed, so
+ * the caller frees the request.
  */
 static bool usb_host_req_cpr_replay(USBHostRequest *r,
                                     struct libusb_transfer *xfer)
@@ -446,6 +448,9 @@ static bool usb_host_req_cpr_replay(USBHostRequest *r,
         xfer->status = LIBUSB_TRANSFER_COMPLETED;
         return false;
     }
+    if (r->p->state == USB_PACKET_ASYNC) {
+        r->host->cpr_inflight = g_slist_append(r->host->cpr_inflight, r->p);
+    }
     return true;
 }
 
@@ -1254,17 +1259,26 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
     return 0;
 }
 
+static void usb_host_cpr_reissue(USBHostDevice *s);
+
 /*
  * Quiesce the URB data plane around a CPR switchover: drain when the VM stops
  * for CPR -- before the device/irqchip snapshot, so completions raised during
- * the drain are migrated too.
+ * the drain are migrated too -- and re-issue on resume if CPR was aborted.
  */
 static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
 {
     USBHostDevice *s = opaque;
     MigMode mode;
 
-    if (running || !s->dh || state != RUN_STATE_FINISH_MIGRATE) {
+    if (!s->dh) {
+        return;
+    }
+    if (running) {
+        usb_host_cpr_reissue(s);
+        return;
+    }
+    if (state != RUN_STATE_FINISH_MIGRATE) {
         return;
     }
     mode = migrate_mode();
@@ -1356,6 +1370,45 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
     return ret;
 }
 
+static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+
+/*
+ * If CPR is aborted the source resumes with the drained packets still owned
+ * by the controller as in-flight; re-issue them on the preserved fd.  Control
+ * transfers can't be reconstructed from the packet, so error those out.
+ */
+static void usb_host_cpr_reissue(USBHostDevice *s)
+{
+    USBDevice *udev = USB_DEVICE(s);
+    GSList *it;
+    USBPacket *p;
+
+    if (!s->cpr_drained) {
+        return;
+    }
+
+    for (it = s->cpr_inflight; it; it = it->next) {
+        p = it->data;
+        /* Replay from the start of the TD */
+        p->actual_length = 0;
+        if (p->ep->nr == 0) {
+            /* control endpoint: can't be reconstructed, so error it out */
+            p->status = USB_RET_IOERROR;
+            usb_generic_async_ctrl_complete(udev, p);
+        } else {
+            usb_host_handle_data(udev, p);
+            if (p->status != USB_RET_ASYNC) {
+                /* Completed synchronously with error */
+                usb_packet_complete(udev, p);
+            }
+        }
+    }
+    g_slist_free(s->cpr_inflight);
+    s->cpr_inflight = NULL;
+    s->cpr_replay_all = false;
+    s->cpr_drained = false;
+}
+
 static void usb_host_realize(USBDevice *udev, Error **errp)
 {
     USBHostDevice *s = USB_HOST_DEVICE(udev);
@@ -1470,6 +1523,8 @@ static void usb_host_unrealize(USBDevice *udev)
     if (s->cpr_vmse) {
         qemu_del_vm_change_state_handler(s->cpr_vmse);
     }
+    g_slist_free(s->cpr_inflight);
+    s->cpr_inflight = NULL;
 }
 
 static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1477,6 +1532,9 @@ static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
     USBHostDevice *s = USB_HOST_DEVICE(udev);
     USBHostRequest *r;
 
+    /* Packet is being cancelled -> it mustn't be re-issued on failed CPR */
+    s->cpr_inflight = g_slist_remove(s->cpr_inflight, p);
+
     if (p->combined) {
         usb_combined_packet_cancel(udev, p);
         return;

-- 
2.47.1


^ permalink raw reply	[relevance 23%]

* [PATCH QEMU HCI-8.0 v6 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800
                     ` (5 preceding siblings ...)
  2026-10-06 10:34 23% ` [PATCH QEMU HCI-8.0 v6 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 22% ` Andrey Drobyshev
  2026-10-06 10:47  5%   ` no-reply
  2026-10-06 10:34 29% ` [PATCH QEMU HCI-8.0 v6 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
                   ` (4 subsequent siblings)
  11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

usb_host_open() reads the product string descriptor from the device
with a synchronous control transfer.  On the CPR target this runs while
the device is being handed over, and the transfer can block there,
hanging the incoming migration.

The string only fills the cosmetic product_desc, so skip the read when
we're the CPR target and fall back to the synthetic "host:bus.addr"
name.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 85509dc2d83..020a685aa41 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1106,7 +1106,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
     udev->speed = speed_map[libusb_speed];
     usb_host_speed_compat(s);
 
-    if (s->ddesc.iProduct) {
+    if (s->ddesc.iProduct && !cpr_is_incoming()) {
         libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
                                            (unsigned char *)udev->product_desc,
                                            sizeof(udev->product_desc));

-- 
2.47.1


^ permalink raw reply	[relevance 22%]

* [PATCH QEMU HCI-8.0 v6 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800
                     ` (6 preceding siblings ...)
  2026-10-06 10:34 22% ` [PATCH QEMU HCI-8.0 v6 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 29% ` Andrey Drobyshev
  2026-10-06 10:54  6%   ` no-reply
                     ` (3 subsequent siblings)
  11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

Unlike cpr-exec, cpr-transfer keeps the source QEMU alive: it hands the
usbfs FD to the new QEMU over the migration channel and only exits once
the target is up.  While the handoff is in flight both processes hold the
same FD, and only one may reap URB events on it - if both do, one reaps
the other's in-flight completions and wedges an isochronous transfer that
cannot be retried.  So reaping is handed off along with the FD:

  * The target doesn't reap until it takes over: it stops events right
    after opening the FD in .realize() and restarts in .post_load().
  * The source stops reaping in .pre_save() and, since it stays alive,
    restarts reaping from the vm-state handler's resume branch on failure.
  * On success the source's .exit() leaves the device untouched - no
    reset, interface release or host-driver rebind - and just closes its
    own copy of the FD.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 109 +++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 96 insertions(+), 13 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 020a685aa41..492f0442974 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -127,6 +127,7 @@ struct USBHostDevice {
     GSList                           *cpr_inflight;
     bool                             cpr_drained;
     bool                             cpr_replay_all;
+    bool                             cpr_handed_off;
 
     /* request queues */
     QTAILQ_HEAD(, USBHostRequest)    requests;
@@ -1260,6 +1261,23 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
 }
 
 static void usb_host_cpr_reissue(USBHostDevice *s);
+static void usb_host_cpr_stop_events(void);
+
+/*
+ * Drain the device for a CPR switchover and, for cpr-transfer, hand reaping of
+ * the shared fd off to the target.  Returns -1 if the drain couldn't complete.
+ */
+static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
+{
+    if (usb_host_cpr_drain_xfers(s) < 0) {
+        return -1;
+    }
+    if (mode == MIG_MODE_CPR_TRANSFER) {
+        usb_host_cpr_stop_events();
+        s->cpr_handed_off = true;
+    }
+    return 0;
+}
 
 /*
  * Quiesce the URB data plane around a CPR switchover: drain when the VM stops
@@ -1285,7 +1303,7 @@ static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
     if (mode != MIG_MODE_CPR_EXEC && mode != MIG_MODE_CPR_TRANSFER) {
         return;
     }
-    if (usb_host_cpr_drain_xfers(s) < 0) {
+    if (usb_host_cpr_quiesce(s, mode) < 0) {
         migration_file_set_error(-EIO, NULL);
     }
 }
@@ -1300,16 +1318,19 @@ static int usb_host_close(USBHostDevice *s)
 
     trace_usb_host_close(s->bus_num, s->addr);
 
-    usb_host_abort_xfers(s);
-    usb_host_iso_free_all(s);
+    if (!s->cpr_handed_off) {
+        usb_host_abort_xfers(s);
+        usb_host_iso_free_all(s);
 
-    if (udev->attached) {
-        usb_device_detach(udev);
+        if (udev->attached) {
+            usb_device_detach(udev);
+        }
+
+        usb_host_release_interfaces(s);
+        libusb_reset_device(s->dh);
+        usb_host_attach_kernel(s);
     }
 
-    usb_host_release_interfaces(s);
-    libusb_reset_device(s->dh);
-    usb_host_attach_kernel(s);
     libusb_close(s->dh);
     s->dh = NULL;
     s->dev = NULL;
@@ -1344,10 +1365,17 @@ static void usb_host_exit_notifier(struct Notifier *n, void *data)
     USBHostDevice *s = container_of(n, USBHostDevice, exit);
 
     if (s->dh) {
-        usb_host_abort_xfers(s);
-        usb_host_release_interfaces(s);
-        libusb_reset_device(s->dh);
-        usb_host_attach_kernel(s);
+        /*
+         * Handed to new QEMU across cpr-transfer: do not reset, release
+         * interfaces or rebind the host driver underneath it; just close
+         * our handle.
+         */
+        if (!s->cpr_handed_off) {
+            usb_host_abort_xfers(s);
+            usb_host_release_interfaces(s);
+            libusb_reset_device(s->dh);
+            usb_host_attach_kernel(s);
+        }
         libusb_close(s->dh);
     }
 }
@@ -1371,6 +1399,7 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
 }
 
 static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+static void usb_host_cpr_restart_events(void);
 
 /*
  * If CPR is aborted the source resumes with the drained packets still owned
@@ -1387,6 +1416,11 @@ static void usb_host_cpr_reissue(USBHostDevice *s)
         return;
     }
 
+    if (s->cpr_handed_off) {
+        usb_host_cpr_restart_events();
+        s->cpr_handed_off = false;
+    }
+
     for (it = s->cpr_inflight; it; it = it->next) {
         p = it->data;
         /* Replay from the start of the TD */
@@ -1455,6 +1489,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
             error_setg(errp, "failed to open host usb device %s", s->hostdevice);
             return;
         }
+        if (cpr_is_incoming()) {
+            /*
+             * The source still owns the shared fd until it hands off.  Don't
+             * reap URBs on it yet, or we would steal the source's in-flight
+             * completions; .post_load() restarts events once we take over.
+             */
+            usb_host_cpr_stop_events();
+        }
     } else
 #endif
     if (s->match.addr && s->match.bus_num &&
@@ -2000,6 +2042,7 @@ static int usb_host_post_load(void *opaque, int version_id)
      * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
      */
     if (cpr_is_incoming()) {
+        usb_host_cpr_restart_events();
         return 0;
     }
 
@@ -2012,6 +2055,46 @@ static int usb_host_post_load(void *opaque, int version_id)
     return 0;
 }
 
+#ifndef CONFIG_WIN32
+
+/*
+ * cpr-transfer shares the usbfs fd with the new QEMU, so stop reaping URBs on
+ * it here -- only one process may drive the device.  Restart if migration
+ * fails and the source resumes.
+ */
+static void usb_host_cpr_stop_events(void)
+{
+    const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+    libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
+    if (poll) {
+        for (int i = 0; poll[i] != NULL; i++) {
+            usb_host_del_fd(poll[i]->fd, ctx);
+        }
+        free(poll);
+    }
+}
+
+static void usb_host_cpr_restart_events(void)
+{
+    const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+    libusb_set_pollfd_notifiers(ctx, usb_host_add_fd, usb_host_del_fd, ctx);
+    if (poll) {
+        for (int i = 0; poll[i] != NULL; i++) {
+            usb_host_add_fd(poll[i]->fd, poll[i]->events, ctx);
+        }
+        free(poll);
+    }
+}
+
+#else
+
+static void usb_host_cpr_stop_events(void) {}
+static void usb_host_cpr_restart_events(void) {}
+
+#endif
+
 static int usb_host_pre_save(void *opaque)
 {
     USBHostDevice *s = opaque;
@@ -2025,7 +2108,7 @@ static int usb_host_pre_save(void *opaque)
     if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
         s->dh && !s->cpr_drained) {
         s->cpr_replay_all = true;
-        if (usb_host_cpr_drain_xfers(s) < 0) {
+        if (usb_host_cpr_quiesce(s, mode) < 0) {
             return -1;
         }
     }

-- 
2.47.1


^ permalink raw reply	[relevance 29%]

* [PATCH QEMU HCI-8.0 v6 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
                     ` (8 preceding siblings ...)
  @ 2026-10-06 10:34 10% ` Andrey Drobyshev
  2026-10-06 10:57  6%   ` no-reply
    2026-10-06 10:34 20% ` [PATCH QEMU HCI-8.0 v6 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
  11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

On CPR incoming we skip the usb_host_open() rescan, so libusb only sets
up the endpoints from the device's default configuration.  If the guest
had enabled an endpoint by selecting a non-zero altsetting (e.g. an
isochronous streaming endpoint), that endpoint is now missing, and the
guest's transfers to it simply stall.

The altsetting itself is restored by vmload now, so let's call
usb_host_ep_update() on the CPR target to rebuild the endpoints out of
it.

The kernel claims are still there on the preserved FD, but libusb's own
handle knows nothing about them, so a later SET_INTERFACE would fail with
LIBUSB_ERROR_NOT_FOUND.  Let's re-claim the interfaces on the target as
well to bring libusb's view back in sync - on the preserved FD this is a
no-op at the usbfs level anyway.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 56 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 492f0442974..b3c46aa8de0 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -946,7 +946,7 @@ static void usb_host_ep_update(USBHostDevice *s)
         alt = udev->altsetting[intf->bInterfaceNumber];
 
         if (alt != 0) {
-            assert(alt < conf->interface[i].num_altsetting);
+            assert(alt >= 0 && alt < conf->interface[i].num_altsetting);
             intf = &conf->interface[i].altsetting[alt];
         }
 
@@ -2033,6 +2033,53 @@ static void usb_host_post_load_bh(void *opaque)
     usb_host_auto_check(NULL);
 }
 
+/*
+ * Reconfigure the CPR target after vmload: reject a migrated altsetting the
+ * device can't honour, then re-claim the interfaces so libusb's view matches
+ * the preserved fd (which already holds the claims).
+ */
+static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
+{
+    USBDevice *udev = USB_DEVICE(s);
+    struct libusb_config_descriptor *conf;
+    int n, rc;
+
+    rc = libusb_get_active_config_descriptor(s->dev, &conf);
+    if (rc != 0) {
+        usb_host_libusb_error("libusb_get_active_config_descriptor", rc);
+        return -1;
+    }
+
+    for (int i = 0; i < conf->bNumInterfaces; i++) {
+        int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
+        int alt = udev->altsetting[ifnum];
+
+        if (alt < 0 || alt >= conf->interface[i].num_altsetting) {
+            error_report("usb-host: migrated altsetting %d out of range for "
+                         "interface %d", alt, ifnum);
+            libusb_free_config_descriptor(conf);
+            return -1;
+        }
+    }
+
+    n = MIN(conf->bNumInterfaces, USB_MAX_INTERFACES);
+    for (int i = 0; i < n; i++) {
+        rc = libusb_claim_interface(s->dh, i);
+        if (rc != 0) {
+            usb_host_libusb_error("libusb_claim_interface", rc);
+            libusb_free_config_descriptor(conf);
+            return -1;
+        }
+        s->ifs[i].claimed = true;
+    }
+
+    udev->ninterfaces = conf->bNumInterfaces;
+    udev->configuration = conf->bConfigurationValue;
+    libusb_free_config_descriptor(conf);
+
+    return 0;
+}
+
 static int usb_host_post_load(void *opaque, int version_id)
 {
     USBHostDevice *dev = opaque;
@@ -2042,6 +2089,14 @@ static int usb_host_post_load(void *opaque, int version_id)
      * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
      */
     if (cpr_is_incoming()) {
+        /*
+         * The device stayed open, so rebuild libusb's claims and endpoints,
+         * and start reaping now that the source has handed the fd off.
+         */
+        if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
+            return -1;
+        }
+        usb_host_ep_update(dev);
         usb_host_cpr_restart_events();
         return 0;
     }

-- 
2.47.1


^ permalink raw reply	[relevance 10%]

* [PATCH QEMU HCI-8.0 v6 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800
                     ` (10 preceding siblings ...)
  @ 2026-10-06 10:34 20% ` Andrey Drobyshev
  11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

A previous commit added an unconditional blocker for CPR migrations.
That is to refuse CPR gracefully instead of crashing and for the previous
commits to remain bisectable.

Now the restore path is complete: the FD is preserved, in-flight URBs
are drained and replayed, the device is handed off across cpr-transfer,
and the endpoints and isochronous streams are reconstructed on the
target.  So let's lift the CPR blocker for the devices which satisfy the
preconditions.

Still, the remaining preconditions for CPR to work are:

  * We must open host device FD ourselves, so that libusb owns the FD.
    This way we're able to preserve the FD during CPR;
  * Device ID must be provided as a unique stable key for CPR FD registry.

Use them to guard the blocker addition.  Apart from that, prohibit CPR on
WIN32 platform and on hosts with older libusb versions.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 39 +++++++++++++++++++++++++++++++--------
 1 file changed, 31 insertions(+), 8 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b3c46aa8de0..258e1d5c471 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1524,16 +1524,39 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
         usb_host_auto_check(NULL);
     }
 
-    error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+#ifdef CONFIG_WIN32
+    error_setg(&s->cpr_blocker,
+               "usb-host device %s does not support CPR: "
+               "WIN32 platform is not supported",
                DEVICE(s)->id ?: "(anonymous)");
-    if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
-                                  MIG_MODE_CPR_TRANSFER,
-                                  MIG_MODE_CPR_EXEC, -1) < 0) {
-        if (s->needs_autoscan) {
-            QTAILQ_REMOVE(&hostdevs, s, next);
+#elif LIBUSB_API_VERSION < 0x01000107
+    error_setg(&s->cpr_blocker,
+               "usb-host device %s does not support CPR: "
+               "libusb API version 0x%08x is too old",
+               DEVICE(s)->id ?: "(anonymous)", (unsigned) LIBUSB_API_VERSION);
+#else
+    if (!(s->hostdevice && DEVICE(s)->id)) {
+        /*
+         * CPR needs the host fd owned by libusb (the hostdevice= property)
+         * and a device id to key the CPR fd registry.
+         */
+        error_setg(&s->cpr_blocker,
+                   "usb-host device %s does not support CPR: "
+                   "hostdevice= and a device id are required",
+                   DEVICE(s)->id ?: "(anonymous)");
+    }
+#endif
+
+    if (s->cpr_blocker) {
+        if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+                                      MIG_MODE_CPR_TRANSFER,
+                                      MIG_MODE_CPR_EXEC, -1) < 0) {
+            if (s->needs_autoscan) {
+                QTAILQ_REMOVE(&hostdevs, s, next);
+            }
+            usb_host_close(s);
+            return;
         }
-        usb_host_close(s);
-        return;
     }
 
     s->exit.notify = usb_host_exit_notifier;

-- 
2.47.1


^ permalink raw reply	[relevance 20%]

* Re: [PATCH QEMU HCI-8.0 v6 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
  2026-10-06 10:34 31% ` [PATCH QEMU HCI-8.0 v6 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:37  7%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:37 UTC (permalink / raw)
  To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev, den

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] usb-host: usb_host_open() stores hostfd before early checks that skip the new close
--

commit 0fc31623dd0ee74837ec2c01d3f3ced2e30b7f95
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: don't leak hostdev FD on open failure #VSTOR-137800

This patch stores the caller-supplied hostfd in s->hostfd at the top of
usb_host_open() when no libusb_device is given. It also closes s->hostfd
on the fail: path, so the FD is released when libusb_wrap_sys_device() or
a later open step fails.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> index b74670ae25696..83645fbe0ced0 100644
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c
> @@ -954,6 +954,11 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
>      int rc;
>      Error *local_err = NULL;
>  
> +    /* Make sure failure path frees caller-owned FD */
> +    if (!dev) {
> +        s->hostfd = hostfd;
> +    }
> +
>      if (s->bh_postld_pending) {
>          return -1;
>      }
>      if (s->dh != NULL) {
>          goto fail;
>      }

[Severity: Medium]
Can this leak the FD when usb_host_open() returns early?

The store of s->hostfd happens before the bh_postld_pending check:

    if (!dev) {
        s->hostfd = hostfd;
    }

    if (s->bh_postld_pending) {
        return -1;
    }

That early return bypasses the fail: label and its new close(s->hostfd)
block. The caller-owned FD is then neither closed nor released, and
s->hostfd keeps holding it.

usb_host_realize() does not close the FD either. It only calls
error_setg() and returns:

    rc = usb_host_open(s, NULL, fd);
    if (rc < 0) {
        error_setg(errp, "failed to open host usb device %s", s->hostdevice);
        return;
    }

The s->dh != NULL case has a related problem. It does jump to fail:, but
s->hostfd has already been overwritten. The FD that s->hostfd previously
owned is lost, and the new caller's FD is closed instead.

Today the only caller that passes a real FD is usb_host_realize(). There
s->hostfd starts at -1 and s->dh is NULL, so both early exits look
unreachable and the problem is latent. I also checked the end of the
series. The store is still placed before the bh_postld_pending check, so
a later patch does not fix this.

Would it be better to assign s->hostfd only after those two early checks?
Alternatively, the early checks could be routed through the fail: path in
a way that does not clobber an FD that is already owned.

> @@ -978,7 +983,6 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
>          if (rc != 0) {
>              goto fail;
>          }
> -        s->hostfd  = hostfd;
>          dev = libusb_get_device(s->dh);

[ ... ]

> @@ -1066,6 +1070,10 @@ fail:
>          s->dh = NULL;
>          s->dev = NULL;
>      }
> +    if (s->hostfd != -1) {
> +        close(s->hostfd);
> +        s->hostfd = -1;
> +    }
>      return -1;
>  }

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=1

^ permalink raw reply	[relevance 7%]

* Re: [PATCH QEMU HCI-8.0 v6 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
  2026-10-06 10:34 23% ` [PATCH QEMU HCI-8.0 v6 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:46  6%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:46 UTC (permalink / raw)
  To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev, den

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] usb-host: cpr_reissue() completes failed combined IN packets with plain usb_packet_complete()
- [Medium] usb-host: cpr_vm_state() returns on NULL dh before reissue, leaving CPR state stale
- [Medium] usb-host: replay from offset 0 under cpr_replay_all duplicates OUT data and loses IN data
--

commit cf25e5a0b05e40289cd96941160f3044c5ee49c9
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800

This patch records the packets suppressed during the CPR drain on a new
cpr_inflight list. If CPR is aborted, the vm-state handler's resume
branch re-issues them on the preserved fd. Control transfers are
completed with an error so that the guest driver retries them.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> @@ -446,6 +448,9 @@ static bool usb_host_req_cpr_replay(USBHostRequest *r,
>          xfer->status = LIBUSB_TRANSFER_COMPLETED;
>          return false;
>      }
> +    if (r->p->state == USB_PACKET_ASYNC) {
> +        r->host->cpr_inflight = g_slist_append(r->host->cpr_inflight, r->p);
> +    }
>      return true;
>  }

[Severity: Medium]
When cpr_replay_all is set by usb_host_pre_save(), a cancelled URB with
a non-zero actual_length is also suppressed and queued here:

    if (xfer->actual_length != 0 && !r->host->cpr_replay_all) {
        ...
    }

If CPR is then aborted, usb_host_cpr_reissue() restarts that packet from
offset 0:

    /* Replay from the start of the TD */
    p->actual_length = 0;

Can this duplicate data? For an OUT endpoint, the part already sent to
the device would be sent again, since usb_host_handle_data() copies the
whole payload and resubmits it. For an IN endpoint, the bytes already
read from the device would be discarded, so the guest would never see
them.

Also, cpr_inflight is filled in libusb reap order. Is that the same as
guest submission order when several URBs are pipelined on one endpoint?
I could not confirm this from the code.

The commit message does not mention this limitation.

[ ... ]

> @@ -1254,17 +1259,26 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
>  static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
>  {
>      USBHostDevice *s = opaque;
>      MigMode mode;
>  
> -    if (running || !s->dh || state != RUN_STATE_FINISH_MIGRATE) {
> +    if (!s->dh) {
> +        return;
> +    }
> +    if (running) {
> +        usb_host_cpr_reissue(s);
> +        return;
> +    }
> +    if (state != RUN_STATE_FINISH_MIGRATE) {
>          return;
>      }

[Severity: Medium]
Can this early return on a NULL dh leave stale CPR state? Before this
patch the !s->dh check only guarded the drain. It now also skips
usb_host_cpr_reissue() on resume.

Consider this sequence:

usb_host_cpr_drain_xfers()
    suppresses requests and queues their packets on cpr_inflight
usb_host_nodev_bh() or unplug
    usb_host_close()
        s->dh = NULL
resume
    usb_host_cpr_vm_state(running)
        if (!s->dh) return;

Here cpr_drained and cpr_replay_all stay set. A later CPR attempt then
skips the drain, because usb_host_pre_save() tests
"s->dh && !s->cpr_drained".

In the base patch, usb_host_close() calls usb_device_detach(). The HCD
then cancels the packets through usb_host_cancel_packet(), which drops
them from cpr_inflight, so the packets are not stranded in that case.

With the later patch "usb-host: hand off the device across
cpr-transfer", usb_host_close() skips usb_host_abort_xfers() and
usb_device_detach() when cpr_handed_off is set. In cpr-transfer mode, a
device loss between the drain and the resume would then leave the guest
packets ASYNC with no request behind them and nothing to complete them.
cpr_handed_off would also stay true.

Should the resume branch run the reissue and the state reset even when
dh is NULL?

[ ... ]

> @@ -1356,6 +1370,45 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
> +static void usb_host_cpr_reissue(USBHostDevice *s)
> +{
[ ... ]
> +        } else {
> +            usb_host_handle_data(udev, p);
> +            if (p->status != USB_RET_ASYNC) {
> +                /* Completed synchronously with error */
> +                usb_packet_complete(udev, p);
> +            }
> +        }

[Severity: Medium]
Does this completion handle combined packets correctly?
usb_host_handle_data() can return a non-ASYNC status synchronously:

- USB_RET_NODEV when libusb_submit_transfer() fails
- USB_RET_STALL on a halted endpoint
- USB_RET_STALL when streams are unsupported

For a bulk IN endpoint where usb_host_use_combining() is true, the
packet on cpr_inflight is the first packet of a USBCombinedPacket. A
plain usb_packet_complete() then completes only that first packet.

The normal completion path in usb_host_req_complete_data() does this
instead:

    if (usb_host_use_combining(r->p->ep)) {
        usb_combined_input_packet_complete(USB_DEVICE(s), r->p);
    } else {
        usb_packet_complete(USB_DEVICE(s), r->p);
    }

Can the other sub-packets be left in USB_PACKET_ASYNC with p->combined
still set, and combined->first pointing at an already completed packet?
That could wedge the pipelined bulk-IN endpoint.

The existing usb_host_req_complete_err() uses the same plain call, but
this reissue error path is new in this patch. The later patch "usb-host:
make CPR migration blocker conditional" does not change this code.

[ ... ]

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=6

^ permalink raw reply	[relevance 6%]

* Re: [PATCH QEMU HCI-8.0 v6 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800
  2026-10-06 10:34 22% ` [PATCH QEMU HCI-8.0 v6 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:47  5%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:47 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] usb-host: cpr_is_incoming() guard in usb_host_open() skips product string for all opens
- [Low] usb-host: CPR product-string skip is an uncommented workaround for an unexplained hang
--

commit b936df5f0185ffcfa3c56baf845d05beba3ac351
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: skip product-string read on CPR incoming #VSTOR-137800

This patch skips the synchronous libusb product string read in
usb_host_open() when the process is the CPR target. In that case it uses
the synthetic "host:bus.addr" name for product_desc instead.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> index 85509dc2d83e..020a685aa419 100644
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> @@ -1106,7 +1106,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
>      udev->speed = speed_map[libusb_speed];
>      usb_host_speed_compat(s);
>  
> -    if (s->ddesc.iProduct) {
> +    if (s->ddesc.iProduct && !cpr_is_incoming()) {

[Severity: Low]
Is this guard broader than the CPR hostfd hand-over it is meant to
cover?

cpr_is_incoming() returns incoming_mode != MIG_MODE_NONE, which is a
process-wide flag that stays set for the whole incoming migration. It
is not a per-device indicator.

Any usb_host_open() call in that window therefore takes the synthetic
name branch. That includes the autoscan path:

usb_host_auto_check()
    usb_host_open(s, devs[i], 0)

It also includes the explicit bus/addr realize path. Neither of these
is the hostfd hand-over described in the commit message.

Since product_desc is only written in usb_host_open(), would those
devices keep the "host:bus.addr" name after migration completes?

Could the check be narrowed to the device that was actually handed over
by CPR, for example by keying off hostfd?

[Severity: Low]
This isn't a functional problem, but could a comment be added here
explaining why the product string read is skipped on the CPR target?

The commit message says only that the transfer "can block". The cause
is not recorded in the code, and other cpr_is_incoming() checks in this
file carry comments.

It is also unclear whether the other libusb calls that still run in
usb_host_open() before usb_device_attach() can block in the same way:

libusb_wrap_sys_device()
usb_host_detach_kernel()
usb_host_ep_update()
usb_host_speed_compat()

>          libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
>                                             (unsigned char *)udev->product_desc,
>                                             sizeof(udev->product_desc));

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=7

^ permalink raw reply	[relevance 5%]

* Re: [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800
  2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:48  5%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:48 UTC (permalink / raw)
  To: svt-core, Andrey Drobyshev; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Medium] usb-host: xHCI replay of suppressed URBs relies on a dequeue pointer that is already advanced
- [Medium] usb-host: pre_save fallback drain can deliver completions after xHCI state is saved
- [Medium] usb-host: iso drain cancels in-flight URBs once but completions can resubmit new ones
- [Medium] usb-host: a cancelled OUT or control URB that moved data is reported as a short success
- [Medium] usb-host: drain timeout path can leave newly submitted URBs live and untracked
--

commit e9cd7ce570abec8bcd0aa45a09ea274defa372c5
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: drain in-flight URBs across CPR #VSTOR-137800

This patch registers a vm_change_state handler that cancels and reaps all
pending libusb transfers when the VM stops for CPR. It also adds a
.pre_save() fallback for VMs that are already paused, and cancels and
reaps isochronous URBs as well. Cancelled URBs that moved no data are
suppressed so the target can replay them.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> +/*
> + * Classify a reap during the CPR drain (r->p is still set).  One that moved no
> + * data is suppressed for the target to replay; one that moved some is kept as a
> + * short transfer.  Returns true if suppressed, so the caller frees the request.
> + */
> +static bool usb_host_req_cpr_replay(USBHostRequest *r,
> +                                    struct libusb_transfer *xfer)
> +{
> +    if (xfer->status != LIBUSB_TRANSFER_CANCELLED) {
> +        return false;
> +    }

[Severity: Medium]
The commit message says the .pre_save() fallback suppresses "every URB for
the target to replay". Is that accurate?

In usb_host_req_cpr_replay(), any status other than
LIBUSB_TRANSFER_CANCELLED returns false, so a URB that completed in the
kernel during the fallback drain is still delivered:

    usb_host_req_complete_data()
      -> usb_packet_complete()

Can this run after the xHCI and interrupt controller state has already
been saved? usb_host_pre_save() runs as part of device-state save, not
before the snapshot as the vm-state handler does. If so, event ring,
interrupter and endpoint state changed by that completion might be missing
from the migrated image.

> +    if (xfer->actual_length != 0 && !r->host->cpr_replay_all) {
> +        /* deliver as a short transfer */
> +        xfer->status = LIBUSB_TRANSFER_COMPLETED;
> +        return false;
> +    }
> +    return true;
> +}

[Severity: Medium]
Can this corrupt an OUT stream? The status is rewritten to
LIBUSB_TRANSFER_COMPLETED for every direction and transfer type, with no
check of r->in or the control stage.

For a bulk or interrupt OUT, or a control write, that was cancelled after
moving some bytes, the device has consumed a fragment. The guest is then
told the transfer ended short, and the target can neither replay it nor
see an error. A CBW or a data stage could be affected this way.

Should this conversion be limited to IN transfers?

[Severity: Medium]
When usb_host_req_cpr_replay() returns true, the completion is suppressed
and the request is freed. The replay then relies on the xHCI endpoint
context in guest RAM still pointing at the start of this TD.

Is that the case when several TDs are outstanding on one endpoint?
xhci_kick_epctx() advances ring->dequeue via xhci_ring_fetch() for each TD
it fires. xhci_complete() calls xhci_kick_epctx(), which calls
xhci_set_ep_state(EP_RUNNING) at its start:

    xhci_complete()
      -> xhci_kick_epctx()
           -> xhci_set_ep_state()
                ctx[2] = ring->dequeue | ring->ccs;

That writes the advanced dequeue to guest memory while later TDs are
still queued or in flight. usb_xhci_post_load() rebuilds the ring from
that value. Would a suppressed TD2 then be skipped on the target, so that
its completion event is never generated?

[ ... ]

> +/*
> + * Quiesce the device for a CPR switchover.  Cancel and reap all URBs,
> + * so that no URB submitted by this process can outlive it.
> + */
> +static int usb_host_cpr_drain_xfers(USBHostDevice *s)
> +{

[ ... ]

> +        if (--limit == 0) {
> +            /*
> +             * The device won't release these URBs.  Error the packets out so
> +             * the resumed guest retries them, leak the requests (libusb still
> +             * owns their transfers) and abort the migration.
> +             */
> +            QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
> +                usb_host_req_complete_err(r, USB_RET_IOERROR);
> +            }
> +            usb_host_drop_requests(s);
> +            return -1;
> +        }

[Severity: Medium]
On this timeout path, can usb_host_drop_requests() orphan live URBs?

usb_host_req_complete_err() calls usb_packet_complete(), which can process
queued packets on the endpoint:

    usb_host_req_complete_err()
      -> usb_packet_complete()
           -> usb_process_one()
                -> usb_host_handle_data() / control handler
                     -> libusb_submit_transfer()

Those paths append new entries to s->requests. usb_host_drop_requests()
then unlinks every entry without cancelling it, so the new URBs stay live
on the preserved fd and are never reaped.

> +    /*
> +     * Iso URBs are on the rings, not s->requests.  Cancel them: one in
> +     * flight when the guest stops may never complete on its own, and none
> +     * may be left on the preserved fd.  The target replays from the ring.
> +     */
> +    QTAILQ_FOREACH(ring, &s->isorings, next) {
> +        QTAILQ_FOREACH(xfer, &ring->inflight, next) {
> +            libusb_cancel_transfer(xfer->xfer);
> +        }
> +    }
> +
> +    /*
> +     * Cap the reap at 2x a full ring:
> +     * iso_urb_count URBs x iso_urb_frames packets each
> +     */
> +    limit = 2 * s->iso_urb_count * s->iso_urb_frames;
> +    while (usb_host_iso_inflight(s)) {
> +        struct timeval tv = { .tv_usec = 1000 };
> +        libusb_handle_events_timeout(ctx, &tv);
> +        if (limit-- == 0) {
> +            return -1;
> +        }
> +    }

[Severity: Medium]
Are the iso URBs cancelled only once here? Completing a cancelled IN iso
URB runs usb_host_req_complete_iso(), which does:

    QTAILQ_INSERT_TAIL(&xfer->ring->copy, xfer, next);
    usb_wakeup(xfer->ring->ep, 0);

That can reach xhci_wakeup_endpoint(), then a kick, then
usb_host_iso_data_in(), which submits every xfer on ring->unused into
ring->inflight. The loop above never re-cancels those, and the bulk loop
earlier in usb_host_cpr_drain_xfers() has already finished, so it does not
recheck s->requests.

Can this either time out and return -1, which aborts the migration, or
leave freshly submitted iso or bulk URBs live across exec?

[ ... ]

> +static int usb_host_pre_save(void *opaque)
> +{

[ ... ]

> +    if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
> +        s->dh && !s->cpr_drained) {
> +        s->cpr_replay_all = true;
> +        if (usb_host_cpr_drain_xfers(s) < 0) {
> +            return -1;
> +        }
> +    }

[Severity: Medium]
Is it safe to run the drain from usb_host_pre_save()? Packet completions
raised here can modify xHCI state that may already have been serialized,
depending on the order of the xHCI and usb-host vmstate entries in the
save sequence. Could that ordering be confirmed, or the drain be moved
earlier?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=5

^ permalink raw reply	[relevance 5%]

* Re: [PATCH QEMU HCI-8.0 v6 09/12] usb: migrate the interface altsetting #VSTOR-137800
  @ 2026-10-06 10:52  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:52 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] usb: altsetting subsection in generic vmstate_usb_device has no consumer for emulated devices
--

commit 6d71f98dc9c0687ea03b267e596a32a451816b68
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb: migrate the interface altsetting #VSTOR-137800

This commit adds a vmstate subsection to the generic vmstate_usb_device
that migrates the USBDevice altsetting[] array. The subsection is emitted
only in the CPR migration modes and only when some interface is on a
non-zero altsetting.

> diff --git a/hw/usb/bus.c b/hw/usb/bus.c
> --- a/hw/usb/bus.c
> +++ b/hw/usb/bus.c

[ ... ]

> +static bool usb_altsetting_needed(void *opaque)
> +{
> +    USBDevice *dev = opaque;
> +    int i;
> +
> +    /*
> +     * Only the CPR target consumes this, to rebuild its endpoints; other
> +     * migrations reopen the device and reconstruct the altsetting.
> +     */
> +    if (!cpr_incoming_needed(NULL)) {
> +        return false;
> +    }
> +    for (i = 0; i < USB_MAX_INTERFACES; i++) {
> +        if (dev->altsetting[i]) {
> +            return true;
> +        }
> +    }
> +    return false;
> +}
> +
> +static const VMStateDescription vmstate_usb_device_altsetting = {
> +    .name = "USBDevice/altsetting",

[Severity: Low]
Is this gate specific enough to the devices that consume the state?
usb_altsetting_needed() checks only cpr_incoming_needed() and whether
some dev->altsetting[i] is non-zero. It does not check whether the
device is a usb-host device.

The subsection lives in the generic vmstate_usb_device, so an emulated
USB device that supports SET_INTERFACE and sits on a non-zero altsetting
(an audio-style device, for example) will also emit and load
altsetting[] in CPR modes.

The generic usb_device_post_load() only recomputes dev->attached:

    if (dev->state == USB_STATE_NOTATTACHED) {
        dev->attached = false;
    } else {
        dev->attached = true;
    }

Nothing there rebuilds dev->ifaces[] or calls usb_desc_ep_init() from
the loaded altsetting[]. On the CPR target such a device could report
the restored altsetting via GET_INTERFACE while its endpoints still
follow altsetting 0.

No emulated device that triggers this today has been identified, so this
is speculative.

The later patch in the series, "usb-host: reconfigure endpoints on CPR
incoming", adds usb_host_cpr_reclaim_interfaces(), which range-checks the
migrated altsetting. It also adds a usb_host_ep_update() call in
usb_host_post_load() and changes the assert in usb_host_ep_update() to
check alt >= 0. That covers the usb-host consumer and the unvalidated
index concern. The series adds no consumer for non-usb-host devices.

Should the subsection be restricted to usb-host devices, or should the
generic post_load rebuild the interface and endpoint state from
altsetting[]?

> +    .version_id = 1,
> +    .minimum_version_id = 1,
> +    .needed = usb_altsetting_needed,
> +    .fields = (const VMStateField[]) {
> +        VMSTATE_INT32_ARRAY(altsetting, USBDevice, USB_MAX_INTERFACES),
> +        VMSTATE_END_OF_LIST(),
> +    }
> +};

[ ... ]

> +    .subsections = (const VMStateDescription * const []) {
> +        &vmstate_usb_device_altsetting,
> +        NULL
>      }
>  };

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=9

^ permalink raw reply	[relevance 3%]

* Re: [PATCH QEMU HCI-8.0 v6 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800
  2026-10-06 10:34 29% ` [PATCH QEMU HCI-8.0 v6 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:54  6%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:54 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] usb-host: cpr_handed_off is set before the handoff outcome is known and never cleared on close
- [Medium] usb-host: target restarts URB reaping in post_load() before migration is committed
- [Medium] usb-host: per-device CPR handoff stops and restarts events for the whole libusb context
- [Low] usb-host: commit message says .exit() closes the FD, but exit notifier only calls libusb_close()
--

commit d075dcc6f67ba19d35e41dafb7069f178e496bbf
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: hand off the device across cpr-transfer #VSTOR-137800

This patch makes the usb-host device hand off URB reaping together with the
usbfs fd across cpr-transfer. The target stops reaping until post_load. The
source stops reaping when it quiesces, restarts on resume after a failure,
and skips device teardown at exit after a successful handoff.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> +static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
> +{
> +    if (usb_host_cpr_drain_xfers(s) < 0) {
> +        return -1;
> +    }
> +    if (mode == MIG_MODE_CPR_TRANSFER) {
> +        usb_host_cpr_stop_events();
> +        s->cpr_handed_off = true;
> +    }
> +    return 0;
> +}

[Severity: Medium]
Is cpr_handed_off being set too early here? It is set from the vm-state
FINISH_MIGRATE branch or from usb_host_pre_save(), before the outcome of
the migration is known. The flag then means "handed off, awaiting outcome"
rather than "handoff succeeded".

The only place that clears it is usb_host_cpr_reissue(), which runs only
when the VM resumes with cpr_drained set and s->dh non-NULL.

Consider a failed or cancelled cpr-transfer where the VM stays paused, and
the user then quits or does device_del. Both usb_host_exit_notifier() and
usb_host_close() take the skip path:

    if (!s->cpr_handed_off) {
        usb_host_abort_xfers(s);
        ...
        usb_host_attach_kernel(s);
    }

The source is still the only owner of the device in that case. Does this
leave the interfaces claimed, skip libusb_reset_device(), and never rebind
the host driver? In usb_host_close() the guest port also stays attached,
and the iso rings are not freed.

usb_host_close() also sets s->dh = NULL without clearing the flag. After
that, usb_host_cpr_vm_state() returns early on !s->dh, so
usb_host_cpr_reissue() can never clear it. Would a later reopen of the same
USBHostDevice through autoscan then inherit cpr_handed_off == true and
cpr_drained?

Could the flag be cleared in usb_host_close(), or the skip decision be
based on a state that is only set once the migration has completed?

[ ... ]

> @@ -1344,10 +1365,17 @@ static void usb_host_exit_notifier(struct Notifier *n, void *data)
>      USBHostDevice *s = container_of(n, USBHostDevice, exit);
>  
>      if (s->dh) {
> +        /*
> +         * Handed to new QEMU across cpr-transfer: do not reset, release
> +         * interfaces or rebind the host driver underneath it; just close
> +         * our handle.
> +         */
> +        if (!s->cpr_handed_off) {
> +            usb_host_abort_xfers(s);
> +            usb_host_release_interfaces(s);
> +            libusb_reset_device(s->dh);
> +            usb_host_attach_kernel(s);
> +        }
>          libusb_close(s->dh);
>      }
>  }

[Severity: Low]
This isn't a bug, but the commit message says "On success the source's
.exit() leaves the device untouched ... and just closes its own copy of the
FD."

usb_host_exit_notifier() only calls libusb_close(s->dh). That does not
close the application-owned usbfs fd of a libusb_wrap_sys_device() handle.
s->hostfd is closed only in usb_host_close(). Could the message be
corrected?

The message also says "The source stops reaping in .pre_save()". Normally
this happens in the vm-state handler through usb_host_cpr_quiesce().
pre_save only does it as a fallback when the VM was already paused.

[ ... ]

> @@ -2000,6 +2042,7 @@ static int usb_host_post_load(void *opaque, int version_id)
>       * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
>       */
>      if (cpr_is_incoming()) {
> +        usb_host_cpr_restart_events();
>          return 0;
>      }

[Severity: Medium]
Can the target start reaping too early here? The target calls
usb_host_cpr_restart_events() when the device section is loaded, not when
migration has completed.

Suppose another device section fails to load, or the channel breaks, while
the target is still alive. The source then resumes, and usb_host_cpr_reissue()
calls usb_host_cpr_restart_events() on its side:

    if (s->cpr_handed_off) {
        usb_host_cpr_restart_events();
        s->cpr_handed_off = false;
    }

Both processes could then reap URB completions on the shared usbfs fd. That
is the isochronous wedge this patch is meant to prevent. Is anything making
the target's takeover conditional on the source having committed? This
window is narrow and depends on the target surviving a failed migration.

[ ... ]

> +static void usb_host_cpr_stop_events(void)
> +{
> +    const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
> +
> +    libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
> +    if (poll) {
> +        for (int i = 0; poll[i] != NULL; i++) {
> +            usb_host_del_fd(poll[i]->fd, ctx);
> +        }
> +        free(poll);
> +    }
> +}
> +
> +static void usb_host_cpr_restart_events(void)
> +{
> +    const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
> +
> +    libusb_set_pollfd_notifiers(ctx, usb_host_add_fd, usb_host_del_fd, ctx);
> +    if (poll) {
> +        for (int i = 0; poll[i] != NULL; i++) {
> +            usb_host_add_fd(poll[i]->fd, poll[i]->events, ctx);
> +        }
> +        free(poll);
> +    }
> +}

[Severity: Medium]
Do these helpers act on more than the one device being handed off? Both
call libusb_set_pollfd_notifiers(ctx, ...) and walk all of
libusb_get_pollfds(ctx) on the static libusb context, while they are driven
from per-device hooks (usb_host_realize(), usb_host_post_load(),
usb_host_pre_save() and the vm-state handler) and the handoff state
(cpr_handed_off) is per device.

With more than one hostdevice= usb-host device:

On the target, the first device's usb_host_post_load() calls
usb_host_cpr_restart_events() and re-enables reaping for every device,
including devices whose post_load has not yet run and whose source side may
not be handed off.

On the source, the first device's usb_host_cpr_reissue() after a failure
restarts events for devices that are still handed off.

While the notifiers are NULL, fds of devices realized or hot-added in that
window are never registered either.

Is the multi-device case intended to be supported? The conditional blocker
added later in the series ("usb-host: make CPR migration blocker
conditional") still seems to allow several hostdevice= devices with ids.

[ ... ]

> @@ -1455,6 +1489,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
>              error_setg(errp, "failed to open host usb device %s", s->hostdevice);
>              return;
>          }
> +        if (cpr_is_incoming()) {
> +            /*
> +             * The source still owns the shared fd until it hands off.  Don't
> +             * reap URBs on it yet, or we would steal the source's in-flight
> +             * completions; .post_load() restarts events once we take over.
> +             */
> +            usb_host_cpr_stop_events();
> +        }

In usb_host_realize() on the target, does this disable event handling for
the whole libusb context whenever any hostdevice= device is realized? See
the previous comment on usb_host_cpr_stop_events().

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=8

^ permalink raw reply	[relevance 6%]

* Re: [PATCH QEMU HCI-8.0 v6 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
  2026-10-06 10:34 10% ` [PATCH QEMU HCI-8.0 v6 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:57  6%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:57 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev, den

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] usb-host: CPR post_load fails on an unconfigured device (NOT_FOUND treated as fatal)
- [Medium] usb-host: re-claim loop uses array index instead of bInterfaceNumber
- [Medium] usb-host: device-supplied bInterfaceNumber indexes altsetting[16] unchecked; ninterfaces unclamped
- [Low] usb-host: usb_host_ep_update() result ignored on CPR path, so partial rebuild reports success
--

commit 7fc1fbcbe45c4e899a6393d8acc4e2e9151dac29
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800

This commit makes the CPR incoming path in usb_host_post_load() re-claim the
interfaces through libusb and call usb_host_ep_update(). The goal is to
rebuild the endpoints for a non-zero altsetting restored by vmload. It also
adds an "alt >= 0" check to an assert in usb_host_ep_update().

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> +static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
> +{
> +    USBDevice *udev = USB_DEVICE(s);
> +    struct libusb_config_descriptor *conf;
> +    int n, rc;
> +
> +    rc = libusb_get_active_config_descriptor(s->dev, &conf);
> +    if (rc != 0) {
> +        usb_host_libusb_error("libusb_get_active_config_descriptor", rc);
> +        return -1;
> +    }

[Severity: Medium]
Can this abort the whole incoming CPR vmload for a device that has no active
configuration?

For a device in the address state (for example after SET_CONFIGURATION(0) or
a device reset), libusb_get_active_config_descriptor() returns
LIBUSB_ERROR_NOT_FOUND.  Here any rc != 0 returns -1, and
usb_host_post_load() propagates it:

    if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
        return -1;
    }

Before this patch the CPR branch always returned 0.  The existing
usb_host_claim_interfaces() treats the same condition as benign:

    if (rc == LIBUSB_ERROR_NOT_FOUND) {
        /* address state - ignore */
        return USB_RET_SUCCESS;
    }

and usb_host_ep_update() simply returns.  Should NOT_FOUND be tolerated here
as well?

Also, if the host still has an active configuration but the guest never
configured the device, does this function claim every interface and then
overwrite the guest-visible state?

    udev->ninterfaces = conf->bNumInterfaces;
    udev->configuration = conf->bConfigurationValue;

The source had both of these at 0.  The later patch "usb-host: make CPR
migration blocker conditional" only changes the blocker and does not touch
this helper.

> +
> +    for (int i = 0; i < conf->bNumInterfaces; i++) {
> +        int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
> +        int alt = udev->altsetting[ifnum];

[Severity: Medium]
Can this read past the end of udev->altsetting[]?

ifnum comes from the device's bInterfaceNumber (a uint8_t, up to 255), while
altsetting[] has USB_MAX_INTERFACES (16) entries.  This validation loop runs
over all conf->bNumInterfaces entries with no bound on ifnum.

The claim loop below is clamped with MIN(conf->bNumInterfaces,
USB_MAX_INTERFACES), but the function then stores the unclamped value and
returns success:

    udev->ninterfaces = conf->bNumInterfaces;

Can a device with more than 16 interfaces then trip this assert in a later
usb_desc_set_config()?

    assert(dev->ninterfaces <= USB_MAX_INTERFACES);

usb_host_claim_interfaces() would have returned a stall in that case.

usb_host_ep_update(), which is now also called from the CPR path, has the
same unchecked access:

    alt = udev->altsetting[intf->bInterfaceNumber];

This isn't a bug introduced by this patch, but the new call site makes it
reachable on the CPR path.  The patch only adds "alt >= 0" to the assert
there.

> +
> +        if (alt < 0 || alt >= conf->interface[i].num_altsetting) {
> +            error_report("usb-host: migrated altsetting %d out of range for "
> +                         "interface %d", alt, ifnum);
> +            libusb_free_config_descriptor(conf);
> +            return -1;
> +        }
> +    }
> +
> +    n = MIN(conf->bNumInterfaces, USB_MAX_INTERFACES);
> +    for (int i = 0; i < n; i++) {
> +        rc = libusb_claim_interface(s->dh, i);
> +        if (rc != 0) {
> +            usb_host_libusb_error("libusb_claim_interface", rc);
> +            libusb_free_config_descriptor(conf);
> +            return -1;
> +        }
> +        s->ifs[i].claimed = true;
> +    }

[Severity: Medium]
Is the loop index i the right value to pass to libusb_claim_interface()?

i is the position in conf->interface[], but libusb and usbfs expect the
interface number, which is altsetting[0].bInterfaceNumber.  The validation
loop above already uses bInterfaceNumber for this.  The two can differ for
devices whose interface numbers don't start at 0 or aren't contiguous, such
as {0,2} or {1,2}.

Claiming a non-existent number fails, so the function returns -1 and the
migration aborts.  Where the claim does succeed, the real interfaces are not
recorded in libusb's claimed set, so a later SET_INTERFACE can still fail
with LIBUSB_ERROR_NOT_FOUND.  That is the problem this patch sets out to fix.

s->ifs[i].claimed is also indexed by position, so usb_host_release_interfaces()
would act on the wrong numbers.

By contrast, usb_host_claim_interfaces() probes by interface number and
tolerates gaps:

    for (i = 0; i < USB_MAX_INTERFACES; i++) {
        trace_usb_host_claim_interface(s->bus_num, s->addr, configuration, i);
        rc = libusb_claim_interface(s->dh, i);
        if (rc == 0) {
            s->ifs[i].claimed = true;

Should the new code claim by bInterfaceNumber and index ifs[] the same way?
The later patches in the series do not change this.

[ ... ]

>      if (cpr_is_incoming()) {
> +        /*
> +         * The device stayed open, so rebuild libusb's claims and endpoints,
> +         * and start reaping now that the source has handed the fd off.
> +         */
> +        if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
> +            return -1;
> +        }
> +        usb_host_ep_update(dev);
>          usb_host_cpr_restart_events();
>          return 0;
>      }

[Severity: Low]
Should the result of usb_host_ep_update() be checked here?

usb_host_ep_update() is void.  After usb_ep_reset() it returns silently on
"invalid endpoint address", "duplicate endpoint address", or a missing
active configuration, which leaves a partial endpoint table.
usb_host_post_load() then returns 0, so guest transfers to the missing
endpoints would stall without any report.

usb_host_ep_update() also calls usb_ep_set_halted(udev, pid, ep, 0) for every
endpoint, so any halted state the guest had on the source is lost, since it
is not migrated.  This second point may be speculative.  The failure
conditions would normally already have occurred in usb_host_open() on the
same device, so this is probably a rare edge case.

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=10

^ permalink raw reply	[relevance 6%]

* Re: [PATCH QEMU HCI-8.0 v6 11/12] hcd-xhci: resync isochronous endpoints after CPR #VSTOR-137800
  @ 2026-10-06 10:57  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:57 UTC (permalink / raw)
  To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev, den

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] usb-xhci: CPR iso resync fakes a missed-service error and drops the TD without fixing iso state
- [Low] usb-xhci: cpr_iso_resync flag is never cleared on idle, stop or reset, so a later in-sync iso TD is dropped
--

commit ff20d366f72942427c2474f20e5f7aa84692f640
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

hcd-xhci: resync isochronous endpoints after CPR #VSTOR-137800

This patch adds a cpr_iso_resync flag to XHCIEPContext. usb_xhci_post_load() sets it for running isochronous endpoints when CPR is incoming. The next isochronous TD submitted through xhci_submit() is then completed with CC_MISSED_SERVICE_ERROR without being sent to the device. The aim is to make the guest driver resync after resume.

> diff --git a/hw/usb/hcd-xhci.c b/hw/usb/hcd-xhci.c
> --- a/hw/usb/hcd-xhci.c
> +++ b/hw/usb/hcd-xhci.c

[ ... ]

> @@ -1811,6 +1813,19 @@ static int xhci_submit(XHCIState *xhci, XHCITransfer *xfer, XHCIEPContext *epctx
>          xfer->pkts = 1;
>          xfer->iso_xfer = true;
>          xfer->timed_xfer = true;
> +        if (epctx->cpr_iso_resync) {
> +            /*
> +             * First iso TD after a CPR resume: our transfer state was
> +             * rebuilt from guest memory and no longer lines up with what
> +             * the guest consumed.  Report a missed service, like real HW,
> +             * to kick the driver's skip logic into resyncing.
> +             */
> +            epctx->cpr_iso_resync = false;
> +            xfer->status = CC_MISSED_SERVICE_ERROR;
> +            xhci_xfer_report(xfer);
> +            xfer->complete = 1;
> +            return 0;
> +        }

[Severity: Medium]
Can this early return leave the isochronous scheduling state unrebuilt?

The branch returns before xhci_calc_iso_kick() and
xhci_check_intr_iso_kick(), and before xhci_setup_packet() and
usb_handle_packet(). So epctx->mfindex_last is not updated for the
dropped TD and stays at its initial zero value. The TD is also never
passed to the device. An ISO OUT frame is lost, and an ISO IN buffer is
never filled.

Does the resync then depend entirely on the guest driver reacting to
CC_MISSED_SERVICE_ERROR? The commit message only describes the Linux
behaviour. Would other guests handle the synthetic error the same way?

[Severity: Low]
Can the flag stay set for longer than "the first isochronous transfer
after a CPR resume"?

The flag is consumed only here in xhci_submit(), on whichever isochronous
TD is submitted next. Consider xhci_kick_epctx():

    length = xhci_ring_chain_length(xhci, ring);
    if (length <= 0) {
        if (epctx->type == ET_ISO_OUT || epctx->type == ET_ISO_IN) {
            ... CC_RING_OVERRUN / CC_RING_UNDERRUN event
        }
        break;
    }

If the ring is empty at resume time, the loop breaks before xhci_submit().
The same happens if xhci_slot_ok() fails or the endpoint is halted.
The flag then survives the resume kick. The stop, reset and set-dequeue
paths do not clear it either.

Can a later isochronous TD that is already in sync then be dropped with a
spurious CC_MISSED_SERVICE_ERROR?

[ ... ]

> @@ -3510,6 +3525,11 @@ static int usb_xhci_post_load(void *opaque, int version_id)
>              xhci_init_epctx(epctx, pctx, ep_ctx);
>              epctx->state = state;
>              if (state == EP_RUNNING) {
> +                if (cpr_is_incoming() &&
> +                    (epctx->type == ET_ISO_IN || epctx->type == ET_ISO_OUT)) {
> +                    /* only iso streams need resyncing; see xhci_submit() */
> +                    epctx->cpr_iso_resync = true;
> +                }

[Severity: Medium]
Why is the resync restricted to cpr_is_incoming()?

usb_xhci_post_load() rebuilds the endpoint contexts through
xhci_alloc_epctx() and xhci_init_epctx() in the same way for ordinary
live migration. The commit message says that "we rebuild the controller's
transient transfer and scheduling state out of guest memory" across CPR.
A normal-migration destination gets the same rebuilt state but no resync.

Could the commit message explain why only CPR is affected?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=11

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH hci-8.1 1/5] selftests/ve: check CPUID override emulation for VE tasks
  @ 2026-10-06 14:56  3% ` Konstantin Khorenko
  2026-10-06 15:07  3%   ` no-reply
    1 sibling, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-10-06 14:56 UTC (permalink / raw)
  To: Pavel Tikhomirov, Vasileios Almpanis; +Cc: devel

Write a one-entry /proc/vz/cpuid_override table that clears one leaf 1
feature bit and compare what a task in a fresh VE and the host see:
the VE task must get the table values, the host the hardware ones.

A second test flushes the table after the task was armed and expects the
hardware values again, since faulting stays on for the task and the
emulation has to fall back to the real CPUID.

The child blocks before executing CPUID: faulting is switched on for a
task at its next context switch, not when the flag is set.

Without the fix both children die with SIGSEGV. The tests skip on CPUs
without CPUID faulting and restore the previous table on exit. The
teardown runs in the parent because the harness skips a child-side
teardown once an ASSERT has fired.

https://virtuozzo.atlassian.net/browse/VSTOR-148843

Feature: x86: cpu pools (cpuid override)
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/selftests/ve/Makefile                |   1 +
 .../testing/selftests/ve/ve_cpuid_override_test.c  | 227 +++++++++++++++++++++
 2 files changed, 228 insertions(+)

diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index c6efe7c4b4fba..e7932408b1305 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -5,5 +5,6 @@ CFLAGS += -g -Wall -O2
 TEST_GEN_PROGS += ve_ns_owner_test
 TEST_GEN_PROGS += ve_perms_test
 TEST_GEN_PROGS += ve_mount_accounting_test
+TEST_GEN_PROGS += ve_cpuid_override_test
 
 include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
new file mode 100644
index 0000000000000..c066d67d72a82
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * /proc/vz/cpuid_override selftests
+ *
+ * A non-empty override table turns CPUID faulting on for every task that
+ * enters a VE, and the #GP handler has to emulate the instruction for
+ * them. The tests check that a task in a VE sees the table values, that
+ * the host keeps seeing the hardware values, and that a task which was
+ * armed while the table was set falls back to the hardware once the
+ * table is flushed. A kernel that does not emulate the fault kills the
+ * child with SIGSEGV instead.
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/wait.h>
+
+#include "../kselftest_harness.h"
+#include "ve_selftest.h"
+
+#define CPUID_OVERRIDE_PATH	"/proc/vz/cpuid_override"
+#define CPUID_LEAF		1
+#define CPUID_TABLE_LEN		128
+#define CPUID_TABLE_MAX		4096
+
+struct cpuid_regs {
+	unsigned int eax, ebx, ecx, edx;
+};
+
+static void do_cpuid(unsigned int leaf, struct cpuid_regs *r)
+{
+	asm volatile("cpuid"
+		     : "=a" (r->eax), "=b" (r->ebx), "=c" (r->ecx), "=d" (r->edx)
+		     : "a" (leaf), "c" (0));
+}
+
+static int write_cpuid_override(const char *table)
+{
+	int fd, ret;
+
+	fd = open(CPUID_OVERRIDE_PATH, O_WRONLY);
+	if (fd < 0)
+		return -1;
+	ret = write(fd, table, strlen(table));
+	close(fd);
+	return ret == (int)strlen(table) ? 0 : -1;
+}
+
+/*
+ * The child enters a fresh VE, so it is armed with the table state of that
+ * moment, then reports its view of the leaf to the parent over a pipe. It
+ * blocks between the two steps: faulting is switched on for the task at its
+ * next context switch, and the parent may change the table meanwhile.
+ */
+struct child_ctx {
+	int result_pipe[2];
+	int go_pipe[2];
+};
+
+static int cpuid_child(struct child_ctx *ctx)
+{
+	struct cpuid_regs r;
+	char go;
+
+	close(ctx->result_pipe[0]);
+	close(ctx->go_pipe[1]);
+
+	if (unshare(CLONE_NEWVE) < 0)
+		return 21;
+	if (write(ctx->result_pipe[1], "R", 1) != 1)
+		return 22;
+	if (read(ctx->go_pipe[0], &go, 1) != 1)
+		return 23;
+
+	do_cpuid(CPUID_LEAF, &r);
+	if (write(ctx->result_pipe[1], &r, sizeof(r)) != sizeof(r))
+		return 24;
+	return 0;
+}
+
+FIXTURE(ve_cpuid_override)
+{
+	int cgv2_fd;
+	int ctid;
+	struct cpuid_regs host;
+	unsigned int bit;
+	char table[CPUID_TABLE_LEN];
+	char saved[CPUID_TABLE_MAX];
+};
+
+FIXTURE_SETUP(ve_cpuid_override)
+{
+	int fd;
+
+	fd = open(CPUID_OVERRIDE_PATH, O_RDONLY);
+	if (fd < 0)
+		SKIP(return, "%s: %s", CPUID_OVERRIDE_PATH, strerror(errno));
+	close(fd);
+
+	ASSERT_GE(read_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->saved,
+			       sizeof(self->saved)), 0);
+
+	self->cgv2_fd = mount_cg2_fd();
+	ASSERT_GE(self->cgv2_fd, 0);
+	ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
+				VE_CONTROLLERS), 0);
+	self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
+	ASSERT_GE(self->ctid, 0);
+
+	/* Mask one feature bit the host has; SSE2 when present. */
+	do_cpuid(CPUID_LEAF, &self->host);
+	self->bit = 1u << 26;
+	if (!(self->host.edx & self->bit))
+		self->bit = self->host.edx & -self->host.edx;
+	ASSERT_NE(self->bit, 0);
+
+	snprintf(self->table, sizeof(self->table),
+		 "0x%08x: 0x%08x 0x%08x 0x%08x 0x%08x\n", CPUID_LEAF,
+		 self->host.eax, self->host.ebx, self->host.ecx,
+		 self->host.edx & ~self->bit);
+}
+
+/*
+ * Runs in the parent: a child-side teardown is skipped when an ASSERT fires,
+ * and the table must be restored even then. The fd from setup belongs to the
+ * child, so the cgroup2 tree is mounted again here.
+ */
+FIXTURE_TEARDOWN_PARENT(ve_cpuid_override)
+{
+	int cgv2_fd;
+
+	write_cpuid_override(self->saved[0] ? self->saved : "\n");
+
+	cgv2_fd = mount_cg2_fd();
+	if (cgv2_fd < 0)
+		return;
+	destroy_ve(cgv2_fd, self->ctid);
+	close(cgv2_fd);
+}
+
+static pid_t spawn_cpuid_child(struct __test_metadata *_metadata,
+			       int cgv2_fd, int ctid, struct child_ctx *ctx)
+{
+	char ready;
+	pid_t pid;
+
+	ASSERT_EQ(pipe(ctx->result_pipe), 0);
+	ASSERT_EQ(pipe(ctx->go_pipe), 0);
+	ASSERT_GE(enter_cgroup(cgv2_fd, ctid), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0)
+		_exit(cpuid_child(ctx));
+
+	ASSERT_GE(enter_cgroup(cgv2_fd, 0), 0);
+	close(ctx->result_pipe[1]);
+	close(ctx->go_pipe[0]);
+
+	ASSERT_EQ(read(ctx->result_pipe[0], &ready, 1), 1);
+	ASSERT_EQ(ready, 'R');
+	return pid;
+}
+
+static void collect_cpuid_child(struct __test_metadata *_metadata, pid_t pid,
+				struct child_ctx *ctx, struct cpuid_regs *r)
+{
+	int status, len;
+
+	len = read(ctx->result_pipe[0], r, sizeof(*r));
+	ASSERT_GE(waitpid(pid, &status, 0), 0);
+	if (WIFSIGNALED(status))
+		TH_LOG("child killed by signal %d", WTERMSIG(status));
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+	ASSERT_EQ(len, sizeof(*r));
+
+	close(ctx->result_pipe[0]);
+	close(ctx->go_pipe[1]);
+}
+
+TEST_F(ve_cpuid_override, ve_sees_table_host_sees_hardware)
+{
+	struct child_ctx ctx;
+	struct cpuid_regs r;
+	pid_t pid;
+
+	ASSERT_EQ(write_cpuid_override(self->table), 0);
+
+	pid = spawn_cpuid_child(_metadata, self->cgv2_fd, self->ctid, &ctx);
+	ASSERT_EQ(write(ctx.go_pipe[1], "G", 1), 1);
+	collect_cpuid_child(_metadata, pid, &ctx, &r);
+
+	EXPECT_EQ(r.eax, self->host.eax);
+	EXPECT_EQ(r.ebx, self->host.ebx);
+	EXPECT_EQ(r.ecx, self->host.ecx);
+	EXPECT_EQ(r.edx, self->host.edx & ~self->bit);
+
+	do_cpuid(CPUID_LEAF, &r);
+	EXPECT_NE(r.edx & self->bit, 0);
+}
+
+TEST_F(ve_cpuid_override, flushed_table_falls_back_to_hardware)
+{
+	struct child_ctx ctx;
+	struct cpuid_regs r;
+	pid_t pid;
+
+	ASSERT_EQ(write_cpuid_override(self->table), 0);
+
+	pid = spawn_cpuid_child(_metadata, self->cgv2_fd, self->ctid, &ctx);
+
+	ASSERT_EQ(write_cpuid_override("\n"), 0);
+	ASSERT_EQ(write(ctx.go_pipe[1], "G", 1), 1);
+	collect_cpuid_child(_metadata, pid, &ctx, &r);
+
+	EXPECT_EQ(r.eax, self->host.eax);
+	EXPECT_NE(r.edx & self->bit, 0);
+}
+
+TEST_HARNESS_MAIN

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 3/5] selftests/ve: check CPUID override right after entering a VE
  @ 2026-10-06 15:03  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:03 UTC (permalink / raw)
  To: Pavel Tikhomirov, Vasileios Almpanis, Konstantin Khorenko; +Cc: devel

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] selftests/ve: faulting child in wait_go=0 case fails at ready-read, not diagnosed
- [Low] selftests/ve: armed_before_first_reschedule cannot guarantee no reschedule, may falsely pass
--

commit 56789e953f13472fc78d80df081028a09179dc56
Author: Konstantin Khorenko <khorenko@virtuozzo.com>

selftests/ve: check CPUID override right after entering a VE

This commit adds a test case in which the child executes CPUID straight
after unshare(CLONE_NEWVE), with no blocking in between. It expects the
table values. A new wait_go field in struct child_ctx selects between the
old blocking flow and the new flow.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-148843

> diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
> --- a/tools/testing/selftests/ve/ve_cpuid_override_test.c
> +++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c

[ ... ]

> @@ -73,12 +74,18 @@ static int cpuid_child(struct child_ctx *ctx)
>  
>  	if (unshare(CLONE_NEWVE) < 0)
>  		return 21;
> -	if (write(ctx->result_pipe[1], "R", 1) != 1)
> -		return 22;
> -	if (read(ctx->go_pipe[0], &go, 1) != 1)
> -		return 23;
> -
> +	if (ctx->wait_go) {
> +		if (write(ctx->result_pipe[1], "R", 1) != 1)
> +			return 22;
> +		if (read(ctx->go_pipe[0], &go, 1) != 1)
> +			return 23;
> +	}
> +
> +	/* Without wait_go nothing wakes the parent before the CPUID. */
>  	do_cpuid(CPUID_LEAF, &r);
> +
> +	if (!ctx->wait_go && write(ctx->result_pipe[1], "R", 1) != 1)
> +		return 22;

[Severity: Low]
If the kernel does not emulate the CPUID fault, can the child die with
SIGSEGV inside do_cpuid() in the wait_go=0 case, before it writes the "R"
byte?

In that case the parent's read() of the ready byte in spawn_cpuid_child()
would see EOF, and ASSERT_EQ(read(...), 1) would abort the test there.

collect_cpuid_child() would then not be reached, so the "child killed by
signal" TH_LOG is not printed and waitpid() is never called. The child is
left as a zombie until the harness exits.

The test still fails, but with a misleading assertion. The later patch
"selftests/ve: check CPUID override emulation for a prefixed CPUID" keeps
the same ordering of "R" after CPUID for wait_go=0, so this does not appear
to be fixed later in the series.

>  	if (write(ctx->result_pipe[1], &r, sizeof(r)) != sizeof(r))
>  		return 24;
>  	return 0;

[ ... ]

> +/* No context switch between entering the VE and the CPUID. */
> +TEST_F(ve_cpuid_override, armed_before_first_reschedule)
> +{
> +	struct child_ctx ctx = { .wait_go = 0 };
> +	struct cpuid_regs r;
> +	pid_t pid;
> +
> +	ASSERT_EQ(write_cpuid_override(self->table), 0);
> +
> +	pid = spawn_cpuid_child(_metadata, self->cgv2_fd, self->ctid, &ctx);
> +	collect_cpuid_child(_metadata, pid, &ctx, &r);
> +
> +	EXPECT_EQ(r.edx, self->host.edx & ~self->bit);
> +}

[Severity: Low]
Can this test pass on a kernel without the immediate-arming fix?

The comment says "No context switch between entering the VE and the
CPUID", but cpuid_child() only avoids voluntary blocking between
unshare(CLONE_NEWVE) and do_cpuid(). Nothing pins the child to a CPU or
prevents preemption, a timer tick, or a migration in that window.

Any such context switch would apply the CPUID-faulting state through
__switch_to_xtra(). The child would then see the table values even without
the fix.

The test is a single run with no retries, so it can miss the bug on a
loaded host. The later patches "x86/traps: decode the faulting CPUID with
the instruction decoder" and "selftests/ve: check CPUID override emulation
for a prefixed CPUID" add neither iterations nor pinning to this case.
Should the comment be softened, or should the test repeat the sequence?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-b4-prep-khorenko-vstor-148656-cpuid-v1-0-741609f4f03a@virtuozzo.com?part=3

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 1/5] selftests/ve: check CPUID override emulation for VE tasks
  2026-10-06 14:56  3% ` [Devel] [PATCH hci-8.1 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
@ 2026-10-06 15:07  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:07 UTC (permalink / raw)
  To: Pavel Tikhomirov, Vasileios Almpanis, Konstantin Khorenko; +Cc: devel

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [Medium] selftests/ve: child may run CPUID before faulting is armed, so tests are racy
- [Low] selftests/ve: ve_cpuid_override_test binary missing from .gitignore
- [Low] selftests/ve: x86-only cpuid asm built unconditionally for all ARCH
- [Low] selftests/ve: 4096-byte snapshot of cpuid_override can truncate the table, so restore fails
- [Low] selftests/ve: VE cgroup leaks when an ASSERT fails in FIXTURE_SETUP after make_ve()
- [Low] selftests/ve: leaf 1 EAX compared against single-CPU sample while child is unpinned
--

commit 931878a5cb863ada037d978eae5d17e57f54cba3
Author: Konstantin Khorenko <khorenko@virtuozzo.com>

selftests/ve: check CPUID override emulation for VE tasks

This commit adds a selftest that writes a one-entry /proc/vz/cpuid_override
table and checks that a task in a fresh VE sees the table values while the
host sees the hardware values. A second test flushes the table after the
task was armed and expects the hardware values again.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-148843

> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
> --- a/tools/testing/selftests/ve/Makefile
> +++ b/tools/testing/selftests/ve/Makefile
> @@ -5,5 +5,6 @@ CFLAGS += -g -Wall -O2
>  TEST_GEN_PROGS += ve_ns_owner_test
>  TEST_GEN_PROGS += ve_perms_test
>  TEST_GEN_PROGS += ve_mount_accounting_test
> +TEST_GEN_PROGS += ve_cpuid_override_test

[Severity: Low]
This isn't a bug in functionality, but the new binary ve_cpuid_override_test
is not added to tools/testing/selftests/ve/.gitignore, which lists only
ve_ns_owner_test, ve_perms_test and ve_mount_accounting_test.

Running make in tools/testing/selftests/ve will leave this binary untracked
in git status. Should it get an entry there?

[Severity: Low]
The test is added unconditionally for every architecture, and do_cpuid()
below uses x86-only inline asm.

Since tools/testing/selftests/Makefile has an unconditional "TARGETS += ve",
can this break the build of the whole ve selftest directory on non-x86 ARCH,
and on 32-bit PIC x86 because of the "=b" constraint? Would an arch check in
this Makefile be appropriate?

[ ... ]

> diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
> new file mode 100644
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c

[ ... ]

> +static int cpuid_child(struct child_ctx *ctx)
> +{
> +	struct cpuid_regs r;
> +	char go;
> +
> +	close(ctx->result_pipe[0]);
> +	close(ctx->go_pipe[1]);
> +
> +	if (unshare(CLONE_NEWVE) < 0)
> +		return 21;
> +	if (write(ctx->result_pipe[1], "R", 1) != 1)
> +		return 22;
> +	if (read(ctx->go_pipe[0], &go, 1) != 1)
> +		return 23;
> +
> +	do_cpuid(CPUID_LEAF, &r);

[Severity: Medium]
Is the child guaranteed to be armed before it executes CPUID here?

CPUID faulting is only switched on in __switch_to_xtra() at the next
context switch, when prev_cpuid != next_cpuid:

arch/x86/kernel/process.c:__switch_to_xtra() {
    ...
	prev_cpuid = (tifp & _TIF_NOCPUID) || (tifp & _TIF_CPUID_OVERRIDE);
	next_cpuid = (tifn & _TIF_NOCPUID) || (tifn & _TIF_CPUID_OVERRIDE);
	if (prev_cpuid != next_cpuid)
		set_cpuid_faulting(next_cpuid);
    ...
}

The only blocking point of the child is read(ctx->go_pipe[0]). In
ve_sees_table_host_sees_hardware() the parent writes "G" right after
spawn_cpuid_child() has read "R".

On SMP the byte can already be in the pipe when the child calls read(). The
read then returns without sleeping, no context switch occurs, and the child
executes CPUID natively. It would then report the hardware EDX and fail
EXPECT_EQ(r.edx, self->host.edx & ~self->bit).

In flushed_table_falls_back_to_hardware() the parent calls
write_cpuid_override("\n") right after reading "R", and nothing waits for
the child to sleep. Can the child end up never being armed, so that the test
passes without exercising the armed-then-flushed path?

Later patches in the series (including "selftests/ve: check CPUID override
right after entering a VE") keep this handshake, and the final series state
still has the "R" then immediate "G" pattern with no wait for the child to
sleep, so the race is not fixed there either.

> +	if (write(ctx->result_pipe[1], &r, sizeof(r)) != sizeof(r))
> +		return 24;
> +	return 0;
> +}

[ ... ]

> +FIXTURE_SETUP(ve_cpuid_override)
> +{
> +	int fd;
> +
> +	fd = open(CPUID_OVERRIDE_PATH, O_RDONLY);
> +	if (fd < 0)
> +		SKIP(return, "%s: %s", CPUID_OVERRIDE_PATH, strerror(errno));
> +	close(fd);
> +
> +	ASSERT_GE(read_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->saved,
> +			       sizeof(self->saved)), 0);

[Severity: Low]
Can this snapshot be truncated? self->saved is CPUID_TABLE_MAX (4096) bytes,
and read_file_at() fills at most buflen-1 bytes without reporting an error.

With up to 128 entries of roughly 45 to 64 bytes each, a large pre-existing
table can exceed 4095 bytes. If the cut falls mid-entry, the restore write in
FIXTURE_TEARDOWN_PARENT() fails with -EINVAL. If it falls on an entry
boundary, a shorter table is installed.

> +
> +	self->cgv2_fd = mount_cg2_fd();
> +	ASSERT_GE(self->cgv2_fd, 0);
> +	ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
> +				VE_CONTROLLERS), 0);
> +	self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
> +	ASSERT_GE(self->ctid, 0);
> +
> +	/* Mask one feature bit the host has; SSE2 when present. */
> +	do_cpuid(CPUID_LEAF, &self->host);
> +	self->bit = 1u << 26;
> +	if (!(self->host.edx & self->bit))
> +		self->bit = self->host.edx & -self->host.edx;
> +	ASSERT_NE(self->bit, 0);

[Severity: Low]
If an ASSERT fails in FIXTURE_SETUP after make_ve() (for example
ASSERT_NE(self->bit, 0)), will the VE cgroup be leaked?

The harness runs teardown only when setup has completed, so the statement in
the commit message that "The teardown runs in the parent because the harness
skips a child-side teardown once an ASSERT has fired" only covers the test
body, not a failure in setup. The ASSERT_NE path is very unlikely on x86, so
the impact looks small.

[ ... ]

> +FIXTURE_TEARDOWN_PARENT(ve_cpuid_override)
> +{
> +	int cgv2_fd;
> +
> +	write_cpuid_override(self->saved[0] ? self->saved : "\n");

[Severity: Low]
Should the return value of write_cpuid_override() be checked here? This is
the only restore of host state, and if it fails, the test's one-entry table
stays installed with no diagnostic.

[ ... ]

> +TEST_F(ve_cpuid_override, flushed_table_falls_back_to_hardware)
> +{
> +	struct child_ctx ctx;
> +	struct cpuid_regs r;
> +	pid_t pid;
> +
> +	ASSERT_EQ(write_cpuid_override(self->table), 0);
> +
> +	pid = spawn_cpuid_child(_metadata, self->cgv2_fd, self->ctid, &ctx);
> +
> +	ASSERT_EQ(write_cpuid_override("\n"), 0);
> +	ASSERT_EQ(write(ctx.go_pipe[1], "G", 1), 1);
> +	collect_cpuid_child(_metadata, pid, &ctx, &r);
> +
> +	EXPECT_EQ(r.eax, self->host.eax);
> +	EXPECT_NE(r.edx & self->bit, 0);

[Severity: Low]
Can EXPECT_EQ(r.eax, self->host.eax) fail spuriously on hybrid CPUs?

In this test the child's CPUID falls through to the real hardware, but
self->host was sampled by the parent in FIXTURE_SETUP(), and neither process
is pinned with sched_setaffinity(). If leaf 1 EAX (family/model/stepping)
differs between core types and the child runs on a different core type, the
comparison can fail without any kernel bug.

> +}
> +
> +TEST_HARNESS_MAIN

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-b4-prep-khorenko-vstor-148656-cpuid-v1-0-741609f4f03a@virtuozzo.com?part=1

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* [PATCH QEMU HCI-8.0 v7 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
  @ 2026-10-06 15:35 30% ` Andrey Drobyshev
  2026-10-06 15:38  0%   ` no-reply
  2026-10-06 15:35  6% ` [PATCH QEMU HCI-8.0 v7 02/14] usb-host: don't leak config descriptor on claim " Andrey Drobyshev
                   ` (11 subsequent siblings)
  12 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

The FD passed into usb_host_open() for a wrapped device is only stored in
s->hostfd after libusb_wrap_sys_device() succeeds, and the failure path
doesn't close it.  if wrapping fails, or a later step of the open sequence
fails (e.g. usb_device_attach), the FD is leaked, as usb_host_close() only
runs for fully opened devices, and the realize error path doesn't clean it
up either.

Store the FD in s->hostfd before wrapping and close it on the failure
path.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b74670ae256..d4a8c68e6d4 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -961,6 +961,11 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
         goto fail;
     }
 
+    /* Track the caller-owned FD so the fail: path frees it. */
+    if (!dev) {
+        s->hostfd = hostfd;
+    }
+
     if (dev) {
         bus_num = libusb_get_bus_number(dev);
         addr = libusb_get_device_address(dev);
@@ -978,7 +983,6 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
         if (rc != 0) {
             goto fail;
         }
-        s->hostfd  = hostfd;
         dev = libusb_get_device(s->dh);
         bus_num = libusb_get_bus_number(dev);
         addr = libusb_get_device_address(dev);
@@ -1066,6 +1070,10 @@ fail:
         s->dh = NULL;
         s->dev = NULL;
     }
+    if (s->hostfd != -1) {
+        close(s->hostfd);
+        s->hostfd = -1;
+    }
     return -1;
 }
 

-- 
2.47.1


^ permalink raw reply	[relevance 30%]

* [PATCH QEMU HCI-8.0 v7 03/14] usb-host: add migration blocker for CPR modes #VSTOR-137800
    2026-10-06 15:35 30% ` [PATCH QEMU HCI-8.0 v7 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
  2026-10-06 15:35  6% ` [PATCH QEMU HCI-8.0 v7 02/14] usb-host: don't leak config descriptor on claim " Andrey Drobyshev
@ 2026-10-06 15:35 13% ` Andrey Drobyshev
  2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
                   ` (9 subsequent siblings)
  12 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

We're about to add support for CPR migration to usb-host in the upcoming
commits.  Right now CPR is crashing with usb-host devices.  Let's add
migration blocker upfront, for both cpr-exec and cpr-transfer.  The
blocker is added unconditionally, so that we refuse CPR migration instead
of crashing on it.  That is for the sake of bisectability of the upcoming
commits.  It is going to be lifted (conditioned) in a following patch
once CPR support is added.  Also, blocker is added at the end of
usb_host_realize(), so we have to cleanup all the side effects done
earlier in .realize() in case migrate_add_blocker_modes() fails.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 7c8b65ad5af..e3bd7bcf749 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -46,6 +46,7 @@
 #endif
 
 #include "qapi/error.h"
+#include "migration/blocker.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -118,6 +119,9 @@ struct USBHostDevice {
     bool                             bh_postld_pending;
     Notifier                         exit;
 
+    /* CPR migration */
+    Error                            *cpr_blocker;
+
     /* request queues */
     QTAILQ_HEAD(, USBHostRequest)    requests;
     QTAILQ_HEAD(, USBHostIsoRing)    isorings;
@@ -1256,6 +1260,18 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
         usb_host_auto_check(NULL);
     }
 
+    error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+               DEVICE(s)->id ?: "(anonymous)");
+    if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+                                  MIG_MODE_CPR_TRANSFER,
+                                  MIG_MODE_CPR_EXEC, -1) < 0) {
+        if (s->needs_autoscan) {
+            QTAILQ_REMOVE(&hostdevs, s, next);
+        }
+        usb_host_close(s);
+        return;
+    }
+
     s->exit.notify = usb_host_exit_notifier;
     qemu_add_exit_notifier(&s->exit);
 }
@@ -1279,6 +1295,7 @@ static void usb_host_unrealize(USBDevice *udev)
         QTAILQ_REMOVE(&hostdevs, s, next);
     }
     usb_host_close(s);
+    migrate_del_blocker(&s->cpr_blocker);
 }
 
 static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)

-- 
2.47.1


^ permalink raw reply	[relevance 13%]

* [PATCH QEMU HCI-8.0 v7 02/14] usb-host: don't leak config descriptor on claim failure #VSTOR-137800
    2026-10-06 15:35 30% ` [PATCH QEMU HCI-8.0 v7 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:35  6% ` Andrey Drobyshev
  2026-10-06 15:35 13% ` [PATCH QEMU HCI-8.0 v7 03/14] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
                   ` (10 subsequent siblings)
  12 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

usb_host_claim_interfaces() fetches the active config descriptor but
frees it only on the success path.  When not every interface can be
claimed it returns USB_RET_STALL without freeing it, leaking the
descriptor.

Free it before the error return.

Fixes: 896b6757f9c4 ("usb-host: support devices with sparse/non-sequential USB interfaces")
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index d4a8c68e6d4..7c8b65ad5af 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1381,6 +1381,7 @@ static int usb_host_claim_interfaces(USBHostDevice *s, int configuration)
         }
     }
     if (claimed != conf->bNumInterfaces) {
+        libusb_free_config_descriptor(conf);
         return USB_RET_STALL;
     }
 

-- 
2.47.1


^ permalink raw reply	[relevance 6%]

* [PATCH QEMU HCI-8.0 v7 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800
                     ` (2 preceding siblings ...)
  2026-10-06 15:35 13% ` [PATCH QEMU HCI-8.0 v7 03/14] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:35 26% ` Andrey Drobyshev
  2026-10-06 15:35 25% ` [PATCH QEMU HCI-8.0 v7 05/14] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
                   ` (8 subsequent siblings)
  12 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

In .realize(), save freshly opened host device FD into the CPR registry,
and then reuse it on CPR target.  Also, add deletion of that FD from
registry to usb_host_open() / usb_host_close() cleanup paths.

For this to work, we also need to skip the .post-load() code which
closes, detaches the device, and then rescans the host bus to reopen
matching hostdevs.  For the CPR-case migration we don't want any of that
as the hostdev FD stays preserved, and the guest shouldn't notice the
switchover.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 33 ++++++++++++++++++++++++++++++++-
 1 file changed, 32 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index e3bd7bcf749..beab8edc619 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -47,6 +47,7 @@
 
 #include "qapi/error.h"
 #include "migration/blocker.h"
+#include "migration/cpr.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -949,6 +950,21 @@ static void usb_host_ep_update(USBHostDevice *s)
     libusb_free_config_descriptor(conf);
 }
 
+static char *usb_host_cpr_fd_name(USBHostDevice *s)
+{
+    /* Prefix the key so it can't clash with another CPR label */
+    return DEVICE(s)->id ? g_strdup_printf("usb-host/%s", DEVICE(s)->id)
+                         : NULL;
+}
+
+static void usb_host_cpr_delete_fd(USBHostDevice *s)
+{
+    g_autofree char *name = usb_host_cpr_fd_name(s);
+    if (name) {
+        cpr_delete_fd(name, 0);
+    }
+}
+
 static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
 {
     USBDevice *udev = USB_DEVICE(s);
@@ -1075,6 +1091,7 @@ fail:
         s->dev = NULL;
     }
     if (s->hostfd != -1) {
+        usb_host_cpr_delete_fd(s);
         close(s->hostfd);
         s->hostfd = -1;
     }
@@ -1136,6 +1153,7 @@ static int usb_host_close(USBHostDevice *s)
     s->dev = NULL;
 
     if (s->hostfd != -1) {
+        usb_host_cpr_delete_fd(s);
         close(s->hostfd);
         s->hostfd = -1;
     }
@@ -1222,9 +1240,12 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
 
 #if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
     if (s->hostdevice) {
+        g_autofree char *name = usb_host_cpr_fd_name(s);
         int fd;
+
         s->needs_autoscan = false;
-        fd = qemu_open(s->hostdevice, O_RDWR, errp);
+        fd = name ? cpr_open_fd(s->hostdevice, O_RDWR, name, 0, errp)
+                  : qemu_open(s->hostdevice, O_RDWR, errp);
         if (fd < 0) {
             return;
         }
@@ -1764,6 +1785,16 @@ static int usb_host_post_load(void *opaque, int version_id)
 {
     USBHostDevice *dev = opaque;
 
+    /*
+     * For CPR migration, device wasn't released/reset, and the guest
+     * is unaware of the switchover.  The detach/rescan performed in
+     * usb_host_post_load_bh() only exists for cross-host migration.
+     * Skip it for CPR.
+     */
+    if (cpr_is_incoming()) {
+        return 0;
+    }
+
     if (!dev->bh_postld) {
         dev->bh_postld = qemu_bh_new_guarded(usb_host_post_load_bh, dev,
                                              &DEVICE(dev)->mem_reentrancy_guard);

-- 
2.47.1


^ permalink raw reply	[relevance 26%]

* [PATCH QEMU HCI-8.0 v7 05/14] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800
                     ` (3 preceding siblings ...)
  2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:35 25% ` Andrey Drobyshev
  2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
                   ` (7 subsequent siblings)
  12 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

usb_host_abort_xfers() cancels every pending request and then pumps
libusb events until all of them are reaped, with a bounded wait.  Split
that reap-wait loop out into usb_host_reap_xfers().  No functional
change.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index beab8edc619..1a79cdf9e4d 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1098,15 +1098,11 @@ fail:
     return -1;
 }
 
-static void usb_host_abort_xfers(USBHostDevice *s)
+static void usb_host_reap_xfers(USBHostDevice *s)
 {
     USBHostRequest *r, *rtmp;
     int limit = 100;
 
-    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
-        usb_host_req_abort(r);
-    }
-
     while (QTAILQ_FIRST(&s->requests) != NULL) {
         struct timeval tv;
         memset(&tv, 0, sizeof(tv));
@@ -1128,6 +1124,16 @@ static void usb_host_abort_xfers(USBHostDevice *s)
     }
 }
 
+static void usb_host_abort_xfers(USBHostDevice *s)
+{
+    USBHostRequest *r, *rtmp;
+
+    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+        usb_host_req_abort(r);
+    }
+    usb_host_reap_xfers(s);
+}
+
 static int usb_host_close(USBHostDevice *s)
 {
     USBDevice *udev = USB_DEVICE(s);

-- 
2.47.1


^ permalink raw reply	[relevance 25%]

* [PATCH QEMU HCI-8.0 v7 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800
                     ` (4 preceding siblings ...)
  2026-10-06 15:35 25% ` [PATCH QEMU HCI-8.0 v7 05/14] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:35 26% ` Andrey Drobyshev
  2026-10-06 15:45  4%   ` no-reply
  2026-10-06 15:35 24% ` [PATCH QEMU HCI-8.0 v7 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
                   ` (6 subsequent siblings)
  12 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

URBs submitted by the old QEMU must not outlive it across a CPR
switchover: the kernel would complete them into buffers of an address
space that is gone after exec, and the new libusb context would reap URB
pointers it never submitted.

Register a vm_change_state handler that, when the VM stops for CPR
(RUN_STATE_FINISH_MIGRATE), cancels and reaps every pending transfer.
It runs before the vCPU/device snapshot, so a completion raised during
the drain lands in the still-live interrupt controller and is migrated
too.  A transfer that already completed in the kernel is delivered
normally; one cancelled after moving some data is delivered as a short
transfer; one that moved nothing is suppressed, and the target replays
it from the start of the TD via usb_xhci_post_load().  A VM already
paused when migration starts never hits that transition, so .pre_save()
drains as a fallback, suppressing every URB for the target to replay.

Isochronous URBs live on their own rings; cancel and reap them too -- the
target reconstructs the stream from the guest's transfer ring.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 236 +++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 209 insertions(+), 27 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 1a79cdf9e4d..e631b683ed1 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -48,6 +48,7 @@
 #include "qapi/error.h"
 #include "migration/blocker.h"
 #include "migration/cpr.h"
+#include "migration/misc.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -122,6 +123,9 @@ struct USBHostDevice {
 
     /* CPR migration */
     Error                            *cpr_blocker;
+    VMChangeStateEntry               *cpr_vmse;
+    bool                             cpr_drained;
+    bool                             cpr_replay_all;
 
     /* request queues */
     QTAILQ_HEAD(, USBHostRequest)    requests;
@@ -405,7 +409,10 @@ static void usb_host_req_free(USBHostRequest *r)
 #ifdef CONFIG_WIN32
     request_count--;
 #endif
-    QTAILQ_REMOVE(&r->host->requests, r, next);
+    /* Skip if a timed-out reap already unlinked it (avoid a double remove) */
+    if (QTAILQ_IN_USE(r, next)) {
+        QTAILQ_REMOVE(&r->host->requests, r, next);
+    }
     libusb_free_transfer(r->xfer);
     g_free(r->buffer);
     g_free(r);
@@ -423,6 +430,29 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
     return NULL;
 }
 
+/*
+ * Classify a reap during the CPR drain (r->p is still set).  The paused-VM
+ * fallback (cpr_replay_all) can't deliver completions, so it suppresses every
+ * reap for the target to replay; otherwise one that moved no data is suppressed
+ * and one that moved some is kept as a short transfer.  Returns true if
+ * suppressed, so the caller frees the request.
+ */
+static bool usb_host_req_cpr_replay(USBHostRequest *r,
+                                    struct libusb_transfer *xfer)
+{
+    if (!r->host->cpr_replay_all) {
+        if (xfer->status != LIBUSB_TRANSFER_CANCELLED) {
+            return false;
+        }
+        if (xfer->actual_length != 0) {
+            /* deliver as a short transfer */
+            xfer->status = LIBUSB_TRANSFER_COMPLETED;
+            return false;
+        }
+    }
+    return true;
+}
+
 static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
 {
     USBHostRequest *r = xfer->user_data;
@@ -432,6 +462,9 @@ static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
     if (r->p == NULL) {
         goto out; /* request was canceled */
     }
+    if (usb_host_req_cpr_replay(r, xfer)) {
+        goto out; /* CPR drain: the target will replay this packet */
+    }
 
     r->p->status = status_map[xfer->status];
     r->p->actual_length = xfer->actual_length;
@@ -482,6 +515,9 @@ static void LIBUSB_CALL usb_host_req_complete_data(struct libusb_transfer *xfer)
     if (r->p == NULL) {
         goto out; /* request was canceled */
     }
+    if (usb_host_req_cpr_replay(r, xfer)) {
+        goto out; /* CPR drain: the target will replay this packet */
+    }
 
     r->p->status = status_map[xfer->status];
     if (r->in && xfer->actual_length) {
@@ -502,22 +538,29 @@ out:
     }
 }
 
-static void usb_host_req_abort(USBHostRequest *r)
+/* Fail r's in-flight packet with an error status and detach it from r. */
+static void usb_host_req_complete_err(USBHostRequest *r, int status)
 {
-    USBHostDevice  *s = r->host;
-    bool inflight = (r->p && r->p->state == USB_PACKET_ASYNC);
+    USBHostDevice *s = r->host;
 
-    if (inflight) {
-        r->p->status = USB_RET_NODEV;
-        trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
-                                    r->p->status, r->p->actual_length);
-        if (r->p->ep->nr == 0) {
-            usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
-        } else {
-            usb_packet_complete(USB_DEVICE(s), r->p);
-        }
-        r->p = NULL;
+    if (!r->p || r->p->state != USB_PACKET_ASYNC) {
+        return;
+    }
+    r->p->status = status;
+    trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
+                                r->p->status, r->p->actual_length);
+    if (r->p->ep->nr == 0) {
+        usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
+    } else {
+        usb_packet_complete(USB_DEVICE(s), r->p);
+    }
+    r->p = NULL;
+}
 
+static void usb_host_req_abort(USBHostRequest *r)
+{
+    if (r->p && r->p->state == USB_PACKET_ASYNC) {
+        usb_host_req_complete_err(r, USB_RET_NODEV);
         libusb_cancel_transfer(r->xfer);
     }
 }
@@ -1098,9 +1141,18 @@ fail:
     return -1;
 }
 
-static void usb_host_reap_xfers(USBHostDevice *s)
+/* libusb still owns cancelled-but-unreaped transfers: unlink, don't free. */
+static void usb_host_drop_requests(USBHostDevice *s)
 {
     USBHostRequest *r, *rtmp;
+
+    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+        QTAILQ_REMOVE(&s->requests, r, next);
+    }
+}
+
+static void usb_host_reap_xfers(USBHostDevice *s)
+{
     int limit = 100;
 
     while (QTAILQ_FIRST(&s->requests) != NULL) {
@@ -1110,15 +1162,10 @@ static void usb_host_reap_xfers(USBHostDevice *s)
         libusb_handle_events_timeout(ctx, &tv);
         if (--limit == 0) {
             /*
-             * Don't wait forever for libusb calling the complete
-             * callback (which will unlink and free the request).
-             *
-             * Leaking memory here, to make sure libusb will not
-             * access memory which we have released already.
+             * Don't wait forever for libusb to call the complete callback
+             * (which unlinks and frees the request).
              */
-            QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
-                QTAILQ_REMOVE(&s->requests, r, next);
-            }
+            usb_host_drop_requests(s);
             return;
         }
     }
@@ -1134,6 +1181,117 @@ static void usb_host_abort_xfers(USBHostDevice *s)
     usb_host_reap_xfers(s);
 }
 
+static bool usb_host_iso_inflight(USBHostDevice *s)
+{
+    USBHostIsoRing *ring;
+
+    QTAILQ_FOREACH(ring, &s->isorings, next) {
+        if (!QTAILQ_EMPTY(&ring->inflight)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/*
+ * Quiesce the device for a CPR switchover.  Cancel and reap all URBs,
+ * so that no URB submitted by this process can outlive it.
+ */
+static int usb_host_cpr_drain_xfers(USBHostDevice *s)
+{
+    USBHostRequest *r, *rtmp;
+    USBHostIsoRing *ring;
+    USBHostIsoXfer *xfer;
+    int limit = 100;
+
+    s->cpr_drained = true;
+
+    /*
+     * Cancel and reap every URB.  Completing a reaped URB can make xHCI queue
+     * the next TD, so re-cancel whatever reappears until the queue stays empty.
+     */
+    while (!QTAILQ_EMPTY(&s->requests)) {
+        struct timeval tv = { .tv_usec = 2500 };
+
+        QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+            libusb_cancel_transfer(r->xfer);
+        }
+        libusb_handle_events_timeout(ctx, &tv);
+        if (--limit == 0) {
+            /*
+             * The device won't release these URBs.  Error the packets out so
+             * the resumed guest retries them, leak the requests (libusb still
+             * owns their transfers) and abort the migration.
+             */
+            QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+                usb_host_req_complete_err(r, USB_RET_IOERROR);
+            }
+            usb_host_drop_requests(s);
+            return -1;
+        }
+    }
+
+    /*
+     * Iso URBs are on the rings, not s->requests.  Cancel them: one in
+     * flight when the guest stops may never complete on its own, and none
+     * may be left on the preserved fd.  The target replays from the ring.
+     */
+    QTAILQ_FOREACH(ring, &s->isorings, next) {
+        QTAILQ_FOREACH(xfer, &ring->inflight, next) {
+            libusb_cancel_transfer(xfer->xfer);
+        }
+    }
+
+    /*
+     * Cap the reap at 2x a full ring:
+     * iso_urb_count URBs x iso_urb_frames packets each
+     */
+    limit = 2 * s->iso_urb_count * s->iso_urb_frames;
+    while (usb_host_iso_inflight(s)) {
+        struct timeval tv = { .tv_usec = 1000 };
+        libusb_handle_events_timeout(ctx, &tv);
+        if (limit-- == 0) {
+            return -1;
+        }
+    }
+    usb_host_iso_free_all(s);
+    return 0;
+}
+
+/*
+ * Quiesce the URB data plane around a CPR switchover: drain the in-flight
+ * transfers before the device/irqchip snapshot, so completions raised during
+ * the drain are migrated too.
+ */
+static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
+{
+    if (usb_host_cpr_drain_xfers(s) < 0) {
+        return -1;
+    }
+    return 0;
+}
+
+/*
+ * Run the CPR quiesce from the vm-state handler, on the stop-for-CPR
+ * transition before the device/irqchip snapshot.
+ */
+static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
+{
+    USBHostDevice *s = opaque;
+    MigMode mode;
+
+    if (running || !s->dh || state != RUN_STATE_FINISH_MIGRATE) {
+        return;
+    }
+    mode = migrate_mode();
+    if (mode != MIG_MODE_CPR_EXEC && mode != MIG_MODE_CPR_TRANSFER) {
+        return;
+    }
+    if (usb_host_cpr_quiesce(s, mode) < 0) {
+        migration_file_set_error(-EIO, NULL);
+    }
+}
+
 static int usb_host_close(USBHostDevice *s)
 {
     USBDevice *udev = USB_DEVICE(s);
@@ -1301,6 +1459,8 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
 
     s->exit.notify = usb_host_exit_notifier;
     qemu_add_exit_notifier(&s->exit);
+
+    s->cpr_vmse = qemu_add_vm_change_state_handler(usb_host_cpr_vm_state, s);
 }
 
 static void usb_host_instance_init(Object *obj)
@@ -1323,6 +1483,9 @@ static void usb_host_unrealize(USBDevice *udev)
     }
     usb_host_close(s);
     migrate_del_blocker(&s->cpr_blocker);
+    if (s->cpr_vmse) {
+        qemu_del_vm_change_state_handler(s->cpr_vmse);
+    }
 }
 
 static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1792,10 +1955,8 @@ static int usb_host_post_load(void *opaque, int version_id)
     USBHostDevice *dev = opaque;
 
     /*
-     * For CPR migration, device wasn't released/reset, and the guest
-     * is unaware of the switchover.  The detach/rescan performed in
-     * usb_host_post_load_bh() only exists for cross-host migration.
-     * Skip it for CPR.
+     * CPR keeps the device open and the guest unaware, so skip the detach and
+     * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
      */
     if (cpr_is_incoming()) {
         return 0;
@@ -1810,10 +1971,31 @@ static int usb_host_post_load(void *opaque, int version_id)
     return 0;
 }
 
+static int usb_host_pre_save(void *opaque)
+{
+    USBHostDevice *s = opaque;
+    MigMode mode = migrate_mode();
+
+    /*
+     * The vm-state handler drains on the stop-for-CPR transition, but a VM
+     * already paused never hits it.  Drain here instead; the stopped guest
+     * can't take completions, so suppress every URB and let the target replay.
+     */
+    if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
+        s->dh && !s->cpr_drained) {
+        s->cpr_replay_all = true;
+        if (usb_host_cpr_quiesce(s, mode) < 0) {
+            return -1;
+        }
+    }
+    return 0;
+}
+
 static const VMStateDescription vmstate_usb_host = {
     .name = "usb-host",
     .version_id = 1,
     .minimum_version_id = 1,
+    .pre_save = usb_host_pre_save,
     .post_load = usb_host_post_load,
     .fields = (const VMStateField[]) {
         VMSTATE_USB_DEVICE(parent_obj, USBHostDevice),

-- 
2.47.1


^ permalink raw reply	[relevance 26%]

* [PATCH QEMU HCI-8.0 v7 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
                     ` (5 preceding siblings ...)
  2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:35 24% ` Andrey Drobyshev
  2026-10-06 15:46  4%   ` no-reply
  2026-10-06 15:35 22% ` [PATCH QEMU HCI-8.0 v7 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
                   ` (5 subsequent siblings)
  12 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

If CPR fails, the source VM resumes with the drained packets still owned
by the host controller as in-flight, and nothing left to complete them.

Remember the packets suppressed during the drain on a list, and re-issue
them on the preserved fd from the vm-state handler's resume branch -- it
runs on any resume but only acts once we have actually drained
(cpr_drained).  Control transfers cannot be reconstructed from the packet
alone, so complete those as errors and let the guest driver retry.

A re-issued transfer is replayed from the start of the TD, so a partial
transfer loses its progress -- an OUT fragment already consumed by the
device may be sent twice.  Only the paused-VM .pre_save() fallback queues
partial transfers for re-issue; the normal running-VM drain delivers them
as short transfers and is unaffected.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 79 ++++++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 74 insertions(+), 5 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index e631b683ed1..d90117e4292 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -124,6 +124,7 @@ struct USBHostDevice {
     /* CPR migration */
     Error                            *cpr_blocker;
     VMChangeStateEntry               *cpr_vmse;
+    GSList                           *cpr_inflight;
     bool                             cpr_drained;
     bool                             cpr_replay_all;
 
@@ -169,6 +170,7 @@ static void usb_host_release_interfaces(USBHostDevice *s);
 static void usb_host_nodev(USBHostDevice *s);
 static void usb_host_detach_kernel(USBHostDevice *s);
 static void usb_host_attach_kernel(USBHostDevice *s);
+static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
 
 /* ------------------------------------------------------------------------ */
 
@@ -433,9 +435,10 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
 /*
  * Classify a reap during the CPR drain (r->p is still set).  The paused-VM
  * fallback (cpr_replay_all) can't deliver completions, so it suppresses every
- * reap for the target to replay; otherwise one that moved no data is suppressed
- * and one that moved some is kept as a short transfer.  Returns true if
- * suppressed, so the caller frees the request.
+ * reap; otherwise one that moved no data is suppressed and one that moved some
+ * is kept as a short transfer.  Suppressed packets are queued on cpr_inflight
+ * for the target to replay.  Returns true if suppressed, so the caller frees
+ * the request.
  */
 static bool usb_host_req_cpr_replay(USBHostRequest *r,
                                     struct libusb_transfer *xfer)
@@ -450,6 +453,9 @@ static bool usb_host_req_cpr_replay(USBHostRequest *r,
             return false;
         }
     }
+    if (r->p->state == USB_PACKET_ASYNC) {
+        r->host->cpr_inflight = g_slist_append(r->host->cpr_inflight, r->p);
+    }
     return true;
 }
 
@@ -1271,16 +1277,74 @@ static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
     return 0;
 }
 
+/*
+ * If CPR is aborted the source resumes with the drained packets still owned
+ * by the controller as in-flight; re-issue them on the preserved fd.  Control
+ * transfers can't be reconstructed from the packet, so error those out.
+ */
+static void usb_host_cpr_reissue(USBHostDevice *s)
+{
+    USBDevice *udev = USB_DEVICE(s);
+    GSList *it;
+    USBPacket *p;
+
+    if (!s->cpr_drained) {
+        return;
+    }
+
+    for (it = s->cpr_inflight; it; it = it->next) {
+        p = it->data;
+        /*
+         * Replay from the start of the TD.  A transfer that already moved some
+         * data is re-run in full, so an OUT fragment the device consumed may be
+         * sent twice.  Only the paused-VM pre_save fallback queues such partial
+         * transfers here; the normal drain delivers them as short transfers.
+         */
+        p->actual_length = 0;
+        if (p->ep->nr == 0) {
+            /* control endpoint: can't be reconstructed, so error it out */
+            p->status = USB_RET_IOERROR;
+            usb_generic_async_ctrl_complete(udev, p);
+        } else {
+            usb_host_handle_data(udev, p);
+            if (p->status != USB_RET_ASYNC) {
+                /* completed synchronously with an error */
+                if (usb_host_use_combining(p->ep)) {
+                    usb_combined_input_packet_complete(udev, p);
+                } else {
+                    usb_packet_complete(udev, p);
+                }
+            }
+        }
+    }
+    g_slist_free(s->cpr_inflight);
+    s->cpr_inflight = NULL;
+    s->cpr_replay_all = false;
+    s->cpr_drained = false;
+}
+
 /*
  * Run the CPR quiesce from the vm-state handler, on the stop-for-CPR
- * transition before the device/irqchip snapshot.
+ * transition before the device/irqchip snapshot, and re-issue the drained
+ * packets on resume if CPR was aborted.
  */
 static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
 {
     USBHostDevice *s = opaque;
     MigMode mode;
 
-    if (running || !s->dh || state != RUN_STATE_FINISH_MIGRATE) {
+    if (running) {
+        /*
+         * Resume: re-issue drained packets and reset state, even if the
+         * device was lost between the drain and now (dh == NULL).
+         */
+        usb_host_cpr_reissue(s);
+        return;
+    }
+    if (!s->dh) {
+        return;
+    }
+    if (state != RUN_STATE_FINISH_MIGRATE) {
         return;
     }
     mode = migrate_mode();
@@ -1486,6 +1550,8 @@ static void usb_host_unrealize(USBDevice *udev)
     if (s->cpr_vmse) {
         qemu_del_vm_change_state_handler(s->cpr_vmse);
     }
+    g_slist_free(s->cpr_inflight);
+    s->cpr_inflight = NULL;
 }
 
 static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1493,6 +1559,9 @@ static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
     USBHostDevice *s = USB_HOST_DEVICE(udev);
     USBHostRequest *r;
 
+    /* Packet is being cancelled -> it mustn't be re-issued on failed CPR */
+    s->cpr_inflight = g_slist_remove(s->cpr_inflight, p);
+
     if (p->combined) {
         usb_combined_packet_cancel(udev, p);
         return;

-- 
2.47.1


^ permalink raw reply	[relevance 24%]

* [PATCH QEMU HCI-8.0 v7 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800
                     ` (6 preceding siblings ...)
  2026-10-06 15:35 24% ` [PATCH QEMU HCI-8.0 v7 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:35 22% ` Andrey Drobyshev
  2026-10-06 15:47  5%   ` no-reply
  2026-10-06 15:36 29% ` [PATCH QEMU HCI-8.0 v7 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
                   ` (4 subsequent siblings)
  12 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

usb_host_open() reads the product string descriptor from the device
with a synchronous control transfer.  On the CPR target this runs while
the device is being handed over, and the transfer can block there,
hanging the incoming migration.

The string only fills the cosmetic product_desc, so skip the read when
we're the CPR target and fall back to the synthetic "host:bus.addr"
name.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index d90117e4292..271456abb0a 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1111,7 +1111,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
     udev->speed = speed_map[libusb_speed];
     usb_host_speed_compat(s);
 
-    if (s->ddesc.iProduct) {
+    if (s->ddesc.iProduct && !cpr_is_incoming()) {
         libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
                                            (unsigned char *)udev->product_desc,
                                            sizeof(udev->product_desc));

-- 
2.47.1


^ permalink raw reply	[relevance 22%]

* [PATCH QEMU HCI-8.0 v7 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800
                     ` (7 preceding siblings ...)
  2026-10-06 15:35 22% ` [PATCH QEMU HCI-8.0 v7 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:36 29% ` Andrey Drobyshev
  2026-10-06 15:52  6%   ` no-reply
                     ` (3 subsequent siblings)
  12 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:36 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

Unlike cpr-exec, cpr-transfer keeps the source QEMU alive: it hands the
usbfs FD to the new QEMU over the migration channel and only exits once
the target is up.  While the handoff is in flight both processes hold the
same FD, and only one may reap URB events on it - if both do, one reaps
the other's in-flight completions and wedges an isochronous transfer that
cannot be retried.  So reaping is handed off along with the FD:

  * The target doesn't reap until it takes over: it stops events right
    after opening the FD in .realize() and restarts in .post_load().
  * The source stops reaping in .pre_save() and, since it stays alive,
    restarts reaping from the vm-state handler's resume branch on failure.
  * On success the source's .exit() leaves the device untouched - no
    reset, interface release or host-driver rebind - and just closes its
    own copy of the FD.

Reaping is stopped and restarted on the whole libusb context, which is
shared by all usb-host devices, so cpr-transfer currently supports a
single hostdevice= usb-host at a time.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 80 ++++++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 68 insertions(+), 12 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 09ffb5d48f7..387040d6e1a 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -127,6 +127,7 @@ struct USBHostDevice {
     GSList                           *cpr_inflight;
     bool                             cpr_drained;
     bool                             cpr_replay_all;
+    bool                             cpr_handed_off;
 
     /* request queues */
     QTAILQ_HEAD(, USBHostRequest)    requests;
@@ -283,6 +284,23 @@ static void usb_host_start_events(void)
     }
 }
 
+/*
+ * cpr-transfer shares the usbfs fd with the new QEMU, so stop reaping URBs on
+ * it here -- only one process may drive the device.
+ */
+static void usb_host_stop_events(void)
+{
+    const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+    libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
+    if (poll) {
+        for (int i = 0; poll[i] != NULL; i++) {
+            usb_host_del_fd(poll[i]->fd, ctx);
+        }
+        free(poll);
+    }
+}
+
 #else
 
 static QEMUTimer *poll_timer;
@@ -306,6 +324,9 @@ static void usb_host_timer(void *opaque)
     usb_host_timer_kick();
 }
 
+static void usb_host_start_events(void) {}
+static void usb_host_stop_events(void) {}
+
 #endif /* !CONFIG_WIN32 */
 
 static int usb_host_init(void)
@@ -1272,13 +1293,18 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
 /*
  * Quiesce the URB data plane around a CPR switchover: drain the in-flight
  * transfers before the device/irqchip snapshot, so completions raised during
- * the drain are migrated too.
+ * the drain are migrated too.  For cpr-transfer, also hand reaping of the
+ * shared fd off to the target by stopping events on this side.
  */
 static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
 {
     if (usb_host_cpr_drain_xfers(s) < 0) {
         return -1;
     }
+    if (mode == MIG_MODE_CPR_TRANSFER) {
+        usb_host_stop_events();
+        s->cpr_handed_off = true;
+    }
     return 0;
 }
 
@@ -1297,6 +1323,11 @@ static void usb_host_cpr_reissue(USBHostDevice *s)
         return;
     }
 
+    if (s->cpr_handed_off) {
+        usb_host_start_events();
+        s->cpr_handed_off = false;
+    }
+
     for (it = s->cpr_inflight; it; it = it->next) {
         p = it->data;
         /*
@@ -1371,16 +1402,19 @@ static int usb_host_close(USBHostDevice *s)
 
     trace_usb_host_close(s->bus_num, s->addr);
 
-    usb_host_abort_xfers(s);
-    usb_host_iso_free_all(s);
+    if (!s->cpr_handed_off) {
+        usb_host_abort_xfers(s);
+        usb_host_iso_free_all(s);
 
-    if (udev->attached) {
-        usb_device_detach(udev);
+        if (udev->attached) {
+            usb_device_detach(udev);
+        }
+
+        usb_host_release_interfaces(s);
+        libusb_reset_device(s->dh);
+        usb_host_attach_kernel(s);
     }
 
-    usb_host_release_interfaces(s);
-    libusb_reset_device(s->dh);
-    usb_host_attach_kernel(s);
     libusb_close(s->dh);
     s->dh = NULL;
     s->dev = NULL;
@@ -1415,10 +1449,17 @@ static void usb_host_exit_notifier(struct Notifier *n, void *data)
     USBHostDevice *s = container_of(n, USBHostDevice, exit);
 
     if (s->dh) {
-        usb_host_abort_xfers(s);
-        usb_host_release_interfaces(s);
-        libusb_reset_device(s->dh);
-        usb_host_attach_kernel(s);
+        /*
+         * Handed to new QEMU across cpr-transfer: do not reset, release
+         * interfaces or rebind the host driver underneath it; just close
+         * our handle.
+         */
+        if (!s->cpr_handed_off) {
+            usb_host_abort_xfers(s);
+            usb_host_release_interfaces(s);
+            libusb_reset_device(s->dh);
+            usb_host_attach_kernel(s);
+        }
         libusb_close(s->dh);
     }
 }
@@ -1487,6 +1528,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
             error_setg(errp, "failed to open host usb device %s", s->hostdevice);
             return;
         }
+        if (cpr_is_incoming()) {
+            /*
+             * The source still owns the shared fd until it hands off.  Don't
+             * reap URBs on it yet, or we would steal the source's in-flight
+             * completions; .post_load() restarts events once we take over.
+             */
+            usb_host_stop_events();
+        }
     } else
 #endif
     if (s->match.addr && s->match.bus_num &&
@@ -1550,6 +1599,12 @@ static void usb_host_unrealize(USBDevice *udev)
     if (s->needs_autoscan) {
         QTAILQ_REMOVE(&hostdevs, s, next);
     }
+    /*
+     * An explicit device_del means we still own the device (a successful
+     * cpr-transfer handoff exits the process instead), so clear the handoff
+     * flag to force a full reset/release in usb_host_close().
+     */
+    s->cpr_handed_off = false;
     usb_host_close(s);
     migrate_del_blocker(&s->cpr_blocker);
     if (s->cpr_vmse) {
@@ -2033,6 +2088,7 @@ static int usb_host_post_load(void *opaque, int version_id)
      * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
      */
     if (cpr_is_incoming()) {
+        usb_host_start_events();
         return 0;
     }
 

-- 
2.47.1


^ permalink raw reply	[relevance 29%]

* [PATCH QEMU HCI-8.0 v7 12/14] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
                     ` (9 preceding siblings ...)
  @ 2026-10-06 15:36 10% ` Andrey Drobyshev
  2026-10-06 15:57  6%   ` no-reply
    2026-10-06 15:36 20% ` [PATCH QEMU HCI-8.0 v7 14/14] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
  12 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:36 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

On CPR incoming we skip the usb_host_open() rescan, so libusb only sets
up the endpoints from the device's default configuration.  If the guest
had enabled an endpoint by selecting a non-zero altsetting (e.g. an
isochronous streaming endpoint), that endpoint is now missing, and the
guest's transfers to it simply stall.

The altsetting itself is restored by vmload now, so let's call
usb_host_ep_update() on the CPR target to rebuild the endpoints out of
it.

The kernel claims are still there on the preserved FD, but libusb's own
handle knows nothing about them, so a later SET_INTERFACE would fail with
LIBUSB_ERROR_NOT_FOUND.  Let's re-claim the interfaces on the target as
well to bring libusb's view back in sync - on the preserved FD this is a
no-op at the usbfs level anyway.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 72 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 387040d6e1a..cbe1b91c46d 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -976,7 +976,7 @@ static void usb_host_ep_update(USBHostDevice *s)
         alt = udev->altsetting[intf->bInterfaceNumber];
 
         if (alt != 0) {
-            assert(alt < conf->interface[i].num_altsetting);
+            assert(alt >= 0 && alt < conf->interface[i].num_altsetting);
             intf = &conf->interface[i].altsetting[alt];
         }
 
@@ -2079,6 +2079,69 @@ static void usb_host_post_load_bh(void *opaque)
     usb_host_auto_check(NULL);
 }
 
+/*
+ * Reconfigure the CPR target after vmload: reject a migrated altsetting the
+ * device can't honour, then re-claim the interfaces so libusb's view matches
+ * the preserved fd (which already holds the claims).
+ */
+static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
+{
+    USBDevice *udev = USB_DEVICE(s);
+    struct libusb_config_descriptor *conf;
+    int rc;
+
+    rc = libusb_get_active_config_descriptor(s->dev, &conf);
+    if (rc == LIBUSB_ERROR_NOT_FOUND) {
+        /* device is unconfigured (address state), nothing to reclaim */
+        return 0;
+    }
+    if (rc != 0) {
+        usb_host_libusb_error("libusb_get_active_config_descriptor", rc);
+        return -1;
+    }
+
+    /*
+     * Validate all migrated interface/altsetting numbers before touching the
+     * device, so garbage migration data can't leave it half-claimed.
+     */
+    for (int i = 0; i < conf->bNumInterfaces; i++) {
+        int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
+        int alt;
+
+        if (ifnum >= USB_MAX_INTERFACES) {
+            error_report("usb-host: interface number %d out of range", ifnum);
+            libusb_free_config_descriptor(conf);
+            return -1;
+        }
+        alt = udev->altsetting[ifnum];
+        if (alt < 0 || alt >= conf->interface[i].num_altsetting) {
+            error_report("usb-host: migrated altsetting %d out of range for "
+                         "interface %d", alt, ifnum);
+            libusb_free_config_descriptor(conf);
+            return -1;
+        }
+    }
+
+    /* Data is sane: claim each interface on the fd and record it. */
+    for (int i = 0; i < conf->bNumInterfaces; i++) {
+        int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
+
+        rc = libusb_claim_interface(s->dh, ifnum);
+        if (rc != 0) {
+            usb_host_libusb_error("libusb_claim_interface", rc);
+            libusb_free_config_descriptor(conf);
+            return -1;
+        }
+        s->ifs[ifnum].claimed = true;
+    }
+
+    udev->ninterfaces = conf->bNumInterfaces;
+    udev->configuration = conf->bConfigurationValue;
+    libusb_free_config_descriptor(conf);
+
+    return 0;
+}
+
 static int usb_host_post_load(void *opaque, int version_id)
 {
     USBHostDevice *dev = opaque;
@@ -2088,6 +2151,14 @@ static int usb_host_post_load(void *opaque, int version_id)
      * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
      */
     if (cpr_is_incoming()) {
+        /*
+         * The device stayed open, so rebuild libusb's claims and endpoints,
+         * and start reaping now that the source has handed the fd off.
+         */
+        if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
+            return -1;
+        }
+        usb_host_ep_update(dev);
         usb_host_start_events();
         return 0;
     }

-- 
2.47.1


^ permalink raw reply	[relevance 10%]

* [PATCH QEMU HCI-8.0 v7 14/14] usb-host: make CPR migration blocker conditional #VSTOR-137800
                     ` (11 preceding siblings ...)
  @ 2026-10-06 15:36 20% ` Andrey Drobyshev
  12 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:36 UTC (permalink / raw)
  To: svt-core; +Cc: den, andrey.drobyshev

A previous commit added an unconditional blocker for CPR migrations.
That is to refuse CPR gracefully instead of crashing and for the previous
commits to remain bisectable.

Now the restore path is complete: the FD is preserved, in-flight URBs
are drained and replayed, the device is handed off across cpr-transfer,
and the endpoints and isochronous streams are reconstructed on the
target.  So let's lift the CPR blocker for the devices which satisfy the
preconditions.

Still, the remaining preconditions for CPR to work are:

  * We must open host device FD ourselves, so that libusb owns the FD.
    This way we're able to preserve the FD during CPR;
  * Device ID must be provided as a unique stable key for CPR FD registry.

Use them to guard the blocker addition.  Apart from that, prohibit CPR on
WIN32 platform and on hosts with older libusb versions.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 39 +++++++++++++++++++++++++++++++--------
 1 file changed, 31 insertions(+), 8 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index cbe1b91c46d..d86657f5eb3 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1563,16 +1563,39 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
         usb_host_auto_check(NULL);
     }
 
-    error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+#ifdef CONFIG_WIN32
+    error_setg(&s->cpr_blocker,
+               "usb-host device %s does not support CPR: "
+               "WIN32 platform is not supported",
                DEVICE(s)->id ?: "(anonymous)");
-    if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
-                                  MIG_MODE_CPR_TRANSFER,
-                                  MIG_MODE_CPR_EXEC, -1) < 0) {
-        if (s->needs_autoscan) {
-            QTAILQ_REMOVE(&hostdevs, s, next);
+#elif LIBUSB_API_VERSION < 0x01000107
+    error_setg(&s->cpr_blocker,
+               "usb-host device %s does not support CPR: "
+               "libusb API version 0x%08x is too old",
+               DEVICE(s)->id ?: "(anonymous)", (unsigned) LIBUSB_API_VERSION);
+#else
+    if (!(s->hostdevice && DEVICE(s)->id)) {
+        /*
+         * CPR needs the host fd owned by libusb (the hostdevice= property)
+         * and a device id to key the CPR fd registry.
+         */
+        error_setg(&s->cpr_blocker,
+                   "usb-host device %s does not support CPR: "
+                   "hostdevice= and a device id are required",
+                   DEVICE(s)->id ?: "(anonymous)");
+    }
+#endif
+
+    if (s->cpr_blocker) {
+        if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+                                      MIG_MODE_CPR_TRANSFER,
+                                      MIG_MODE_CPR_EXEC, -1) < 0) {
+            if (s->needs_autoscan) {
+                QTAILQ_REMOVE(&hostdevs, s, next);
+            }
+            usb_host_close(s);
+            return;
         }
-        usb_host_close(s);
-        return;
     }
 
     s->exit.notify = usb_host_exit_notifier;

-- 
2.47.1


^ permalink raw reply	[relevance 20%]

* Re: [PATCH QEMU HCI-8.0 v7 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
  2026-10-06 15:35 30% ` [PATCH QEMU HCI-8.0 v7 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:38  0%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:38 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev

> The FD passed into usb_host_open() for a wrapped device is only stored in
> s->hostfd after libusb_wrap_sys_device() succeeds, and the failure path
> doesn't close it.  if wrapping fails, or a later step of the open sequence
> fails (e.g. usb_device_attach), the FD is leaked, as usb_host_close() only
> runs for fully opened devices, and the realize error path doesn't clean it
> up either.
> 
> Store the FD in s->hostfd before wrapping and close it on the failure
> path.
> 
> Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

Sashiko has reviewed this patch and found no issues. It looks great!

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=1


^ permalink raw reply	[relevance 0%]

* Re: [PATCH QEMU HCI-8.0 v7 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800
  2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:45  4%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:45 UTC (permalink / raw)
  To: svt-core, Andrey Drobyshev; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] usb-host: CPR drain suppresses completed URBs and reports short OUT as full
- [Medium] usb-host: iso drain cancels in-flight URBs once, so resubmitted URBs survive
- [Medium] usb-host: timeout path completes packets mid-iteration then drops live URBs
- [Medium] usb-host: usb_host_req_cpr_replay() not gated on an active CPR drain
--

commit e57108ef005a6c96c6eda4382d78b25d2524c377
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: drain in-flight URBs across CPR #VSTOR-137800

This patch registers a vm_change_state handler that cancels and reaps all
pending usb-host URBs when the VM stops for CPR. It also adds a
.pre_save() fallback for a VM that is already paused, and cancels and
reaps isochronous URBs on their own rings. Transfers that moved no data
are suppressed so the target can replay them.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> +/*
> + * Classify a reap during the CPR drain (r->p is still set).  The paused-VM
> + * fallback (cpr_replay_all) can't deliver completions, so it suppresses every
> + * reap for the target to replay; otherwise one that moved no data is suppressed
> + * and one that moved some is kept as a short transfer.  Returns true if
> + * suppressed, so the caller frees the request.
> + */
> +static bool usb_host_req_cpr_replay(USBHostRequest *r,
> +                                    struct libusb_transfer *xfer)
> +{
> +    if (!r->host->cpr_replay_all) {
> +        if (xfer->status != LIBUSB_TRANSFER_CANCELLED) {
> +            return false;
> +        }
> +        if (xfer->actual_length != 0) {
> +            /* deliver as a short transfer */
> +            xfer->status = LIBUSB_TRANSFER_COMPLETED;
> +            return false;
> +        }
> +    }
> +    return true;
> +}

[Severity: Medium]
Does this function lose data or duplicate side effects across CPR?

In the paused-VM fallback, usb_host_pre_save() sets cpr_replay_all = true.
usb_host_req_cpr_replay() then skips its status check and returns true
for every reap, including LIBUSB_TRANSFER_COMPLETED ones.

The packet is suppressed and replayed by the target.  An IN URB that had
already received data loses it, since the device will not resend.  A bulk
OUT or control URB that already executed is sent again.

This also seems to contradict the commit message statement "A transfer
that already completed in the kernel is delivered normally".

The final series state still has this behaviour.  The later patch
"usb-host: re-issue drained URBs if CPR is aborted" only documents it in a
comment ("an OUT fragment the device consumed may be sent twice") and does
not change it.

On the normal vm_change_state path, a CANCELLED URB with actual_length != 0
is rewritten to COMPLETED:

    if (xfer->actual_length != 0) {
        /* deliver as a short transfer */
        xfer->status = LIBUSB_TRANSFER_COMPLETED;
        return false;
    }

usb_host_req_complete_data() only trims the packet length for IN:

    r->p->status = status_map[xfer->status];
    if (r->in && xfer->actual_length) {
        usb_packet_copy(r->p, r->buffer, xfer->actual_length);
    }

For an OUT packet, p->actual_length was already set to the full length
when the guest data was copied at submit time.  Can the guest be told that
the whole OUT TD succeeded when the device received only
xfer->actual_length bytes, with nothing replaying the remainder?

[Severity: Medium]
Separately, is usb_host_req_cpr_replay() meant to apply outside an active
CPR drain?

usb_host_req_complete_ctrl() and usb_host_req_complete_data() now call it
unconditionally for every completion with r->p != NULL.  With
cpr_replay_all false, nothing checks cpr_drained or any other drain-active
state.

A transfer that arrives with LIBUSB_TRANSFER_CANCELLED outside a CPR drain
would behave differently:

- actual_length == 0: the request is freed and the packet is never
  completed, which could leave it in USB_PACKET_ASYNC.
- actual_length != 0: the status is rewritten to COMPLETED.

Previously such a transfer was mapped by status_map[] to USB_RET_IOERROR.
Guest-initiated cancels clear r->p in usb_host_cancel_packet() and
usb_host_req_abort(), so they are not affected.

I could not confirm that libusb reports CANCELLED for a transfer with a
live packet outside CPR, so this one is speculative.  Should the logic be
gated on an active drain?

[ ... ]

> +static int usb_host_cpr_drain_xfers(USBHostDevice *s)
> +{

[ ... ]

> +        libusb_handle_events_timeout(ctx, &tv);
> +        if (--limit == 0) {
> +            /*
> +             * The device won't release these URBs.  Error the packets out so
> +             * the resumed guest retries them, leak the requests (libusb still
> +             * owns their transfers) and abort the migration.
> +             */
> +            QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
> +                usb_host_req_complete_err(r, USB_RET_IOERROR);
> +            }
> +            usb_host_drop_requests(s);
> +            return -1;
> +        }
> +    }

[Severity: Medium]
Can this timeout path leave live, uncancelled URBs behind?

The call chain is:

usb_host_cpr_drain_xfers()
  usb_host_req_complete_err()
    usb_packet_complete()
      usb_process_one()
        usb_host_handle_data()
          libusb_submit_transfer()

For a non-ep0 packet, usb_packet_complete() pops the next queued packet on
the endpoint and processes it.  That can allocate a new request, append it
to s->requests, and submit a new URB while QTAILQ_FOREACH_SAFE is running.

usb_host_drop_requests() then unlinks every request, including the newly
submitted ones, without cancelling them.  Would such a URB stay on the
preserved fd with no tracking request, and later complete into a callback
with stale r->p or r->host?

This needs the 100-iteration timeout plus a queued packet behind the failed
one on the same endpoint, which xHCI can have.

[ ... ]

> +    QTAILQ_FOREACH(ring, &s->isorings, next) {
> +        QTAILQ_FOREACH(xfer, &ring->inflight, next) {
> +            libusb_cancel_transfer(xfer->xfer);
> +        }
> +    }
> +
> +    /*
> +     * Cap the reap at 2x a full ring:
> +     * iso_urb_count URBs x iso_urb_frames packets each
> +     */
> +    limit = 2 * s->iso_urb_count * s->iso_urb_frames;
> +    while (usb_host_iso_inflight(s)) {
> +        struct timeval tv = { .tv_usec = 1000 };
> +        libusb_handle_events_timeout(ctx, &tv);
> +        if (limit-- == 0) {
> +            return -1;
> +        }
> +    }
> +    usb_host_iso_free_all(s);
> +    return 0;
> +}

[Severity: Medium]
Can iso URBs be resubmitted while this reap loop runs, and then escape
cancellation?

The iso URBs on ring->inflight are cancelled in a single pass, and the
reap loop does not cancel again.  For an IN endpoint,
usb_host_req_complete_iso() moves the xfer to ring->copy and calls
usb_wakeup():

    QTAILQ_INSERT_TAIL(&xfer->ring->copy, xfer, next);
    usb_wakeup(xfer->ring->ep, 0);

usb_wakeup() reaches xhci_wakeup_endpoint(), which calls xhci_kick_ep()
and xhci_kick_epctx() without checking the VM run state.  The kick can
reprocess an iso IN packet through usb_host_handle_data() and
usb_host_iso_data_in(), which moves ring->unused entries to inflight with
libusb_submit_transfer().

If that happens, the reap loop either runs out its limit and returns -1,
or it exits with new iso URBs from the old process still submitted on the
preserved fd.  The control/bulk loop above re-cancels on every iteration,
but this one does not.

I did not prove that a guest iso TD is always available at that point.
Should the iso phase re-cancel inflight URBs on each iteration as well?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=6

^ permalink raw reply	[relevance 4%]

* Re: [PATCH QEMU HCI-8.0 v7 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
  2026-10-06 15:35 24% ` [PATCH QEMU HCI-8.0 v7 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:46  4%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:46 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] usb-host: replay_all queues completed URBs, so aborted CPR re-sends or drops data
--

commit 2947a35eaf543430e9227a58d6c2b6610a15e890
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800

This patch records the packets suppressed during the CPR drain on a
cpr_inflight list. If CPR is aborted, the vm-state handler's resume branch
re-issues them on the preserved fd. Control transfers are completed with an
error instead of being re-issued.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> @@ -450,6 +453,9 @@ static bool usb_host_req_cpr_replay(USBHostRequest *r,
>              return false;
>          }
>      }
> +    if (r->p->state == USB_PACKET_ASYNC) {
> +        r->host->cpr_inflight = g_slist_append(r->host->cpr_inflight, r->p);
> +    }
>      return true;
>  }

[Severity: Medium]
When cpr_replay_all is set by the paused-VM usb_host_pre_save() fallback,
the status and actual_length checks above are skipped. Does
usb_host_req_cpr_replay() then queue every reaped URB on cpr_inflight,
including URBs that completed successfully?

The request is then freed without its data being copied into the packet.

[ ... ]

> +    for (it = s->cpr_inflight; it; it = it->next) {
> +        p = it->data;
> +        /*
> +         * Replay from the start of the TD.  A transfer that already moved some
> +         * data is re-run in full, so an OUT fragment the device consumed may be
> +         * sent twice.  Only the paused-VM pre_save fallback queues such partial
> +         * transfers here; the normal drain delivers them as short transfers.
> +         */
> +        p->actual_length = 0;
> +        if (p->ep->nr == 0) {
> +            /* control endpoint: can't be reconstructed, so error it out */
> +            p->status = USB_RET_IOERROR;
> +            usb_generic_async_ctrl_complete(udev, p);
> +        } else {
> +            usb_host_handle_data(udev, p);

If CPR aborts after that fallback, usb_host_cpr_reissue() re-submits these
fully completed transfers from the start:

 - A completed OUT transfer can be sent to the physical device a second
   time.
 - A completed IN transfer has its data discarded, and a new read is
   issued.
 - A completed control transfer is failed with USB_RET_IOERROR, even
   though the device already executed it.

The commit message and the comment above only mention a partial OUT
fragment being sent twice. Could they, or the queueing in
usb_host_req_cpr_replay(), be changed to cover fully completed transfers
too?

This could matter for non-idempotent streams such as mass storage, serial
or printer traffic.

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=7

^ permalink raw reply	[relevance 4%]

* Re: [PATCH QEMU HCI-8.0 v7 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800
  2026-10-06 15:35 22% ` [PATCH QEMU HCI-8.0 v7 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:47  5%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:47 UTC (permalink / raw)
  To: svt-core, Andrey Drobyshev; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] usb-host: cpr_is_incoming() guard drops product string for any open
- [Low] usb-host: only the string read is skipped; other control I/O in open still runs
--

commit e0b84d4b8555ddc11f7b8275a12f5907c2ba320f
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: skip product-string read on CPR incoming #VSTOR-137800

This patch skips the libusb product string descriptor read in
usb_host_open() when cpr_is_incoming() is true. It uses the synthetic
"host:bus.addr" name for product_desc instead.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> index d90117e4292d..271456abb0a1 100644
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> @@ -1111,7 +1111,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
>      udev->speed = speed_map[libusb_speed];
>      usb_host_speed_compat(s);
>  
> -    if (s->ddesc.iProduct) {
> +    if (s->ddesc.iProduct && !cpr_is_incoming()) {

[Severity: Low]
Is this guard too broad? cpr_is_incoming() is defined as
"incoming_mode != MIG_MODE_NONE", so it is true for the whole
incoming-migration phase. It is not a per-device check.

usb_host_open() is reached from several paths:

    usb_host_realize()    (hostfd and bus/addr paths)
    usb_host_auto_check() (autoscan)

Any device opened while the flag is set takes the synthetic branch. That
includes autoscan or hotplug matches of devices that are not being handed
over.

product_desc is set once in usb_host_open() and is not refreshed. Can
those devices end up keeping the "host:bus.addr" name after the migration
completes?

[Severity: Low]
Is the product string read the only call in usb_host_open() that can block
during the handover? The neighbouring calls still run unguarded:

    usb_host_detach_kernel(s);
    ...
    usb_host_speed_compat(s);    /* libusb_get_config_descriptor() */

The fail: path also calls libusb_reset_device().

If synchronous control transfers can block on the CPR target, could the
incoming migration still hang in one of these calls? The commit message
doesn't say what causes the blocking, so it is unclear whether skipping
only the string read is enough.

>          libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
>                                             (unsigned char *)udev->product_desc,
>                                             sizeof(udev->product_desc));

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=8

^ permalink raw reply	[relevance 5%]

* Re: [PATCH QEMU HCI-8.0 v7 11/14] usb: migrate the interface altsetting #VSTOR-137800
  @ 2026-10-06 15:51  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:51 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] usb: altsetting subsection in generic vmstate_usb_device has no consumer for emulated devices
--

commit c0dc143b080568d7b0883600b0776ce496b368ca
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb: migrate the interface altsetting #VSTOR-137800

This patch adds a vmstate subsection to the shared USBDevice vmstate that
carries the altsetting[] array. The subsection is emitted only in the CPR
migration modes and only when some altsetting[] entry is non-zero.

> diff --git a/hw/usb/bus.c b/hw/usb/bus.c
> --- a/hw/usb/bus.c
> +++ b/hw/usb/bus.c

[ ... ]

> +static bool usb_altsetting_needed(void *opaque)
> +{
> +    USBDevice *dev = opaque;
> +    int i;
> +
> +    /*
> +     * Only the CPR target consumes this, to rebuild its endpoints; other
> +     * migrations reopen the device and reconstruct the altsetting.
> +     */
> +    if (!cpr_incoming_needed(NULL)) {
> +        return false;
> +    }
> +    for (i = 0; i < USB_MAX_INTERFACES; i++) {
> +        if (dev->altsetting[i]) {
> +            return true;
> +        }
> +    }
> +    return false;
> +}

[Severity: Low]
Is this gate too broad? usb_altsetting_needed() checks only
cpr_incoming_needed() and whether any dev->altsetting[i] is non-zero. It
does not check the device type, but the stated reason (the CPR target
rebuilds its endpoints) seems to hold only for usb-host.

[ ... ]

> +    .subsections = (const VMStateDescription * const []) {
> +        &vmstate_usb_device_altsetting,
> +        NULL
>      }

The subsection is attached to the shared vmstate_usb_device, so it is
emitted and loaded for every USB device type, including emulated devices.

For an emulated device, usb_desc_set_interface() in hw/usb/desc.c updates
the three together:

    old = dev->altsetting[index];
    dev->altsetting[index] = value;
    dev->ifaces[index] = iface;
    usb_desc_ep_init(dev);

After a CPR load, only altsetting[] is restored. ifaces[] and the endpoint
table stay at altsetting 0.

Can GET_INTERFACE then report the guest's non-zero altsetting while the
device model behaves as if altsetting 0 were active? Before this patch the
state was wrong but self-consistent. It is still wrong after the patch, so
emulated devices gain nothing from it.

The consumer in this series is only in usb_host_post_load() in
hw/usb/host-libusb.c, which calls usb_host_cpr_reclaim_interfaces() and
usb_host_ep_update(). Nothing re-applies ifaces[] or calls
usb_desc_ep_init() for emulated devices.

I did not verify which emulated devices have multiple altsettings or can be
CPR-migrated, so this may be of limited reach. Would it make sense to limit
the subsection to usb-host, or to restore ifaces[] and the endpoints in a
generic post_load?

Separately, the commit message says "The active altsetting of each
interface is host controller state". Is that accurate? altsetting[] is a
field of USBDevice, not of the host controller.

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=11

^ permalink raw reply	[relevance 3%]

* Re: [PATCH QEMU HCI-8.0 v7 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800
  2026-10-06 15:36 29% ` [PATCH QEMU HCI-8.0 v7 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:52  6%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:52 UTC (permalink / raw)
  To: svt-core, Andrey Drobyshev; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Medium] usb-host: cpr_handed_off set before migration outcome is known, skips cleanup on failure
- [Medium] usb-host: cpr_is_incoming() in realize() stops global event reaping with no restart
- [Medium] usb-host: unrealize() clears handoff flag without restarting the global event reaping
- [Low] usb-host: unrealize() forces full reset even if the target may already own the device
- [Low] usb-host: commit message does not match where reaping is stopped and restarted
--

commit d5825316f822699756346d45e052e4ba605a47fe
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: hand off the device across cpr-transfer #VSTOR-137800

This patch hands the usbfs FD and URB reaping off from the source QEMU to
the target QEMU during cpr-transfer. The source stops reaping at quiesce
time and skips device reset, interface release and driver rebind on exit.
The target stops reaping in realize() and restarts it in post_load().

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> +static void usb_host_stop_events(void)
> +{
> +    const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
> +
> +    libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
> +    if (poll) {
> +        for (int i = 0; poll[i] != NULL; i++) {
> +            usb_host_del_fd(poll[i]->fd, ctx);
> +        }
> +        free(poll);
> +    }
> +}

[ ... ]

>  static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
>  {
>      if (usb_host_cpr_drain_xfers(s) < 0) {
>          return -1;
>      }
> +    if (mode == MIG_MODE_CPR_TRANSFER) {
> +        usb_host_stop_events();
> +        s->cpr_handed_off = true;
> +    }
>      return 0;
>  }

[Severity: Medium]
Is it safe to set cpr_handed_off here, before the outcome of the migration
is known?

The flag is cleared only in usb_host_cpr_reissue(), which runs on a running
transition, and in usb_host_unrealize().

Consider the case where usb_host_pre_save() calls usb_host_cpr_quiesce() as
the fallback for an already-paused VM, and the migration then fails.  Nothing
resumes the VM, so cpr_handed_off stays true.

usb_host_exit_notifier() then skips the following on every later quit and
only calls libusb_close():

    usb_host_abort_xfers(s);
    usb_host_release_interfaces(s);
    libusb_reset_device(s->dh);
    usb_host_attach_kernel(s);

Can this leave the device without a reset and without the host kernel
driver rebound after a failed or cancelled cpr-transfer?

The commit message says the skip happens only "On success", but the exit
notifier cannot tell success from failure.

[ ... ]

> @@ -1371,16 +1402,19 @@ static int usb_host_close(USBHostDevice *s)
>
>      trace_usb_host_close(s->bus_num, s->addr);
>
> -    usb_host_abort_xfers(s);
> -    usb_host_iso_free_all(s);
> +    if (!s->cpr_handed_off) {
> +        usb_host_abort_xfers(s);
> +        usb_host_iso_free_all(s);
>
> -    if (udev->attached) {
> -        usb_device_detach(udev);
> +        if (udev->attached) {
> +            usb_device_detach(udev);
> +        }
> +
> +        usb_host_release_interfaces(s);
> +        libusb_reset_device(s->dh);
> +        usb_host_attach_kernel(s);
>      }
>
> -    usb_host_release_interfaces(s);
> -    libusb_reset_device(s->dh);
> -    usb_host_attach_kernel(s);
>      libusb_close(s->dh);
>      s->dh = NULL;
>      s->dev = NULL;

[Severity: Medium]
The cpr_handed_off gate here also skips the QEMU-local steps
usb_host_abort_xfers(), usb_host_iso_free_all() and usb_device_detach(),
not only the reset, release and rebind that must not run under the target.

If usb_host_close() is reached while the flag is stale, for example via
usb_host_nodev_bh() or usb_host_auto_check(), s->dh and s->dev are still set
to NULL.  Can that leave a guest-attached device with no libusb handle?

Should the gate cover only the device-affecting calls (release, reset,
rebind)?

[ ... ]

> @@ -1487,6 +1528,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
> +        if (cpr_is_incoming()) {
> +            /*
> +             * The source still owns the shared fd until it hands off.  Don't
> +             * reap URBs on it yet, or we would steal the source's in-flight
> +             * completions; .post_load() restarts events once we take over.
> +             */
> +            usb_host_stop_events();
> +        }

[Severity: Medium]
Can this stop event reaping permanently for a device that is not migrated?

cpr_is_incoming() is "incoming_mode != MIG_MODE_NONE", and incoming_mode
does not appear to be reset after the migration completes.

If a usb-host device with hostdevice= is hotplugged on the target after the
CPR migration, usb_host_realize() takes this branch and calls
usb_host_stop_events(), which clears the pollfd notifiers and fd handlers on
the shared libusb ctx.

usb_host_post_load() is the only place that restarts events, and a hotplugged
device has no vmstate section to load.  Would URB completions then stop being
reaped for that device and for every other usb-host device?

[Severity: Low]
This isn't a functional problem, but the commit message and the code differ
in a few places:

    The target doesn't reap until it takes over: it stops events right
    after opening the FD in .realize() and restarts in .post_load().

The condition used is cpr_is_incoming(), which is true for any incoming CPR
mode, including cpr-exec, where the fd is not shared.

    The source stops reaping in .pre_save() and, since it stays alive,
    restarts reaping from the vm-state handler's resume branch on failure.

The stop happens in usb_host_cpr_quiesce(), which usb_host_cpr_vm_state()
calls on the FINISH_MIGRATE transition.  usb_host_pre_save() reaches it only
as a fallback for an already-paused VM.  Could the commit message be updated
to describe this?

[ ... ]

> @@ -1550,6 +1599,12 @@ static void usb_host_unrealize(USBDevice *udev)
> +    /*
> +     * An explicit device_del means we still own the device (a successful
> +     * cpr-transfer handoff exits the process instead), so clear the handoff
> +     * flag to force a full reset/release in usb_host_close().
> +     */
> +    s->cpr_handed_off = false;
>      usb_host_close(s);
>      migrate_del_blocker(&s->cpr_blocker);
>      if (s->cpr_vmse) {
>          qemu_del_vm_change_state_handler(s->cpr_vmse);
>      }

[Severity: Medium]
Does this path leave event reaping stopped for the other usb-host devices?

Suppose usb_host_cpr_quiesce() has called usb_host_stop_events() on the whole
ctx, and the migration then fails or is cancelled while the VM is paused.  No
running transition occurs, so usb_host_cpr_reissue() never calls
usb_host_start_events().

A later device_del reaches usb_host_unrealize(), which sets
cpr_handed_off = false and removes cpr_vmse without calling
usb_host_start_events().  usb_host_init() returns early once ctx exists, so
nothing else restarts reaping.

Can transfers on the other usb-host devices (hostdevice=, autoscan or
bus/addr) hang here, since the stop applies to the shared context?  The
commit message says only one hostdevice= device is supported, but nothing in
the code appears to enforce that.

[Severity: Low]
Can this unconditional clearing of cpr_handed_off cause a reset of a device
the target already owns?

The comment assumes that unrealize means the source still owns the device.
During the handoff window the source has cpr_handed_off = true and the target
may already be reaping.  If a device_del or guest eject reaches
usb_host_unrealize() then, usb_host_close() runs usb_host_abort_xfers(),
usb_host_release_interfaces(), libusb_reset_device() and
usb_host_attach_kernel() on the shared fd.

Is it intended that this disrupts the target's session?

[ ... ]

> @@ -2033,6 +2088,7 @@ static int usb_host_post_load(void *opaque, int version_id)
>      if (cpr_is_incoming()) {
> +        usb_host_start_events();
>          return 0;
>      }

This is the only restart point on the target, and it runs only for devices
whose vmstate section is loaded, as noted in the usb_host_realize() comment
above.

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=10

^ permalink raw reply	[relevance 6%]

* Re: [PATCH QEMU HCI-8.0 v7 13/14] hcd-xhci: resync isochronous endpoints after CPR #VSTOR-137800
  @ 2026-10-06 15:56  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:56 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] usb-xhci: cpr_iso_resync flag stays armed if post-CPR kick submits no ISO TD
- [Medium] usb-xhci: CPR iso desync worked around with fake missed-service error
--

commit 775b0f619a3e5117de7a9bf189a6e7024d14f114
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

hcd-xhci: resync isochronous endpoints after CPR #VSTOR-137800

This patch sets a per-endpoint flag in usb_xhci_post_load() for running
isochronous endpoints when a CPR resume is incoming. The next isochronous
TD seen by xhci_submit() is then completed with CC_MISSED_SERVICE_ERROR
instead of being sent to the device. The aim is to make the guest driver
skip ahead and resync.

> diff --git a/hw/usb/hcd-xhci.c b/hw/usb/hcd-xhci.c
> --- a/hw/usb/hcd-xhci.c
> +++ b/hw/usb/hcd-xhci.c

[ ... ]

> @@ -1047,6 +1049,11 @@ static void xhci_set_ep_state(XHCIState *xhci, XHCIEPContext *epctx,
>      ctx[0] &= ~EP_STATE_MASK;
>      ctx[0] |= state;
>  
> +    if (state != EP_RUNNING) {
> +        /* a stop/reset before the first kick ends the post-CPR resync window */
> +        epctx->cpr_iso_resync = false;
> +    }
> +

[Severity: Medium]
Can cpr_iso_resync stay armed after the post-CPR kick?

usb_xhci_post_load() sets the flag for every EP_RUNNING isochronous
endpoint, without checking whether a TD is pending, and then arms
kick_timer.

The kick reaches xhci_kick_epctx(), which calls
xhci_set_ep_state(..., EP_RUNNING). The clear above only runs when
state != EP_RUNNING, so the flag survives.

xhci_kick_epctx() can also leave without calling xhci_submit(). It returns
early when xhci_slot_ok() fails, when the endpoint is halted, or when
!ring->dequeue. It breaks out of the loop when
xhci_ring_chain_length() <= 0:

    length = xhci_ring_chain_length(xhci, ring);
    if (length <= 0) {
        if (epctx->type == ET_ISO_OUT || epctx->type == ET_ISO_IN) {
            ... CC_RING_OVERRUN : CC_RING_UNDERRUN;
        }
        break;
    }

It also breaks out when xhci_ep_alloc_xfer() returns NULL. None of these
paths clear the flag.

If the guest queues an isochronous TD later, long after resume, will that
TD hit the resync branch in xhci_submit()? It would then be completed with
CC_MISSED_SERVICE_ERROR and never sent to the device.

This differs from the comment and the commit message, which describe the
window as the "first kick" or the "first isochronous transfer after a CPR
resume". As written, the window ends at the first isochronous TD submitted,
however late it arrives.

An isochronous endpoint that is EP_RUNNING but idle across CPR, for
example an opened USB audio device with no TDs queued yet, would lose one
valid TD this way.

Should the flag be cleared at the end of the post-CPR kick, whether or not
a TD was submitted?

[ ... ]

> @@ -1811,6 +1818,19 @@ static int xhci_submit(XHCIState *xhci, XHCITransfer *xfer, XHCIEPContext *epctx
>          xfer->pkts = 1;
>          xfer->iso_xfer = true;
>          xfer->timed_xfer = true;
> +        if (epctx->cpr_iso_resync) {
> +            /*
> +             * First iso TD after a CPR resume: our transfer state was
> +             * rebuilt from guest memory and no longer lines up with what
> +             * the guest consumed.  Report a missed service, like real HW,
> +             * to kick the driver's skip logic into resyncing.
> +             */
> +            epctx->cpr_iso_resync = false;
> +            xfer->status = CC_MISSED_SERVICE_ERROR;
> +            xhci_xfer_report(xfer);
> +            xfer->complete = 1;
> +            return 0;
> +        }

[Severity: Medium]
Is a fabricated CC_MISSED_SERVICE_ERROR the right way to handle this?

In xhci_submit(), the first isochronous TD per endpoint is completed with
an error and never sent to the device. For ISO IN the data is lost, and for
ISO OUT the data is never sent.

Recovery depends on the guest driver's skip logic. The commit message
describes Linux behaviour. Will other guests such as Windows or BSD resync
from a Missed Service event? That is not established by the patch.

The commit message says the controller's "transient transfer and scheduling
state" no longer matches what the guest consumed, but it does not say which
state diverges. It also does not say why the dequeue pointer in the endpoint
context is insufficient.

In usb_xhci_post_load(), the endpoint is rebuilt only through
xhci_alloc_epctx() and xhci_init_epctx():

    xhci_init_epctx(epctx, pctx, ep_ctx);
    epctx->state = state;

After this, mfindex_last is 0 and the transfer list is empty. Could
restoring or re-deriving that scheduling state be a better fix than
discarding the TD?

[ ... ]

> @@ -3510,6 +3530,11 @@ static int usb_xhci_post_load(void *opaque, int version_id)
>              xhci_init_epctx(epctx, pctx, ep_ctx);
>              epctx->state = state;
>              if (state == EP_RUNNING) {
> +                if (cpr_is_incoming() &&
> +                    (epctx->type == ET_ISO_IN || epctx->type == ET_ISO_OUT)) {
> +                    /* only iso streams need resyncing; see xhci_submit() */
> +                    epctx->cpr_iso_resync = true;
> +                }
>                  /* kick endpoint after vmload is finished */
>                  timer_mod(epctx->kick_timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL));
>              }

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=13

^ permalink raw reply	[relevance 3%]

* Re: [PATCH QEMU HCI-8.0 v7 12/14] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
  2026-10-06 15:36 10% ` [PATCH QEMU HCI-8.0 v7 12/14] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:57  6%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:57 UTC (permalink / raw)
  To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev, den

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Medium] usb-host: failed CPR post_load makes target exit notifier reset the shared device
- [Medium] usb-host: CPR reclaim claims all interfaces and sets configuration even if guest never configured
- [Medium] usb-host: CPR post_load dereferences NULL dev/dh when device was not opened
- [Low] usb-host: usb_host_ep_update() on CPR target resets endpoint halt state
- [Low] usb-host: reclaim comment claims no half-claimed state but claim loop can fail midway
--

commit 557f801d929f427a0cdb08c818b08fd2db0316df
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800

This patch makes the CPR target call usb_host_ep_update() after vmload, so
endpoints for a non-zero altsetting are rebuilt. It also re-claims the
interfaces through libusb so libusb's view matches the preserved fd. The
new helper usb_host_cpr_reclaim_interfaces() validates the migrated
altsetting values before claiming.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> +static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
> +{
> +    USBDevice *udev = USB_DEVICE(s);
> +    struct libusb_config_descriptor *conf;
> +    int rc;
> +
> +    rc = libusb_get_active_config_descriptor(s->dev, &conf);

[Severity: Medium]
Can s->dev (and later s->dh in libusb_claim_interface()) be NULL here?

usb_host_post_load() takes the CPR branch whenever cpr_is_incoming(), and
neither usb_host_cpr_reclaim_interfaces() nor usb_host_ep_update() checks
dev->dh or dev->dev first.

On the target, only the hostdevice= path opens the device in realize().
With autoscan (hostvendor/hostproduct/hostport), usb_host_auto_check()
only calls usb_host_open() under runstate_is_running().

If the target command line differs from the source's, a NULL pointer would
be passed to libusb.  This is somewhat speculative.  Would an
"if (!dev->dh)" guard make sense?

[ ... ]

> +    /* Data is sane: claim each interface on the fd and record it. */
> +    for (int i = 0; i < conf->bNumInterfaces; i++) {
> +        int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
> +
> +        rc = libusb_claim_interface(s->dh, ifnum);
> +        if (rc != 0) {
> +            usb_host_libusb_error("libusb_claim_interface", rc);
> +            libusb_free_config_descriptor(conf);
> +            return -1;
> +        }
> +        s->ifs[ifnum].claimed = true;
> +    }
> +
> +    udev->ninterfaces = conf->bNumInterfaces;
> +    udev->configuration = conf->bConfigurationValue;

[Severity: Medium]
Does this claim every interface and set udev->configuration even when the
guest never configured the device on the source?

On the source, claims are only made in usb_host_claim_interfaces(), which
is reached from usb_host_set_config().  usb_host_open() only detaches
kernel drivers.

If the guest had not issued SET_CONFIGURATION at CPR time,
libusb_get_active_config_descriptor() can still succeed for a
kernel-autoconfigured device.  The target would then create claims the
source never made.  It would also set configuration and ninterfaces from
the host's active config, so the guest would see a configured device.

If migration fails and the source resumes, its ifs[].claimed flags and
configuration would disagree with the kernel state.

The commit message says the re-claim is "a no-op at the usbfs level
anyway".  Does that hold when the source never claimed the interfaces?  I
could not confirm that this case is reachable in practice.

> +    libusb_free_config_descriptor(conf);
> +
> +    return 0;
> +}

[Severity: Low]
The comment above this function and the one in the validation loop say
validation happens first so "garbage migration data can't leave it
half-claimed".  However, the claim loop can still fail partway through
libusb_claim_interface().

In that case the function returns -1 with the earlier interfaces claimed
and marked in s->ifs[].claimed, and nothing rolls that back.  Could the
comment be adjusted, or the claims be undone on failure?

>  static int usb_host_post_load(void *opaque, int version_id)
>  {
>      USBHostDevice *dev = opaque;
[ ... ]
>      if (cpr_is_incoming()) {
> +        /*
> +         * The device stayed open, so rebuild libusb's claims and endpoints,
> +         * and start reaping now that the source has handed the fd off.
> +         */
> +        if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
> +            return -1;
> +        }

[Severity: Medium]
Can this new failure return cause the target's exit notifier to reset the
shared device?

If usb_host_cpr_reclaim_interfaces() returns -1, the incoming migration
fails and the target process exits through usb_host_exit_notifier().  On
the target, s->dh is set and s->cpr_handed_off is false, because it is
only set by usb_host_cpr_quiesce() on the source:

usb_host_exit_notifier() {
    ...
    if (!s->cpr_handed_off) {
        usb_host_abort_xfers(s);
        usb_host_release_interfaces(s);
        libusb_reset_device(s->dh);
        usb_host_attach_kernel(s);
    }
    ...
}

That would reset and release a device on an fd that the still-live source
expects to keep.  Any s->ifs[].claimed flags set by a partial reclaim also
feed usb_host_release_interfaces().

The notifier still gates this only on cpr_handed_off, and the later patch
"usb-host: make CPR migration blocker conditional" does not change that.
Should the target be marked as not owning the device when post_load fails?

> +        usb_host_ep_update(dev);

[Severity: Low]
Does usb_host_ep_update() here reset per-endpoint runtime state that the
guest changed on the source?

It calls usb_ep_reset() and then usb_ep_set_halted(udev, pid, ep, 0) for
every endpoint, and the vmstate carries only the altsetting.  A halted
endpoint would be un-halted without CLEAR_FEATURE(ENDPOINT_HALT), and
max_streams would be rebuilt in default form.

>          usb_host_start_events();
>          return 0;
>      }

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=12

^ permalink raw reply	[relevance 6%]

* [PATCH QEMU HCI-8.0 v8 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
  @ 2026-10-06 16:27 30% ` Andrey Drobyshev
  2026-10-06 16:31  6%   ` no-reply
  2026-10-06 16:27  6% ` [PATCH QEMU HCI-8.0 v8 02/14] usb-host: don't leak config descriptor on claim " Andrey Drobyshev
                   ` (10 subsequent siblings)
  11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 16:27 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

The FD passed into usb_host_open() for a wrapped device is only stored in
s->hostfd after libusb_wrap_sys_device() succeeds, and the failure path
doesn't close it.  if wrapping fails, or a later step of the open sequence
fails (e.g. usb_device_attach), the FD is leaked, as usb_host_close() only
runs for fully opened devices, and the realize error path doesn't clean it
up either.

Store the FD in s->hostfd before wrapping and close it on the failure
path.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b74670ae256..d4a8c68e6d4 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -961,6 +961,11 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
         goto fail;
     }
 
+    /* Track the caller-owned FD so the fail: path frees it. */
+    if (!dev) {
+        s->hostfd = hostfd;
+    }
+
     if (dev) {
         bus_num = libusb_get_bus_number(dev);
         addr = libusb_get_device_address(dev);
@@ -978,7 +983,6 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
         if (rc != 0) {
             goto fail;
         }
-        s->hostfd  = hostfd;
         dev = libusb_get_device(s->dh);
         bus_num = libusb_get_bus_number(dev);
         addr = libusb_get_device_address(dev);
@@ -1066,6 +1070,10 @@ fail:
         s->dh = NULL;
         s->dev = NULL;
     }
+    if (s->hostfd != -1) {
+        close(s->hostfd);
+        s->hostfd = -1;
+    }
     return -1;
 }
 

-- 
2.47.1


^ permalink raw reply	[relevance 30%]

* [PATCH QEMU HCI-8.0 v8 03/14] usb-host: add migration blocker for CPR modes #VSTOR-137800
    2026-10-06 16:27 30% ` [PATCH QEMU HCI-8.0 v8 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
  2026-10-06 16:27  6% ` [PATCH QEMU HCI-8.0 v8 02/14] usb-host: don't leak config descriptor on claim " Andrey Drobyshev
@ 2026-10-06 16:27 13% ` Andrey Drobyshev
  2026-10-06 16:27 26% ` [PATCH QEMU HCI-8.0 v8 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 16:27 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

We're about to add support for CPR migration to usb-host in the upcoming
commits.  Right now CPR is crashing with usb-host devices.  Let's add
migration blocker upfront, for both cpr-exec and cpr-transfer.  The
blocker is added unconditionally, so that we refuse CPR migration instead
of crashing on it.  That is for the sake of bisectability of the upcoming
commits.  It is going to be lifted (conditioned) in a following patch
once CPR support is added.  Also, blocker is added at the end of
usb_host_realize(), so we have to cleanup all the side effects done
earlier in .realize() in case migrate_add_blocker_modes() fails.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 7c8b65ad5af..e3bd7bcf749 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -46,6 +46,7 @@
 #endif
 
 #include "qapi/error.h"
+#include "migration/blocker.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -118,6 +119,9 @@ struct USBHostDevice {
     bool                             bh_postld_pending;
     Notifier                         exit;
 
+    /* CPR migration */
+    Error                            *cpr_blocker;
+
     /* request queues */
     QTAILQ_HEAD(, USBHostRequest)    requests;
     QTAILQ_HEAD(, USBHostIsoRing)    isorings;
@@ -1256,6 +1260,18 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
         usb_host_auto_check(NULL);
     }
 
+    error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+               DEVICE(s)->id ?: "(anonymous)");
+    if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+                                  MIG_MODE_CPR_TRANSFER,
+                                  MIG_MODE_CPR_EXEC, -1) < 0) {
+        if (s->needs_autoscan) {
+            QTAILQ_REMOVE(&hostdevs, s, next);
+        }
+        usb_host_close(s);
+        return;
+    }
+
     s->exit.notify = usb_host_exit_notifier;
     qemu_add_exit_notifier(&s->exit);
 }
@@ -1279,6 +1295,7 @@ static void usb_host_unrealize(USBDevice *udev)
         QTAILQ_REMOVE(&hostdevs, s, next);
     }
     usb_host_close(s);
+    migrate_del_blocker(&s->cpr_blocker);
 }
 
 static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)

-- 
2.47.1


^ permalink raw reply	[relevance 13%]

* [PATCH QEMU HCI-8.0 v8 02/14] usb-host: don't leak config descriptor on claim failure #VSTOR-137800
    2026-10-06 16:27 30% ` [PATCH QEMU HCI-8.0 v8 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:27  6% ` Andrey Drobyshev
  2026-10-06 16:27 13% ` [PATCH QEMU HCI-8.0 v8 03/14] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 16:27 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

usb_host_claim_interfaces() fetches the active config descriptor but
frees it only on the success path.  When not every interface can be
claimed it returns USB_RET_STALL without freeing it, leaking the
descriptor.

Free it before the error return.

Fixes: 896b6757f9c4 ("usb-host: support devices with sparse/non-sequential USB interfaces")
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index d4a8c68e6d4..7c8b65ad5af 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1381,6 +1381,7 @@ static int usb_host_claim_interfaces(USBHostDevice *s, int configuration)
         }
     }
     if (claimed != conf->bNumInterfaces) {
+        libusb_free_config_descriptor(conf);
         return USB_RET_STALL;
     }
 

-- 
2.47.1


^ permalink raw reply	[relevance 6%]

* [PATCH QEMU HCI-8.0 v8 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800
                     ` (4 preceding siblings ...)
  2026-10-06 16:27 25% ` [PATCH QEMU HCI-8.0 v8 05/14] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:27 26% ` Andrey Drobyshev
  2026-10-06 16:38  5%   ` no-reply
  2026-10-06 16:27 24% ` [PATCH QEMU HCI-8.0 v8 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
                   ` (5 subsequent siblings)
  11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 16:27 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

URBs submitted by the old QEMU must not outlive it across a CPR
switchover: the kernel would complete them into buffers of an address
space that is gone after exec, and the new libusb context would reap URB
pointers it never submitted.

Register a vm_change_state handler that, when the VM stops for CPR
(RUN_STATE_FINISH_MIGRATE), cancels and reaps every pending transfer.
It runs before the vCPU/device snapshot, so a completion raised during
the drain lands in the still-live interrupt controller and is migrated
too.  A transfer that already completed in the kernel is delivered
normally; one cancelled after moving some data is delivered as a short
transfer; one that moved nothing is suppressed, and the target replays
it from the start of the TD via usb_xhci_post_load().  A VM already
paused when migration starts never hits that transition, so .pre_save()
drains as a fallback, suppressing every URB for the target to replay.

Isochronous URBs live on their own rings; cancel and reap them too -- the
target reconstructs the stream from the guest's transfer ring.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 249 +++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 222 insertions(+), 27 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 1a79cdf9e4d..413ddc12ee4 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -48,6 +48,7 @@
 #include "qapi/error.h"
 #include "migration/blocker.h"
 #include "migration/cpr.h"
+#include "migration/misc.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -122,6 +123,9 @@ struct USBHostDevice {
 
     /* CPR migration */
     Error                            *cpr_blocker;
+    VMChangeStateEntry               *cpr_vmse;
+    bool                             cpr_drained;
+    bool                             cpr_replay_all;
 
     /* request queues */
     QTAILQ_HEAD(, USBHostRequest)    requests;
@@ -405,7 +409,10 @@ static void usb_host_req_free(USBHostRequest *r)
 #ifdef CONFIG_WIN32
     request_count--;
 #endif
-    QTAILQ_REMOVE(&r->host->requests, r, next);
+    /* Skip if a timed-out reap already unlinked it (avoid a double remove) */
+    if (QTAILQ_IN_USE(r, next)) {
+        QTAILQ_REMOVE(&r->host->requests, r, next);
+    }
     libusb_free_transfer(r->xfer);
     g_free(r->buffer);
     g_free(r);
@@ -423,6 +430,41 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
     return NULL;
 }
 
+/*
+ * Classify a reap during the CPR drain (r->p is still set).  The paused-VM
+ * fallback (cpr_replay_all) can't deliver completions, so it suppresses every
+ * reap for the target to replay; otherwise one that moved no data is suppressed
+ * and one that moved some is kept as a short transfer.  Returns true if
+ * suppressed, so the caller frees the request.
+ */
+static bool usb_host_req_cpr_replay(USBHostRequest *r,
+                                    struct libusb_transfer *xfer)
+{
+    if (!r->host->cpr_drained) {
+        /* no CPR drain in progress: normal completion */
+        return false;
+    }
+    if (!r->host->cpr_replay_all) {
+        if (xfer->status != LIBUSB_TRANSFER_CANCELLED) {
+            return false;
+        }
+        if (xfer->actual_length != 0) {
+            /* deliver as a short transfer */
+            xfer->status = LIBUSB_TRANSFER_COMPLETED;
+            if (!r->in) {
+                /*
+                 * complete_data() only trims the length for IN; for OUT
+                 * report what the device actually consumed rather than
+                 * the full submit-time size.
+                 */
+                r->p->actual_length = xfer->actual_length;
+            }
+            return false;
+        }
+    }
+    return true;
+}
+
 static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
 {
     USBHostRequest *r = xfer->user_data;
@@ -432,6 +474,9 @@ static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
     if (r->p == NULL) {
         goto out; /* request was canceled */
     }
+    if (usb_host_req_cpr_replay(r, xfer)) {
+        goto out; /* CPR drain: the target will replay this packet */
+    }
 
     r->p->status = status_map[xfer->status];
     r->p->actual_length = xfer->actual_length;
@@ -482,6 +527,9 @@ static void LIBUSB_CALL usb_host_req_complete_data(struct libusb_transfer *xfer)
     if (r->p == NULL) {
         goto out; /* request was canceled */
     }
+    if (usb_host_req_cpr_replay(r, xfer)) {
+        goto out; /* CPR drain: the target will replay this packet */
+    }
 
     r->p->status = status_map[xfer->status];
     if (r->in && xfer->actual_length) {
@@ -502,22 +550,29 @@ out:
     }
 }
 
-static void usb_host_req_abort(USBHostRequest *r)
+/* Fail r's in-flight packet with an error status and detach it from r. */
+static void usb_host_req_complete_err(USBHostRequest *r, int status)
 {
-    USBHostDevice  *s = r->host;
-    bool inflight = (r->p && r->p->state == USB_PACKET_ASYNC);
+    USBHostDevice *s = r->host;
 
-    if (inflight) {
-        r->p->status = USB_RET_NODEV;
-        trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
-                                    r->p->status, r->p->actual_length);
-        if (r->p->ep->nr == 0) {
-            usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
-        } else {
-            usb_packet_complete(USB_DEVICE(s), r->p);
-        }
-        r->p = NULL;
+    if (!r->p || r->p->state != USB_PACKET_ASYNC) {
+        return;
+    }
+    r->p->status = status;
+    trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
+                                r->p->status, r->p->actual_length);
+    if (r->p->ep->nr == 0) {
+        usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
+    } else {
+        usb_packet_complete(USB_DEVICE(s), r->p);
+    }
+    r->p = NULL;
+}
 
+static void usb_host_req_abort(USBHostRequest *r)
+{
+    if (r->p && r->p->state == USB_PACKET_ASYNC) {
+        usb_host_req_complete_err(r, USB_RET_NODEV);
         libusb_cancel_transfer(r->xfer);
     }
 }
@@ -1098,9 +1153,18 @@ fail:
     return -1;
 }
 
-static void usb_host_reap_xfers(USBHostDevice *s)
+/* libusb still owns cancelled-but-unreaped transfers: unlink, don't free. */
+static void usb_host_drop_requests(USBHostDevice *s)
 {
     USBHostRequest *r, *rtmp;
+
+    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+        QTAILQ_REMOVE(&s->requests, r, next);
+    }
+}
+
+static void usb_host_reap_xfers(USBHostDevice *s)
+{
     int limit = 100;
 
     while (QTAILQ_FIRST(&s->requests) != NULL) {
@@ -1110,15 +1174,10 @@ static void usb_host_reap_xfers(USBHostDevice *s)
         libusb_handle_events_timeout(ctx, &tv);
         if (--limit == 0) {
             /*
-             * Don't wait forever for libusb calling the complete
-             * callback (which will unlink and free the request).
-             *
-             * Leaking memory here, to make sure libusb will not
-             * access memory which we have released already.
+             * Don't wait forever for libusb to call the complete callback
+             * (which unlinks and frees the request).
              */
-            QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
-                QTAILQ_REMOVE(&s->requests, r, next);
-            }
+            usb_host_drop_requests(s);
             return;
         }
     }
@@ -1134,6 +1193,118 @@ static void usb_host_abort_xfers(USBHostDevice *s)
     usb_host_reap_xfers(s);
 }
 
+static bool usb_host_iso_inflight(USBHostDevice *s)
+{
+    USBHostIsoRing *ring;
+
+    QTAILQ_FOREACH(ring, &s->isorings, next) {
+        if (!QTAILQ_EMPTY(&ring->inflight)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+/*
+ * Quiesce the device for a CPR switchover.  Cancel and reap all URBs,
+ * so that no URB submitted by this process can outlive it.
+ */
+static int usb_host_cpr_drain_xfers(USBHostDevice *s)
+{
+    USBHostRequest *r, *rtmp;
+    USBHostIsoRing *ring;
+    USBHostIsoXfer *xfer;
+    int limit = 100;
+
+    s->cpr_drained = true;
+
+    /*
+     * Cancel and reap every URB.  Completing a reaped URB can make xHCI queue
+     * the next TD, so re-cancel whatever reappears until the queue stays empty.
+     */
+    while (!QTAILQ_EMPTY(&s->requests)) {
+        struct timeval tv = { .tv_usec = 2500 };
+
+        QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+            libusb_cancel_transfer(r->xfer);
+        }
+        libusb_handle_events_timeout(ctx, &tv);
+        if (--limit == 0) {
+            /*
+             * The device won't release these URBs.  Error the packets out so
+             * the resumed guest retries them, leak the requests (libusb still
+             * owns their transfers) and abort the migration.
+             */
+            QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+                usb_host_req_complete_err(r, USB_RET_IOERROR);
+            }
+            usb_host_drop_requests(s);
+            return -1;
+        }
+    }
+
+    /*
+     * Iso URBs are on the rings, not s->requests.  Cancel them: one in
+     * flight when the guest stops may never complete on its own, and none
+     * may be left on the preserved fd.  The target replays from the ring.
+     * Reaping an iso IN URB wakes the endpoint, which can make xHCI submit
+     * fresh iso URBs mid-drain, so re-cancel here on every iteration too.
+     *
+     * Cap the reap at 2x a full ring:
+     * iso_urb_count URBs x iso_urb_frames packets each
+     */
+    limit = 2 * s->iso_urb_count * s->iso_urb_frames;
+    while (usb_host_iso_inflight(s)) {
+        struct timeval tv = { .tv_usec = 1000 };
+
+        QTAILQ_FOREACH(ring, &s->isorings, next) {
+            QTAILQ_FOREACH(xfer, &ring->inflight, next) {
+                libusb_cancel_transfer(xfer->xfer);
+            }
+        }
+        libusb_handle_events_timeout(ctx, &tv);
+        if (limit-- == 0) {
+            return -1;
+        }
+    }
+    usb_host_iso_free_all(s);
+    return 0;
+}
+
+/*
+ * Quiesce the URB data plane around a CPR switchover: drain the in-flight
+ * transfers before the device/irqchip snapshot, so completions raised during
+ * the drain are migrated too.
+ */
+static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
+{
+    if (usb_host_cpr_drain_xfers(s) < 0) {
+        return -1;
+    }
+    return 0;
+}
+
+/*
+ * Run the CPR quiesce from the vm-state handler, on the stop-for-CPR
+ * transition before the device/irqchip snapshot.
+ */
+static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
+{
+    USBHostDevice *s = opaque;
+    MigMode mode;
+
+    if (running || !s->dh || state != RUN_STATE_FINISH_MIGRATE) {
+        return;
+    }
+    mode = migrate_mode();
+    if (mode != MIG_MODE_CPR_EXEC && mode != MIG_MODE_CPR_TRANSFER) {
+        return;
+    }
+    if (usb_host_cpr_quiesce(s, mode) < 0) {
+        migration_file_set_error(-EIO, NULL);
+    }
+}
+
 static int usb_host_close(USBHostDevice *s)
 {
     USBDevice *udev = USB_DEVICE(s);
@@ -1301,6 +1472,8 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
 
     s->exit.notify = usb_host_exit_notifier;
     qemu_add_exit_notifier(&s->exit);
+
+    s->cpr_vmse = qemu_add_vm_change_state_handler(usb_host_cpr_vm_state, s);
 }
 
 static void usb_host_instance_init(Object *obj)
@@ -1323,6 +1496,9 @@ static void usb_host_unrealize(USBDevice *udev)
     }
     usb_host_close(s);
     migrate_del_blocker(&s->cpr_blocker);
+    if (s->cpr_vmse) {
+        qemu_del_vm_change_state_handler(s->cpr_vmse);
+    }
 }
 
 static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1792,10 +1968,8 @@ static int usb_host_post_load(void *opaque, int version_id)
     USBHostDevice *dev = opaque;
 
     /*
-     * For CPR migration, device wasn't released/reset, and the guest
-     * is unaware of the switchover.  The detach/rescan performed in
-     * usb_host_post_load_bh() only exists for cross-host migration.
-     * Skip it for CPR.
+     * CPR keeps the device open and the guest unaware, so skip the detach and
+     * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
      */
     if (cpr_is_incoming()) {
         return 0;
@@ -1810,10 +1984,31 @@ static int usb_host_post_load(void *opaque, int version_id)
     return 0;
 }
 
+static int usb_host_pre_save(void *opaque)
+{
+    USBHostDevice *s = opaque;
+    MigMode mode = migrate_mode();
+
+    /*
+     * The vm-state handler drains on the stop-for-CPR transition, but a VM
+     * already paused never hits it.  Drain here instead; the stopped guest
+     * can't take completions, so suppress every URB and let the target replay.
+     */
+    if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
+        s->dh && !s->cpr_drained) {
+        s->cpr_replay_all = true;
+        if (usb_host_cpr_quiesce(s, mode) < 0) {
+            return -1;
+        }
+    }
+    return 0;
+}
+
 static const VMStateDescription vmstate_usb_host = {
     .name = "usb-host",
     .version_id = 1,
     .minimum_version_id = 1,
+    .pre_save = usb_host_pre_save,
     .post_load = usb_host_post_load,
     .fields = (const VMStateField[]) {
         VMSTATE_USB_DEVICE(parent_obj, USBHostDevice),

-- 
2.47.1


^ permalink raw reply	[relevance 26%]

* [PATCH QEMU HCI-8.0 v8 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800
                     ` (2 preceding siblings ...)
  2026-10-06 16:27 13% ` [PATCH QEMU HCI-8.0 v8 03/14] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:27 26% ` Andrey Drobyshev
  2026-10-06 16:38  4%   ` no-reply
  2026-10-06 16:27 25% ` [PATCH QEMU HCI-8.0 v8 05/14] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
                   ` (7 subsequent siblings)
  11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 16:27 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

In .realize(), save freshly opened host device FD into the CPR registry,
and then reuse it on CPR target.  Also, add deletion of that FD from
registry to usb_host_open() / usb_host_close() cleanup paths.

For this to work, we also need to skip the .post-load() code which
closes, detaches the device, and then rescans the host bus to reopen
matching hostdevs.  For the CPR-case migration we don't want any of that
as the hostdev FD stays preserved, and the guest shouldn't notice the
switchover.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 33 ++++++++++++++++++++++++++++++++-
 1 file changed, 32 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index e3bd7bcf749..beab8edc619 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -47,6 +47,7 @@
 
 #include "qapi/error.h"
 #include "migration/blocker.h"
+#include "migration/cpr.h"
 #include "migration/vmstate.h"
 #include "monitor/monitor.h"
 #include "qemu/error-report.h"
@@ -949,6 +950,21 @@ static void usb_host_ep_update(USBHostDevice *s)
     libusb_free_config_descriptor(conf);
 }
 
+static char *usb_host_cpr_fd_name(USBHostDevice *s)
+{
+    /* Prefix the key so it can't clash with another CPR label */
+    return DEVICE(s)->id ? g_strdup_printf("usb-host/%s", DEVICE(s)->id)
+                         : NULL;
+}
+
+static void usb_host_cpr_delete_fd(USBHostDevice *s)
+{
+    g_autofree char *name = usb_host_cpr_fd_name(s);
+    if (name) {
+        cpr_delete_fd(name, 0);
+    }
+}
+
 static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
 {
     USBDevice *udev = USB_DEVICE(s);
@@ -1075,6 +1091,7 @@ fail:
         s->dev = NULL;
     }
     if (s->hostfd != -1) {
+        usb_host_cpr_delete_fd(s);
         close(s->hostfd);
         s->hostfd = -1;
     }
@@ -1136,6 +1153,7 @@ static int usb_host_close(USBHostDevice *s)
     s->dev = NULL;
 
     if (s->hostfd != -1) {
+        usb_host_cpr_delete_fd(s);
         close(s->hostfd);
         s->hostfd = -1;
     }
@@ -1222,9 +1240,12 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
 
 #if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
     if (s->hostdevice) {
+        g_autofree char *name = usb_host_cpr_fd_name(s);
         int fd;
+
         s->needs_autoscan = false;
-        fd = qemu_open(s->hostdevice, O_RDWR, errp);
+        fd = name ? cpr_open_fd(s->hostdevice, O_RDWR, name, 0, errp)
+                  : qemu_open(s->hostdevice, O_RDWR, errp);
         if (fd < 0) {
             return;
         }
@@ -1764,6 +1785,16 @@ static int usb_host_post_load(void *opaque, int version_id)
 {
     USBHostDevice *dev = opaque;
 
+    /*
+     * For CPR migration, device wasn't released/reset, and the guest
+     * is unaware of the switchover.  The detach/rescan performed in
+     * usb_host_post_load_bh() only exists for cross-host migration.
+     * Skip it for CPR.
+     */
+    if (cpr_is_incoming()) {
+        return 0;
+    }
+
     if (!dev->bh_postld) {
         dev->bh_postld = qemu_bh_new_guarded(usb_host_post_load_bh, dev,
                                              &DEVICE(dev)->mem_reentrancy_guard);

-- 
2.47.1


^ permalink raw reply	[relevance 26%]

* [PATCH QEMU HCI-8.0 v8 05/14] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800
                     ` (3 preceding siblings ...)
  2026-10-06 16:27 26% ` [PATCH QEMU HCI-8.0 v8 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:27 25% ` Andrey Drobyshev
  2026-10-06 16:27 26% ` [PATCH QEMU HCI-8.0 v8 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
                   ` (6 subsequent siblings)
  11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 16:27 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

usb_host_abort_xfers() cancels every pending request and then pumps
libusb events until all of them are reaped, with a bounded wait.  Split
that reap-wait loop out into usb_host_reap_xfers().  No functional
change.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index beab8edc619..1a79cdf9e4d 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1098,15 +1098,11 @@ fail:
     return -1;
 }
 
-static void usb_host_abort_xfers(USBHostDevice *s)
+static void usb_host_reap_xfers(USBHostDevice *s)
 {
     USBHostRequest *r, *rtmp;
     int limit = 100;
 
-    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
-        usb_host_req_abort(r);
-    }
-
     while (QTAILQ_FIRST(&s->requests) != NULL) {
         struct timeval tv;
         memset(&tv, 0, sizeof(tv));
@@ -1128,6 +1124,16 @@ static void usb_host_abort_xfers(USBHostDevice *s)
     }
 }
 
+static void usb_host_abort_xfers(USBHostDevice *s)
+{
+    USBHostRequest *r, *rtmp;
+
+    QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+        usb_host_req_abort(r);
+    }
+    usb_host_reap_xfers(s);
+}
+
 static int usb_host_close(USBHostDevice *s)
 {
     USBDevice *udev = USB_DEVICE(s);

-- 
2.47.1


^ permalink raw reply	[relevance 25%]

* [PATCH QEMU HCI-8.0 v8 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
                     ` (5 preceding siblings ...)
  2026-10-06 16:27 26% ` [PATCH QEMU HCI-8.0 v8 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:27 24% ` Andrey Drobyshev
  2026-10-06 16:44  3%   ` no-reply
  2026-10-06 16:27 22% ` [PATCH QEMU HCI-8.0 v8 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
                   ` (4 subsequent siblings)
  11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 16:27 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

If CPR fails, the source VM resumes with the drained packets still owned
by the host controller as in-flight, and nothing left to complete them.

Remember the packets suppressed during the drain on a list, and re-issue
them on the preserved fd from the vm-state handler's resume branch -- it
runs on any resume but only acts once we have actually drained
(cpr_drained).  Control transfers cannot be reconstructed from the packet
alone, so complete those as errors and let the guest driver retry.

A re-issued transfer is replayed from the start of the TD, so a partial
transfer loses its progress -- an OUT fragment already consumed by the
device may be sent twice.  Only the paused-VM .pre_save() fallback queues
partial transfers for re-issue; the normal running-VM drain delivers them
as short transfers and is unaffected.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 79 ++++++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 74 insertions(+), 5 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 413ddc12ee4..9cc7d72aa63 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -124,6 +124,7 @@ struct USBHostDevice {
     /* CPR migration */
     Error                            *cpr_blocker;
     VMChangeStateEntry               *cpr_vmse;
+    GSList                           *cpr_inflight;
     bool                             cpr_drained;
     bool                             cpr_replay_all;
 
@@ -169,6 +170,7 @@ static void usb_host_release_interfaces(USBHostDevice *s);
 static void usb_host_nodev(USBHostDevice *s);
 static void usb_host_detach_kernel(USBHostDevice *s);
 static void usb_host_attach_kernel(USBHostDevice *s);
+static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
 
 /* ------------------------------------------------------------------------ */
 
@@ -433,9 +435,10 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
 /*
  * Classify a reap during the CPR drain (r->p is still set).  The paused-VM
  * fallback (cpr_replay_all) can't deliver completions, so it suppresses every
- * reap for the target to replay; otherwise one that moved no data is suppressed
- * and one that moved some is kept as a short transfer.  Returns true if
- * suppressed, so the caller frees the request.
+ * reap; otherwise one that moved no data is suppressed and one that moved some
+ * is kept as a short transfer.  Suppressed packets are queued on cpr_inflight
+ * for the target to replay.  Returns true if suppressed, so the caller frees
+ * the request.
  */
 static bool usb_host_req_cpr_replay(USBHostRequest *r,
                                     struct libusb_transfer *xfer)
@@ -462,6 +465,9 @@ static bool usb_host_req_cpr_replay(USBHostRequest *r,
             return false;
         }
     }
+    if (r->p->state == USB_PACKET_ASYNC) {
+        r->host->cpr_inflight = g_slist_append(r->host->cpr_inflight, r->p);
+    }
     return true;
 }
 
@@ -1284,16 +1290,74 @@ static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
     return 0;
 }
 
+/*
+ * If CPR is aborted the source resumes with the drained packets still owned
+ * by the controller as in-flight; re-issue them on the preserved fd.  Control
+ * transfers can't be reconstructed from the packet, so error those out.
+ */
+static void usb_host_cpr_reissue(USBHostDevice *s)
+{
+    USBDevice *udev = USB_DEVICE(s);
+    GSList *it;
+    USBPacket *p;
+
+    if (!s->cpr_drained) {
+        return;
+    }
+
+    for (it = s->cpr_inflight; it; it = it->next) {
+        p = it->data;
+        /*
+         * Replay from the start of the TD.  A transfer that already moved some
+         * data is re-run in full, so an OUT fragment the device consumed may be
+         * sent twice.  Only the paused-VM pre_save fallback queues such partial
+         * transfers here; the normal drain delivers them as short transfers.
+         */
+        p->actual_length = 0;
+        if (p->ep->nr == 0) {
+            /* control endpoint: can't be reconstructed, so error it out */
+            p->status = USB_RET_IOERROR;
+            usb_generic_async_ctrl_complete(udev, p);
+        } else {
+            usb_host_handle_data(udev, p);
+            if (p->status != USB_RET_ASYNC) {
+                /* completed synchronously with an error */
+                if (usb_host_use_combining(p->ep)) {
+                    usb_combined_input_packet_complete(udev, p);
+                } else {
+                    usb_packet_complete(udev, p);
+                }
+            }
+        }
+    }
+    g_slist_free(s->cpr_inflight);
+    s->cpr_inflight = NULL;
+    s->cpr_replay_all = false;
+    s->cpr_drained = false;
+}
+
 /*
  * Run the CPR quiesce from the vm-state handler, on the stop-for-CPR
- * transition before the device/irqchip snapshot.
+ * transition before the device/irqchip snapshot, and re-issue the drained
+ * packets on resume if CPR was aborted.
  */
 static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
 {
     USBHostDevice *s = opaque;
     MigMode mode;
 
-    if (running || !s->dh || state != RUN_STATE_FINISH_MIGRATE) {
+    if (running) {
+        /*
+         * Resume: re-issue drained packets and reset state, even if the
+         * device was lost between the drain and now (dh == NULL).
+         */
+        usb_host_cpr_reissue(s);
+        return;
+    }
+    if (!s->dh) {
+        return;
+    }
+    if (state != RUN_STATE_FINISH_MIGRATE) {
         return;
     }
     mode = migrate_mode();
@@ -1499,6 +1563,8 @@ static void usb_host_unrealize(USBDevice *udev)
     if (s->cpr_vmse) {
         qemu_del_vm_change_state_handler(s->cpr_vmse);
     }
+    g_slist_free(s->cpr_inflight);
+    s->cpr_inflight = NULL;
 }
 
 static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1506,6 +1572,9 @@ static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
     USBHostDevice *s = USB_HOST_DEVICE(udev);
     USBHostRequest *r;
 
+    /* Packet is being cancelled -> it mustn't be re-issued on failed CPR */
+    s->cpr_inflight = g_slist_remove(s->cpr_inflight, p);
+
     if (p->combined) {
         usb_combined_packet_cancel(udev, p);
         return;

-- 
2.47.1


^ permalink raw reply	[relevance 24%]

* [PATCH QEMU HCI-8.0 v8 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800
                     ` (6 preceding siblings ...)
  2026-10-06 16:27 24% ` [PATCH QEMU HCI-8.0 v8 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:27 22% ` Andrey Drobyshev
  2026-10-06 16:41  3%   ` no-reply
  2026-10-06 16:28 29% ` [PATCH QEMU HCI-8.0 v8 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
                   ` (3 subsequent siblings)
  11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 16:27 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

usb_host_open() reads the product string descriptor from the device
with a synchronous control transfer.  On the CPR target this runs while
the device is being handed over, and the transfer can block there,
hanging the incoming migration.

The string only fills the cosmetic product_desc, so skip the read when
we're the CPR target and fall back to the synthetic "host:bus.addr"
name.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 9cc7d72aa63..6c00ce528b1 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1123,7 +1123,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
     udev->speed = speed_map[libusb_speed];
     usb_host_speed_compat(s);
 
-    if (s->ddesc.iProduct) {
+    if (s->ddesc.iProduct && !cpr_is_incoming()) {
         libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
                                            (unsigned char *)udev->product_desc,
                                            sizeof(udev->product_desc));

-- 
2.47.1


^ permalink raw reply	[relevance 22%]

* [PATCH QEMU HCI-8.0 v8 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800
                     ` (7 preceding siblings ...)
  2026-10-06 16:27 22% ` [PATCH QEMU HCI-8.0 v8 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:28 29% ` Andrey Drobyshev
  2026-10-06 16:50  7%   ` no-reply
  2026-10-06 16:28 10% ` [PATCH QEMU HCI-8.0 v8 12/14] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
                   ` (2 subsequent siblings)
  11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 16:28 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

Unlike cpr-exec, cpr-transfer keeps the source QEMU alive: it hands the
usbfs FD to the new QEMU over the migration channel and only exits once
the target is up.  While the handoff is in flight both processes hold the
same FD, and only one may reap URB events on it - if both do, one reaps
the other's in-flight completions and wedges an isochronous transfer that
cannot be retried.  So reaping is handed off along with the FD:

  * The target doesn't reap until it takes over: it stops events right
    after opening the FD in .realize() and restarts in .post_load().
    Until then it must not touch the device at all: if the incoming
    migration fails, the target exits while the source resumes, so the
    target's exit must not reset the device or release the interfaces
    underneath the source.
  * The source stops reaping when it quiesces the device on the
    stop-for-CPR transition (or in the .pre_save() fallback for an
    already-paused VM) and, since it stays alive, restarts reaping from
    the vm-state handler's resume branch on failure.
  * On success the source's .exit() leaves the device untouched - no
    reset, interface release or host-driver rebind - and just closes its
    own copy of the FD.

Reaping is stopped and restarted on the whole libusb context, which is
shared by all usb-host devices, so cpr-transfer currently supports a
single hostdevice= usb-host at a time.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 91 +++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 79 insertions(+), 12 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 70ae0a38167..1412f92c12e 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -127,6 +127,7 @@ struct USBHostDevice {
     GSList                           *cpr_inflight;
     bool                             cpr_drained;
     bool                             cpr_replay_all;
+    bool                             cpr_handed_off;
 
     /* request queues */
     QTAILQ_HEAD(, USBHostRequest)    requests;
@@ -283,6 +284,23 @@ static void usb_host_start_events(void)
     }
 }
 
+/*
+ * cpr-transfer shares the usbfs fd with the new QEMU, so stop reaping URBs on
+ * it here -- only one process may drive the device.
+ */
+static void usb_host_stop_events(void)
+{
+    const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+    libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
+    if (poll) {
+        for (int i = 0; poll[i] != NULL; i++) {
+            usb_host_del_fd(poll[i]->fd, ctx);
+        }
+        free(poll);
+    }
+}
+
 #else
 
 static QEMUTimer *poll_timer;
@@ -306,6 +324,9 @@ static void usb_host_timer(void *opaque)
     usb_host_timer_kick();
 }
 
+static void usb_host_start_events(void) {}
+static void usb_host_stop_events(void) {}
+
 #endif /* !CONFIG_WIN32 */
 
 static int usb_host_init(void)
@@ -1285,13 +1306,18 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
 /*
  * Quiesce the URB data plane around a CPR switchover: drain the in-flight
  * transfers before the device/irqchip snapshot, so completions raised during
- * the drain are migrated too.
+ * the drain are migrated too.  For cpr-transfer, also hand reaping of the
+ * shared fd off to the target by stopping events on this side.
  */
 static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
 {
     if (usb_host_cpr_drain_xfers(s) < 0) {
         return -1;
     }
+    if (mode == MIG_MODE_CPR_TRANSFER) {
+        usb_host_stop_events();
+        s->cpr_handed_off = true;
+    }
     return 0;
 }
 
@@ -1310,6 +1336,11 @@ static void usb_host_cpr_reissue(USBHostDevice *s)
         return;
     }
 
+    if (s->cpr_handed_off) {
+        usb_host_start_events();
+        s->cpr_handed_off = false;
+    }
+
     for (it = s->cpr_inflight; it; it = it->next) {
         p = it->data;
         /*
@@ -1384,16 +1415,19 @@ static int usb_host_close(USBHostDevice *s)
 
     trace_usb_host_close(s->bus_num, s->addr);
 
-    usb_host_abort_xfers(s);
-    usb_host_iso_free_all(s);
+    if (!s->cpr_handed_off) {
+        usb_host_abort_xfers(s);
+        usb_host_iso_free_all(s);
 
-    if (udev->attached) {
-        usb_device_detach(udev);
+        if (udev->attached) {
+            usb_device_detach(udev);
+        }
+
+        usb_host_release_interfaces(s);
+        libusb_reset_device(s->dh);
+        usb_host_attach_kernel(s);
     }
 
-    usb_host_release_interfaces(s);
-    libusb_reset_device(s->dh);
-    usb_host_attach_kernel(s);
     libusb_close(s->dh);
     s->dh = NULL;
     s->dev = NULL;
@@ -1428,10 +1462,17 @@ static void usb_host_exit_notifier(struct Notifier *n, void *data)
     USBHostDevice *s = container_of(n, USBHostDevice, exit);
 
     if (s->dh) {
-        usb_host_abort_xfers(s);
-        usb_host_release_interfaces(s);
-        libusb_reset_device(s->dh);
-        usb_host_attach_kernel(s);
+        /*
+         * Handed to new QEMU across cpr-transfer: do not reset, release
+         * interfaces or rebind the host driver underneath it; just close
+         * our handle.
+         */
+        if (!s->cpr_handed_off) {
+            usb_host_abort_xfers(s);
+            usb_host_release_interfaces(s);
+            libusb_reset_device(s->dh);
+            usb_host_attach_kernel(s);
+        }
         libusb_close(s->dh);
     }
 }
@@ -1500,6 +1541,18 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
             error_setg(errp, "failed to open host usb device %s", s->hostdevice);
             return;
         }
+        if (cpr_get_incoming_mode() == MIG_MODE_CPR_TRANSFER) {
+            /*
+             * The source still owns the shared fd until it hands off: don't
+             * reap URBs on it yet, or we would steal the source's in-flight
+             * completions, and don't reset or release the device on an early
+             * exit either -- if the incoming migration fails, the source
+             * resumes with it.  .post_load() restarts events and takes
+             * ownership once the source has handed the fd off.
+             */
+            usb_host_stop_events();
+            s->cpr_handed_off = true;
+        }
     } else
 #endif
     if (s->match.addr && s->match.bus_num &&
@@ -1563,6 +1616,17 @@ static void usb_host_unrealize(USBDevice *udev)
     if (s->needs_autoscan) {
         QTAILQ_REMOVE(&hostdevs, s, next);
     }
+    /*
+     * An explicit device_del means we still own the device (a successful
+     * cpr-transfer handoff exits the process instead): restart the reaping
+     * stopped by the handoff and clear the flag to force a full
+     * reset/release in usb_host_close().  Except on a CPR target that
+     * hasn't taken over yet -- there the source still drives the fd.
+     */
+    if (s->cpr_handed_off && !cpr_is_incoming()) {
+        usb_host_start_events();
+        s->cpr_handed_off = false;
+    }
     usb_host_close(s);
     migrate_del_blocker(&s->cpr_blocker);
     if (s->cpr_vmse) {
@@ -2046,6 +2110,9 @@ static int usb_host_post_load(void *opaque, int version_id)
      * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
      */
     if (cpr_is_incoming()) {
+        usb_host_start_events();
+        /* the takeover is complete: from here on this process owns the fd */
+        dev->cpr_handed_off = false;
         return 0;
     }
 

-- 
2.47.1


^ permalink raw reply	[relevance 29%]

* [PATCH QEMU HCI-8.0 v8 12/14] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
                     ` (8 preceding siblings ...)
  2026-10-06 16:28 29% ` [PATCH QEMU HCI-8.0 v8 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:28 10% ` Andrey Drobyshev
  2026-10-06 16:52  6%   ` no-reply
    2026-10-06 16:28 20% ` [PATCH QEMU HCI-8.0 v8 14/14] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
  11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 16:28 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

On CPR incoming we skip the usb_host_open() rescan, so libusb only sets
up the endpoints from the device's default configuration.  If the guest
had enabled an endpoint by selecting a non-zero altsetting (e.g. an
isochronous streaming endpoint), that endpoint is now missing, and the
guest's transfers to it simply stall.

The altsetting itself is restored by vmload now, so let's call
usb_host_ep_update() on the CPR target to rebuild the endpoints out of
it.

The kernel claims are still there on the preserved FD, but libusb's own
handle knows nothing about them, so a later SET_INTERFACE would fail with
LIBUSB_ERROR_NOT_FOUND.  Let's re-claim the interfaces on the target as
well to bring libusb's view back in sync - on the preserved FD this is a
no-op at the usbfs level anyway.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 78 insertions(+), 1 deletion(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 1412f92c12e..076ba08533b 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -988,7 +988,7 @@ static void usb_host_ep_update(USBHostDevice *s)
         alt = udev->altsetting[intf->bInterfaceNumber];
 
         if (alt != 0) {
-            assert(alt < conf->interface[i].num_altsetting);
+            assert(alt >= 0 && alt < conf->interface[i].num_altsetting);
             intf = &conf->interface[i].altsetting[alt];
         }
 
@@ -2101,6 +2101,75 @@ static void usb_host_post_load_bh(void *opaque)
     usb_host_auto_check(NULL);
 }
 
+/*
+ * Reconfigure the CPR target after vmload: reject a migrated altsetting the
+ * device can't honour, then re-claim the interfaces so libusb's view matches
+ * the preserved fd (which already holds the claims).
+ */
+static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
+{
+    USBDevice *udev = USB_DEVICE(s);
+    struct libusb_config_descriptor *conf;
+    int rc;
+
+    if (!s->dh) {
+        /* e.g. a target whose command line doesn't open the device */
+        error_report("usb-host: CPR target has no open host device");
+        return -1;
+    }
+
+    rc = libusb_get_active_config_descriptor(s->dev, &conf);
+    if (rc == LIBUSB_ERROR_NOT_FOUND) {
+        /* device is unconfigured (address state), nothing to reclaim */
+        return 0;
+    }
+    if (rc != 0) {
+        usb_host_libusb_error("libusb_get_active_config_descriptor", rc);
+        return -1;
+    }
+
+    /*
+     * Validate all migrated interface/altsetting numbers before touching the
+     * device, so garbage migration data can't leave it half-claimed.
+     */
+    for (int i = 0; i < conf->bNumInterfaces; i++) {
+        int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
+        int alt;
+
+        if (ifnum >= USB_MAX_INTERFACES) {
+            error_report("usb-host: interface number %d out of range", ifnum);
+            libusb_free_config_descriptor(conf);
+            return -1;
+        }
+        alt = udev->altsetting[ifnum];
+        if (alt < 0 || alt >= conf->interface[i].num_altsetting) {
+            error_report("usb-host: migrated altsetting %d out of range for "
+                         "interface %d", alt, ifnum);
+            libusb_free_config_descriptor(conf);
+            return -1;
+        }
+    }
+
+    /* Data is sane: claim each interface on the fd and record it. */
+    for (int i = 0; i < conf->bNumInterfaces; i++) {
+        int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
+
+        rc = libusb_claim_interface(s->dh, ifnum);
+        if (rc != 0) {
+            usb_host_libusb_error("libusb_claim_interface", rc);
+            libusb_free_config_descriptor(conf);
+            return -1;
+        }
+        s->ifs[ifnum].claimed = true;
+    }
+
+    udev->ninterfaces = conf->bNumInterfaces;
+    udev->configuration = conf->bConfigurationValue;
+    libusb_free_config_descriptor(conf);
+
+    return 0;
+}
+
 static int usb_host_post_load(void *opaque, int version_id)
 {
     USBHostDevice *dev = opaque;
@@ -2110,6 +2179,14 @@ static int usb_host_post_load(void *opaque, int version_id)
      * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
      */
     if (cpr_is_incoming()) {
+        /*
+         * The device stayed open, so rebuild libusb's claims and endpoints,
+         * and start reaping now that the source has handed the fd off.
+         */
+        if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
+            return -1;
+        }
+        usb_host_ep_update(dev);
         usb_host_start_events();
         /* the takeover is complete: from here on this process owns the fd */
         dev->cpr_handed_off = false;

-- 
2.47.1


^ permalink raw reply	[relevance 10%]

* [PATCH QEMU HCI-8.0 v8 14/14] usb-host: make CPR migration blocker conditional #VSTOR-137800
                     ` (10 preceding siblings ...)
  @ 2026-10-06 16:28 20% ` Andrey Drobyshev
  11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 16:28 UTC (permalink / raw)
  To: svt-core; +Cc: andrey.drobyshev, den

A previous commit added an unconditional blocker for CPR migrations.
That is to refuse CPR gracefully instead of crashing and for the previous
commits to remain bisectable.

Now the restore path is complete: the FD is preserved, in-flight URBs
are drained and replayed, the device is handed off across cpr-transfer,
and the endpoints and isochronous streams are reconstructed on the
target.  So let's lift the CPR blocker for the devices which satisfy the
preconditions.

Still, the remaining preconditions for CPR to work are:

  * We must open host device FD ourselves, so that libusb owns the FD.
    This way we're able to preserve the FD during CPR;
  * Device ID must be provided as a unique stable key for CPR FD registry.

Use them to guard the blocker addition.  Apart from that, prohibit CPR on
WIN32 platform and on hosts with older libusb versions.

Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
 hw/usb/host-libusb.c | 39 +++++++++++++++++++++++++++++++--------
 1 file changed, 31 insertions(+), 8 deletions(-)

diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 076ba08533b..f88e6cde611 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1580,16 +1580,39 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
         usb_host_auto_check(NULL);
     }
 
-    error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+#ifdef CONFIG_WIN32
+    error_setg(&s->cpr_blocker,
+               "usb-host device %s does not support CPR: "
+               "WIN32 platform is not supported",
                DEVICE(s)->id ?: "(anonymous)");
-    if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
-                                  MIG_MODE_CPR_TRANSFER,
-                                  MIG_MODE_CPR_EXEC, -1) < 0) {
-        if (s->needs_autoscan) {
-            QTAILQ_REMOVE(&hostdevs, s, next);
+#elif LIBUSB_API_VERSION < 0x01000107
+    error_setg(&s->cpr_blocker,
+               "usb-host device %s does not support CPR: "
+               "libusb API version 0x%08x is too old",
+               DEVICE(s)->id ?: "(anonymous)", (unsigned) LIBUSB_API_VERSION);
+#else
+    if (!(s->hostdevice && DEVICE(s)->id)) {
+        /*
+         * CPR needs the host fd owned by libusb (the hostdevice= property)
+         * and a device id to key the CPR fd registry.
+         */
+        error_setg(&s->cpr_blocker,
+                   "usb-host device %s does not support CPR: "
+                   "hostdevice= and a device id are required",
+                   DEVICE(s)->id ?: "(anonymous)");
+    }
+#endif
+
+    if (s->cpr_blocker) {
+        if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+                                      MIG_MODE_CPR_TRANSFER,
+                                      MIG_MODE_CPR_EXEC, -1) < 0) {
+            if (s->needs_autoscan) {
+                QTAILQ_REMOVE(&hostdevs, s, next);
+            }
+            usb_host_close(s);
+            return;
         }
-        usb_host_close(s);
-        return;
     }
 
     s->exit.notify = usb_host_exit_notifier;

-- 
2.47.1


^ permalink raw reply	[relevance 20%]

* Re: [PATCH QEMU HCI-8.0 v8 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
  2026-10-06 16:27 30% ` [PATCH QEMU HCI-8.0 v8 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:31  6%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 16:31 UTC (permalink / raw)
  To: svt-core, Andrey Drobyshev; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] usb-host: usb_host_open() fail path may double-close wrapped hostfd after libusb_close()
--

commit e4ff0c7f70381f2bead74b81a45f4480bcb0c76b
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: don't leak hostdev FD on open failure #VSTOR-137800

This patch stores the caller-supplied hostfd in s->hostfd before
libusb_wrap_sys_device() is called. It also closes that fd on the fail:
path of usb_host_open(), so a failed open no longer leaks it.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> @@ -1066,6 +1070,10 @@ fail:
>          s->dh = NULL;
>          s->dev = NULL;
>      }
> +    if (s->hostfd != -1) {
> +        close(s->hostfd);
> +        s->hostfd = -1;
> +    }
>      return -1;
>  }

[Severity: Low]
Can this close() be a double close of the wrapped fd?

Consider the path where usb_host_open() is called with dev == NULL,
libusb_wrap_sys_device() succeeds, and a later step such as
usb_device_attach() fails. Control reaches fail: and runs:

    libusb_close(s->dh);
    s->dh = NULL;
    s->dev = NULL;
}
if (s->hostfd != -1) {
    close(s->hostfd);

Because s->hostfd is now set before the wrap call, this close() runs
right after libusb_close(s->dh).

If the libusb version in use closes a wrapped fd in libusb_close(), the fd
is closed twice. The second close() could then hit an unrelated descriptor
if another thread reused the number in between.

I could not check this against libusb sources. Current Linux backends mark
wrapped fds with fd_keep and do not close them, but older releases such as
1.0.23 may behave differently.

usb_host_close() already uses the same libusb_close() then close() order,
so the new error path relies on the same assumption. Is that assumption
valid for all libusb versions that QEMU supports?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v8-0-72ae4fb8977b@virtuozzo.com?part=1

^ permalink raw reply	[relevance 6%]

* Re: [PATCH QEMU HCI-8.0 v8 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800
  2026-10-06 16:27 26% ` [PATCH QEMU HCI-8.0 v8 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:38  4%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 16:38 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] usb-host: cpr_is_incoming() in post_load stays true for the whole process lifetime
--

commit 09775f4cee7af08e9fba2832c78f5e0afc56f940
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: preserve hostdev FD during CPR migration #VSTOR-137800

This patch saves the host device FD in the CPR registry at realize time
and reuses it on the CPR target. It also deletes the FD from the registry
in the usb_host_open() failure path and in usb_host_close(). Finally, it
makes usb_host_post_load() skip the detach/rescan bottom half when CPR is
incoming.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> @@ -1764,6 +1785,16 @@ static int usb_host_post_load(void *opaque, int version_id)
>  {
>      USBHostDevice *dev = opaque;
>  
> +    /*
> +     * For CPR migration, device wasn't released/reset, and the guest
> +     * is unaware of the switchover.  The detach/rescan performed in
> +     * usb_host_post_load_bh() only exists for cross-host migration.
> +     * Skip it for CPR.
> +     */
> +    if (cpr_is_incoming()) {
> +        return 0;
> +    }
> +

[Severity: Low]
Does cpr_is_incoming() stay true for the whole lifetime of a CPR target
process, rather than only during the CPR vmstate load?

cpr_is_incoming() is defined in migration/cpr.c as:

    return incoming_mode != MIG_MODE_NONE;

incoming_mode is set via cpr_set_incoming_mode() in cpr_state_load(), and
nothing in migration/cpr.c appears to reset it to MIG_MODE_NONE afterwards
(checked at the end of the series).

If a later, non-CPR vmstate load of a usb-host device happens in the same
process (for example loadvm of a snapshot), usb_host_post_load() would
return early. The detach/rescan emulation in usb_host_post_load_bh() would
then be skipped, and in the final series state the CPR reclaim path would
run instead.

This is speculative, since it depends on a snapshot or other vmstate load
occurring in a CPR target process, and I did not confirm that this is a
supported flow. Is there a way to restrict this check to the CPR load
itself?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v8-0-72ae4fb8977b@virtuozzo.com?part=4

^ permalink raw reply	[relevance 4%]

* Re: [PATCH QEMU HCI-8.0 v8 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800
  2026-10-06 16:27 26% ` [PATCH QEMU HCI-8.0 v8 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:38  5%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 16:38 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] usb-host: cpr_replay_all drops already-completed URBs, losing IN data
- [Low] usb-host: drain timeout path drops newly queued URBs without cancelling them
--

commit 5a6ed65d6723a2887c0ae5c9eb6d765f5c531174
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: drain in-flight URBs across CPR #VSTOR-137800

This patch registers a vm_change_state handler that cancels and reaps all
pending usb-host URBs when the VM stops for a CPR switchover. It also adds
a pre_save fallback for VMs that were already paused, and cancels
isochronous URBs on their own rings.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> +static bool usb_host_req_cpr_replay(USBHostRequest *r,
> +                                    struct libusb_transfer *xfer)
> +{
> +    if (!r->host->cpr_drained) {
> +        /* no CPR drain in progress: normal completion */
> +        return false;
> +    }
> +    if (!r->host->cpr_replay_all) {
> +        if (xfer->status != LIBUSB_TRANSFER_CANCELLED) {
> +            return false;
> +        }
> +        if (xfer->actual_length != 0) {
> +            /* deliver as a short transfer */
> +            xfer->status = LIBUSB_TRANSFER_COMPLETED;
> +            if (!r->in) {
> +                r->p->actual_length = xfer->actual_length;
> +            }
> +            return false;
> +        }
> +    }
> +    return true;
> +}

[Severity: Medium]
When cpr_replay_all is set, the status and actual_length checks are
skipped and usb_host_req_cpr_replay() returns true for every reaped URB.
Does this also discard URBs that the kernel already completed with
LIBUSB_TRANSFER_COMPLETED and actual_length > 0?

For an IN endpoint, the data the device already returned would be dropped.
The target then replays the TD from its start, so that data appears to be
lost. A completed OUT or control request could be sent a second time.

The commit message says "A transfer that already completed in the kernel is
delivered normally". That only holds when cpr_replay_all is false. The
message also says the fallback suppresses every URB, so the two statements
seem to disagree. Could the commit message be made consistent with the code,
or could completed URBs be delivered in the replay_all case too?

The path is: the VM is already paused when a CPR migration starts, so the
FINISH_MIGRATE handler never runs. usb_host_pre_save() then sets
cpr_replay_all and calls usb_host_cpr_quiesce(). A URB that completed in
the kernel but has not been reaped yet then goes through
usb_host_req_complete_data() or usb_host_req_complete_ctrl() and is
suppressed.

Later patches in the series do not change this. The final
usb_host_req_cpr_replay() still returns true for every reap when
cpr_replay_all is set. The later patch "usb-host: re-issue drained URBs if
CPR is aborted" only adds replay for an aborted migration. Its own comment
says the paused-VM fallback queues partially transferred OUT URBs and may
send them twice.

[ ... ]

> +static int usb_host_cpr_drain_xfers(USBHostDevice *s)
> +{
[ ... ]
> +    while (!QTAILQ_EMPTY(&s->requests)) {
> +        struct timeval tv = { .tv_usec = 2500 };
> +
> +        QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
> +            libusb_cancel_transfer(r->xfer);
> +        }
> +        libusb_handle_events_timeout(ctx, &tv);
> +        if (--limit == 0) {
> +            /*
> +             * The device won't release these URBs.  Error the packets out so
> +             * the resumed guest retries them, leak the requests (libusb still
> +             * owns their transfers) and abort the migration.
> +             */
> +            QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
> +                usb_host_req_complete_err(r, USB_RET_IOERROR);
> +            }
> +            usb_host_drop_requests(s);
> +            return -1;
> +        }
> +    }

[Severity: Low]
On the 100-iteration timeout, can this path leave URBs live that were never
cancelled? usb_host_req_complete_err() calls usb_packet_complete(), which
can start the next queued packet:

usb_host_cpr_drain_xfers()
  usb_host_req_complete_err()
    usb_packet_complete()
      usb_process_one()
        usb_host_handle_data()
          usb_host_req_alloc()   /* new request appended to s->requests */

usb_host_drop_requests() then unlinks every request on s->requests,
including the newly queued ones. Those were never passed to
libusb_cancel_transfer(), and their packets stay in USB_PACKET_ASYNC.

The URBs would stay live in the kernel on the preserved fd, which is what
this patch is meant to prevent. Their late completion callbacks would run
with r->host still set, so they could touch stale state if the device is
closed or unrealized first.

This is an error path on a migration that is already failing. It mirrors
the existing leak in usb_host_reap_xfers().

Later patches, including "usb-host: re-issue drained URBs if CPR is
aborted", do not change this timeout path. The final code still calls
usb_host_req_complete_err() followed by usb_host_drop_requests().

[ ... ]

> +    limit = 2 * s->iso_urb_count * s->iso_urb_frames;
> +    while (usb_host_iso_inflight(s)) {
> +        struct timeval tv = { .tv_usec = 1000 };
> +
> +        QTAILQ_FOREACH(ring, &s->isorings, next) {
> +            QTAILQ_FOREACH(xfer, &ring->inflight, next) {
> +                libusb_cancel_transfer(xfer->xfer);
> +            }
> +        }
> +        libusb_handle_events_timeout(ctx, &tv);
> +        if (limit-- == 0) {
> +            return -1;
> +        }
> +    }

[Severity: Low]
Does the iso timeout path also return with iso URBs still in flight and the
rings still allocated? On limit-- == 0, usb_host_cpr_drain_xfers() returns
-1 before usb_host_iso_free_all(). The cancelled iso URBs may remain on
ring->inflight.

[ ... ]

> +    if (usb_host_cpr_quiesce(s, mode) < 0) {
> +        migration_file_set_error(-EIO, NULL);
> +    }

[Severity: Low]
In usb_host_cpr_vm_state(), the drain failure is reported with
migration_file_set_error(-EIO, NULL). Since the Error is NULL, no
diagnostic reaches the user. Could an Error describing the usb-host drain
failure be passed instead?

[ ... ]

> +    if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
> +        s->dh && !s->cpr_drained) {
> +        s->cpr_replay_all = true;
> +        if (usb_host_cpr_quiesce(s, mode) < 0) {
> +            return -1;
> +        }
> +    }

[ ... ]

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v8-0-72ae4fb8977b@virtuozzo.com?part=6

^ permalink raw reply	[relevance 5%]

* Re: [PATCH QEMU HCI-8.0 v8 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800
  2026-10-06 16:27 22% ` [PATCH QEMU HCI-8.0 v8 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:41  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 16:41 UTC (permalink / raw)
  To: svt-core, Andrey Drobyshev; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] usb-host: CPR-incoming workaround skips only one blocking call, with no root cause
- [Low] usb-host: cpr_is_incoming() is a global check, so unrelated opens lose product name
--

commit 49b39041283392e4d6c44ac4bd1c34c893215179
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: skip product-string read on CPR incoming #VSTOR-137800

This patch skips the libusb_get_string_descriptor_ascii() call in
usb_host_open() when cpr_is_incoming() is true. In that case product_desc
falls back to the synthetic "host:bus.addr" name. The stated reason is that
the synchronous control transfer can block during CPR handover and hang the
incoming migration.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> index 9cc7d72aa63a..6c00ce528b1e 100644
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c
> @@ -1123,7 +1123,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
>      udev->speed = speed_map[libusb_speed];
>      usb_host_speed_compat(s);
>  
> -    if (s->ddesc.iProduct) {
> +    if (s->ddesc.iProduct && !cpr_is_incoming()) {
>          libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
>                                             (unsigned char *)udev->product_desc,
>                                             sizeof(udev->product_desc));

[Severity: Low]
Is skipping only this one transfer enough to avoid the hang? The commit
message says the transfer "can block there, hanging the incoming
migration", but no root cause is given.

If the cause is the device handover state, other calls in usb_host_open()
during CPR incoming could block in the same way:

usb_host_open()
    libusb_wrap_sys_device() / libusb_open()
    usb_host_detach_kernel()
    libusb_get_device_descriptor()
    usb_host_speed_compat()
        libusb_get_config_descriptor()
    ioctl(hostfd, USBDEVFS_GET_SPEED, NULL)
    usb_device_attach()

Could the root cause be described, and a comment added at this guard
explaining why only the product string read is skipped?

Also, nothing appears to re-read the real product string after CPR
completes. Does product_desc then stay as "host:bus.addr" where the source
showed the real name?

[Severity: Low]
This isn't a functional bug, but cpr_is_incoming() is a process-global check
(incoming_mode != MIG_MODE_NONE), not a per-device indicator that the device
was inherited via CPR.

incoming_mode is cleared only by cpr_set_incoming_mode(MIG_MODE_NONE) in
migration_incoming_state_destroy(). So the window is wider than the device
handover.

Can this also affect other opens during that window? For example,
usb_host_open() reached from usb_host_auto_check() or from the realize
paths would skip the product read for a device that was not handed over by
CPR. Such a device would get the synthetic "host:bus.addr" product_desc,
which is visible in info usb, in log messages and in traces.

The commit message does not mention this wider effect. Would a per-device
condition be more appropriate?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v8-0-72ae4fb8977b@virtuozzo.com?part=8

^ permalink raw reply	[relevance 3%]

* Re: [PATCH QEMU HCI-8.0 v8 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
  2026-10-06 16:27 24% ` [PATCH QEMU HCI-8.0 v8 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:44  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 16:44 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] usb-host: paused-VM CPR drain replays fully completed URBs, not just partial OUTs
--

commit 9334effcb0d32b4173bab08303e8d2fef677d756
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800

This commit queues the packets suppressed during the CPR drain on a new
cpr_inflight list. If CPR is aborted, the vm-state handler's resume branch
re-issues them on the preserved fd. Control transfers are completed with
USB_RET_IOERROR instead.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> index 413ddc12ee43..9cc7d72aa63a 100644
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> @@ -433,9 +435,10 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
>  /*
>   * Classify a reap during the CPR drain (r->p is still set).  The paused-VM
>   * fallback (cpr_replay_all) can't deliver completions, so it suppresses every
> - * reap for the target to replay; otherwise one that moved no data is suppressed
> - * and one that moved some is kept as a short transfer.  Returns true if
> - * suppressed, so the caller frees the request.
> + * reap; otherwise one that moved no data is suppressed and one that moved some
> + * is kept as a short transfer.  Suppressed packets are queued on cpr_inflight
> + * for the target to replay.  Returns true if suppressed, so the caller frees
> + * the request.
>   */
>  static bool usb_host_req_cpr_replay(USBHostRequest *r,
>                                      struct libusb_transfer *xfer)
> @@ -462,6 +465,9 @@ static bool usb_host_req_cpr_replay(USBHostRequest *r,
>              return false;
>          }
>      }
> +    if (r->p->state == USB_PACKET_ASYNC) {
> +        r->host->cpr_inflight = g_slist_append(r->host->cpr_inflight, r->p);
> +    }
>      return true;
>  }

[Severity: Medium]
When cpr_replay_all is set by usb_host_pre_save() (paused-VM fallback), the
whole "if (!r->host->cpr_replay_all) { ... }" block is skipped. Control
then reaches the g_slist_append() for every reaped URB, whatever
xfer->status or xfer->actual_length is.

That includes URBs that finished with LIBUSB_TRANSFER_COMPLETED.

> [ ... ]
>
> +static void usb_host_cpr_reissue(USBHostDevice *s)
> +{

[ ... ]

> +    for (it = s->cpr_inflight; it; it = it->next) {
> +        p = it->data;
> +        /*
> +         * Replay from the start of the TD.  A transfer that already moved some
> +         * data is re-run in full, so an OUT fragment the device consumed may be
> +         * sent twice.  Only the paused-VM pre_save fallback queues such partial
> +         * transfers here; the normal drain delivers them as short transfers.
> +         */
> +        p->actual_length = 0;
> +        if (p->ep->nr == 0) {
> +            /* control endpoint: can't be reconstructed, so error it out */
> +            p->status = USB_RET_IOERROR;
> +            usb_generic_async_ctrl_complete(udev, p);
> +        } else {
> +            usb_host_handle_data(udev, p);

Is the comment here, and the commit message, accurate about which
transfers get replayed?

The commit message says "an OUT fragment already consumed by the device may
be sent twice. Only the paused-VM .pre_save() fallback queues partial
transfers for re-issue". The code comment makes the same claim.

The path is usb_host_pre_save() setting cpr_replay_all = true, then
usb_host_cpr_quiesce(), then usb_host_req_cpr_replay() queuing every reap.
In that path fully completed URBs are queued as well, not only partial
ones.

On resume after an aborted CPR, usb_host_cpr_reissue() then handles them
as follows:

- A completed OUT transfer is sent to the device a second time.

- A completed IN transfer is resubmitted with actual_length reset to 0,
  so the data already read is lost and the device will not resend it.

- A completed control transfer, for example SET_CONFIGURATION or another
  SET_* request, is reported to the guest as USB_RET_IOERROR through
  usb_generic_async_ctrl_complete().

Should the commit message and the comment describe this wider scope? Or
should usb_host_req_cpr_replay() deliver URBs with xfer->status ==
LIBUSB_TRANSFER_COMPLETED normally, even in cpr_replay_all mode, instead
of queueing them for replay?

> [ ... ]

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v8-0-72ae4fb8977b@virtuozzo.com?part=7

^ permalink raw reply	[relevance 3%]

* Re: [PATCH QEMU HCI-8.0 v8 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800
  2026-10-06 16:28 29% ` [PATCH QEMU HCI-8.0 v8 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:50  7%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 16:50 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] usb-host: cpr-transfer target takes fd ownership in post_load() before migration commits
- [Medium] usb-host: target usb_host_open() fail path resets shared device before cpr_handed_off is set
- [Low] usb-host: cpr_handed_off left set on source after failed cpr-transfer with VM paused
- [Low] usb-host: cpr_handed_off guard in usb_host_close() also skips logical detach and cleanup
--

commit 642a3db85ee636f9e50d1ff27121f39b2c1b9026
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: hand off the device across cpr-transfer #VSTOR-137800

This patch hands reaping of the shared usbfs fd from the cpr-transfer source to the target. The target stops events in realize() and restarts them in post_load(). The source stops events when it quiesces and restarts them on resume. On success the source's exit path leaves the device untouched.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> @@ -1285,13 +1306,18 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
>  static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
>  {
>      if (usb_host_cpr_drain_xfers(s) < 0) {
>          return -1;
>      }
> +    if (mode == MIG_MODE_CPR_TRANSFER) {
> +        usb_host_stop_events();
> +        s->cpr_handed_off = true;
> +    }
>      return 0;
>  }

[Severity: Low]
cpr_handed_off is set here before the outcome of the handoff is known. It
is cleared only on VM resume (usb_host_cpr_reissue()) or device_del
(usb_host_unrealize()). The same applies to the usb_host_pre_save()
fallback for an already-paused VM, which also calls usb_host_cpr_quiesce().

Suppose the cpr-transfer migration fails or is cancelled and the VM stays
paused. The resume branch never runs, so the flag stays set and reaping
stays stopped for the whole libusb context.

A later QEMU quit then reaches usb_host_exit_notifier():

    if (!s->cpr_handed_off) {
        usb_host_abort_xfers(s);
        usb_host_release_interfaces(s);
        libusb_reset_device(s->dh);
        usb_host_attach_kernel(s);
    }
    libusb_close(s->dh);

Can this path tell a failed handoff from a successful one? As written, it
skips the reset and the host driver rebind even though no handoff happened.

[ ... ]

> @@ -1310,6 +1336,11 @@ static void usb_host_cpr_reissue(USBHostDevice *s)
>          return;
>      }
>  
> +    if (s->cpr_handed_off) {
> +        usb_host_start_events();
> +        s->cpr_handed_off = false;
> +    }

[Severity: Medium]
Following the failed-migration path, the source resumes and restarts
reaping here. If the target already called usb_host_start_events() in
usb_host_post_load(), both processes now reap URBs on the shared fd. See
the comment on usb_host_post_load() below.

> @@ -1384,16 +1415,19 @@ static int usb_host_close(USBHostDevice *s)
>  
>      trace_usb_host_close(s->bus_num, s->addr);
>  
> -    usb_host_abort_xfers(s);
> -    usb_host_iso_free_all(s);
> +    if (!s->cpr_handed_off) {
> +        usb_host_abort_xfers(s);
> +        usb_host_iso_free_all(s);
>  
> -    if (udev->attached) {
> -        usb_device_detach(udev);
> +        if (udev->attached) {
> +            usb_device_detach(udev);
> +        }
> +
> +        usb_host_release_interfaces(s);
> +        libusb_reset_device(s->dh);
> +        usb_host_attach_kernel(s);
>      }

[Severity: Low]
This guard covers more than the physical reset, release and rebind. It
also skips usb_host_abort_xfers(), usb_host_iso_free_all() and
usb_device_detach(). Should only the physical operations be gated?

Two callers can reach this with the flag set:

usb_host_realize()
  usb_host_open()   -> usb_device_attach()
  migrate_add_blocker_modes() fails
  usb_host_close()  -> usb_device_detach() skipped

On the target, the attach done by usb_host_open() is never paired with a
detach. usb_qdev_realize() only calls usb_release_port() on failure, so
the port is left with a stale attach.

usb_host_nodev_bh()
  usb_host_close()

If a surprise removal occurs between the source's quiesce and resume, the
handle is closed without detaching the device from the guest. The guest
then sees NODEV errors instead of a detach, while s->dh is NULL.

[ ... ]

> @@ -1500,6 +1541,18 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
>              error_setg(errp, "failed to open host usb device %s", s->hostdevice);
>              return;
>          }
> +        if (cpr_get_incoming_mode() == MIG_MODE_CPR_TRANSFER) {
> +            [ ... ]
> +            usb_host_stop_events();
> +            s->cpr_handed_off = true;
> +        }

[Severity: Medium]
The flag and the event stop are applied only after usb_host_open() has
succeeded. Can the failure path inside usb_host_open() reset the device
under the live source?

usb_host_realize()
  cpr_open_fd()
  usb_host_open(s, NULL, fd)
    ...
  fail:
    usb_host_release_interfaces(s);
    libusb_reset_device(s->dh);
    usb_host_attach_kernel(s);
    libusb_close(s->dh);

usb_host_open() does real work on the shared fd:
libusb_wrap_sys_device(), usb_host_detach_kernel(), usb_host_ep_update()
and usb_device_attach(). If a later step fails, for example
usb_device_attach() rejecting a speed mismatch, the fail: label runs
unconditionally and is not gated by cpr_handed_off. This conflicts with
the commit message statement that the target "must not touch the device at
all" until it takes over.

[ ... ]

> @@ -1563,6 +1616,17 @@ static void usb_host_unrealize(USBDevice *udev)
>      [ ... ]
> +    if (s->cpr_handed_off && !cpr_is_incoming()) {
> +        usb_host_start_events();
> +        s->cpr_handed_off = false;
> +    }
>      usb_host_close(s);

[Severity: Medium]
migration_incoming_state_destroy() calls cpr_set_incoming_mode(MIG_MODE_NONE),
so cpr_is_incoming() becomes false once an incoming migration has failed.

Consider a target that survives the failure, for example with exit-on-error
off, and still has cpr_handed_off == true. A device_del then takes the "we
own it" branch. It restarts events and runs the full reset and release
while the source has resumed with the device. Is cpr_is_incoming() a
reliable test for "the source still drives the fd" here?

[ ... ]

> @@ -2046,6 +2110,9 @@ static int usb_host_post_load(void *opaque, int version_id)
>       */
>      if (cpr_is_incoming()) {
> +        usb_host_start_events();
> +        /* the takeover is complete: from here on this process owns the fd */
> +        dev->cpr_handed_off = false;
>          return 0;
>      }

[Severity: Medium]
Ownership is taken here when this device's vmstate section is loaded, not
when the whole migration has succeeded. Later device sections, the end of
the stream and the source-side completion all come after this point.

If the migration fails after this section loads, for example from a later
device's load error, a broken stream or a cancel, the source resumes. Then
usb_host_cpr_reissue() restarts its own reaping:

target: usb_host_post_load()
          usb_host_start_events();
          dev->cpr_handed_off = false;
source: usb_host_cpr_vm_state() -> usb_host_cpr_reissue()
          usb_host_start_events();

Both processes then reap URBs on the same usbfs fd. This is the situation
the commit message says must be avoided, since one process can reap the
other's in-flight isochronous completions.

When the target then exits, usb_host_exit_notifier() sees
cpr_handed_off == false and runs:

usb_host_abort_xfers(s);
usb_host_release_interfaces(s);
libusb_reset_device(s->dh);
usb_host_attach_kernel(s);

This resets the device and rebinds the host driver under the resumed
source, which the commit message says the target's exit must not do.

Could the takeover be deferred until the migration has committed, so a
late failure leaves the target's flag set and its events stopped?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v8-0-72ae4fb8977b@virtuozzo.com?part=10

^ permalink raw reply	[relevance 7%]

* Re: [PATCH QEMU HCI-8.0 v8 12/14] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
  2026-10-06 16:28 10% ` [PATCH QEMU HCI-8.0 v8 12/14] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 16:52  6%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 16:52 UTC (permalink / raw)
  To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev, den

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] usb-host: CPR reclaim claims interfaces and sets configuration for guests that never configured the device
--

commit 4d875277381be99a72691b7786690c4fc8a9ce77
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800

On CPR incoming, this patch rebuilds the libusb endpoints with
usb_host_ep_update(). It also adds usb_host_cpr_reclaim_interfaces(), which
re-claims interfaces so that libusb's handle matches the preserved fd.

> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c

[ ... ]

> +static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
> +{

[ ... ]

> +    /* Data is sane: claim each interface on the fd and record it. */
> +    for (int i = 0; i < conf->bNumInterfaces; i++) {
> +        int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
> +
> +        rc = libusb_claim_interface(s->dh, ifnum);
> +        if (rc != 0) {
> +            usb_host_libusb_error("libusb_claim_interface", rc);
> +            libusb_free_config_descriptor(conf);
> +            return -1;
> +        }
> +        s->ifs[ifnum].claimed = true;
> +    }
> +
> +    udev->ninterfaces = conf->bNumInterfaces;
> +    udev->configuration = conf->bConfigurationValue;

[Severity: Low]
Can this claim interfaces and set udev->configuration for a guest that never
configured the device?

In usb_host_open(), interfaces are not claimed, and udev->ninterfaces and
udev->configuration are not set. They are only set through
usb_host_set_config()->usb_host_claim_interfaces() when the guest issues
SET_CONFIGURATION.

usb_host_cpr_reclaim_interfaces() instead derives its work from
libusb_get_active_config_descriptor(), which reflects the host kernel's
active configuration. It then stores:

    udev->ninterfaces = conf->bNumInterfaces;
    udev->configuration = conf->bConfigurationValue;

Nothing from the migrated state is consulted, such as the source's
s->ifs[].claimed. If the guest left the device in the address state while the
kernel's active configuration is non-zero, the target could claim interfaces
the source never claimed. It could also report a non-zero configuration that
the guest never selected.

So the commit message statement "on the preserved FD this is a no-op at the
usbfs level anyway" seems to hold only when the guest had already configured
the device. I did not trace the guest-side GET_CONFIGURATION handling.

Separately, if libusb_claim_interface() fails partway through the loop, for
example with EBUSY, the function returns -1 after earlier interfaces have
already been claimed and marked in s->ifs[ifnum].claimed:

    rc = libusb_claim_interface(s->dh, ifnum);
    if (rc != 0) {
        ...
        return -1;
    }
    s->ifs[ifnum].claimed = true;

There is no rollback. This seems to contradict the comment above the
validation loop, "so garbage migration data can't leave it half-claimed".
Would it make sense to release the earlier claims on this error path?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v8-0-72ae4fb8977b@virtuozzo.com?part=12

^ permalink raw reply	[relevance 6%]

* Re: [PATCH QEMU HCI-8.0 v8 13/14] hcd-xhci: resync isochronous endpoints after CPR #VSTOR-137800
  @ 2026-10-06 16:53  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 16:53 UTC (permalink / raw)
  To: svt-core, Andrey Drobyshev; +Cc: den, andrey.drobyshev

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] usb-xhci: cpr_iso_resync flag is not consumed by an empty-ring kick and drops a later valid TD
- [Medium] usb-xhci: CPR iso resync relies on a fabricated CC_MISSED_SERVICE_ERROR that only Linux guests may handle
--

commit b3cbde381fc3538ebb617db181b3540f42917e1d
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

hcd-xhci: resync isochronous endpoints after CPR #VSTOR-137800

This patch sets a per-endpoint flag in usb_xhci_post_load() for running
isochronous endpoints when CPR is incoming. The first isochronous TD
that reaches xhci_submit() is then completed with CC_MISSED_SERVICE_ERROR
instead of being sent to the device. The aim is to make the guest driver
skip and resync.

> diff --git a/hw/usb/hcd-xhci.c b/hw/usb/hcd-xhci.c
> --- a/hw/usb/hcd-xhci.c
> +++ b/hw/usb/hcd-xhci.c

[ ... ]

> @@ -1047,6 +1049,11 @@ static void xhci_set_ep_state(XHCIState *xhci, XHCIEPContext *epctx,
>      ctx[0] &= ~EP_STATE_MASK;
>      ctx[0] |= state;
>  
> +    if (state != EP_RUNNING) {
> +        /* a stop/reset before the first kick ends the post-CPR resync window */
> +        epctx->cpr_iso_resync = false;
> +    }
> +

[ ... ]

> @@ -1811,6 +1818,19 @@ static int xhci_submit(XHCIState *xhci, XHCITransfer *xfer, XHCIEPContext *epctx
>          xfer->pkts = 1;
>          xfer->iso_xfer = true;
>          xfer->timed_xfer = true;
> +        if (epctx->cpr_iso_resync) {
> +            /*
> +             * First iso TD after a CPR resume: our transfer state was
> +             * rebuilt from guest memory and no longer lines up with what
> +             * the guest consumed.  Report a missed service, like real HW,
> +             * to kick the driver's skip logic into resyncing.
> +             */
> +            epctx->cpr_iso_resync = false;
> +            xfer->status = CC_MISSED_SERVICE_ERROR;
> +            xhci_xfer_report(xfer);
> +            xfer->complete = 1;
> +            return 0;
> +        }

[Severity: Medium]
Can a later, correctly ordered isochronous TD be dropped here?

The comment in xhci_set_ep_state() says the resync window ends at the
first kick. The code ends it only at the first isochronous submit or at
the first non-running state change.

The post-load kick runs xhci_kick_epctx(). That function can return
without reaching xhci_submit():

    - it returns on !xhci_slot_ok()
    - it returns on !ring->dequeue
    - it breaks out of the loop when xhci_ring_chain_length() <= 0,
      which for ET_ISO_OUT/ET_ISO_IN only reports CC_RING_OVERRUN or
      CC_RING_UNDERRUN

xhci_kick_epctx() then calls xhci_set_ep_state(..., EP_RUNNING). The new
clearing code only fires for state != EP_RUNNING, so cpr_iso_resync
stays set.

The next isochronous TD the guest queues may arrive long after resume
and be in sync. It would hit this block in xhci_submit(), never be sent
to the device, and the guest would get a CC_MISSED_SERVICE_ERROR. For
iso OUT this loses one chunk of data and makes the guest run its skip
logic needlessly.

Should cpr_iso_resync be cleared when the post-load kick finishes,
rather than only on the first iso submit?

[Severity: Medium]
Does this approach depend on guest-specific handling of
CC_MISSED_SERVICE_ERROR?

The commit message says the error "arms the guest driver's skip logic".
That matches Linux xhci-hcd behaviour (ep->skip). Nothing in the patch
shows how other guests would handle this fabricated event. If a guest
treats it as a hard error or ignores it, the endpoint could stay wedged,
and the first TD's data is dropped anyway.

> @@ -3510,6 +3530,11 @@ static int usb_xhci_post_load(void *opaque, int version_id)
>              xhci_init_epctx(epctx, pctx, ep_ctx);
>              epctx->state = state;
>              if (state == EP_RUNNING) {
> +                if (cpr_is_incoming() &&
> +                    (epctx->type == ET_ISO_IN || epctx->type == ET_ISO_OUT)) {
> +                    /* only iso streams need resyncing; see xhci_submit() */
> +                    epctx->cpr_iso_resync = true;
> +                }

Also, usb_xhci_post_load() rebuilds the same transient state, including
mfindex_last via xhci_init_epctx(), for ordinary incoming migration.

Only the cpr_is_incoming() case gets the resync here.

Is the difference between CPR and regular incoming migration
intentional? If so, could the commit message explain it?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v8-0-72ae4fb8977b@virtuozzo.com?part=13

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH hci-8.1 v2 1/5] selftests/ve: check CPUID override emulation for VE tasks
  @ 2026-10-06 17:19  4% ` Konstantin Khorenko
  2026-10-06 17:29  3%   ` no-reply
  0 siblings, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-10-06 17:19 UTC (permalink / raw)
  To: Pavel Tikhomirov, Vasileios Almpanis; +Cc: devel

Write a one-entry /proc/vz/cpuid_override table that clears one leaf 1
feature bit and compare what a task in a fresh VE and the host see:
the VE task must get the table values, the host the hardware ones. A
second test flushes the table after the task was armed and expects the
hardware values again, since faulting stays on for the task and the
emulation has to fall back to the real CPUID.

The child stops itself with SIGSTOP after entering the VE and the
parent resumes it before the CPUID: faulting is switched on for a task
at its next context switch, not when the flag is set, and a stopped
task is off the CPU for sure. For the second test the table is flushed
while the child stands still.

Without the fix both children die with SIGSEGV. The tests skip on CPUs
without CPUID faulting and restore the previous table on exit. The
teardown runs in the parent because the harness skips a child-side
teardown once an ASSERT has fired. The test is built on x86 only.

https://virtuozzo.atlassian.net/browse/VSTOR-148843

Feature: x86: cpu pools (cpuid override)
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/selftests/ve/.gitignore              |   1 +
 tools/testing/selftests/ve/Makefile                |   7 +
 .../testing/selftests/ve/ve_cpuid_override_test.c  | 236 +++++++++++++++++++++
 3 files changed, 244 insertions(+)

diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index 3df4d05888dce..bdc322c645d95 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,3 +1,4 @@
 ve_ns_owner_test
 ve_perms_test
 ve_mount_accounting_test
+ve_cpuid_override_test
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index c6efe7c4b4fba..cd50a86fdb226 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -6,4 +6,11 @@ TEST_GEN_PROGS += ve_ns_owner_test
 TEST_GEN_PROGS += ve_perms_test
 TEST_GEN_PROGS += ve_mount_accounting_test
 
+ifeq ($(ARCH),)
+ARCH := $(shell uname -m 2>/dev/null || echo not)
+endif
+ifeq ($(ARCH),$(filter $(ARCH),x86 x86_64))
+TEST_GEN_PROGS += ve_cpuid_override_test
+endif
+
 include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
new file mode 100644
index 0000000000000..291b88404a53e
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c
@@ -0,0 +1,236 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * /proc/vz/cpuid_override selftests
+ *
+ * A non-empty override table turns CPUID faulting on for every task that
+ * enters a VE, and the #GP handler has to emulate the instruction for
+ * them. The tests check that a task in a VE sees the table values, that
+ * the host keeps seeing the hardware values, and that a task which was
+ * armed while the table was set falls back to the hardware once the
+ * table is flushed. A kernel that does not emulate the fault kills the
+ * child with SIGSEGV instead.
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/wait.h>
+
+#include "../kselftest_harness.h"
+#include "ve_selftest.h"
+
+#define CPUID_OVERRIDE_PATH	"/proc/vz/cpuid_override"
+#define CPUID_LEAF		1
+#define CPUID_TABLE_LEN		128
+/* 128 entries of up to 67 characters each, as the kernel prints them. */
+#define CPUID_TABLE_MAX		16384
+
+struct cpuid_regs {
+	unsigned int eax, ebx, ecx, edx;
+};
+
+/* x86_64 only, so ebx is not the PIC base and "=b" is safe. */
+static void do_cpuid(unsigned int leaf, struct cpuid_regs *r)
+{
+	asm volatile("cpuid"
+		     : "=a" (r->eax), "=b" (r->ebx), "=c" (r->ecx), "=d" (r->edx)
+		     : "a" (leaf), "c" (0));
+}
+
+static int write_cpuid_override(const char *table)
+{
+	int fd, ret;
+
+	fd = open(CPUID_OVERRIDE_PATH, O_WRONLY);
+	if (fd < 0)
+		return -1;
+	ret = write(fd, table, strlen(table));
+	close(fd);
+	return ret == (int)strlen(table) ? 0 : -1;
+}
+
+/*
+ * The child enters a fresh VE, so it is armed with the table state of that
+ * moment, then reports its view of the leaf to the parent over a pipe. It
+ * stops itself in between: a stopped task is off the CPU, so the resume is
+ * a context switch, and the parent may change the table while the child
+ * stands still.
+ */
+struct child_ctx {
+	int result_pipe[2];
+};
+
+static int cpuid_child(struct child_ctx *ctx)
+{
+	struct cpuid_regs r;
+
+	close(ctx->result_pipe[0]);
+
+	if (unshare(CLONE_NEWVE) < 0)
+		return 21;
+	if (raise(SIGSTOP))
+		return 22;
+
+	do_cpuid(CPUID_LEAF, &r);
+
+	if (write(ctx->result_pipe[1], &r, sizeof(r)) != sizeof(r))
+		return 24;
+	return 0;
+}
+
+FIXTURE(ve_cpuid_override)
+{
+	int cgv2_fd;
+	int ctid;
+	struct cpuid_regs host;
+	unsigned int bit;
+	char table[CPUID_TABLE_LEN];
+	char saved[CPUID_TABLE_MAX];
+};
+
+FIXTURE_SETUP(ve_cpuid_override)
+{
+	int fd, len;
+
+	fd = open(CPUID_OVERRIDE_PATH, O_RDONLY);
+	if (fd < 0)
+		SKIP(return, "%s: %s", CPUID_OVERRIDE_PATH, strerror(errno));
+	close(fd);
+
+	len = read_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->saved,
+			   sizeof(self->saved));
+	ASSERT_GE(len, 0);
+	ASSERT_LT(len, sizeof(self->saved) - 1);
+
+	/* Mask one feature bit the host has; SSE2 when present. */
+	do_cpuid(CPUID_LEAF, &self->host);
+	self->bit = 1u << 26;
+	if (!(self->host.edx & self->bit))
+		self->bit = self->host.edx & -self->host.edx;
+	ASSERT_NE(self->bit, 0);
+
+	snprintf(self->table, sizeof(self->table),
+		 "0x%08x: 0x%08x 0x%08x 0x%08x 0x%08x\n", CPUID_LEAF,
+		 self->host.eax, self->host.ebx, self->host.ecx,
+		 self->host.edx & ~self->bit);
+
+	/* Last, so that a failed ASSERT above leaves no VE cgroup behind. */
+	self->cgv2_fd = mount_cg2_fd();
+	ASSERT_GE(self->cgv2_fd, 0);
+	ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
+				VE_CONTROLLERS), 0);
+	self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
+	ASSERT_GE(self->ctid, 0);
+}
+
+/*
+ * Runs in the parent: a child-side teardown is skipped when an ASSERT fires,
+ * and the table must be restored even then. The fd from setup belongs to the
+ * child, so the cgroup2 tree is mounted again here.
+ */
+FIXTURE_TEARDOWN_PARENT(ve_cpuid_override)
+{
+	int cgv2_fd;
+
+	if (write_cpuid_override(self->saved[0] ? self->saved : "\n"))
+		TH_LOG("could not restore %s: %s", CPUID_OVERRIDE_PATH,
+		       strerror(errno));
+
+	cgv2_fd = mount_cg2_fd();
+	if (cgv2_fd < 0)
+		return;
+	destroy_ve(cgv2_fd, self->ctid);
+	close(cgv2_fd);
+}
+
+/* Fork the child into the VE and return once it stands still there. */
+static pid_t spawn_cpuid_child(struct __test_metadata *_metadata,
+			       int cgv2_fd, int ctid, struct child_ctx *ctx)
+{
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(pipe(ctx->result_pipe), 0);
+	ASSERT_GE(enter_cgroup(cgv2_fd, ctid), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0)
+		_exit(cpuid_child(ctx));
+
+	ASSERT_GE(enter_cgroup(cgv2_fd, 0), 0);
+	close(ctx->result_pipe[1]);
+
+	ASSERT_GE(waitpid(pid, &status, WUNTRACED), 0);
+	if (WIFEXITED(status))
+		TH_LOG("child exited with %d before stopping",
+		       WEXITSTATUS(status));
+	ASSERT_TRUE(WIFSTOPPED(status));
+	return pid;
+}
+
+static void resume_cpuid_child(struct __test_metadata *_metadata, pid_t pid)
+{
+	ASSERT_EQ(kill(pid, SIGCONT), 0);
+}
+
+static void collect_cpuid_child(struct __test_metadata *_metadata, pid_t pid,
+				struct child_ctx *ctx, struct cpuid_regs *r)
+{
+	int status, len;
+
+	len = read(ctx->result_pipe[0], r, sizeof(*r));
+	ASSERT_GE(waitpid(pid, &status, 0), 0);
+	if (WIFSIGNALED(status))
+		TH_LOG("child killed by signal %d", WTERMSIG(status));
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+	ASSERT_EQ(len, sizeof(*r));
+
+	close(ctx->result_pipe[0]);
+}
+
+TEST_F(ve_cpuid_override, ve_sees_table_host_sees_hardware)
+{
+	struct child_ctx ctx;
+	struct cpuid_regs r;
+	pid_t pid;
+
+	ASSERT_EQ(write_cpuid_override(self->table), 0);
+
+	pid = spawn_cpuid_child(_metadata, self->cgv2_fd, self->ctid, &ctx);
+	resume_cpuid_child(_metadata, pid);
+	collect_cpuid_child(_metadata, pid, &ctx, &r);
+
+	EXPECT_EQ(r.eax, self->host.eax);
+	EXPECT_EQ(r.ebx, self->host.ebx);
+	EXPECT_EQ(r.ecx, self->host.ecx);
+	EXPECT_EQ(r.edx, self->host.edx & ~self->bit);
+
+	do_cpuid(CPUID_LEAF, &r);
+	EXPECT_NE(r.edx & self->bit, 0);
+}
+
+TEST_F(ve_cpuid_override, flushed_table_falls_back_to_hardware)
+{
+	struct child_ctx ctx;
+	struct cpuid_regs r;
+	pid_t pid;
+
+	ASSERT_EQ(write_cpuid_override(self->table), 0);
+
+	pid = spawn_cpuid_child(_metadata, self->cgv2_fd, self->ctid, &ctx);
+
+	ASSERT_EQ(write_cpuid_override("\n"), 0);
+	resume_cpuid_child(_metadata, pid);
+	collect_cpuid_child(_metadata, pid, &ctx, &r);
+
+	EXPECT_NE(r.edx & self->bit, 0);
+}
+
+TEST_HARNESS_MAIN

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH hci-8.1 v2 1/5] selftests/ve: check CPUID override emulation for VE tasks
  2026-10-06 17:19  4% ` [Devel] [PATCH hci-8.1 v2 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
@ 2026-10-06 17:29  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 17:29 UTC (permalink / raw)
  To: Konstantin Khorenko, Vasileios Almpanis, Pavel Tikhomirov; +Cc: devel

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] selftests/ve: restore of saved cpuid_override table fails for tables of 4096 bytes or more
- [Low] selftests/ve: stopped child is never killed or reaped when an ASSERT fires
- [Low] selftests/ve: Makefile overwrites global ARCH, breaking native LLVM=1 builds on non-x86
--

commit 6db1939d0f0292549e9feb0b4de5043c4eac6ba7
Author: Konstantin Khorenko <khorenko@virtuozzo.com>

selftests/ve: check CPUID override emulation for VE tasks

This adds a selftest that writes a one-entry /proc/vz/cpuid_override table
and checks that a task in a fresh VE sees the table values while the host
sees the hardware values. A second test flushes the table after the task
was armed and expects the hardware values again. The test is built on x86
only.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-148843

> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
> --- a/tools/testing/selftests/ve/Makefile
> +++ b/tools/testing/selftests/ve/Makefile
> @@ -6,4 +6,11 @@ TEST_GEN_PROGS += ve_ns_owner_test
>  TEST_GEN_PROGS += ve_perms_test
>  TEST_GEN_PROGS += ve_mount_accounting_test
>  
> +ifeq ($(ARCH),)
> +ARCH := $(shell uname -m 2>/dev/null || echo not)
> +endif
> +ifeq ($(ARCH),$(filter $(ARCH),x86 x86_64))
> +TEST_GEN_PROGS += ve_cpuid_override_test
> +endif
> +
>  include ../lib.mk

[Severity: Low]
Can setting ARCH here break native LLVM=1 builds on non-x86 hosts?

The block assigns ARCH from uname -m before include ../lib.mk.  In lib.mk
the clang target selection depends on ARCH being empty:

    ifeq ($(ARCH),)
    CLANG_TARGET_FLAGS := $(shell $(CLANG) -print-target-triple)
    else
    CLANG_TARGET_FLAGS := $(CLANG_TARGET_FLAGS_$(ARCH))
    endif

With "make LLVM=1 -C tools/testing/selftests/ve" on a host such as aarch64
or ppc64le, with ARCH and CROSS_COMPILE unset, ARCH is now non-empty and
CLANG_TARGET_FLAGS_$(ARCH) is empty.  lib.mk then hits "$(error Specify
CROSS_COMPILE or add '--target=' option to lib.mk)", so
ve_ns_owner_test, ve_perms_test and ve_mount_accounting_test also stop
building.

Would a private variable such as uname_M be better than overwriting ARCH?
The series-end diff of tools/testing/selftests/ve/ shows no later change to
this Makefile.

> diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
> new file mode 100644
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c

[ ... ]

> +/* 128 entries of up to 67 characters each, as the kernel prints them. */
> +#define CPUID_TABLE_MAX		16384

[ ... ]

> +FIXTURE_TEARDOWN_PARENT(ve_cpuid_override)
> +{
> +	int cgv2_fd;
> +
> +	if (write_cpuid_override(self->saved[0] ? self->saved : "\n"))
> +		TH_LOG("could not restore %s: %s", CPUID_OVERRIDE_PATH,
> +		       strerror(errno));
> +
> +	cgv2_fd = mount_cg2_fd();
> +	if (cgv2_fd < 0)
> +		return;
> +	destroy_ve(cgv2_fd, self->ctid);
> +	close(cgv2_fd);
> +}

[Severity: Medium]
Can the restore of the saved table fail for large tables?

FIXTURE_SETUP reads the printed table into the 16384-byte saved[] buffer
and accepts anything below 16383 bytes.  FIXTURE_TEARDOWN_PARENT then
writes it back with a single write() through write_cpuid_override().

In arch/x86/kernel/cpuid_fault.c, cpuid_override_write() starts with:

    err = -E2BIG;
    if (count >= PAGE_SIZE)
            goto out;

A table that prints to 4096 bytes or more (roughly 61 or more entries at
about 67 characters each) is read fine but rejected with -E2BIG on
restore.  Such a table can only have been loaded by writing it in chunks.

Teardown only logs the failure with TH_LOG, so the host-wide table stays
replaced after the test and later VE tasks see the wrong leaf 1 values.
That also contradicts the commit message statement "restore the previous
table on exit".  Should the restore write the table in chunks below
PAGE_SIZE, or should setup skip when the saved table is too large to
restore?  The later patches in the series do not change the restore logic.

[Severity: Low]
Can a stopped child be left behind when an ASSERT fires?

FIXTURE_TEARDOWN_PARENT neither kills nor reaps the child, and the pid
returned by spawn_cpuid_child() is not kept in the fixture.  The child has
no PR_SET_PDEATHSIG.

> +	pid = spawn_cpuid_child(_metadata, self->cgv2_fd, self->ctid, &ctx);
> +
> +	ASSERT_EQ(write_cpuid_override("\n"), 0);
> +	resume_cpuid_child(_metadata, pid);
> +	collect_cpuid_child(_metadata, pid, &ctx, &r);

If ASSERT_EQ(write_cpuid_override("\n"), 0) fails, or the kill(SIGCONT)
ASSERT in resume_cpuid_child() fails, or any ASSERT in
collect_cpuid_child() fails, the test body returns before the child is
resumed and waited for.

The child stays SIGSTOPped inside the VE cgroup and is reparented to init.
Then destroy_ve() in FIXTURE_TEARDOWN_PARENT fails with EBUSY because the
child is still in the cgroup.

Could the pid be stored in the fixture so teardown can kill and reap it
before destroy_ve()?  The later patches add more callers of
spawn_cpuid_child() but do not add any such cleanup.

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-b4-prep-khorenko-vstor-148656-cpuid-v2-0-f6d1192c7d60@virtuozzo.com?part=1

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH hci-8.1 v3 1/5] selftests/ve: check CPUID override emulation for VE tasks
  @ 2026-10-06 20:44  4% ` Konstantin Khorenko
  2026-10-06 20:52  3%   ` no-reply
  0 siblings, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-10-06 20:44 UTC (permalink / raw)
  To: Pavel Tikhomirov, Vasileios Almpanis; +Cc: devel

Write a one-entry /proc/vz/cpuid_override table that clears one leaf 1
feature bit and compare what a task in a fresh VE and the host see:
the VE task must get the table values, the host the hardware ones. A
second test flushes the table after the task was armed and expects the
hardware values again, since faulting stays on for the task and the
emulation has to fall back to the real CPUID.

The child stops itself with SIGSTOP after entering the VE and the
parent resumes it before the CPUID: faulting is switched on for a task
at its next context switch, not when the flag is set, and a stopped
task is off the CPU for sure. For the second test the table is flushed
while the child stands still.

Without the fix both children die with SIGSEGV. The tests skip on CPUs
without CPUID faulting and restore the previous table on exit; a table
the kernel cannot take back in one write fails the setup instead. The
teardown runs in the parent because the harness skips a child-side
teardown once an ASSERT has fired, and it kills a child that such an
ASSERT left stopped in the VE cgroup. The test is built on x86 only.

https://virtuozzo.atlassian.net/browse/VSTOR-148843

Feature: x86: cpu pools (cpuid override)
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/selftests/ve/.gitignore              |   1 +
 tools/testing/selftests/ve/Makefile                |  10 +
 .../testing/selftests/ve/ve_cpuid_override_test.c  | 247 +++++++++++++++++++++
 3 files changed, 258 insertions(+)

diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index 3df4d05888dce..bdc322c645d95 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,3 +1,4 @@
 ve_ns_owner_test
 ve_perms_test
 ve_mount_accounting_test
+ve_cpuid_override_test
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index c6efe7c4b4fba..d919c1c289c8b 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -6,4 +6,14 @@ TEST_GEN_PROGS += ve_ns_owner_test
 TEST_GEN_PROGS += ve_perms_test
 TEST_GEN_PROGS += ve_mount_accounting_test
 
+# ARCH stays untouched: lib.mk picks the clang target by its emptiness.
+ifeq ($(ARCH),)
+ve_arch := $(shell uname -m 2>/dev/null || echo not)
+else
+ve_arch := $(ARCH)
+endif
+ifeq ($(ve_arch),$(filter $(ve_arch),x86 x86_64))
+TEST_GEN_PROGS += ve_cpuid_override_test
+endif
+
 include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
new file mode 100644
index 0000000000000..0e3537099ddb2
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c
@@ -0,0 +1,247 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * /proc/vz/cpuid_override selftests
+ *
+ * A non-empty override table turns CPUID faulting on for every task that
+ * enters a VE, and the #GP handler has to emulate the instruction for
+ * them. The tests check that a task in a VE sees the table values, that
+ * the host keeps seeing the hardware values, and that a task which was
+ * armed while the table was set falls back to the hardware once the
+ * table is flushed. A kernel that does not emulate the fault kills the
+ * child with SIGSEGV instead.
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/wait.h>
+
+#include "../kselftest_harness.h"
+#include "ve_selftest.h"
+
+#define CPUID_OVERRIDE_PATH	"/proc/vz/cpuid_override"
+#define CPUID_LEAF		1
+#define CPUID_TABLE_LEN		128
+/* The kernel takes a table back only in one write below a page. */
+#define CPUID_WRITE_MAX		4096
+#define CPUID_TABLE_MAX		(2 * CPUID_WRITE_MAX)
+
+struct cpuid_regs {
+	unsigned int eax, ebx, ecx, edx;
+};
+
+/* x86_64 only, so ebx is not the PIC base and "=b" is safe. */
+static void do_cpuid(unsigned int leaf, struct cpuid_regs *r)
+{
+	asm volatile("cpuid"
+		     : "=a" (r->eax), "=b" (r->ebx), "=c" (r->ecx), "=d" (r->edx)
+		     : "a" (leaf), "c" (0));
+}
+
+static int write_cpuid_override(const char *table)
+{
+	int fd, ret;
+
+	fd = open(CPUID_OVERRIDE_PATH, O_WRONLY);
+	if (fd < 0)
+		return -1;
+	ret = write(fd, table, strlen(table));
+	close(fd);
+	return ret == (int)strlen(table) ? 0 : -1;
+}
+
+/*
+ * The child enters a fresh VE, so it is armed with the table state of that
+ * moment, then reports its view of the leaf to the parent over a pipe. It
+ * stops itself in between: a stopped task is off the CPU, so the resume is
+ * a context switch, and the parent may change the table while the child
+ * stands still.
+ */
+struct child_ctx {
+	int result_pipe[2];
+};
+
+static int cpuid_child(struct child_ctx *ctx)
+{
+	struct cpuid_regs r;
+
+	close(ctx->result_pipe[0]);
+
+	if (unshare(CLONE_NEWVE) < 0)
+		return 21;
+	if (raise(SIGSTOP))
+		return 22;
+
+	do_cpuid(CPUID_LEAF, &r);
+
+	if (write(ctx->result_pipe[1], &r, sizeof(r)) != sizeof(r))
+		return 24;
+	return 0;
+}
+
+FIXTURE(ve_cpuid_override)
+{
+	int cgv2_fd;
+	int ctid;
+	struct cpuid_regs host;
+	unsigned int bit;
+	pid_t child;
+	char table[CPUID_TABLE_LEN];
+	char saved[CPUID_TABLE_MAX];
+};
+
+FIXTURE_SETUP(ve_cpuid_override)
+{
+	int fd, len;
+
+	fd = open(CPUID_OVERRIDE_PATH, O_RDONLY);
+	if (fd < 0)
+		SKIP(return, "%s: %s", CPUID_OVERRIDE_PATH, strerror(errno));
+	close(fd);
+
+	len = read_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->saved,
+			   sizeof(self->saved));
+	ASSERT_GE(len, 0);
+	ASSERT_LT(len, CPUID_WRITE_MAX)
+		TH_LOG("a table of %d bytes cannot be restored", len);
+
+	/* Mask one feature bit the host has; SSE2 when present. */
+	do_cpuid(CPUID_LEAF, &self->host);
+	self->bit = 1u << 26;
+	if (!(self->host.edx & self->bit))
+		self->bit = self->host.edx & -self->host.edx;
+	ASSERT_NE(self->bit, 0);
+
+	snprintf(self->table, sizeof(self->table),
+		 "0x%08x: 0x%08x 0x%08x 0x%08x 0x%08x\n", CPUID_LEAF,
+		 self->host.eax, self->host.ebx, self->host.ecx,
+		 self->host.edx & ~self->bit);
+
+	/* Last, so that a failed ASSERT above leaves no VE cgroup behind. */
+	self->cgv2_fd = mount_cg2_fd();
+	ASSERT_GE(self->cgv2_fd, 0);
+	ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
+				VE_CONTROLLERS), 0);
+	self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
+	ASSERT_GE(self->ctid, 0);
+}
+
+/*
+ * Runs in the parent: a child-side teardown is skipped when an ASSERT fires,
+ * and the table must be restored even then. The fd from setup belongs to the
+ * child, so the cgroup2 tree is mounted again here.
+ */
+FIXTURE_TEARDOWN_PARENT(ve_cpuid_override)
+{
+	int cgv2_fd;
+
+	if (write_cpuid_override(self->saved[0] ? self->saved : "\n"))
+		TH_LOG("could not restore %s: %s", CPUID_OVERRIDE_PATH,
+		       strerror(errno));
+
+	/* A child left stopped by a failed ASSERT keeps the VE cgroup busy. */
+	if (self->child > 0)
+		kill(self->child, SIGKILL);
+
+	cgv2_fd = mount_cg2_fd();
+	if (cgv2_fd < 0)
+		return;
+	destroy_ve(cgv2_fd, self->ctid);
+	close(cgv2_fd);
+}
+
+/* Fork the child into the VE and return once it stands still there. */
+static pid_t spawn_cpuid_child(struct __test_metadata *_metadata,
+			       FIXTURE_DATA(ve_cpuid_override) * self,
+			       struct child_ctx *ctx)
+{
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(pipe(ctx->result_pipe), 0);
+	ASSERT_GE(enter_cgroup(self->cgv2_fd, self->ctid), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0)
+		_exit(cpuid_child(ctx));
+	self->child = pid;
+
+	ASSERT_GE(enter_cgroup(self->cgv2_fd, 0), 0);
+	close(ctx->result_pipe[1]);
+
+	ASSERT_GE(waitpid(pid, &status, WUNTRACED), 0);
+	if (WIFEXITED(status))
+		TH_LOG("child exited with %d before stopping",
+		       WEXITSTATUS(status));
+	ASSERT_TRUE(WIFSTOPPED(status));
+	return pid;
+}
+
+static void resume_cpuid_child(struct __test_metadata *_metadata, pid_t pid)
+{
+	ASSERT_EQ(kill(pid, SIGCONT), 0);
+}
+
+static void collect_cpuid_child(struct __test_metadata *_metadata,
+				FIXTURE_DATA(ve_cpuid_override) * self,
+				struct child_ctx *ctx, struct cpuid_regs *r)
+{
+	int status, len;
+
+	len = read(ctx->result_pipe[0], r, sizeof(*r));
+	ASSERT_GE(waitpid(self->child, &status, 0), 0);
+	self->child = 0;
+	if (WIFSIGNALED(status))
+		TH_LOG("child killed by signal %d", WTERMSIG(status));
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+	ASSERT_EQ(len, sizeof(*r));
+
+	close(ctx->result_pipe[0]);
+}
+
+TEST_F(ve_cpuid_override, ve_sees_table_host_sees_hardware)
+{
+	struct child_ctx ctx;
+	struct cpuid_regs r;
+	pid_t pid;
+
+	ASSERT_EQ(write_cpuid_override(self->table), 0);
+
+	pid = spawn_cpuid_child(_metadata, self, &ctx);
+	resume_cpuid_child(_metadata, pid);
+	collect_cpuid_child(_metadata, self, &ctx, &r);
+
+	EXPECT_EQ(r.eax, self->host.eax);
+	EXPECT_EQ(r.ebx, self->host.ebx);
+	EXPECT_EQ(r.ecx, self->host.ecx);
+	EXPECT_EQ(r.edx, self->host.edx & ~self->bit);
+
+	do_cpuid(CPUID_LEAF, &r);
+	EXPECT_NE(r.edx & self->bit, 0);
+}
+
+TEST_F(ve_cpuid_override, flushed_table_falls_back_to_hardware)
+{
+	struct child_ctx ctx;
+	struct cpuid_regs r;
+	pid_t pid;
+
+	ASSERT_EQ(write_cpuid_override(self->table), 0);
+
+	pid = spawn_cpuid_child(_metadata, self, &ctx);
+
+	ASSERT_EQ(write_cpuid_override("\n"), 0);
+	resume_cpuid_child(_metadata, pid);
+	collect_cpuid_child(_metadata, self, &ctx, &r);
+
+	EXPECT_NE(r.edx & self->bit, 0);
+}
+
+TEST_HARNESS_MAIN

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH hci-8.1 v3 1/5] selftests/ve: check CPUID override emulation for VE tasks
  2026-10-06 20:44  4% ` [Devel] [PATCH hci-8.1 v3 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
@ 2026-10-06 20:52  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 20:52 UTC (permalink / raw)
  To: Vasileios Almpanis, Konstantin Khorenko, Pavel Tikhomirov; +Cc: devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] selftests/ve: stale self->child PID is killed in teardown after early reap
--

commit 2e6583525965f296409edf5f5ba686bf9a7c7bce
Author: Konstantin Khorenko <khorenko@virtuozzo.com>

selftests/ve: check CPUID override emulation for VE tasks

This commit adds a selftest for /proc/vz/cpuid_override. It checks that a
task in a fresh VE sees the override table values while the host sees the
hardware values. It also checks that a task armed before the table is
flushed falls back to the real CPUID afterwards.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-148843

> diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
> new file mode 100644
> index 000000000000..0e3537099ddb
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c

[ ... ]

> +FIXTURE_TEARDOWN_PARENT(ve_cpuid_override)
> +{
> +	int cgv2_fd;
> +
> +	if (write_cpuid_override(self->saved[0] ? self->saved : "\n"))
> +		TH_LOG("could not restore %s: %s", CPUID_OVERRIDE_PATH,
> +		       strerror(errno));
> +
> +	/* A child left stopped by a failed ASSERT keeps the VE cgroup busy. */
> +	if (self->child > 0)
> +		kill(self->child, SIGKILL);

[Severity: Low]
Can this kill() hit a PID that no longer belongs to the test child?

In spawn_cpuid_child(), self->child is set right after fork():

	self->child = pid;
	...
	ASSERT_GE(waitpid(pid, &status, WUNTRACED), 0);
	if (WIFEXITED(status))
		TH_LOG("child exited with %d before stopping",
		       WEXITSTATUS(status));
	ASSERT_TRUE(WIFSTOPPED(status));

If the child exits or is killed before it stops (for example cpuid_child()
returning 21, 22 or 24), waitpid() reaps it. ASSERT_TRUE(WIFSTOPPED(status))
then fails and aborts the test, but self->child is not cleared on that path.

self->child is only cleared in collect_cpuid_child(), which this path never
reaches. The teardown runs in the parent and sees the stale non-zero
self->child. It then calls kill(self->child, SIGKILL) on a PID that has
already been reaped and could have been reused by an unrelated process.

Would it make sense to clear self->child in spawn_cpuid_child() once
waitpid() has reaped the child, before the ASSERT_TRUE(WIFSTOPPED(status))
check?

[ ... ]

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-b4-prep-khorenko-vstor-148656-cpuid-v3-0-16141b8ca07d@virtuozzo.com?part=1

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH hci-8.1 v2 3/6] ve/printk: Specify ve in printks
      @ 2026-10-06 21:53 15% ` Vladimir Riabchun
  2026-10-06 22:03  3%   ` no-reply
  2026-10-06 21:53  4% ` [Devel] [PATCH hci-8.1 v2 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
                   ` (2 subsequent siblings)
  5 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-06 21:53 UTC (permalink / raw)
  To: vasileios.almpanis, Konstantin Khorenko; +Cc: devel

Introduce ve_printk helpers that accept VE structure,
so we can provide correct VE where get_exec_env is unreliable.

This is true for ve_printk_deferred functions, and their users.
Now VE ratelimiting requires specified VE, and this requirement
is further forwarded to ve_net_ratelimited kind.

https://virtuozzo.atlassian.net/browse/VSTOR-146839

Feature: ve printk
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
 include/linux/net.h                 | 16 ++++----
 include/linux/printk.h              | 28 ++++++++++---
 include/linux/ratelimit_types.h     |  6 ++-
 kernel/printk/printk.c              | 78 ++++++++++++++++++++++++++++++-------
 lib/ratelimit.c                     |  7 ++--
 net/core/utils.c                    |  4 +-
 net/netfilter/nf_conntrack_core.c   |  2 +-
 net/netfilter/nf_conntrack_expect.c |  2 +-
 net/netfilter/xt_TCPMSS.c           |  4 +-
 9 files changed, 110 insertions(+), 37 deletions(-)

diff --git a/include/linux/net.h b/include/linux/net.h
index 3f3bbe9f149d..4c34b9a0386f 100644
--- a/include/linux/net.h
+++ b/include/linux/net.h
@@ -265,7 +265,7 @@ struct socket *sockfd_lookup(int fd, int *err);
 struct socket *sock_from_file(struct file *file);
 #define		     sockfd_put(sock) fput(sock->file)
 int net_ratelimit(void);
-int ve_net_ratelimit(int dst);
+int ve_net_ratelimit(int dst, struct ve_struct *ve);
 
 #define net_ratelimited_function(function, ...)			\
 do {								\
@@ -273,10 +273,10 @@ do {								\
 		function(__VA_ARGS__);				\
 } while (0)
 
-#define ve_net_ratelimited_function(function, dst, ...)		\
+#define ve_net_ratelimited_function(function, dst, ve, ...)	\
 do {								\
-	if (ve_net_ratelimit(dst))				\
-		function(dst, __VA_ARGS__);			\
+	if (ve_net_ratelimit(dst, ve))				\
+		function(dst, ve, __VA_ARGS__);			\
 } while (0)
 
 #define net_emerg_ratelimited(fmt, ...)				\
@@ -311,10 +311,10 @@ do {									\
 	no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
 #endif
 
-#define net_velog_ratelimited(fmt, ...)                                \
-	ve_net_ratelimited_function(ve_printk, VE_LOG, fmt, ##__VA_ARGS__)
-#define net_veboth_ratelimited(fmt, ...)                               \
-	ve_net_ratelimited_function(ve_printk, VE_LOG_BOTH, fmt, ##__VA_ARGS__)
+#define net_velog_ratelimited(ve, fmt, ...)						\
+	ve_net_ratelimited_function(__ve_printk, VE_LOG, ve, fmt, ##__VA_ARGS__)
+#define net_veboth_ratelimited(ve, fmt, ...)						\
+	ve_net_ratelimited_function(__ve_printk, VE_LOG_BOTH, ve, fmt, ##__VA_ARGS__)
 
 #define net_get_random_once(buf, nbytes)			\
 	get_random_once((buf), (nbytes))
diff --git a/include/linux/printk.h b/include/linux/printk.h
index 3b0af0deb6c3..a9470c81f8db 100644
--- a/include/linux/printk.h
+++ b/include/linux/printk.h
@@ -76,6 +76,7 @@ extern int console_printk[];
 #define VE0_LOG		1
 #define VE_LOG		2
 #define VE_LOG_BOTH	(VE0_LOG | VE_LOG)
+struct ve_struct;
 
 extern void console_verbose(void);
 
@@ -160,6 +161,11 @@ int ve_vprintk_emit(int facility, int level,
 		    const struct dev_printk_info *dev_info,
 		    const char *fmt, va_list args);
 
+asmlinkage __printf(5, 0)
+int __ve_vprintk_emit(struct ve_struct *ve, int facility, int level,
+		    const struct dev_printk_info *dev_info,
+		    const char *fmt, va_list args);
+
 asmlinkage __printf(1, 0)
 int vprintk(const char *fmt, va_list args);
 __printf(1, 0)
@@ -171,8 +177,11 @@ int _printk(const char *fmt, ...);
 asmlinkage __printf(2, 3) __cold
 int ve_printk(int dst, const char *fmt, ...);
 
-asmlinkage __printf(2, 3) __cold
-int ve_printk_deferred(int dst, const char *fmt, ...);
+asmlinkage __printf(3, 4) __cold
+int __ve_printk(int dst, struct ve_struct *ve, const char *fmt, ...);
+
+asmlinkage __printf(3, 4) __cold
+int ve_printk_deferred(int dst, struct ve_struct *ve, const char *fmt, ...);
 
 struct ve_struct;
 int ve_log_init(struct ve_struct *ve);
@@ -248,8 +257,13 @@ int ve_printk(int dst, const char *s, ...)
 {
 	return 0;
 }
-static inline __printf(2, 3) __cold
-int ve_printk_deferred(int dst, const char *s, ...)
+static inline __printf(3, 4) __cold
+int __ve_printk(int dst, struct ve_struct *ve, const char *s, ...)
+{
+	return 0;
+}
+static inline __printf(3, 4) __cold
+int ve_printk_deferred(int dst, struct ve_struct *ve, const char *s, ...)
 {
 	return 0;
 }
@@ -746,8 +760,12 @@ struct pi_entry {
 	static DEFINE_RATELIMIT_STATE(_rs,				\
 				      DEFAULT_RATELIMIT_INTERVAL,	\
 				      DEFAULT_RATELIMIT_BURST);		\
+	struct ve_struct *__ve = get_exec_env();			\
+									\
+	if (WARN_ON_ONCE((dst & VE_LOG) && !in_task()))			\
+		__ve = get_ve0();					\
 									\
-	if (ve_ratelimit(dst, &_rs))					\
+	if (ve_ratelimit(dst, __ve, &_rs))				\
 		ve_printk(dst, fmt, ##__VA_ARGS__);			\
 })
 #else
diff --git a/include/linux/ratelimit_types.h b/include/linux/ratelimit_types.h
index 3902066f0fee..aa9eee2a2455 100644
--- a/include/linux/ratelimit_types.h
+++ b/include/linux/ratelimit_types.h
@@ -44,7 +44,9 @@ struct ratelimit_state {
 extern int ___ratelimit(struct ratelimit_state *rs, const char *func);
 #define __ratelimit(state) ___ratelimit(state, __func__)
 
-extern int __ve_ratelimit(int dst, struct ratelimit_state *rs, const char *func);
-#define ve_ratelimit(dst, state) __ve_ratelimit(dst, state, __func__)
+struct ve_struct;
+extern int __ve_ratelimit(int dst, struct ve_struct *ve,
+	struct ratelimit_state *rs, const char *func);
+#define ve_ratelimit(dst, ve, state) __ve_ratelimit(dst, ve, state, __func__)
 
 #endif /* _LINUX_RATELIMIT_TYPES_H */
diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
index b06fc835d7c7..d5680fda32ca 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -567,6 +567,16 @@ struct log_state *ve_log_state(void)
 	return log;
 }
 
+struct log_state *__ve_log_state(struct ve_struct *ve)
+{
+	struct log_state *log = &init_log_state;
+#ifdef CONFIG_VE
+	if (ve->log_state)
+		log = ve->log_state;
+#endif
+	return log;
+}
+
 void log_poll_wait(struct file *filp, poll_table *p)
 {
 	poll_wait(filp, &ve_log_state()->wait, p);
@@ -2525,26 +2535,45 @@ asmlinkage int ve_vprintk_emit(int facility, int level,
 }
 EXPORT_SYMBOL(ve_vprintk_emit);
 
+asmlinkage int __ve_vprintk_emit(struct ve_struct *ve, int facility, int level,
+			       const struct dev_printk_info *dev_info,
+			       const char *fmt, va_list args)
+{
+	return vprintk_emit_log(__ve_log_state(ve),
+				facility, level, dev_info,
+				fmt, args);
+}
+EXPORT_SYMBOL(__ve_vprintk_emit);
+
 int vprintk_default(const char *fmt, va_list args)
 {
 	return vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, fmt, args);
 }
 EXPORT_SYMBOL_GPL(vprintk_default);
 
-static asmlinkage int ve_vprintk(int dst, const char *fmt, va_list args)
+static asmlinkage int ve_vprintk(int dst, struct ve_struct *ve,
+				const char *fmt, va_list args)
 {
 	va_list args2;
-	int r = 0;
+	int r = 0, ve_provided = (ve != NULL);
 
 	WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
 
+	if (!ve)
+		ve = get_exec_env();
+
 	va_copy(args2, args);
-	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
+	if (ve_is_super(ve) || (dst & VE0_LOG))
 		r = vprintk(fmt, args);
 
 	/* Report IRQ usage here - printing to VE requires explicit ve reference */
-	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
-		r = ve_vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, fmt, args2);
+	if ((dst & VE_LOG)) {
+		if (!ve_provided && WARN_ON_ONCE(!in_task()))
+			goto done;
+		if (!ve_is_super(ve))
+			r = __ve_vprintk_emit(ve, 0, LOGLEVEL_DEFAULT, NULL, fmt, args2);
+	}
+done:
 	va_end(args2);
 
 	return r;
@@ -2560,39 +2589,62 @@ asmlinkage int ve_printk(int dst, const char *fmt, ...)
 	int r;
 
 	va_start(args, fmt);
-	r = ve_vprintk(dst, fmt, args);
+	r = ve_vprintk(dst, NULL, fmt, args);
 	va_end(args);
 
 	return r;
 }
 EXPORT_SYMBOL(ve_printk);
 
-static asmlinkage int ve_vprintk_deferred(int dst, const char *fmt, va_list args)
+asmlinkage int __ve_printk(int dst, struct ve_struct *ve, const char *fmt, ...)
+{
+	va_list args;
+	int r;
+
+	va_start(args, fmt);
+	r = ve_vprintk(dst, ve, fmt, args);
+	va_end(args);
+
+	return r;
+}
+EXPORT_SYMBOL(__ve_printk);
+
+static asmlinkage int ve_vprintk_deferred(int dst, struct ve_struct *ve, 
+					const char *fmt, va_list args)
 {
 	va_list args2;
-	int r = 0;
+	int r = 0, ve_provided = (ve != NULL);
 
 	WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
 
+	if (!ve)
+		ve = get_exec_env();
+
 	va_copy(args2, args);
-	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
+	if (ve_is_super(ve) || (dst & VE0_LOG))
 		r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
 
 	/* Report IRQ usage here - printing to VE requires explicit ve reference */
-	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
-		r = ve_vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args2);
+	if ((dst & VE_LOG)) {
+		if (!ve_provided && WARN_ON_ONCE(!in_task()))
+			goto done;
+		if (!ve_is_super(ve))
+			r = __ve_vprintk_emit(ve, 0, LOGLEVEL_SCHED, NULL, fmt, args2);
+	}
+done:
 	va_end(args2);
 
 	return r;
 }
 
-asmlinkage int ve_printk_deferred(int dst, const char *fmt, ...)
+asmlinkage int ve_printk_deferred(int dst, struct ve_struct *ve,
+	const char *fmt, ...)
 {
 	va_list args;
 	int r;
 
 	va_start(args, fmt);
-	r = ve_vprintk_deferred(dst, fmt, args);
+	r = ve_vprintk_deferred(dst, ve, fmt, args);
 	va_end(args);
 
 	return r;
diff --git a/lib/ratelimit.c b/lib/ratelimit.c
index 98409e870eea..98c45bb19e50 100644
--- a/lib/ratelimit.c
+++ b/lib/ratelimit.c
@@ -11,6 +11,7 @@
 #include <linux/ratelimit.h>
 #include <linux/jiffies.h>
 #include <linux/export.h>
+#include <linux/ve_proto.h>
 
 /*
  * __ratelimit - rate limiting
@@ -24,7 +25,7 @@
  * 0 means callbacks will be suppressed.
  * 1 means go ahead and do it.
  */
-int __ve_ratelimit(int dst, struct ratelimit_state *rs, const char *func)
+int __ve_ratelimit(int dst, struct ve_struct *ve, struct ratelimit_state *rs, const char *func)
 {
 	/* Paired with WRITE_ONCE() in .proc_handler().
 	 * Changing two values seperately could be inconsistent
@@ -55,7 +56,7 @@ int __ve_ratelimit(int dst, struct ratelimit_state *rs, const char *func)
 
 		if (m) {
 			if (!(rs->flags & RATELIMIT_MSG_ON_RELEASE)) {
-				ve_printk_deferred(dst, KERN_WARNING
+				ve_printk_deferred(dst, ve, KERN_WARNING
 						"%s: %d callbacks suppressed\n", func, m);
 			}
 		}
@@ -77,6 +78,6 @@ EXPORT_SYMBOL(__ve_ratelimit);
 
 int ___ratelimit(struct ratelimit_state *rs, const char *func)
 {
-	return __ve_ratelimit(VE0_LOG, rs, func);
+	return __ve_ratelimit(VE0_LOG, get_ve0(), rs, func);
 }
 EXPORT_SYMBOL(___ratelimit);
diff --git a/net/core/utils.c b/net/core/utils.c
index cffc3bc44baa..bf4e53205a8e 100644
--- a/net/core/utils.c
+++ b/net/core/utils.c
@@ -41,9 +41,9 @@ int net_ratelimit(void)
 }
 EXPORT_SYMBOL(net_ratelimit);
 
-int ve_net_ratelimit(int dst)
+int ve_net_ratelimit(int dst, struct ve_struct *ve)
 {
-	return ve_ratelimit(dst, &net_ratelimit_state);
+	return ve_ratelimit(dst, ve, &net_ratelimit_state);
 }
 EXPORT_SYMBOL(ve_net_ratelimit);
 
diff --git a/net/netfilter/nf_conntrack_core.c b/net/netfilter/nf_conntrack_core.c
index f078136edddd..8900a41b48b6 100644
--- a/net/netfilter/nf_conntrack_core.c
+++ b/net/netfilter/nf_conntrack_core.c
@@ -1701,7 +1701,7 @@ __nf_conntrack_alloc(struct net *net,
 			if (!conntrack_gc_work.early_drop)
 				conntrack_gc_work.early_drop = true;
 			atomic_dec(&cnet->count);
-			net_veboth_ratelimited(KERN_WARNING "VE%s: "
+			net_veboth_ratelimited(net->owner_ve, KERN_WARNING "VE%s: "
 					       "nf_conntrack table full in netns %u, dropping packet\n",
 					       ve_name(net->owner_ve), net->ns.inum);
 			return ERR_PTR(-ENOMEM);
diff --git a/net/netfilter/nf_conntrack_expect.c b/net/netfilter/nf_conntrack_expect.c
index 4b077b2cb432..23e78957bb79 100644
--- a/net/netfilter/nf_conntrack_expect.c
+++ b/net/netfilter/nf_conntrack_expect.c
@@ -508,7 +508,7 @@ static inline int __nf_ct_expect_check(struct nf_conntrack_expect *expect,
 
 	cnet = nf_ct_pernet(net);
 	if (cnet->expect_count >= cnet->expect_max) {
-		net_veboth_ratelimited(KERN_WARNING "VE%s: "
+		net_veboth_ratelimited(net->owner_ve, KERN_WARNING "VE%s: "
 					"nf_conntrack: expectation table full in netns %u\n",
 					ve_name(net->owner_ve), net->ns.inum);
 		ret = -EMFILE;
diff --git a/net/netfilter/xt_TCPMSS.c b/net/netfilter/xt_TCPMSS.c
index 822b3f71570e..0b9ccc8987e2 100644
--- a/net/netfilter/xt_TCPMSS.c
+++ b/net/netfilter/xt_TCPMSS.c
@@ -105,8 +105,8 @@ tcpmss_mangle_packet(struct sk_buff *skb,
 		unsigned int min_mtu = min(dst_mtu(skb_dst(skb)), in_mtu);
 
 		if (min_mtu <= minlen) {
-			net_velog_ratelimited("unknown or invalid path-MTU (%u)\n",
-					    min_mtu);
+			net_velog_ratelimited(net->owner_ve, "unknown or invalid path-MTU (%u)\n",
+					      min_mtu);
 			return -1;
 		}
 		newmss = min_mtu - minlen;

-- 
2.52.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 15%]

* [Devel] [PATCH hci-8.1 v2 6/6] ve: add neigh_tbl failcounter
                     ` (4 preceding siblings ...)
  2026-10-06 21:53  8% ` [Devel] [PATCH hci-8.1 v2 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
@ 2026-10-06 21:53  9% ` Vladimir Riabchun
  2026-10-06 22:06  4%   ` no-reply
  5 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-06 21:53 UTC (permalink / raw)
  To: vasileios.almpanis, Konstantin Khorenko; +Cc: devel

Count hits of the per-VE neigh_tbl limit in ve.failcount.

This one was delayed due to a high risk of IRQ-context hits,
when destination VE can't be obtained from get_exec_env.

https://virtuozzo.atlassian.net/browse/VSTOR-135520
https://virtuozzo.atlassian.net/browse/VSTOR-146839

Feature: per-ve failcounters
Feature: ve printk
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
 include/linux/ve.h   | 1 +
 kernel/ve/ve.c       | 3 +++
 net/core/neighbour.c | 5 +++--
 3 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/include/linux/ve.h b/include/linux/ve.h
index 946df4eebe89..44e04dc75823 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -97,6 +97,7 @@ struct ve_struct {
 
 	atomic_t		arp_neigh_nr;
 	atomic_t		nd_neigh_nr;
+	atomic_t		neigh_tbl_failcount;
 	unsigned long		meminfo_val;
 
 	/*
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index 51cc635be2e2..35fad9658149 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -118,6 +118,7 @@ struct ve_struct ve0 = {
 
 	.arp_neigh_nr		= ATOMIC_INIT(0),
 	.nd_neigh_nr		= ATOMIC_INIT(0),
+	.neigh_tbl_failcount	= ATOMIC_INIT(0),
 	.mnt_avail_nr		= ATOMIC_INIT(INT_MAX),
 	.mnt_max_nr		= INT_MAX,
 	.mnt_failcount		= ATOMIC_INIT(0),
@@ -834,6 +835,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
 
 	atomic_set(&ve->arp_neigh_nr, 0);
 	atomic_set(&ve->nd_neigh_nr, 0);
+	atomic_set(&ve->neigh_tbl_failcount, 0);
 	ve->mnt_max_nr = MNT_MAX_NR_DEFAULT;
 	atomic_set(&ve->mnt_failcount, 0);
 	atomic_set(&ve->mnt_avail_nr, MNT_MAX_NR_DEFAULT);
@@ -1098,6 +1100,7 @@ static const struct ve_failcount_entry {
 	VE_FC_ENTRY(mnt),
 	VE_FC_ENTRY(netif),
 	VE_FC_ENTRY(bpf_prog),
+	VE_FC_ENTRY(neigh_tbl),
 #ifdef CONFIG_AIO
 	VE_FC_ENTRY(aio),
 #endif
diff --git a/net/core/neighbour.c b/net/core/neighbour.c
index f90deb17fb25..6c2fc778a8ab 100644
--- a/net/core/neighbour.c
+++ b/net/core/neighbour.c
@@ -520,8 +520,9 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
 	    (glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
 	     time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
 		if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
-			net_info_ratelimited("%s: neighbor table overflow!\n",
-					     tbl->id);
+			net_info_ratelimited("%s: neighbor table overflow for ve %s!\n",
+					     tbl->id, ve_name(ve));
+			ve_failcount_inc(ve, neigh_tbl);
 			NEIGH_CACHE_STAT_INC(tbl, table_fulls);
 			goto out_entries;
 		}

-- 
2.52.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 9%]

* [Devel] [PATCH hci-8.1 v2 5/6] ve/failcount: Use ve_printk with specified ve
                     ` (3 preceding siblings ...)
  2026-10-06 21:53  4% ` [Devel] [PATCH hci-8.1 v2 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
@ 2026-10-06 21:53  8% ` Vladimir Riabchun
  2026-10-06 21:53  9% ` [Devel] [PATCH hci-8.1 v2 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
  5 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-06 21:53 UTC (permalink / raw)
  To: vasileios.almpanis, Konstantin Khorenko; +Cc: devel

ve_failcount_inc could be called from IRQ context, and we want the message
to go to the right place. Use new  __ve_printk helpers for this.

https://virtuozzo.atlassian.net/browse/VSTOR-146839

Feature: ve printk
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
 include/linux/printk.h | 18 ++++++++++++++----
 include/linux/ve.h     | 16 ++++++++--------
 2 files changed, 22 insertions(+), 12 deletions(-)

diff --git a/include/linux/printk.h b/include/linux/printk.h
index a9470c81f8db..620a6f767150 100644
--- a/include/linux/printk.h
+++ b/include/linux/printk.h
@@ -755,24 +755,31 @@ struct pi_entry {
 		printk(fmt, ##__VA_ARGS__);				\
 })
 
-#define ve_printk_ratelimited(dst, fmt, ...)				\
+#define __ve_printk_ratelimited(dst, ve, fmt, ...)			\
 ({									\
 	static DEFINE_RATELIMIT_STATE(_rs,				\
 				      DEFAULT_RATELIMIT_INTERVAL,	\
 				      DEFAULT_RATELIMIT_BURST);		\
+									\
+	if (ve_ratelimit(dst, ve, &_rs))				\
+		__ve_printk(dst, ve, fmt, ##__VA_ARGS__);		\
+})
+
+#define ve_printk_ratelimited(dst, fmt, ...)				\
+({									\
 	struct ve_struct *__ve = get_exec_env();			\
 									\
 	if (WARN_ON_ONCE((dst & VE_LOG) && !in_task()))			\
 		__ve = get_ve0();					\
-									\
-	if (ve_ratelimit(dst, __ve, &_rs))				\
-		ve_printk(dst, fmt, ##__VA_ARGS__);			\
+	__ve_printk_ratelimited(dst, __ve, fmt, ##__VA_ARGS__);		\
 })
 #else
 #define printk_ratelimited(fmt, ...)					\
 	no_printk(fmt, ##__VA_ARGS__)
 #define ve_printk_ratelimited(dst, fmt, ...)				\
 	no_printk(fmt, ##__VA_ARGS__)
+#define __ve_printk_ratelimited(dst, ve, fmt, ...)			\
+	no_printk(fmt, ##__VA_ARGS__)
 #endif
 
 #define pr_emerg_ratelimited(fmt, ...)					\
@@ -790,6 +797,7 @@ struct pi_entry {
 #define pr_info_ratelimited(fmt, ...)					\
 	printk_ratelimited(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
 /* no pr_cont_ratelimited, don't do that... */
+
 #define ve_pr_emerg_ratelimited(dst, fmt, ...)					\
 	ve_printk_ratelimited(dst, KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)
 #define ve_pr_alert_ratelimited(dst, fmt, ...)					\
@@ -800,6 +808,8 @@ struct pi_entry {
 	ve_printk_ratelimited(dst, KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
 #define ve_pr_warn_ratelimited(dst, fmt, ...)					\
 	ve_printk_ratelimited(dst, KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
+#define __ve_pr_warn_ratelimited(dst, ve, fmt, ...)				\
+	__ve_printk_ratelimited(dst, ve, KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
 #define ve_pr_notice_ratelimited(dst, fmt, ...)					\
 	ve_printk_ratelimited(dst, KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
 #define ve_pr_info_ratelimited(dst, fmt, ...)					\
diff --git a/include/linux/ve.h b/include/linux/ve.h
index a400d21593b2..946df4eebe89 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -156,14 +156,14 @@ extern int nr_ve;
 #define BPF_PROG_MAX_NR_DEFAULT	256	/* number of loaded BPF progs per-VE */
 #define MNT_MAX_NR_DEFAULT	4096	/* number of mounts per-VE */
 
-#define ve_failcount_inc(ve, name)					\
-do {									\
-	struct ve_struct *__ve = (ve);					\
-									\
-	atomic_inc(&__ve->name##_failcount);				\
-	ve_pr_warn_ratelimited(VE_LOG_BOTH, "CT%s: hits the " #name	\
-		" limit, see the '" #name "' counter in ve.failcount "	\
-		"of the container's ve cgroup\n", ve_name(__ve));	\
+#define ve_failcount_inc(ve, name)						\
+do {										\
+	struct ve_struct *__ve = (ve);						\
+										\
+	atomic_inc(&__ve->name##_failcount);					\
+	__ve_pr_warn_ratelimited(VE_LOG_BOTH, __ve, "CT%s: hits the " #name	\
+		" limit, see the '" #name "' counter in ve.failcount "		\
+		"of the container's ve cgroup\n", ve_name(__ve));		\
 } while (0)
 
 #ifdef CONFIG_VE

-- 
2.52.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 8%]

* [Devel] [PATCH hci-8.1 v2 4/6] ve/printk: Fix time virtualization
                     ` (2 preceding siblings ...)
  2026-10-06 21:53 15% ` [Devel] [PATCH hci-8.1 v2 3/6] ve/printk: Specify ve in printks Vladimir Riabchun
@ 2026-10-06 21:53  4% ` Vladimir Riabchun
  2026-10-06 22:04  3%   ` no-reply
  2026-10-06 21:53  8% ` [Devel] [PATCH hci-8.1 v2 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
  2026-10-06 21:53  9% ` [Devel] [PATCH hci-8.1 v2 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
  5 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-06 21:53 UTC (permalink / raw)
  To: vasileios.almpanis, Konstantin Khorenko; +Cc: devel

The usage of get_exec_env() in ve_timens_add_boottime_ns can lead
to time travels - a printk from IRQ might use container time for
printing to host dmesg and vice versa.

Do time adjustments on message submission in vprintk_store_log,
there we know the final destination and VE, so we can get actual
time namespace and offsets there.

https://virtuozzo.atlassian.net/browse/VSTOR-146839

Feature: ve printk
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
 kernel/printk/internal.h |  4 ++++
 kernel/printk/printk.c   | 12 +++++-------
 2 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/kernel/printk/internal.h b/kernel/printk/internal.h
index d00ac40d8401..d678718482ed 100644
--- a/kernel/printk/internal.h
+++ b/kernel/printk/internal.h
@@ -354,11 +354,15 @@ struct log_state {
 	 */
 	struct latched_seq clear_seq;
 	struct printk_ringbuffer *prb;
+	struct ve_struct *owner_ve;
 };
 
+#define GET_VE_LOG(ve) ((struct log_state *) ve->log_state)
+
 extern struct log_state init_log_state;
 
 struct log_state *ve_log_state(void);
+struct log_state *__ve_log_state(struct ve_struct *ve);
 
 bool printk_get_next_message(struct log_state *log,
 			     struct printk_message *pmsg, u64 seq,
diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
index d5680fda32ca..cf4baacd22d8 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -534,6 +534,7 @@ struct log_state init_log_state = {
 		.val[1]	= 0,
 	},
 	.prb = &printk_rb_static,
+	.owner_ve = &ve0,
 };
 
 /* kdump relies on some log_* symbols, let's make it happy */
@@ -698,12 +699,12 @@ static void append_char(char **pp, char *e, char c)
 		*(*pp)++ = c;
 }
 
-static inline u64 ve_timens_add_boottime_ns(u64 nsec)
+static inline u64 ve_timens_add_boottime_ns(u64 nsec, struct ve_struct *ve)
 {
 	struct time_namespace *ve_time_ns;
 	struct timens_offsets *ns_offsets;
 
-	ve_time_ns = ve_get_time_ns(get_exec_env());
+	ve_time_ns = ve_get_time_ns(ve);
 	if (unlikely(!ve_time_ns)) {
 		/* container not yet started */
 		return nsec;
@@ -731,8 +732,6 @@ static ssize_t info_print_ext_header(char *buf, size_t size,
 	caller[0] = '\0';
 #endif
 
-	/* shift the timestamp on the Container uptime value */
-	ts_usec = ve_timens_add_boottime_ns(ts_usec);
 	do_div(ts_usec, 1000);
 
 	return scnprintf(buf, size, "%u,%llu,%llu,%c%s;",
@@ -1431,9 +1430,6 @@ static size_t print_time(u64 ts, char *buf)
 {
 	unsigned long rem_nsec;
 
-	/* shift the timestamp on the Container uptime value */
-	ts = ve_timens_add_boottime_ns(ts);
-
 	rem_nsec = do_div(ts, 1000000000);
 
 	return sprintf(buf, "[%5lu.%06lu]",
@@ -2329,6 +2325,7 @@ static int vprintk_store_log(struct log_state *log, int facility, int level,
 	 * timestamp with respect to the caller.
 	 */
 	ts_nsec = local_clock();
+	ts_nsec = ve_timens_add_boottime_ns(ts_nsec, log->owner_ve);
 
 	caller_id = printk_caller_id();
 
@@ -5294,6 +5291,7 @@ int ve_log_init(struct ve_struct *ve)
 	seqcount_latch_init(&log->clear_seq.latch);
 
 	ve->log_state = log;
+	log->owner_ve = ve;
 	return 0;
 }
 

-- 
2.52.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH hci-8.1 v2 1/6] ve/printk: Add sanity checks in ve_printk functions
  @ 2026-10-06 21:58  4%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 21:58 UTC (permalink / raw)
  To: Konstantin Khorenko, vasileios.almpanis, Vladimir Riabchun; +Cc: devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] printk: ve_vprintk_deferred() raises non-deferred WARN_ON_ONCE() on invalid dst
--

commit 57d6a4cedde45a78dd9af42f6ddde31d3e97b8fb
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

ve/printk: Add sanity checks in ve_printk functions

This commit adds a WARN_ON_ONCE() check on the dst argument in
ve_dev_printk_emit(), ve_vprintk() and ve_vprintk_deferred(). It also
reorders the VE_LOG test in the second if statement of each function.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839

> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c

[ ... ]

> @@ -2567,10 +2569,12 @@ static asmlinkage int ve_vprintk_deferred(int dst, const char *fmt, va_list args
>  	va_list args2;
>  	int r = 0;
>  
> +	WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
> +

[Severity: Low]
Can this WARN_ON_ONCE() in ve_vprintk_deferred() cause problems in
printk-unsafe contexts?

ve_vprintk_deferred() emits through vprintk_emit() at LOGLEVEL_SCHED
so that it can be used where plain printk is not safe. The warning
goes through the regular __warn() path, which prints via the normal,
non-deferred printk.

If a caller passes dst == 0 or a value with bits outside VE_LOG_BOTH
from such a context, the warning could recurse into or deadlock on
console or logbuf locks:

ve_ratelimit() (lib/ratelimit.c)
  ve_printk_deferred(dst, ve, KERN_WARNING ...)
    ve_vprintk_deferred(dst, ...)
      WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));

I did not verify that any current caller passes an invalid dst, so this
is only a possible problem. The warning fires at most once.

Would it make sense to drop the check from the deferred helper or use a
deferred-safe way to report a bad dst?

>  	va_copy(args2, args);
>  	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>  		r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
> -	if (!ve_is_super(get_exec_env()) && (dst & VE_LOG))
> +	if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
>  		r = ve_vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args2);
>  	va_end(args2);

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-ve-printk-irq-v2-0-db1dc4f6ed07@virtuozzo.com?part=1

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH hci-8.1 v2 2/6] ve/printk: Report potentially leaking usage of ve_printk functions
  @ 2026-10-06 21:59  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 21:59 UTC (permalink / raw)
  To: Vladimir Riabchun, Konstantin Khorenko, vasileios.almpanis; +Cc: devel

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] printk: WARN_ON_ONCE() inside ve_vprintk_deferred() defeats deferred printk
- [Low] printk: non-task VE_LOG messages are dropped while host routing uses get_exec_env()
- [Low] printk: WARN_ON_ONCE(!in_task()) runs before the ve_is_super() check
--

commit b0fc9f5937694addbb72bb3c2cea42ba105b7306
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

ve/printk: Report potentially leaking usage of ve_printk functions

This commit adds WARN_ON_ONCE(!in_task()) checks to ve_log_state(),
ve_vprintk(), ve_vprintk_deferred() and ve_dev_printk_emit(). When a
VE_LOG message is requested from non-task context, the message is
rejected (or the host log state is returned) instead of relying on
get_exec_env().

Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839

> diff --git a/drivers/base/core.c b/drivers/base/core.c
> --- a/drivers/base/core.c
> +++ b/drivers/base/core.c

[ ... ]

> @@ -4952,7 +4952,9 @@ int ve_dev_printk_emit(int dst, int level, const struct device *dev, const char
>  	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>  		r = dev_vprintk_emit(level, dev, fmt, args);
> -	if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
> +
> +	/* Report IRQ usage here - printing to VE requires explicit ve reference */
> +	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
>  		r = ve_dev_vprintk_emit(level, dev, fmt, args2);

[Severity: Low]
Is it intended that the WARN_ON_ONCE(!in_task()) is evaluated before the
ve_is_super() check?

In ve_dev_printk_emit(), any non-task caller that passes dst with VE_LOG
will hit the WARN even when the interrupted task is in ve0, where nothing
could leak to a container. Could the ve_is_super() test be placed first so
the WARN only fires when a container could actually be affected?

The same ordering appears in ve_vprintk() and ve_vprintk_deferred().

Also, the host branch just above still uses get_exec_env():

	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
		r = dev_vprintk_emit(level, dev, fmt, args);

In non-task context with dst == VE_LOG, if a host task was interrupted the
message goes to the host log. If a container task was interrupted, the VE
branch is rejected by the WARN guard, the message is dropped, and r stays
0. Doesn't this make the outcome depend on which task was interrupted,
which is the get_exec_env() unreliability the commit message wants to
avoid? ve_log_state() by contrast falls back to the host log. The same
applies to ve_vprintk() and ve_vprintk_deferred().

> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c
> @@ -560,7 +560,8 @@ struct log_state *ve_log_state(void)
>  {
>  	struct log_state *log = &init_log_state;
>  #ifdef CONFIG_VE
> -	if (get_exec_env()->log_state)
> +	/* get_exec_env() is reliable only in task context */
> +	if (!WARN_ON_ONCE(!in_task()) && get_exec_env()->log_state)
>  		log = get_exec_env()->log_state;
>  #endif
>  	return log;

[Severity: Medium]
Can this WARN run while printk-side processing already holds locks?

ve_log_state() is called from ve_vprintk_emit(), so a WARN here can fire in
the middle of a printk call. The WARN prints a banner and backtrace through
the normal printk path.

ve_vprintk_emit()
  ve_log_state()
    WARN_ON_ONCE(!in_task())
      __warn() -> printk

Could this recurse or deadlock if the caller is in scheduler, timer or
other non-task context holding such locks? The WARN fires only once, and I
did not find an in-tree caller that triggers this. In the final series
state, the unconditional WARN in ve_log_state() on this path is still
present.

[ ... ]

> @@ -2540,7 +2541,9 @@ static asmlinkage int ve_vprintk(int dst, const char *fmt, va_list args)
>  	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>  		r = vprintk(fmt, args);
> -	if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
> +
> +	/* Report IRQ usage here - printing to VE requires explicit ve reference */
> +	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
>  		r = ve_vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, fmt, args2);

[Severity: Medium]
Does this make any IRQ or NMI caller of ve_printk() that passes VE_LOG
trigger a WARN? See the comments on ve_log_state() above and on
ve_vprintk_deferred() below regarding emitting a WARN from such contexts.

> @@ -2574,7 +2577,9 @@ static asmlinkage int ve_vprintk_deferred(int dst, const char *fmt, va_list args)
>  	if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>  		r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
> -	if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
> +
> +	/* Report IRQ usage here - printing to VE requires explicit ve reference */
> +	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
>  		r = ve_vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args2);

[Severity: Medium]
Does the synchronous WARN_ON_ONCE() inside ve_vprintk_deferred() defeat the
purpose of the deferred variant?

This helper is meant to be safe from scheduler and IRQ contexts. If a
caller outside task context (hardirq, softirq, NMI or scheduler code) calls
it with dst containing VE_LOG and no explicit ve, the WARN goes through
__warn() and the normal printk path. That path prints a banner and
backtrace and may flush consoles or wake klogd.

Could this self-deadlock or recurse when the caller holds scheduler, timer
or logbuf locks? The WARN fires only once. The only in-tree caller I found,
__ve_ratelimit() in lib/ratelimit.c, runs under rs->lock and passes ve, so
I have no concrete deadlocking caller. In the final series state,
ve_vprintk_deferred() still has the check
"if (!ve_provided && WARN_ON_ONCE(!in_task()))", so the concern remains
when no ve is provided.

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-ve-printk-irq-v2-0-db1dc4f6ed07@virtuozzo.com?part=2

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 v2 3/6] ve/printk: Specify ve in printks
  2026-10-06 21:53 15% ` [Devel] [PATCH hci-8.1 v2 3/6] ve/printk: Specify ve in printks Vladimir Riabchun
@ 2026-10-06 22:03  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 22:03 UTC (permalink / raw)
  To: Vladimir Riabchun, vasileios.almpanis, Konstantin Khorenko; +Cc: devel

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] printk: explicit-VE path skips the in_task() guard, so VE log wakeups can run in softirq
- [Low] printk: stale IRQ comment, trailing whitespace and duplicate struct ve_struct decl
--

commit a71d74a31e0ae4dbd29bc8edddfdbf1bedab2ffe
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

ve/printk: Specify ve in printks

This patch adds ve_printk helpers that take an explicit VE pointer. The
ratelimit helpers and the net_ve*_ratelimited macros now require a VE
argument. The conntrack and TCPMSS callers pass net->owner_ve.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839

> diff --git a/include/linux/printk.h b/include/linux/printk.h
> --- a/include/linux/printk.h
> +++ b/include/linux/printk.h

[ ... ]

> @@ -76,6 +76,7 @@ extern int console_printk[];
>  #define VE0_LOG		1
>  #define VE_LOG		2
>  #define VE_LOG_BOTH	(VE0_LOG | VE_LOG)
> +struct ve_struct;
>  
>  extern void console_verbose(void);

[Severity: Low]
This isn't a bug, but this adds a second forward declaration of struct
ve_struct. printk.h already has one further down, just before
ve_log_init().

> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c

[ ... ]

> -static asmlinkage int ve_vprintk(int dst, const char *fmt, va_list args)
> +static asmlinkage int ve_vprintk(int dst, struct ve_struct *ve,
> +				const char *fmt, va_list args)
>  {
>  	va_list args2;
> -	int r = 0;
> +	int r = 0, ve_provided = (ve != NULL);
>  
>  	WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
>  
> +	if (!ve)
> +		ve = get_exec_env();
> +
>  	va_copy(args2, args);
> -	if (ve_is_super(ve) || (dst & VE0_LOG))
> +	if (ve_is_super(ve) || (dst & VE0_LOG))
>  		r = vprintk(fmt, args);
>  
>  	/* Report IRQ usage here - printing to VE requires explicit ve reference */
> -	if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
> -		r = ve_vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, fmt, args2);
> +	if ((dst & VE_LOG)) {
> +		if (!ve_provided && WARN_ON_ONCE(!in_task()))
> +			goto done;
> +		if (!ve_is_super(ve))
> +			r = __ve_vprintk_emit(ve, 0, LOGLEVEL_DEFAULT, NULL, fmt, args2);
> +	}
> +done:
>  	va_end(args2);

[Severity: Medium]
Before this patch, the VE log branch was skipped whenever in_task() was
false. With an explicit VE the in_task() guard is now bypassed.

Can this path now run from softirq or BH-disabled context?

__nf_conntrack_alloc() runs on the packet path. __nf_ct_expect_check()
runs under spin_lock_bh(). Both call net_veboth_ratelimited(net->owner_ve,
...), which expands to the non-deferred __ve_printk():

__nf_conntrack_alloc()
  net_veboth_ratelimited(net->owner_ve, ...)
    __ve_printk()
      ve_vprintk()            /* ve_provided is true */
        __ve_vprintk_emit(ve, 0, LOGLEVEL_DEFAULT, ...)
          vprintk_emit_log(__ve_log_state(ve), ...)
            wake_up_interruptible(&log->wait)

LOGLEVEL_DEFAULT does not clear ft.legacy_direct, so a non-init log goes
through the wake_up_interruptible() path in vprintk_emit_log().

The waitqueue lock is a spinlock_t. That is fine in softirq on non-RT
kernels. Is it also intended to be reachable from these contexts on
PREEMPT_RT, and from any future explicit-VE caller running in hardirq or
under a raw_spinlock_t?

The commit message only justifies the explicit VE for the deferred
helpers ("This is true for ve_printk_deferred functions, and their
users"). Yet net_velog_ratelimited() and net_veboth_ratelimited() use the
non-deferred __ve_printk().

Also, ve_printk_ratelimited() in printk.h still falls back to ve0 when
!in_task(). So the two ratelimited paths now follow different rules.

[Severity: Low]
This isn't a bug, but the comment "Report IRQ usage here - printing to VE
requires explicit ve reference" was kept in both ve_vprintk() and
ve_vprintk_deferred(). The in_task() check now only applies when no VE
was passed, so the comment no longer matches the code.

[ ... ]

> +static asmlinkage int ve_vprintk_deferred(int dst, struct ve_struct *ve, 
> +					const char *fmt, va_list args)

[Severity: Low]
This isn't a bug, but this line adds trailing whitespace after
"struct ve_struct *ve, ".

[ ... ]

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-ve-printk-irq-v2-0-db1dc4f6ed07@virtuozzo.com?part=3

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 v2 4/6] ve/printk: Fix time virtualization
  2026-10-06 21:53  4% ` [Devel] [PATCH hci-8.1 v2 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
@ 2026-10-06 22:04  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 22:04 UTC (permalink / raw)
  To: Konstantin Khorenko, Vladimir Riabchun, vasileios.almpanis; +Cc: devel

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] printk: time_ns refcount get/put in any-context vprintk_store_log() path
- [Low] printk: boottime shift baked in at store time leaves early records unshifted
- [Low] printk: init_log_state references ve0 without a CONFIG_VE guard
--

commit 5c30ca9d767fcbace4f34b24a8dcf1364e21ebb6
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

ve/printk: Fix time virtualization

This patch moves the container boottime shift from print time (where
get_exec_env() was used) to message store time in vprintk_store_log().
The shift now uses the owning VE of the log_state, so IRQ printks no
longer pick up the wrong container's offsets.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839

> diff --git a/kernel/printk/internal.h b/kernel/printk/internal.h
> --- a/kernel/printk/internal.h
> +++ b/kernel/printk/internal.h

[ ... ]

> +#define GET_VE_LOG(ve) ((struct log_state *) ve->log_state)
> +

[Severity: Low]
This isn't a bug, but GET_VE_LOG() is not used anywhere in kernel/printk,
and it does not parenthesize its argument, so GET_VE_LOG(a + b) would
expand to a + b->log_state. Is the macro needed in this patch?

> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c

[ ... ]

> @@ -534,6 +534,7 @@ struct log_state init_log_state = {
>  		.val[1]	= 0,
>  	},
>  	.prb = &printk_rb_static,
> +	.owner_ve = &ve0,
>  };

[Severity: Low]
Will this build when CONFIG_PRINTK=y and CONFIG_VE=n? The declaration
extern struct ve_struct ve0; in include/linux/ve_proto.h is inside
#ifdef CONFIG_VE, and the !CONFIG_VE branch only provides the get_ve0()
stub.

Other VE-specific code in this file is guarded by #ifdef CONFIG_VE, so
should the .owner_ve initializer be guarded in the same way?

[ ... ]

> -static inline u64 ve_timens_add_boottime_ns(u64 nsec)
> +static inline u64 ve_timens_add_boottime_ns(u64 nsec, struct ve_struct *ve)
>  {
>  	struct time_namespace *ve_time_ns;
>  	struct timens_offsets *ns_offsets;
>  
> -	ve_time_ns = ve_get_time_ns(get_exec_env());
> +	ve_time_ns = ve_get_time_ns(ve);
>  	if (unlikely(!ve_time_ns)) {
>  		/* container not yet started */
>  		return nsec;

[ ... ]

> @@ -2329,6 +2325,7 @@ static int vprintk_store_log(struct log_state *log, int facility, int level,
>  	 * timestamp with respect to the caller.
>  	 */
>  	ts_nsec = local_clock();
> +	ts_nsec = ve_timens_add_boottime_ns(ts_nsec, log->owner_ve);

[Severity: Medium]
Can this take and drop a time_ns reference from a context where the final
put is unsafe?

vprintk_store_log() runs in the any-context store path, after
printk_enter_irqsave(). It now calls:

vprintk_store_log()
  ve_timens_add_boottime_ns()
    ve_get_time_ns()     /* get_time_ns() under rcu_read_lock() */
    put_time_ns()

ve_get_time_ns() holds the reference past the RCU section. Consider the
teardown side in ve_drop_context():

rcu_assign_pointer(ve->ve_nsproxy, NULL);
synchronize_rcu();
put_nsproxy(ve_nsproxy);

If a printk took its reference before the pointer was cleared, can it end
up holding the last time_ns reference? In that case put_time_ns() would
call free_time_ns() from inside the printk store path, which may be in
hardirq or other atomic context.

Separately, this adds an RCU lock plus an atomic inc/dec on every printk
record, including host (ve0) records, where it touches init_time_ns.

Would it be possible to read the offsets under RCU without taking a
reference, cache the boottime offset in log_state, or skip ve0?

[Severity: Low]
Is it intended that records stored while ve->ve_nsproxy is NULL keep the
raw host uptime permanently?

ve_log_init() runs at css_alloc, but ve_nsproxy is only set later by
ve_grab_context(). Until then ve_get_time_ns() returns NULL and
ve_timens_add_boottime_ns() returns the value unshifted:

	if (unlikely(!ve_time_ns)) {
		/* container not yet started */
		return nsec;
	}

Before this patch the shift was applied at read time, so every record
was shifted consistently once the container had started. Now the
container's dmesg can show a mixed timeline, and the raw ts_nsec in the
VE ring buffer differs from what was read before. The commit message does
not mention this change in behavior.

[ ... ]

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-ve-printk-irq-v2-0-db1dc4f6ed07@virtuozzo.com?part=4

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 v2 6/6] ve: add neigh_tbl failcounter
  2026-10-06 21:53  9% ` [Devel] [PATCH hci-8.1 v2 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
@ 2026-10-06 22:06  4%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 22:06 UTC (permalink / raw)
  To: Konstantin Khorenko, vasileios.almpanis, Vladimir Riabchun; +Cc: devel

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Low] net: neigh_alloc() logs one overflow twice and rewords the old message
- [Low] net: neigh_tbl failcount is charged to the VE for global-limit overflows
- [Low] ve: failcount warning shares one static ratelimit state across all containers
--

commit 50b4fd033875ffa61097427a5f8a1d5372007065
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>

ve: add neigh_tbl failcounter

This commit adds a per-VE neigh_tbl_failcount counter, exposes it in
ve.failcount, and increments it from neigh_alloc() when the neighbour
table overflows. It also changes the existing overflow message to include
the VE name.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-135520
Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839

> diff --git a/net/core/neighbour.c b/net/core/neighbour.c
> --- a/net/core/neighbour.c
> +++ b/net/core/neighbour.c

[ ... ]

> @@ -520,8 +520,9 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
>  	    (glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
>  	     time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
>  		if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
> -			net_info_ratelimited("%s: neighbor table overflow!\n",
> -					     tbl->id);
> +			net_info_ratelimited("%s: neighbor table overflow for ve %s!\n",
> +					     tbl->id, ve_name(ve));
> +			ve_failcount_inc(ve, neigh_tbl);

[Severity: Low]
Is the reworded net_info_ratelimited() message still needed here?

ve_failcount_inc() already expands to __ve_pr_warn_ratelimited(VE_LOG_BOTH,
...), which prints "CT%s: hits the neigh_tbl limit, see the 'neigh_tbl'
counter in ve.failcount ..." to both the host log and the VE log.

A single overflow in neigh_alloc() therefore produces two log lines from
two separate ratelimit states.

The commit message also does not mention that the existing "neighbor table
overflow!" text was changed to add the VE name.

[Severity: Low]
Is the failcount charged correctly when no per-VE counter exists?

get_perve_tbl_entries_counter() returns a per-VE counter only for AF_INET
and AF_INET6:

    switch (tbl->family) {
    case AF_INET:
            return &ve->arp_neigh_nr;
    case AF_INET6:
            return &ve->nd_neigh_nr;
    }
    return NULL;

For any other family, cnt is NULL in neigh_alloc(), so entries is set to
glob_entries and the entries >= gc_thresh3 test compares the global
tbl->gc_entries against the global threshold.

ve_failcount_inc(ve, neigh_tbl) is still called unconditionally in that
branch, so the VE owning the device is charged for a table-wide overflow.

The commit message says it counts "hits of the per-VE neigh_tbl limit".
Should the increment be limited to the case where cnt is non-NULL?

[Severity: Low]
Can one container suppress the warning for other containers here?

A container can fill its own neighbour table, for example by flooding
ARP/ND entries on its own netns, and reach this overflow branch in
neigh_alloc(). ve_failcount_inc() then calls __ve_printk_ratelimited(),
which declares one static ratelimit state per call site:

    static DEFINE_RATELIMIT_STATE(_rs,
                                  DEFAULT_RATELIMIT_INTERVAL,
                                  DEFAULT_RATELIMIT_BURST);
    if (ve_ratelimit(dst, ve, &_rs))

Because the state is shared by all VEs, a busy container could use up the
burst budget and hide the warning for other containers. The atomic
failcount itself stays correct.

>  			NEIGH_CACHE_STAT_INC(tbl, table_fulls);
>  			goto out_entries;
>  		}

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-ve-printk-irq-v2-0-db1dc4f6ed07@virtuozzo.com?part=6

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* [Devel] [PATCH hci-8.1 v4 1/5] selftests/ve: check CPUID override emulation for VE tasks
  @ 2026-10-06 22:12  5% ` Konstantin Khorenko
  2026-10-06 22:22  3%   ` no-reply
  0 siblings, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-10-06 22:12 UTC (permalink / raw)
  To: Pavel Tikhomirov, Vasileios Almpanis; +Cc: devel

Write a one-entry /proc/vz/cpuid_override table that clears one leaf 1
feature bit and compare what a task in a fresh VE and the host see:
the VE task must get the table values, the host must keep the hardware
bit. A second test flushes the table after the task was armed and
expects the hardware bit back, since faulting stays on for the task
and the emulation has to fall back to the real CPUID. On its own that
test would also pass on a kernel that never arms faulting; the first
one proves the arming.

The child stops itself with SIGSTOP after entering the VE and the
parent resumes it before the CPUID: faulting is switched on for a task
at its next context switch, not when the flag is set, and a stopped
task is off the CPU for sure. For the second test the table is flushed
while the child stands still.

Without commit 07338fbae5a3 ("x86/traps: emulate CPUID faults for user
tasks in exc_general_protection()") both children die with SIGSEGV.
The tests skip on CPUs without CPUID faulting and restore the previous
table on exit; a table the kernel cannot take back in one write fails
the setup instead. The harness skips FIXTURE_TEARDOWN once an ASSERT
has fired, so the cleanup is a FIXTURE_TEARDOWN_PARENT, which runs
whether or not one did; it also kills a child that such an ASSERT left
stopped in the VE cgroup. The test is built on x86 only.

https://virtuozzo.atlassian.net/browse/VSTOR-148843

Feature: x86: cpu pools (cpuid override)
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/selftests/ve/.gitignore              |   1 +
 tools/testing/selftests/ve/Makefile                |  10 +
 .../testing/selftests/ve/ve_cpuid_override_test.c  | 247 +++++++++++++++++++++
 3 files changed, 258 insertions(+)

diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index 3df4d05888dce..bdc322c645d95 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,3 +1,4 @@
 ve_ns_owner_test
 ve_perms_test
 ve_mount_accounting_test
+ve_cpuid_override_test
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index c6efe7c4b4fba..d919c1c289c8b 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -6,4 +6,14 @@ TEST_GEN_PROGS += ve_ns_owner_test
 TEST_GEN_PROGS += ve_perms_test
 TEST_GEN_PROGS += ve_mount_accounting_test
 
+# ARCH stays untouched: lib.mk picks the clang target by its emptiness.
+ifeq ($(ARCH),)
+ve_arch := $(shell uname -m 2>/dev/null || echo not)
+else
+ve_arch := $(ARCH)
+endif
+ifeq ($(ve_arch),$(filter $(ve_arch),x86 x86_64))
+TEST_GEN_PROGS += ve_cpuid_override_test
+endif
+
 include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
new file mode 100644
index 0000000000000..f2883539bac3f
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c
@@ -0,0 +1,247 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * /proc/vz/cpuid_override selftests
+ *
+ * A non-empty override table turns CPUID faulting on for every task that
+ * enters a VE, and the #GP handler has to emulate the instruction for
+ * them. The tests check that a task in a VE sees the table values, that
+ * the host keeps seeing the hardware values, and that a task which was
+ * armed while the table was set falls back to the hardware once the
+ * table is flushed. A kernel that does not emulate the fault kills the
+ * child with SIGSEGV instead.
+ *
+ * The table is global: run this on a node without live Containers, a task
+ * entering a VE while a case runs is armed for life. A harness timeout
+ * kills the test without its teardown and leaves the test table in place
+ * and the VE cgroup behind.
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/wait.h>
+
+#include "../kselftest_harness.h"
+#include "ve_selftest.h"
+
+#define CPUID_OVERRIDE_PATH	"/proc/vz/cpuid_override"
+#define CPUID_LEAF		1
+#define CPUID_LINE_LEN		128
+/* The kernel takes a table back only in one write below a page. */
+#define CPUID_WRITE_MAX		4096
+#define CPUID_SAVED_MAX		(CPUID_WRITE_MAX + 1)
+
+struct cpuid_regs {
+	unsigned int eax, ebx, ecx, edx;
+};
+
+/* vz builds these tests for x86-64 only; there ebx is not the PIC base. */
+static void do_cpuid(unsigned int leaf, struct cpuid_regs *r)
+{
+	asm volatile("cpuid"
+		     : "=a" (r->eax), "=b" (r->ebx), "=c" (r->ecx), "=d" (r->edx)
+		     : "a" (leaf), "c" (0));
+}
+
+struct child_ctx {
+	int result_pipe[2];
+};
+
+/*
+ * The child enters a fresh VE, so it is armed with the table state of that
+ * moment, then reports its view of the leaf to the parent over a pipe. It
+ * stops itself in between: a stopped task is off the CPU, so the resume is
+ * a context switch, and the parent may change the table while the child
+ * stands still.
+ */
+static int cpuid_child(struct child_ctx *ctx)
+{
+	struct cpuid_regs r;
+
+	close(ctx->result_pipe[0]);
+
+	if (unshare(CLONE_NEWVE) < 0)
+		return 21;
+	if (raise(SIGSTOP))
+		return 22;
+
+	do_cpuid(CPUID_LEAF, &r);
+
+	if (write(ctx->result_pipe[1], &r, sizeof(r)) != sizeof(r))
+		return 23;
+	return 0;
+}
+
+FIXTURE(ve_cpuid_override)
+{
+	int cgv2_fd;
+	int ctid;
+	struct cpuid_regs host;
+	unsigned int bit;
+	pid_t child;
+	char table[CPUID_LINE_LEN];
+	char saved[CPUID_SAVED_MAX];
+};
+
+FIXTURE_SETUP(ve_cpuid_override)
+{
+	int fd, len;
+
+	fd = open(CPUID_OVERRIDE_PATH, O_RDONLY);
+	if (fd < 0)
+		SKIP(return, "%s: %s", CPUID_OVERRIDE_PATH, strerror(errno));
+	close(fd);
+
+	len = read_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->saved,
+			   sizeof(self->saved));
+	ASSERT_GE(len, 0) TH_LOG("%s: %s", CPUID_OVERRIDE_PATH, strerror(errno));
+	ASSERT_LT(len, CPUID_WRITE_MAX)
+		TH_LOG("a table of at least %d bytes cannot be restored", len);
+
+	/* Mask one feature bit the host has; SSE2 when present. */
+	do_cpuid(CPUID_LEAF, &self->host);
+	self->bit = 1u << 26;
+	if (!(self->host.edx & self->bit))
+		self->bit = self->host.edx & -self->host.edx;
+	ASSERT_NE(self->bit, 0u);
+
+	snprintf(self->table, sizeof(self->table),
+		 "0x%08x: 0x%08x 0x%08x 0x%08x 0x%08x\n", CPUID_LEAF,
+		 self->host.eax, self->host.ebx, self->host.ecx,
+		 self->host.edx & ~self->bit);
+
+	/* Last, so that a failed ASSERT above leaves no VE cgroup behind. */
+	self->cgv2_fd = mount_cg2_fd();
+	ASSERT_GE(self->cgv2_fd, 0) TH_LOG("cgroup2 mount: %s", strerror(errno));
+	ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
+				VE_CONTROLLERS), 0);
+	self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
+	ASSERT_GE(self->ctid, 0);
+}
+
+/*
+ * Runs after the test body in whichever process the harness picks, the test
+ * process itself after a failed ASSERT, so the table is restored whether or
+ * not one fired. The setup fd may be gone by then, hence the fresh mount.
+ */
+FIXTURE_TEARDOWN_PARENT(ve_cpuid_override)
+{
+	int cgv2_fd;
+
+	if (write_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH,
+			  self->saved[0] ? self->saved : "\n"))
+		TH_LOG("could not restore %s: %s", CPUID_OVERRIDE_PATH,
+		       strerror(errno));
+
+	/* A child left stopped by a failed ASSERT keeps the VE cgroup busy. */
+	if (self->child > 0)
+		kill(self->child, SIGKILL);
+
+	cgv2_fd = mount_cg2_fd();
+	if (cgv2_fd < 0)
+		return;
+	destroy_ve(cgv2_fd, self->ctid);
+	close(cgv2_fd);
+}
+
+/* Fork the child into the VE and return once it stands still there. */
+static pid_t spawn_cpuid_child(struct __test_metadata *_metadata,
+			       FIXTURE_DATA(ve_cpuid_override) * self,
+			       struct child_ctx *ctx)
+{
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(pipe(ctx->result_pipe), 0);
+	ASSERT_GE(enter_cgroup(self->cgv2_fd, self->ctid), 0);
+
+	pid = fork();
+	if (pid == 0)
+		_exit(cpuid_child(ctx));
+	if (pid > 0)
+		self->child = pid;
+
+	/* Leave the VE cgroup before any ASSERT, or destroy_ve() gets EBUSY. */
+	ASSERT_GE(enter_cgroup(self->cgv2_fd, 0), 0);
+	ASSERT_GE(pid, 0);
+	close(ctx->result_pipe[1]);
+
+	ASSERT_GE(waitpid(pid, &status, WUNTRACED), 0);
+	if (!WIFSTOPPED(status)) {
+		/* Reaped: the pid may be reused, keep it from teardown. */
+		self->child = 0;
+		if (WIFEXITED(status))
+			TH_LOG("child exited with %d", WEXITSTATUS(status));
+		if (WIFSIGNALED(status))
+			TH_LOG("child killed by signal %d", WTERMSIG(status));
+	}
+	ASSERT_TRUE(WIFSTOPPED(status));
+	return pid;
+}
+
+static void resume_cpuid_child(struct __test_metadata *_metadata, pid_t pid)
+{
+	ASSERT_EQ(kill(pid, SIGCONT), 0);
+}
+
+static void collect_cpuid_child(struct __test_metadata *_metadata,
+				FIXTURE_DATA(ve_cpuid_override) * self,
+				struct child_ctx *ctx, struct cpuid_regs *r)
+{
+	int status, len;
+
+	len = read(ctx->result_pipe[0], r, sizeof(*r));
+	ASSERT_GE(waitpid(self->child, &status, 0), 0);
+	self->child = 0;
+	if (WIFSIGNALED(status))
+		TH_LOG("child killed by signal %d", WTERMSIG(status));
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+	ASSERT_EQ(len, (int)sizeof(*r));
+
+	close(ctx->result_pipe[0]);
+}
+
+TEST_F(ve_cpuid_override, ve_sees_table_host_sees_hardware)
+{
+	struct child_ctx ctx;
+	struct cpuid_regs r;
+	pid_t pid;
+
+	ASSERT_EQ(write_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->table), 0);
+
+	pid = spawn_cpuid_child(_metadata, self, &ctx);
+	resume_cpuid_child(_metadata, pid);
+	collect_cpuid_child(_metadata, self, &ctx, &r);
+
+	EXPECT_EQ(r.eax, self->host.eax);
+	EXPECT_EQ(r.ebx, self->host.ebx);
+	EXPECT_EQ(r.ecx, self->host.ecx);
+	EXPECT_EQ(r.edx, self->host.edx & ~self->bit);
+
+	do_cpuid(CPUID_LEAF, &r);
+	EXPECT_NE(r.edx & self->bit, 0u);
+}
+
+TEST_F(ve_cpuid_override, flushed_table_falls_back_to_hardware)
+{
+	struct child_ctx ctx;
+	struct cpuid_regs r;
+	pid_t pid;
+
+	ASSERT_EQ(write_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->table), 0);
+
+	pid = spawn_cpuid_child(_metadata, self, &ctx);
+
+	ASSERT_EQ(write_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, "\n"), 0);
+	resume_cpuid_child(_metadata, pid);
+	collect_cpuid_child(_metadata, self, &ctx, &r);
+
+	EXPECT_NE(r.edx & self->bit, 0u);
+}
+
+TEST_HARNESS_MAIN

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 5%]

* [Devel] [PATCH hci-8.1 v7 1/2] selftests: ve: grant the directories the files sit in
  @ 2026-10-06 22:15 11% ` Eva Kurchatova
  2026-10-06 22:15  6% ` [Devel] [PATCH hci-8.1 v7 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
  1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-10-06 22:15 UTC (permalink / raw)
  To: khorenko; +Cc: devel

The sysfs variant names nested leaves, kernel/vmcoreinfo and
kernel/notes, while the proc one names files at the top of the
filesystem. Both enforcement tests grant the leaves and nothing else,
so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
cannot traverse, and every open answers ENOENT.

Grant x on each directory along the way, taken from the path the
variant already gives. set_entry() gets a PATH_MAX buffer and fails on
truncation instead of writing a cut entry, as ve_open_rel() does.

Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")

https://virtuozzo.atlassian.net/browse/VSTOR-144955

Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/ve/ve_perms_test.c | 34 ++++++++++++++++++++--
 1 file changed, 32 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 25ffbd42c380..8096b1fa3b43 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -114,9 +114,11 @@ static int ve_open_rel(int cgv2_fd, int ctid, const char *prefix,
 static int set_entry(int cgv2_fd, const char *path, const char *entry,
 		     const char *mask)
 {
-	char line[256];
+	char line[PATH_MAX];
 
-	snprintf(line, sizeof(line), "%s %s", entry, mask);
+	if (snprintf(line, sizeof(line), "%s %s", entry, mask) >=
+	    (int)sizeof(line))
+		return -1;
 	return write_file_at(cgv2_fd, path, line);
 }
 
@@ -343,6 +345,32 @@ TEST_F(ve_perms, write_parser)
 	EXPECT_LT(write_file_at(self->cgv2_fd, path, file), 0);
 }
 
+/* A leaf is reachable only once every directory above it can be traversed */
+static int allow_path(int cgv2_fd, const char *path, const char *file)
+{
+	char dir[PATH_MAX];
+	const char *sep;
+
+	for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
+		if (snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file),
+			     file) >= (int)sizeof(dir))
+			return -1;
+		if (set_entry(cgv2_fd, path, dir, "x") < 0)
+			return -1;
+	}
+	return 0;
+}
+
+static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
+{
+	int i;
+
+	for (i = 0; i < NR_FILES; i++)
+		if (allow_path(cgv2_fd, path, files[i]) < 0)
+			return -1;
+	return 0;
+}
+
 /*
  * Enforcement, allowed side: from inside a VE, granting r lets each file be
  * read, and granting x on the directory lets it be traversed (the lookup then
@@ -358,6 +386,7 @@ TEST_F(ve_perms, enforce_allows)
 
 	ve_file(path, sizeof(path), self->ctid_a, variant->fs);
 
+	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
 	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
 	for (i = 0; i < NR_FILES; i++)
 		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
@@ -400,6 +429,7 @@ TEST_F(ve_perms, enforce_denies)
 				      O_RDONLY), 0);
 
 	/* Granted w only: visible but not readable. */
+	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
 	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
 	for (i = 0; i < NR_FILES; i++)
 		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
-- 
2.56.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 11%]

* [Devel] [PATCH hci-8.1 v7 2/2] selftests: ve: introduce vtty pair teardown stress test
    2026-10-06 22:15 11% ` [Devel] [PATCH hci-8.1 v7 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
@ 2026-10-06 22:15  6% ` Eva Kurchatova
  2026-10-06 22:25  4%   ` no-reply
  1 sibling, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-10-06 22:15 UTC (permalink / raw)
  To: khorenko; +Cc: devel

Either end of a vtty pair creates it, and the last close tears it down.

Race condition in older VZKernel vtty implementation allowed both sides
to decide they were the final one and call release_tty() twice, and a
master opening again while a teardown was in flight used to hand out a
reference to a tty_struct about to be freed. Either way, this caused
use-after-free. That issue was fixed in 23abb203a41a ("ve/vtty: fix
use-after-free on concurrent tty close and reopen").

Hammer both ends for ownership correctness & thread safety verification.

The original reproducer needed a few tens of minutes across a dozen
containers, where vzctl interaction with vtty subsystem was a small part
of the overall runtime, therefore it did not fire on every run. This test
opens and closes nothing but the pair, from both ends and without pause,
so the default stays in the tens of seconds and under the timeout of the
suite, and VE_VTTY_SECS or argv[1] allows to control this.

Nothing keeps veids unique and the vtty map is keyed by veid, so a
clash would hand the test a running container's pair. Before writing
ve.veid the test asks the kernel whether a running container holds the
veid and takes the next id when one does, best effort: one started
later is not seen.
It also skips where /tmp is mounted nodev, where mknod() succeeds and
no open does. The log is read every second, and a ring overrun, which
loses records, counts as a complaint.

https://virtuozzo.atlassian.net/browse/VSTOR-144771

Feature: ve selftest
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/ve/.gitignore       |   1 +
 tools/testing/selftests/ve/Makefile         |   3 +-
 tools/testing/selftests/ve/ve_vtty_stress.c | 416 ++++++++++++++++++++
 3 files changed, 419 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/ve/ve_vtty_stress.c

diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index 3df4d05888dc..d8eb5c346c6e 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,3 +1,4 @@
 ve_ns_owner_test
 ve_perms_test
 ve_mount_accounting_test
+ve_vtty_stress
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index c6efe7c4b4fb..d287a73c5e97 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -1,9 +1,10 @@
 # SPDX-License-Identifier: GPL-2.0
 # Makefile for ve selftests.
-CFLAGS += -g -Wall -O2
+CFLAGS += -g -Wall -O2 $(KHDR_INCLUDES)
 
 TEST_GEN_PROGS += ve_ns_owner_test
 TEST_GEN_PROGS += ve_perms_test
 TEST_GEN_PROGS += ve_mount_accounting_test
+TEST_GEN_PROGS += ve_vtty_stress
 
 include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
new file mode 100644
index 000000000000..765735ddea45
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_vtty_stress.c
@@ -0,0 +1,416 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_vtty_stress - open and close a vtty pair from both ends at once.
+ *
+ * Either end of a vtty pair creates it, and the last close tears it down.
+ *
+ * Hammer both ends. The master comes from the vzctl ioctl, which does
+ * not look the VE up. The slave is reachable only from inside the VE, as
+ * TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
+ * vttys index minor - 1, so the test builds a VE with cgroup2, sets
+ * ve.veid to the cgroup it made, and runs the slave side there.
+ *
+ * A warning does not stop the host, so the run reads the kernel log too.
+ * One read() of /dev/kmsg is one record, so the test matches vtty driver
+ * splats, as well as generic kernel warning/oops/kasan reports.
+ *
+ * Usage: ve_vtty_stress [seconds] (default 30, or $VE_VTTY_SECS)
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <sys/statvfs.h>
+#include <linux/major.h>
+#include <linux/vzcalluser.h>
+#include <sys/sysmacros.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "ve_selftest.h"
+#include "../kselftest.h"
+
+#define VTTY_IDX 0 /* The pair both ends work on */
+#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
+#define DEFAULT_SECS 30
+
+/*
+ * Expected open errno codes: the pair may be closing, or gone already,
+ * when an open lands on it.
+ * Anything else is an environment issue or a bug.
+ */
+static bool expected_errno(int err)
+{
+	return err == EBUSY || err == EIO || err == ENXIO;
+}
+
+/* A runner timeout must still reach the cleanup */
+static volatile sig_atomic_t stop;
+
+static void on_signal(int sig)
+{
+	stop = sig;
+}
+
+/* The handler interrupts a wait, the child is still to be reaped */
+static void wait_child(pid_t pid, int *status)
+{
+	*status = 0;
+	while (waitpid(pid, status, 0) < 0 && errno == EINTR)
+		if (stop)
+			kill(pid, SIGKILL);
+}
+
+static int open_master(int ctl_fd, unsigned int veid)
+{
+	struct vzctl_ve_configure c = {
+		.veid = veid,
+		.key = VE_CONFIGURE_OPEN_TTY,
+		.val = VTTY_IDX,
+	};
+
+	return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
+}
+
+/* Any key but the tty one makes ve_configure() look a running VE up */
+static bool veid_in_use(int ctl_fd, unsigned int veid)
+{
+	struct vzctl_ve_configure c = {
+		.veid = veid,
+		.key = VE_CONFIGURE_OS_RELEASE,
+	};
+
+	return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c) < 0 && errno == ENOKEY;
+}
+
+/* The slave end, inside the VE. Returns the errno that stopped it, or 0. */
+static int hammer_slave(const char *node, time_t deadline,
+			unsigned long *opened)
+{
+	int err = 0;
+
+	while (time(NULL) < deadline) {
+		int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
+
+		if (fd >= 0) {
+			(*opened)++;
+			close(fd);
+		} else if (!expected_errno(errno)) {
+			err = errno;
+			break;
+		}
+	}
+
+	return err;
+}
+
+/* The kernel log, since a warning fails nothing by itself. */
+static int kmsg_open(void)
+{
+	int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+
+	if (fd >= 0)
+		lseek(fd, 0, SEEK_END);
+	return fd;
+}
+
+/*
+ * Check for kernel log warnings, one read() per record, hence
+ * the function name is not checked.
+ */
+static int kmsg_check(int fd)
+{
+	/* How a warning, a splat or a sanitizer report starts in a record */
+	static const char *const reports[] = {
+		"WARNING: ", "Oops", ";BUG", "kernel BUG at", "UBSAN: ",
+		"soft lockup - ", "general protection fault", "INFO: task ",
+		"rcu: INFO: ",
+	};
+	char buf[8192];
+	int found = 0;
+	ssize_t r;
+	size_t i;
+
+	if (fd < 0)
+		return 0;
+
+	for (;;) {
+		r = read(fd, buf, sizeof(buf) - 1);
+		if (r < 0) {
+			/* The ring wrapped since the last read, records are gone */
+			if (errno == EPIPE) {
+				ksft_print_msg("kernel log overrun, records lost\n");
+				found++;
+				continue;
+			}
+			/* Anything but the end of the log is a scan that stopped */
+			if (errno != EAGAIN) {
+				ksft_print_msg("kernel log read: %s\n",
+					       strerror(errno));
+				found++;
+			}
+			break;
+		}
+		if (r == 0)
+			break;
+		buf[r] = '\0';
+		/* Known symptoms of a buggy kernel */
+		if (strstr(buf, "vtty") && (strstr(buf, "tty->count") ||
+					    strstr(buf, "bad slave count"))) {
+			ksft_print_msg("vtty mismatched count log: %s", buf);
+			found++;
+		}
+		for (i = 0; i < ARRAY_SIZE(reports); i++) {
+			if (!strstr(buf, reports[i]))
+				continue;
+			ksft_print_msg("kernel log: %s", buf);
+			found++;
+			break;
+		}
+	}
+	return found;
+}
+
+int main(int argc, char **argv)
+{
+	unsigned long master_opened = 0, slave_opened = 0;
+	int cg, ctid, ctl, kmsg, ready[2], count[2], status;
+	int master_err = 0, slave_err, complaints = 0;
+	const char *env = getenv("VE_VTTY_SECS");
+	char path[PATH_MAX], val[16], c;
+	char dir[] = "/tmp/ve_vtty_XXXXXX";
+	char node[sizeof(dir) + 6];
+	struct namespace_ctx ns;
+	struct sigaction sa;
+	struct statvfs st;
+	time_t deadline, now, last = 0;
+	long secs;
+	pid_t pid, parent;
+
+	ksft_print_header();
+	secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
+	if (secs <= 0)
+		secs = DEFAULT_SECS;
+
+	ctl = open("/dev/vzctl", O_RDWR);
+	if (ctl < 0)
+		ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
+			       "(vzdev, vzmon) are needed\n",
+			       strerror(errno));
+
+	cg = mount_cg2_fd();
+	if (cg < 0)
+		ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
+	if (write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS) < 0)
+		ksft_exit_skip("cannot enable the ve controller: %s\n",
+			       strerror(errno));
+
+	/* Nothing is made yet, from here on a signal must reach the cleanup */
+	sa.sa_handler = on_signal;
+	sigemptyset(&sa.sa_mask);
+	sa.sa_flags = 0;
+	sigaction(SIGTERM, &sa, NULL);
+	sigaction(SIGINT, &sa, NULL);
+
+	/* The vtty map is keyed by veid, and ve.veid takes a taken one */
+	for (ctid = CTID_MIN; ; ctid++) {
+		ctid = make_ve(cg, ctid);
+		if (ctid < 0)
+			ksft_exit_skip("cannot make a VE cgroup\n");
+		/* Best effort: a running container holds the veid, try the next */
+		if (!veid_in_use(ctl, ctid))
+			break;
+		destroy_ve(cg, ctid);
+	}
+
+	/* The ve.veid must match between master / slave */
+	snprintf(path, sizeof(path), "%d/ve.veid", ctid);
+	snprintf(val, sizeof(val), "%d", ctid);
+	if (write_file_at(cg, path, val) < 0) {
+		status = errno;
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("cannot set ve.veid: %s\n", strerror(status));
+	}
+
+	/* The log is the only detector of a teardown that does not crash */
+	kmsg = kmsg_open();
+	if (kmsg < 0) {
+		status = errno;
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("cannot read /dev/kmsg: %s\n", strerror(status));
+	}
+
+	/* Not a failure where the pair cannot be had, EBUSY says it exists */
+	status = open_master(ctl, ctid);
+	if (status < 0) {
+		status = errno;
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("no vtty master: %s\n", strerror(status));
+	}
+	close(status);
+
+	/*
+	 * Use temp directory for the node the slave end opens.
+	 * Cleaned up by the master side instead of relying on the slave.
+	 */
+	if (!mkdtemp(dir)) {
+		status = errno;
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("mkdtemp: %s\n", strerror(status));
+	}
+	/* A nodev mount takes the mknod() and refuses every open */
+	if (statvfs(dir, &st) == 0 && (st.f_flag & ST_NODEV)) {
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("%s is on a nodev filesystem\n", dir);
+	}
+	snprintf(node, sizeof(node), "%s/slave", dir);
+	if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0) {
+		status = errno;
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_skip("mknod: %s\n", strerror(status));
+	}
+
+	ksft_set_plan(1);
+
+	if (pipe(ready) || pipe(count)) {
+		status = errno;
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("pipe: %s\n", strerror(status));
+	}
+
+	/* The VE of the cgroup, which the slave side enters */
+	if (prepare_namespaces(&ns, CLONE_NEWVE, cg, ctid) < 0) {
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("cannot prepare the VE namespace\n");
+	}
+
+	deadline = time(NULL) + secs;
+	ksft_print_msg("VE %d, pair %d, %ld seconds\n", ctid, VTTY_IDX, secs);
+
+	parent = getpid();
+	pid = fork();
+	if (pid == 0) {
+		unsigned long n = 0;
+		int err;
+
+		/* The parent stops the run, a signal here just ends this side */
+		signal(SIGTERM, SIG_DFL);
+		signal(SIGINT, SIG_DFL);
+		close(ready[0]);
+		close(count[0]);
+		/*
+		 * Hammering on after the run has given up would hold
+		 * the cgroup open against destroy_ve().
+		 */
+		prctl(PR_SET_PDEATHSIG, SIGKILL);
+		if (getppid() != parent)
+			_exit(0);
+		if (enter_cgroup(cg, ctid) < 0 ||
+		    enter_namespaces(&ns, CLONE_NEWVE) < 0)
+			_exit(errno ? errno : EIO);
+
+		c = 'r';
+		if (write(ready[1], &c, 1) != 1)
+			_exit(errno);
+		err = hammer_slave(node, deadline, &n);
+		/* Send the count either way: it says how far it got. */
+		if (write(count[1], &n, sizeof(n)) != sizeof(n))
+			_exit(errno);
+		_exit(err);
+	} else if (pid < 0) {
+		status = errno;
+		close_namespace_fds(&ns);
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		ksft_exit_fail_msg("fork: %s\n", strerror(status));
+	}
+	close(ready[1]);
+	close(count[1]);
+
+	if (read(ready[0], &c, 1) != 1) {
+		/* A signal lands here too, with the child alive */
+		kill(pid, SIGKILL);
+		wait_child(pid, &status);
+		close_namespace_fds(&ns);
+		unlink(node);
+		rmdir(dir);
+		destroy_ve(cg, ctid);
+		if (stop)
+			ksft_exit_fail_msg("stopped by signal %d\n", (int)stop);
+		ksft_exit_fail_msg("the VE side never came up: %s\n",
+				   strerror(WIFEXITED(status) ?
+					    WEXITSTATUS(status) : EINTR));
+	}
+
+	while (!stop && (now = time(NULL)) < deadline) {
+		int fd = open_master(ctl, ctid);
+
+		if (fd >= 0) {
+			master_opened++;
+			close(fd);
+		} else if (!expected_errno(errno)) {
+			master_err = errno;
+			break;
+		}
+		/* Drain the log as it fills, the ring is small */
+		if (now != last) {
+			complaints += kmsg_check(kmsg);
+			last = now;
+		}
+	}
+
+	if (master_err || stop) {
+		/* The other end runs to its own deadline, stop it */
+		kill(pid, SIGKILL);
+		wait_child(pid, &status);
+		slave_err = 0;
+	} else {
+		if (read(count[0], &slave_opened, sizeof(slave_opened)) !=
+		    sizeof(slave_opened))
+			slave_opened = 0;
+		wait_child(pid, &status);
+		slave_err = WIFEXITED(status) ? WEXITSTATUS(status) : EINTR;
+	}
+
+	ksft_print_msg("%lu master opens, %lu slave opens\n", master_opened,
+		       slave_opened);
+
+	status = complaints + kmsg_check(kmsg);
+	close_namespace_fds(&ns);
+	unlink(node);
+	rmdir(dir);
+	destroy_ve(cg, ctid);
+
+	/* A complaint outranks how the run ended */
+	if (status)
+		ksft_exit_fail_msg("kernel complained %d time(s)\n", status);
+	if (stop)
+		ksft_exit_fail_msg("stopped by signal %d\n", (int)stop);
+	if (master_err)
+		ksft_exit_fail_msg("the master end stopped: %s\n",
+				   strerror(master_err));
+	/* Denied from the first open, by a device policy say */
+	if (!slave_opened && (slave_err == EPERM || slave_err == EACCES ||
+			      slave_err == ENOENT || slave_err == ENODEV))
+		ksft_exit_skip("the slave end cannot be opened: %s\n",
+			       strerror(slave_err));
+	if (slave_err)
+		ksft_exit_fail_msg("the slave end stopped: %s\n",
+				   strerror(slave_err));
+	if (!master_opened)
+		ksft_exit_fail_msg("no master was ever opened\n");
+	if (!slave_opened)
+		ksft_exit_fail_msg("no slave was ever opened\n");
+
+	ksft_test_result_pass("vtty pair closed from both ends\n");
+	ksft_finished();
+}
-- 
2.56.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 6%]

* Re: [Devel] [PATCH hci-8.1 v4 1/5] selftests/ve: check CPUID override emulation for VE tasks
  2026-10-06 22:12  5% ` [Devel] [PATCH hci-8.1 v4 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
@ 2026-10-06 22:22  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 22:22 UTC (permalink / raw)
  To: Konstantin Khorenko, Pavel Tikhomirov, Vasileios Almpanis; +Cc: devel

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] selftests/ve: stopped-child sync does not guarantee a context switch before SIGCONT
- [Low] selftests/ve: x86 gate admits 32-bit builds but do_cpuid() asm needs x86-64
--

commit 7b92cfc5a3a156b03d1e440c0f30bc0e6c9462a6
Author: Konstantin Khorenko <khorenko@virtuozzo.com>

selftests/ve: check CPUID override emulation for VE tasks

This adds a selftest that writes a one-entry /proc/vz/cpuid_override table
and compares the leaf 1 CPUID values seen by a task in a fresh VE with
those seen by the host. A second test flushes the table after the task was
armed and expects the hardware value back. The test is built on x86 only.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-148843

> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
> --- a/tools/testing/selftests/ve/Makefile
> +++ b/tools/testing/selftests/ve/Makefile

[ ... ]

> +# ARCH stays untouched: lib.mk picks the clang target by its emptiness.
> +ifeq ($(ARCH),)
> +ve_arch := $(shell uname -m 2>/dev/null || echo not)
> +else
> +ve_arch := $(ARCH)
> +endif
> +ifeq ($(ve_arch),$(filter $(ve_arch),x86 x86_64))
> +TEST_GEN_PROGS += ve_cpuid_override_test
> +endif

[Severity: Low]
The selftests top-level Makefile maps both i386 and x86_64 to ARCH=x86, so
this gate also enables ve_cpuid_override_test for 32-bit x86 builds.

do_cpuid() in ve_cpuid_override_test.c uses an ebx output constraint, and
its own comment says it is x86-64 only because "there ebx is not the PIC
base":

        asm volatile("cpuid"
                     : "=a" (r->eax), "=b" (r->ebx), "=c" (r->ecx), "=d" (r->edx)
                     : "a" (leaf), "c" (0));

Can this break the build of the whole ve selftests directory with ARCH=x86
on a 32-bit PIC or PIE toolchain, where GCC reports "can't find a register
in class BREG"? Should the gate exclude 32-bit x86?

> diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
> new file mode 100644
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c

[ ... ]

> +	ASSERT_GE(waitpid(pid, &status, WUNTRACED), 0);
> +	if (!WIFSTOPPED(status)) {

[ ... ]

> +static void resume_cpuid_child(struct __test_metadata *_metadata, pid_t pid)
> +{
> +	ASSERT_EQ(kill(pid, SIGCONT), 0);
> +}

[Severity: Medium]
Does this synchronization guarantee a context switch before SIGCONT? The
waitpid(WUNTRACED) in spawn_cpuid_child() only guarantees that the stop was
reported to the parent, not that the child has been switched out.

In the kernel stop path, do_signal_stop() sets TASK_STOPPED and calls
do_notify_parent_cldstop() before it calls schedule():

cpuid_child()
   unshare(CLONE_NEWVE);
   raise(SIGSTOP);
      do_signal_stop()
         set_special_state(TASK_STOPPED);
         do_notify_parent_cldstop();   <- parent's waitpid() can return here
         schedule();

If the parent runs resume_cpuid_child() inside that window, SIGCONT sets
the child back to TASK_RUNNING. When the child then reaches schedule(), it
is no longer in a sleeping state and is not deactivated, so it can keep
running on the same CPU with no context switch.

The commit message says faulting is switched on "at its next context
switch" and that "a stopped task is off the CPU for sure". Without the
switch, faulting is never armed for the child.

In ve_sees_table_host_sees_hardware, the child would then see the hardware
CPUID and the EXPECT_EQ on edx would fail on a correct kernel.

In flushed_table_falls_back_to_hardware, the child would see the hardware
CPUID, which is the expected value, so the test would pass even if the
kernel never armed faulting.

The window is narrow, since the parent must wake and finish waitpid()
first, but it appears to be structurally possible. Is there something in
the test that waits for the child to actually schedule out before SIGCONT
is sent?

I did not find a later fix for this in the series. The follow-up patches
"x86/ve: enable CPUID faulting at once when the current task enters a VE"
and "selftests/ve: check CPUID override right after entering a VE" change
the kernel to arm at once and add a test without the stop. The final-state
test file still uses the same waitpid(WUNTRACED) followed by kill(SIGCONT)
synchronization for the stop-based tests.

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261007-b4-prep-khorenko-vstor-148656-cpuid-v4-0-bf16c29bea24@virtuozzo.com?part=1

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 v7 2/2] selftests: ve: introduce vtty pair teardown stress test
  2026-10-06 22:15  6% ` [Devel] [PATCH hci-8.1 v7 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
@ 2026-10-06 22:25  4%   ` no-reply
  2026-10-06 22:42  0%     ` Eva Kurchatova (Virtuozzo)
  0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-06 22:25 UTC (permalink / raw)
  To: khorenko, Eva Kurchatova; +Cc: devel

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] selftests/ve: kmsg_check() fails on any unrelated kernel warning or log overrun
- [Low] selftests/ve: veid_in_use() relies on ENOKEY from an ioctl key marked unused
- [Low] selftests/ve: 30s default run has little margin under the 45s runner timeout
--

commit cfcd5cbc2bcb13ed42052dd5ac26511475fe149f
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>

selftests: ve: introduce vtty pair teardown stress test

This adds ve_vtty_stress, a selftest that opens and closes a vtty pair from
the master end (vzctl ioctl) and the slave end (inside a VE) at the same
time. It also scans /dev/kmsg for warnings and splats while the test runs.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-144771

> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
> --- a/tools/testing/selftests/ve/Makefile
> +++ b/tools/testing/selftests/ve/Makefile

[ ... ]

> TEST_GEN_PROGS += ve_vtty_stress

[Severity: Low]
The ve selftest directory has no settings file, so the kselftest runner's
default per-test timeout applies. I believe that default is 45 seconds,
but I have not checked it in this tree.

The test runs for 30 seconds by default.

After that it blocks reading the child's count and then runs destroy_ve(),
which may retry for about a second. Setup also happens before the run.

Is there enough margin on a slow or loaded host? A timeout kill would be
reported as a failure that has nothing to do with vtty.

A larger VE_VTTY_SECS or argv[1] value would exceed the default timeout
unless the runner is configured otherwise. The commit message says the
default stays "under the timeout of the suite". Could a settings file be
added to back that up?

> diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
> new file mode 100644
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_vtty_stress.c

[ ... ]

> +/* Any key but the tty one makes ve_configure() look a running VE up */
> +static bool veid_in_use(int ctl_fd, unsigned int veid)
> +{
> +	struct vzctl_ve_configure c = {
> +		.veid = veid,
> +		.key = VE_CONFIGURE_OS_RELEASE,
> +	};
> +
> +	return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c) < 0 && errno == ENOKEY;
> +}

[Severity: Low]
VE_CONFIGURE_OS_RELEASE is defined as "2 /* unused */" in
include/uapi/linux/vzcalluser.h. In kernel/ve/vecalls.c, ve_configure()
behaves like this:

    if (key == VE_CONFIGURE_OPEN_TTY)
            return vtty_open_master(veid, val);

    ve = get_ve_by_id(veid);
    if (!ve)
            return -EINVAL;

    put_ve(ve);
    return err;    /* err is -ENOKEY */

So veid_in_use() treats ENOKEY as "a running VE holds this veid". Is it
safe to depend on the return code of a key that is documented as unused?

If that key is later rejected earlier, for example with -ENOTTY or -EINVAL,
veid_in_use() will always return false. The only guard against taking over
a running container's vtty pair would then disappear without any notice.

Also, a VE that has ve.veid set but is not started is not in ve_idr, so the
probe cannot see it. The commit message only mentions "one started later"
as the case that is not seen.

[ ... ]

> +static int kmsg_check(int fd)
> +{
> +	/* How a warning, a splat or a sanitizer report starts in a record */
> +	static const char *const reports[] = {
> +		"WARNING: ", "Oops", ";BUG", "kernel BUG at", "UBSAN: ",
> +		"soft lockup - ", "general protection fault", "INFO: task ",
> +		"rcu: INFO: ",
> +	};

[ ... ]

> +	for (;;) {
> +		r = read(fd, buf, sizeof(buf) - 1);
> +		if (r < 0) {
> +			/* The ring wrapped since the last read, records are gone */
> +			if (errno == EPIPE) {
> +				ksft_print_msg("kernel log overrun, records lost\n");
> +				found++;
> +				continue;
> +			}

[ ... ]

> +		for (i = 0; i < ARRAY_SIZE(reports); i++) {
> +			if (!strstr(buf, reports[i]))
> +				continue;
> +			ksft_print_msg("kernel log: %s", buf);
> +			found++;
> +			break;
> +		}
> +	}
> +	return found;
> +}

[Severity: Medium]
Only the "vtty" checks in kmsg_check() are tied to vtty. The reports[]
patterns are matched with strstr() anywhere in a record. kmsg_open() seeks
to the end of /dev/kmsg, so every record from any subsystem is read for the
whole run.

Can a warning or hung-task report from an unrelated subsystem, or from a
test running at the same time on a shared CI host, fail this test even when
the vtty code is correct?

The EPIPE branch counts a ring overrun as a complaint. If other log traffic
overruns the ring, does the verdict then depend on host log volume instead
of on vtty behaviour?

The log is drained only about once per second from the master loop. The
final message "kernel complained %d time(s)" does not say that the cause
may be unrelated to vtty.

The commit message accepts the overrun rule deliberately. Could the
matching be narrowed, or the failure message made to say that the report
may be unrelated?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006221704.2507107-1-eva.kurchatova@virtuozzo.com?part=2

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 4%]

* Re: [Devel] [PATCH hci-8.1 v3 1/1] selftests: drv-net-hw: skip the tests that need a real interface
  @ 2026-10-06 22:31  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 22:31 UTC (permalink / raw)
  To: khorenko, Eva Kurchatova; +Cc: devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] selftests: skip_on_veth() ignores NETIF_FIND_DRIVER and falsely skips on real NICs
--

commit 6f44dc091649c1067eaf65154f6f0a7b79b13a25
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>

selftests: drv-net-hw: skip the tests that need a real interface

This adds a skip_on_veth() helper that runs before lib.sh is sourced. It is
called from the ethtool, ethtool_extended_state, ethtool_rmon, hw_stats_l3
and hw_stats_l3_gre tests. It exits with ksft_skip when the first interface
is a veth, or when it does not exist and lib.sh would create it as a veth.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-139651

> diff --git a/tools/testing/selftests/drivers/net/hw/skip_on_veth.sh b/tools/testing/selftests/drivers/net/hw/skip_on_veth.sh
> new file mode 100644
> index 0000000000000..aa2bb98dc2786
> --- /dev/null
> +++ b/tools/testing/selftests/drivers/net/hw/skip_on_veth.sh

[ ... ]

> +# Call before sourcing lib.sh, exiting after it leaves the veth pair behind
> +skip_on_veth()
> +{
> +	# lib.sh takes the interfaces from the command line ahead of the config
> +	local p1=${1:-${NETIFS[p1]-}}
> +	local kind
> +
> +	if [[ -n $p1 ]] && ip link show dev "$p1" &> /dev/null; then
> +		kind=$(ip -j -d link show dev "$p1" | jq -r '.[].linkinfo.info_kind')
> +		[[ $kind == veth ]] || return
> +	elif [[ $NETIF_CREATE != yes || $NETIF_TYPE != veth ]]; then
> +		return
> +	fi
> +	echo "SKIP: needs a real interface, set NETIFS in forwarding.config"
> +	exit 4 # ksft_skip, lib.sh is not sourced yet
> +}

[Severity: Medium]
Can skip_on_veth() skip a test that would have run on real interfaces?

When NETIF_FIND_DRIVER is set in the environment and neither a command
line interface nor NETIFS[p1] is given, p1 is empty in skip_on_veth().
The first branch is not taken. NETIF_CREATE and NETIF_TYPE are still at
their defaults (yes and veth), so the elif condition is false.  The
function then falls through to the echo and exit 4.

lib.sh handles this case later, in net/forwarding/lib.sh:

if [[ $NETIF_FIND_DRIVER ]]; then
	unset NETIFS
	declare -A NETIFS
	netif_find_driver
fi

netif_find_driver() fills NETIFS from real devices matched by driver name.
skip_on_veth() runs before lib.sh is sourced and never looks at
NETIF_FIND_DRIVER.  Other selftests already set it, for example
drivers/net/virtio_net/virtio_net_common.sh sets
NETIF_FIND_DRIVER="virtio_net".

Without this patch the test would have run on the discovered hardware.
With it, the test is reported as SKIP.

The commit message says the check takes the interfaces "the way lib.sh
does, from the command line or forwarding.config".  Is that accurate,
given that the NETIF_FIND_DRIVER path is not covered?

Also, only p1 is inspected.  If p1 is a real interface and p2 is missing,
lib.sh would still create a veth for p2.  Can skip_on_veth() handle that
case too?

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006222651.2510417-1-eva.kurchatova@virtuozzo.com?part=1

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 v7 2/2] selftests: ve: introduce vtty pair teardown stress test
  2026-10-06 22:25  4%   ` no-reply
@ 2026-10-06 22:42  0%     ` Eva Kurchatova (Virtuozzo)
  2026-10-07 17:47  0%       ` Konstantin Khorenko
  0 siblings, 1 reply; 200+ results
From: Eva Kurchatova (Virtuozzo) @ 2026-10-06 22:42 UTC (permalink / raw)
  To: no-reply, khorenko; +Cc: devel


On 10/7/26 01:25, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
> - [Medium] selftests/ve: kmsg_check() fails on any unrelated kernel warning or log overrun
> - [Low] selftests/ve: veid_in_use() relies on ENOKEY from an ioctl key marked unused
> - [Low] selftests/ve: 30s default run has little margin under the 45s runner timeout
> --
>
> commit cfcd5cbc2bcb13ed42052dd5ac26511475fe149f
> Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> selftests: ve: introduce vtty pair teardown stress test
>
> This adds ve_vtty_stress, a selftest that opens and closes a vtty pair from
> the master end (vzctl ioctl) and the slave end (inside a VE) at the same
> time. It also scans /dev/kmsg for warnings and splats while the test runs.
>
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-144771
>
>> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
>> --- a/tools/testing/selftests/ve/Makefile
>> +++ b/tools/testing/selftests/ve/Makefile
> [ ... ]
>
>> TEST_GEN_PROGS += ve_vtty_stress
> [Severity: Low]
> The ve selftest directory has no settings file, so the kselftest runner's
> default per-test timeout applies. I believe that default is 45 seconds,
> but I have not checked it in this tree.
>
> The test runs for 30 seconds by default.
>
> After that it blocks reading the child's count and then runs destroy_ve(),
> which may retry for about a second. Setup also happens before the run.
>
> Is there enough margin on a slow or loaded host? A timeout kill would be
> reported as a failure that has nothing to do with vtty.
>
> A larger VE_VTTY_SECS or argv[1] value would exceed the default timeout
> unless the runner is configured otherwise. The commit message says the
> default stays "under the timeout of the suite". Could a settings file be
> added to back that up?

Yes, the test run time is 30 seconds, which gives enough room to shut

down before the 45 second timeout, and I am not sure why would we

need a settings file to extend that. Whenever this test is invoked with

altered duration, it is expected to be ran manually for bisecting 
purposes, etc.

>> diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
>> new file mode 100644
>> --- /dev/null
>> +++ b/tools/testing/selftests/ve/ve_vtty_stress.c
> [ ... ]
>
>> +/* Any key but the tty one makes ve_configure() look a running VE up */
>> +static bool veid_in_use(int ctl_fd, unsigned int veid)
>> +{
>> +	struct vzctl_ve_configure c = {
>> +		.veid = veid,
>> +		.key = VE_CONFIGURE_OS_RELEASE,
>> +	};
>> +
>> +	return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c) < 0 && errno == ENOKEY;
>> +}
> [Severity: Low]
> VE_CONFIGURE_OS_RELEASE is defined as "2 /* unused */" in
> include/uapi/linux/vzcalluser.h. In kernel/ve/vecalls.c, ve_configure()
> behaves like this:
>
>      if (key == VE_CONFIGURE_OPEN_TTY)
>              return vtty_open_master(veid, val);
>
>      ve = get_ve_by_id(veid);
>      if (!ve)
>              return -EINVAL;
>
>      put_ve(ve);
>      return err;    /* err is -ENOKEY */
>
> So veid_in_use() treats ENOKEY as "a running VE holds this veid". Is it
> safe to depend on the return code of a key that is documented as unused?
>
> If that key is later rejected earlier, for example with -ENOTTY or -EINVAL,
> veid_in_use() will always return false. The only guard against taking over
> a running container's vtty pair would then disappear without any notice.
>
> Also, a VE that has ve.veid set but is not started is not in ve_idr, so the
> probe cannot see it. The commit message only mentions "one started later"
> as the case that is not seen.
>
> [ ... ]

I would like to hear Konstantin Khorenko opinion, as I am not sure about 
this ioctl()

API stability or why it would be marked as unused. Either this patch 
un-obsoletes

that call, or a dedicated ioctl() for checking VEID state should be 
implemented.

>> +static int kmsg_check(int fd)
>> +{
>> +	/* How a warning, a splat or a sanitizer report starts in a record */
>> +	static const char *const reports[] = {
>> +		"WARNING: ", "Oops", ";BUG", "kernel BUG at", "UBSAN: ",
>> +		"soft lockup - ", "general protection fault", "INFO: task ",
>> +		"rcu: INFO: ",
>> +	};
> [ ... ]
>
>> +	for (;;) {
>> +		r = read(fd, buf, sizeof(buf) - 1);
>> +		if (r < 0) {
>> +			/* The ring wrapped since the last read, records are gone */
>> +			if (errno == EPIPE) {
>> +				ksft_print_msg("kernel log overrun, records lost\n");
>> +				found++;
>> +				continue;
>> +			}
> [ ... ]
>
>> +		for (i = 0; i < ARRAY_SIZE(reports); i++) {
>> +			if (!strstr(buf, reports[i]))
>> +				continue;
>> +			ksft_print_msg("kernel log: %s", buf);
>> +			found++;
>> +			break;
>> +		}
>> +	}
>> +	return found;
>> +}
> [Severity: Medium]
> Only the "vtty" checks in kmsg_check() are tied to vtty. The reports[]
> patterns are matched with strstr() anywhere in a record. kmsg_open() seeks
> to the end of /dev/kmsg, so every record from any subsystem is read for the
> whole run.
>
> Can a warning or hung-task report from an unrelated subsystem, or from a
> test running at the same time on a shared CI host, fail this test even when
> the vtty code is correct?
>
> The EPIPE branch counts a ring overrun as a complaint. If other log traffic
> overruns the ring, does the verdict then depend on host log volume instead
> of on vtty behaviour?
>
> The log is drained only about once per second from the master loop. The
> final message "kernel complained %d time(s)" does not say that the cause
> may be unrelated to vtty.
>
> The commit message accepts the overrun rule deliberately. Could the
> matching be narrowed, or the failure message made to say that the report
> may be unrelated?

The test intentionally considers any symptom of a buggy kernel a problem,

since memory corruption in the original issue can surface in various ways.

Sure, this can report a failure due to some unrelated kernel bug, but that

would be still a kernel bug that will be investigated and fixed. An overrun

of the kernel kmsg ring during 1-second interval is an enormous amount

of messages that can't appear under normal circumstances.

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* [Devel] [PATCH hci-8.1 v5 1/5] selftests/ve: check CPUID override emulation for VE tasks
  @ 2026-10-06 22:54  5% ` Konstantin Khorenko
  2026-10-06 23:03  3%   ` no-reply
    1 sibling, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-10-06 22:54 UTC (permalink / raw)
  To: Pavel Tikhomirov, Vasileios Almpanis; +Cc: devel

Write a one-entry /proc/vz/cpuid_override table that clears one leaf 1
feature bit and compare what a task in a fresh VE and the host see:
the VE task must get the table values, the host must keep the hardware
bit. A second test flushes the table after the task was armed and
expects the hardware bit back, since faulting stays on for the task
and the emulation has to fall back to the real CPUID. On its own that
test would also pass on a kernel that never arms faulting; the first
one proves the arming.

Faulting is switched on for a task at its next context switch, not
when the flag is set, so the child stops itself with SIGSTOP after
entering the VE and the parent resumes it before the CPUID. The stop
is reported to the parent before the child leaves the CPU, and a
SIGCONT in that window would let it run on without a switch, so the
child hands over its voluntary context switch count first and the
parent waits for it to grow before the SIGCONT. For the second test
the table is flushed while the child stands still.

Without commit 07338fbae5a3 ("x86/traps: emulate CPUID faults for user
tasks in exc_general_protection()") both children die with SIGSEGV.
The tests skip on CPUs without CPUID faulting and restore the previous
table on exit; a table the kernel cannot take back in one write fails
the setup instead. The harness may skip FIXTURE_TEARDOWN once an ASSERT
has fired, so the cleanup is a FIXTURE_TEARDOWN_PARENT, which runs
exactly once whether or not one did; it also kills a child that such
an ASSERT left stopped in the VE cgroup. The test is built on x86 only.

https://virtuozzo.atlassian.net/browse/VSTOR-148843

Feature: x86: cpu pools (cpuid override)
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/selftests/ve/.gitignore              |   1 +
 tools/testing/selftests/ve/Makefile                |  10 +
 .../testing/selftests/ve/ve_cpuid_override_test.c  | 296 +++++++++++++++++++++
 3 files changed, 307 insertions(+)

diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index 3df4d05888dce..bdc322c645d95 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,3 +1,4 @@
 ve_ns_owner_test
 ve_perms_test
 ve_mount_accounting_test
+ve_cpuid_override_test
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index c6efe7c4b4fba..d919c1c289c8b 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -6,4 +6,14 @@ TEST_GEN_PROGS += ve_ns_owner_test
 TEST_GEN_PROGS += ve_perms_test
 TEST_GEN_PROGS += ve_mount_accounting_test
 
+# ARCH stays untouched: lib.mk picks the clang target by its emptiness.
+ifeq ($(ARCH),)
+ve_arch := $(shell uname -m 2>/dev/null || echo not)
+else
+ve_arch := $(ARCH)
+endif
+ifeq ($(ve_arch),$(filter $(ve_arch),x86 x86_64))
+TEST_GEN_PROGS += ve_cpuid_override_test
+endif
+
 include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
new file mode 100644
index 0000000000000..398f1fef5d246
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c
@@ -0,0 +1,296 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * /proc/vz/cpuid_override selftests
+ *
+ * A non-empty override table turns CPUID faulting on for every task that
+ * enters a VE, and the #GP handler has to emulate the instruction for
+ * them. The tests check that a task in a VE sees the table values, that
+ * the host keeps seeing the hardware values, and that a task which was
+ * armed while the table was set falls back to the hardware once the
+ * table is flushed. A kernel that does not emulate the fault kills the
+ * child with SIGSEGV instead.
+ *
+ * The table is global: run this on a node without live Containers and one
+ * instance at a time, a task entering a VE while a case runs is armed for
+ * life. A harness timeout kills the test without its teardown and leaves
+ * the test table in place and the VE cgroup behind.
+ */
+#define _GNU_SOURCE
+#include <cpuid.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/wait.h>
+
+#include "../kselftest_harness.h"
+#include "ve_selftest.h"
+
+#define CPUID_OVERRIDE_PATH	"/proc/vz/cpuid_override"
+#define CPUID_LEAF		1
+#define CPUID_LINE_LEN		128
+/* The kernel takes a table back only in one write below a page. */
+#define CPUID_WRITE_MAX		4096
+#define CPUID_SAVED_MAX		(CPUID_WRITE_MAX + 1)
+#define SWITCH_WAIT_ROUNDS	100000
+
+struct cpuid_regs {
+	unsigned int eax, ebx, ecx, edx;
+};
+
+static void do_cpuid(unsigned int leaf, struct cpuid_regs *r)
+{
+	__cpuid_count(leaf, 0, r->eax, r->ebx, r->ecx, r->edx);
+}
+
+/* Voluntary context switches of a task, from its /proc status file. */
+static int read_nvcsw(const char *status, unsigned long *nvcsw)
+{
+	char buf[8192], *p;
+
+	if (read_file_at(AT_FDCWD, status, buf, sizeof(buf)) <= 0)
+		return -1;
+	p = strstr(buf, "\nvoluntary_ctxt_switches:");
+	if (!p || sscanf(p, "\nvoluntary_ctxt_switches: %lu", nvcsw) != 1)
+		return -1;
+	return 0;
+}
+
+struct child_ctx {
+	int result_pipe[2];
+};
+
+/*
+ * The child enters a fresh VE, so it is armed with the table state of that
+ * moment, then reports its view of the leaf to the parent over a pipe. It
+ * stops itself in between: a stopped task is off the CPU, so the resume is
+ * a context switch, and the parent may change the table while the child
+ * stands still. It tells the parent its context switch count first, so
+ * that the parent can wait for the switch out.
+ */
+static int cpuid_child(struct child_ctx *ctx)
+{
+	struct cpuid_regs r;
+	unsigned long nvcsw;
+
+	close(ctx->result_pipe[0]);
+
+	if (unshare(CLONE_NEWVE) < 0)
+		return 21;
+	if (read_nvcsw("/proc/self/status", &nvcsw))
+		return 22;
+	if (write(ctx->result_pipe[1], &nvcsw, sizeof(nvcsw)) != sizeof(nvcsw))
+		return 23;
+	if (raise(SIGSTOP))
+		return 24;
+
+	do_cpuid(CPUID_LEAF, &r);
+
+	if (write(ctx->result_pipe[1], &r, sizeof(r)) != sizeof(r))
+		return 25;
+	return 0;
+}
+
+FIXTURE(ve_cpuid_override)
+{
+	int cgv2_fd;
+	int ctid;
+	struct cpuid_regs host;
+	unsigned int bit;
+	pid_t child;
+	char table[CPUID_LINE_LEN];
+	char saved[CPUID_SAVED_MAX];
+};
+
+FIXTURE_SETUP(ve_cpuid_override)
+{
+	int fd, len;
+
+	if (geteuid())
+		SKIP(return, "needs root");
+	fd = open(CPUID_OVERRIDE_PATH, O_RDONLY);
+	if (fd < 0)
+		SKIP(return, "%s: %s", CPUID_OVERRIDE_PATH, strerror(errno));
+	close(fd);
+
+	len = read_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->saved,
+			   sizeof(self->saved));
+	ASSERT_GE(len, 0) TH_LOG("%s: %s", CPUID_OVERRIDE_PATH, strerror(errno));
+	ASSERT_LT(len, CPUID_WRITE_MAX)
+		TH_LOG("a table of at least %d bytes cannot be restored", len);
+
+	/* Mask one feature bit the host has; SSE2 when present. */
+	do_cpuid(CPUID_LEAF, &self->host);
+	self->bit = 1u << 26;
+	if (!(self->host.edx & self->bit))
+		self->bit = self->host.edx & -self->host.edx;
+	ASSERT_NE(self->bit, 0u);
+
+	snprintf(self->table, sizeof(self->table),
+		 "0x%08x: 0x%08x 0x%08x 0x%08x 0x%08x\n", CPUID_LEAF,
+		 self->host.eax, self->host.ebx, self->host.ecx,
+		 self->host.edx & ~self->bit);
+
+	/* Last, so that a failed ASSERT above leaves no VE cgroup behind. */
+	self->cgv2_fd = mount_cg2_fd();
+	ASSERT_GE(self->cgv2_fd, 0) TH_LOG("cgroup2 mount: %s", strerror(errno));
+	ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
+				VE_CONTROLLERS), 0);
+	self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
+	ASSERT_GE(self->ctid, 0);
+}
+
+/*
+ * Runs after the test body in whichever process the harness picks, the test
+ * process itself after a failed ASSERT, so the table is restored whether or
+ * not one fired. The setup fd may be gone by then, hence the fresh mount.
+ */
+FIXTURE_TEARDOWN_PARENT(ve_cpuid_override)
+{
+	int cgv2_fd;
+
+	EXPECT_EQ(write_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH,
+				self->saved[0] ? self->saved : "\n"), 0)
+		TH_LOG("could not restore %s: %s", CPUID_OVERRIDE_PATH,
+		       strerror(errno));
+
+	/* A child left stopped by a failed ASSERT keeps the VE cgroup busy. */
+	if (self->child > 0)
+		kill(self->child, SIGKILL);
+
+	cgv2_fd = mount_cg2_fd();
+	if (cgv2_fd < 0) {
+		TH_LOG("cgroup2 mount: %s, VE cgroup %d left behind",
+		       strerror(errno), self->ctid);
+		return;
+	}
+	destroy_ve(cgv2_fd, self->ctid);
+	close(cgv2_fd);
+}
+
+/*
+ * Stopped is reported to the parent before the child leaves the CPU, so a
+ * SIGCONT sent right away could keep it running without a switch. Wait
+ * until its voluntary context switch count has moved on.
+ */
+static void wait_switched_out(struct __test_metadata *_metadata, pid_t pid,
+			      unsigned long nvcsw_before)
+{
+	char status[64];
+	unsigned long nvcsw;
+	int i;
+
+	snprintf(status, sizeof(status), "/proc/%d/status", pid);
+	for (i = 0; i < SWITCH_WAIT_ROUNDS; i++) {
+		ASSERT_EQ(read_nvcsw(status, &nvcsw), 0);
+		if (nvcsw > nvcsw_before)
+			return;
+		sched_yield();
+	}
+	ASSERT_GT(nvcsw, nvcsw_before);
+}
+
+/* Fork the child into the VE and return once it is off the CPU. */
+static pid_t spawn_cpuid_child(struct __test_metadata *_metadata,
+			       FIXTURE_DATA(ve_cpuid_override) * self,
+			       struct child_ctx *ctx)
+{
+	unsigned long nvcsw;
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(pipe(ctx->result_pipe), 0);
+	ASSERT_GE(enter_cgroup(self->cgv2_fd, self->ctid), 0);
+
+	pid = fork();
+	if (pid == 0)
+		_exit(cpuid_child(ctx));
+	if (pid > 0)
+		self->child = pid;
+
+	/* Leave the VE cgroup before any ASSERT, or destroy_ve() gets EBUSY. */
+	ASSERT_GE(enter_cgroup(self->cgv2_fd, 0), 0);
+	ASSERT_GE(pid, 0);
+	close(ctx->result_pipe[1]);
+
+	ASSERT_GE(waitpid(pid, &status, WUNTRACED), 0);
+	if (!WIFSTOPPED(status)) {
+		/* Reaped: the pid may be reused, keep it from teardown. */
+		self->child = 0;
+		if (WIFEXITED(status))
+			TH_LOG("child exited with %d", WEXITSTATUS(status));
+		if (WIFSIGNALED(status))
+			TH_LOG("child killed by signal %d", WTERMSIG(status));
+	}
+	ASSERT_TRUE(WIFSTOPPED(status));
+	ASSERT_EQ(read(ctx->result_pipe[0], &nvcsw, sizeof(nvcsw)),
+		  (int)sizeof(nvcsw));
+	wait_switched_out(_metadata, pid, nvcsw);
+	return pid;
+}
+
+static void resume_cpuid_child(struct __test_metadata *_metadata, pid_t pid)
+{
+	ASSERT_EQ(kill(pid, SIGCONT), 0);
+}
+
+static void collect_cpuid_child(struct __test_metadata *_metadata,
+				FIXTURE_DATA(ve_cpuid_override) * self,
+				struct child_ctx *ctx, struct cpuid_regs *r)
+{
+	int status, len;
+
+	len = read(ctx->result_pipe[0], r, sizeof(*r));
+	ASSERT_GE(waitpid(self->child, &status, 0), 0);
+	self->child = 0;
+	if (WIFSIGNALED(status))
+		TH_LOG("child killed by signal %d", WTERMSIG(status));
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+	ASSERT_EQ(len, (int)sizeof(*r));
+
+	close(ctx->result_pipe[0]);
+}
+
+TEST_F(ve_cpuid_override, ve_sees_table_host_sees_hardware)
+{
+	struct child_ctx ctx;
+	struct cpuid_regs r;
+	pid_t pid;
+
+	ASSERT_EQ(write_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->table), 0);
+
+	pid = spawn_cpuid_child(_metadata, self, &ctx);
+	resume_cpuid_child(_metadata, pid);
+	collect_cpuid_child(_metadata, self, &ctx, &r);
+
+	EXPECT_EQ(r.eax, self->host.eax);
+	EXPECT_EQ(r.ebx, self->host.ebx);
+	EXPECT_EQ(r.ecx, self->host.ecx);
+	EXPECT_EQ(r.edx, self->host.edx & ~self->bit);
+
+	do_cpuid(CPUID_LEAF, &r);
+	EXPECT_NE(r.edx & self->bit, 0u);
+}
+
+TEST_F(ve_cpuid_override, flushed_table_falls_back_to_hardware)
+{
+	struct child_ctx ctx;
+	struct cpuid_regs r;
+	pid_t pid;
+
+	ASSERT_EQ(write_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->table), 0);
+
+	pid = spawn_cpuid_child(_metadata, self, &ctx);
+
+	ASSERT_EQ(write_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, "\n"), 0);
+	resume_cpuid_child(_metadata, pid);
+	collect_cpuid_child(_metadata, self, &ctx, &r);
+
+	EXPECT_NE(r.edx & self->bit, 0u);
+}
+
+TEST_HARNESS_MAIN

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 5%]

* Re: [Devel] [PATCH hci-8.1 v5 5/5] selftests/ve: check CPUID override emulation for a prefixed CPUID
  @ 2026-10-06 23:03  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 23:03 UTC (permalink / raw)
  To: Pavel Tikhomirov, Vasileios Almpanis, Konstantin Khorenko; +Cc: devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] selftests/ve: do_cpuid_prefixed() 32-bit xchg of ebx truncates callee-saved RBX on x86_64
--

commit a68470c5110f8f0b39dbfe6d2c82d537ef16105e
Author: Konstantin Khorenko <khorenko@virtuozzo.com>

selftests/ve: check CPUID override emulation for a prefixed CPUID

This commit adds a selftest that executes CPUID with an operand-size
prefix (0x66) and expects the overridden table values. It adds a
do_cpuid_prefixed() helper, a prefixed flag in struct child_ctx, and a new
test prefixed_cpuid_is_emulated.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-148843

> diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
> --- a/tools/testing/selftests/ve/ve_cpuid_override_test.c
> +++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c

[ ... ]

> +/* CPUID with an operand-size prefix, which the CPU accepts and ignores. */
> +static void do_cpuid_prefixed(unsigned int leaf, struct cpuid_regs *r)
> +{
> +	/* ebx is the PIC base on i386; swap it out like <cpuid.h> does. */
> +	asm volatile("xchg %%ebx, %1\n\t"
> +		     ".byte 0x66, 0x0f, 0xa2\n\t"
> +		     "xchg %%ebx, %1"
> +		     : "=a" (r->eax), "=&r" (r->ebx), "=c" (r->ecx), "=d" (r->edx)
> +		     : "a" (leaf), "c" (0));
> +}

[Severity: Medium]
Can this asm corrupt RBX on x86_64?

The i386 PIC workaround is applied unconditionally.  On x86_64, a 32-bit
"xchg %%ebx, %1" zero-extends into RBX, and the closing "xchg %%ebx, %1"
only restores the low 32 bits.

RBX is callee-saved and is not listed as clobbered or tied to an operand in
do_cpuid_prefixed().  If the compiler keeps a 64-bit value there, the upper
half is lost.

Since do_cpuid_prefixed() is static, it is likely inlined into
cpuid_child(), which uses ctx and &r right after the asm:

	if (ctx->prefixed)
		do_cpuid_prefixed(CPUID_LEAF, &r);
	else
		do_cpuid(CPUID_LEAF, &r);

	if (write(ctx->result_pipe[1], &r, sizeof(r)) != sizeof(r))

If ctx or &r lives in RBX, could the write() call or the ctx dereference
fault?  That would show up as a SIGSEGV, which is the same symptom this
test uses to detect missing kernel emulation, so the failure could be
misattributed.

Whether a live value actually sits in RBX depends on the compiler version,
optimization level and register allocation.

Would it make sense to guard the swap with #if defined(__i386__) &&
defined(__PIC__), and use a plain cpuid with a "=b" output otherwise?
Declaring rbx as clobbered or using a 64-bit xchg would also work.

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261007-b4-prep-khorenko-vstor-148656-cpuid-v5-0-652c5c9e56da@virtuozzo.com?part=5

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 v5 1/5] selftests/ve: check CPUID override emulation for VE tasks
  2026-10-06 22:54  5% ` [Devel] [PATCH hci-8.1 v5 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
@ 2026-10-06 23:03  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 23:03 UTC (permalink / raw)
  To: Vasileios Almpanis, Konstantin Khorenko, Pavel Tikhomirov; +Cc: devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] selftests/ve: nvcsw baseline sampled too early to prove the child left the CPU
--

commit e70aeb0733728a60c8d4ac220677ea3eddf060e7
Author: Konstantin Khorenko <khorenko@virtuozzo.com>

selftests/ve: check CPUID override emulation for VE tasks

This adds a selftest that writes a one-entry /proc/vz/cpuid_override table
and checks that a task in a fresh VE sees the table values while the host
keeps the hardware values. A second test flushes the table after the task
was armed and expects the hardware values back. The child stops itself with
SIGSTOP so that CPUID faulting is armed at the next context switch, and the
parent waits on the child's voluntary context switch count before sending
SIGCONT.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-148843

> diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
> new file mode 100644
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c

[ ... ]

> +static int cpuid_child(struct child_ctx *ctx)
> +{
> +	struct cpuid_regs r;
> +	unsigned long nvcsw;
> +
> +	close(ctx->result_pipe[0]);
> +
> +	if (unshare(CLONE_NEWVE) < 0)
> +		return 21;
> +	if (read_nvcsw("/proc/self/status", &nvcsw))
> +		return 22;
> +	if (write(ctx->result_pipe[1], &nvcsw, sizeof(nvcsw)) != sizeof(nvcsw))
> +		return 23;
> +	if (raise(SIGSTOP))
> +		return 24;

[Severity: Medium]
Is the voluntary context switch baseline sampled early enough in
cpuid_child()?

The count is read by read_nvcsw() before the write() on the result pipe
and before raise(SIGSTOP):

    read_nvcsw("/proc/self/status", &nvcsw)
    write(ctx->result_pipe[1], &nvcsw, ...)
    raise(SIGSTOP)

If anything between the read and the stop sleeps voluntarily, for example
a page fault that blocks on IO in the first-use glibc write() or raise()
paths, nvcsw grows before the child actually stops.

[ ... ]

> +static void wait_switched_out(struct __test_metadata *_metadata, pid_t pid,
> +			      unsigned long nvcsw_before)
> +{
> +	char status[64];
> +	unsigned long nvcsw;
> +	int i;
> +
> +	snprintf(status, sizeof(status), "/proc/%d/status", pid);
> +	for (i = 0; i < SWITCH_WAIT_ROUNDS; i++) {
> +		ASSERT_EQ(read_nvcsw(status, &nvcsw), 0);
> +		if (nvcsw > nvcsw_before)
> +			return;

[Severity: Medium]
Can this condition tell the context switch caused by the stop apart from
an earlier voluntary switch?

The sequence would be:

    child:  read_nvcsw() -> baseline
    child:  voluntary sleep before raise(SIGSTOP), nvcsw grows
    parent: waitpid(WUNTRACED) returns on the stop
    parent: wait_switched_out() sees nvcsw > nvcsw_before, returns
    parent: kill(pid, SIGCONT) while the child is still in
            do_signal_stop() and has not left the CPU

The child would then resume without a context switch, so CPUID faulting,
which is switched on at the next context switch, is never armed.

In ve_sees_table_host_sees_hardware this would make the child report the
hardware EDX and the test would fail spuriously. In
flushed_table_falls_back_to_hardware the test would pass without proving
that faulting was armed, which is the false pass the commit message says
the first test is meant to rule out.

I checked the end of the series, including the commit "selftests/ve: check
CPUID override emulation for a prefixed CPUID", and the baseline is still
sampled before write() and raise(SIGSTOP). The later patches only add the
stop flag and more test cases, so this is not fixed there.

Would it work to sample nvcsw as the last step before raise(), or to also
require task state T in /proc/<pid>/stat before the parent sends SIGCONT?

[ ... ]

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261007-b4-prep-khorenko-vstor-148656-cpuid-v5-0-652c5c9e56da@virtuozzo.com?part=1

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* [Devel] [PATCH hci-8.1 v6 1/5] selftests/ve: check CPUID override emulation for VE tasks
  @ 2026-10-07 11:57  5% ` Konstantin Khorenko
  2026-10-07 12:04  3%   ` no-reply
  0 siblings, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-10-07 11:57 UTC (permalink / raw)
  To: Pavel Tikhomirov, Vasileios Almpanis; +Cc: devel

Write a one-entry /proc/vz/cpuid_override table that clears one leaf 1
feature bit and compare what a task in a fresh VE and the host see:
the VE task must get the table values, the host must keep the hardware
bit. A second test flushes the table after the task was armed and
expects the hardware bit back, since faulting stays on for the task
and the emulation has to fall back to the real CPUID. On its own that
test would also pass on a kernel that never arms faulting; the first
one proves the arming.

Faulting is switched on for a task at its next context switch, not
when the flag is set, so the child stops itself with SIGSTOP after
entering the VE and the parent resumes it before the CPUID. The stop
is reported to the parent before the child leaves the CPU, and a
SIGCONT in that window would let it run on without a switch, so the
child hands over its voluntary context switch count first and the
parent waits for it to grow before the SIGCONT. Any switch after that
count was taken arms faulting on the way back in, so it does not
matter whether the stop or an earlier sleep is the one counted. For
the second test the table is flushed while the child stands still.

Without commit 07338fbae5a3 ("x86/traps: emulate CPUID faults for user
tasks in exc_general_protection()") both children die with SIGSEGV.
The tests skip on CPUs without CPUID faulting and restore the previous
table on exit; a table the kernel cannot take back in one write fails
the setup instead. The harness may skip FIXTURE_TEARDOWN once an ASSERT
has fired, so the cleanup is a FIXTURE_TEARDOWN_PARENT, which runs
exactly once whether or not one did; it also kills a child that such
an ASSERT left stopped in the VE cgroup. The test is built on x86-64 only.

https://virtuozzo.atlassian.net/browse/VSTOR-148843

Feature: x86: cpu pools (cpuid override)
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
 tools/testing/selftests/ve/.gitignore              |   1 +
 tools/testing/selftests/ve/Makefile                |   6 +
 .../testing/selftests/ve/ve_cpuid_override_test.c  | 298 +++++++++++++++++++++
 3 files changed, 305 insertions(+)

diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index 3df4d05888dce..bdc322c645d95 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,3 +1,4 @@
 ve_ns_owner_test
 ve_perms_test
 ve_mount_accounting_test
+ve_cpuid_override_test
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index c6efe7c4b4fba..3f494a49270e5 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -6,4 +6,10 @@ TEST_GEN_PROGS += ve_ns_owner_test
 TEST_GEN_PROGS += ve_perms_test
 TEST_GEN_PROGS += ve_mount_accounting_test
 
+# CPUID faulting is x86-64 only. ARCH says "x86" for 32-bit too and lib.mk
+# picks the clang target by its emptiness, so decide by the compiler.
+ifneq ($(filter x86_64-%,$(shell $(CC) -dumpmachine 2>/dev/null)),)
+TEST_GEN_PROGS += ve_cpuid_override_test
+endif
+
 include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
new file mode 100644
index 0000000000000..a56e0c7213fb9
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c
@@ -0,0 +1,298 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * /proc/vz/cpuid_override selftests
+ *
+ * A non-empty override table turns CPUID faulting on for every task that
+ * enters a VE, and the #GP handler has to emulate the instruction for
+ * them. The tests check that a task in a VE sees the table values, that
+ * the host keeps seeing the hardware values, and that a task which was
+ * armed while the table was set falls back to the hardware once the
+ * table is flushed. A kernel that does not emulate the fault kills the
+ * child with SIGSEGV instead.
+ *
+ * The table is global: run this on a node without live Containers and one
+ * instance at a time, a task entering a VE while a case runs is armed for
+ * life. A harness timeout kills the test without its teardown and leaves
+ * the test table in place and the VE cgroup behind.
+ */
+#define _GNU_SOURCE
+#include <cpuid.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/wait.h>
+
+#include "../kselftest_harness.h"
+#include "ve_selftest.h"
+
+#define CPUID_OVERRIDE_PATH	"/proc/vz/cpuid_override"
+#define CPUID_LEAF		1
+#define CPUID_LINE_LEN		128
+/* The kernel takes a table back only in one write below a page. */
+#define CPUID_WRITE_MAX		4096
+#define CPUID_SAVED_MAX		(CPUID_WRITE_MAX + 1)
+#define SWITCH_WAIT_ROUNDS	100000
+
+struct cpuid_regs {
+	unsigned int eax, ebx, ecx, edx;
+};
+
+static void do_cpuid(unsigned int leaf, struct cpuid_regs *r)
+{
+	__cpuid_count(leaf, 0, r->eax, r->ebx, r->ecx, r->edx);
+}
+
+/* Voluntary context switches of a task, from its /proc status file. */
+static int read_nvcsw(const char *status, unsigned long *nvcsw)
+{
+	char buf[8192], *p;
+
+	if (read_file_at(AT_FDCWD, status, buf, sizeof(buf)) <= 0)
+		return -1;
+	p = strstr(buf, "\nvoluntary_ctxt_switches:");
+	if (!p || sscanf(p, "\nvoluntary_ctxt_switches: %lu", nvcsw) != 1)
+		return -1;
+	return 0;
+}
+
+struct child_ctx {
+	int result_pipe[2];
+};
+
+/*
+ * The child enters a fresh VE, so it is armed with the table state of that
+ * moment, then reports its view of the leaf to the parent over a pipe. It
+ * stops itself in between: a stopped task is off the CPU, so the resume is
+ * a context switch, and the parent may change the table while the child
+ * stands still. It tells the parent its context switch count first, so
+ * that the parent can wait for the switch out.
+ */
+static int cpuid_child(struct child_ctx *ctx)
+{
+	struct cpuid_regs r;
+	unsigned long nvcsw;
+
+	close(ctx->result_pipe[0]);
+
+	if (unshare(CLONE_NEWVE) < 0)
+		return 21;
+	if (read_nvcsw("/proc/self/status", &nvcsw))
+		return 22;
+	if (write(ctx->result_pipe[1], &nvcsw, sizeof(nvcsw)) != sizeof(nvcsw))
+		return 23;
+	if (raise(SIGSTOP))
+		return 24;
+
+	do_cpuid(CPUID_LEAF, &r);
+
+	if (write(ctx->result_pipe[1], &r, sizeof(r)) != sizeof(r))
+		return 25;
+	return 0;
+}
+
+FIXTURE(ve_cpuid_override)
+{
+	int cgv2_fd;
+	int ctid;
+	struct cpuid_regs host;
+	unsigned int bit;
+	pid_t child;
+	char table[CPUID_LINE_LEN];
+	char saved[CPUID_SAVED_MAX];
+};
+
+FIXTURE_SETUP(ve_cpuid_override)
+{
+	int fd, len;
+
+	if (geteuid())
+		SKIP(return, "needs root");
+	fd = open(CPUID_OVERRIDE_PATH, O_RDONLY);
+	if (fd < 0)
+		SKIP(return, "%s: %s", CPUID_OVERRIDE_PATH, strerror(errno));
+	close(fd);
+
+	len = read_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->saved,
+			   sizeof(self->saved));
+	ASSERT_GE(len, 0) TH_LOG("%s: %s", CPUID_OVERRIDE_PATH, strerror(errno));
+	ASSERT_LT(len, CPUID_WRITE_MAX)
+		TH_LOG("a table of at least %d bytes cannot be restored", len);
+
+	/* Mask one feature bit the host has; SSE2 when present. */
+	do_cpuid(CPUID_LEAF, &self->host);
+	self->bit = 1u << 26;
+	if (!(self->host.edx & self->bit))
+		self->bit = self->host.edx & -self->host.edx;
+	ASSERT_NE(self->bit, 0u);
+
+	snprintf(self->table, sizeof(self->table),
+		 "0x%08x: 0x%08x 0x%08x 0x%08x 0x%08x\n", CPUID_LEAF,
+		 self->host.eax, self->host.ebx, self->host.ecx,
+		 self->host.edx & ~self->bit);
+
+	/* Last, so that a failed ASSERT above leaves no VE cgroup behind. */
+	self->cgv2_fd = mount_cg2_fd();
+	ASSERT_GE(self->cgv2_fd, 0) TH_LOG("cgroup2 mount: %s", strerror(errno));
+	ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
+				VE_CONTROLLERS), 0);
+	self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
+	ASSERT_GE(self->ctid, 0);
+}
+
+/*
+ * Runs after the test body in whichever process the harness picks, the test
+ * process itself after a failed ASSERT, so the table is restored whether or
+ * not one fired. The setup fd may be gone by then, hence the fresh mount.
+ */
+FIXTURE_TEARDOWN_PARENT(ve_cpuid_override)
+{
+	int cgv2_fd;
+
+	EXPECT_EQ(write_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH,
+				self->saved[0] ? self->saved : "\n"), 0)
+		TH_LOG("could not restore %s: %s", CPUID_OVERRIDE_PATH,
+		       strerror(errno));
+
+	/* A child left stopped by a failed ASSERT keeps the VE cgroup busy. */
+	if (self->child > 0)
+		kill(self->child, SIGKILL);
+
+	cgv2_fd = mount_cg2_fd();
+	if (cgv2_fd < 0) {
+		TH_LOG("cgroup2 mount: %s, VE cgroup %d left behind",
+		       strerror(errno), self->ctid);
+		return;
+	}
+	destroy_ve(cgv2_fd, self->ctid);
+	close(cgv2_fd);
+}
+
+/*
+ * Stopped is reported to the parent before the child leaves the CPU, so a
+ * SIGCONT sent right away could keep it running without a switch. Wait
+ * until its voluntary context switch count has moved on: any switch after
+ * the count was taken, which is after the flag was set, arms faulting on
+ * the way back in, whether it is the stop or an earlier sleep.
+ */
+static void wait_switched_out(struct __test_metadata *_metadata, pid_t pid,
+			      unsigned long nvcsw_before)
+{
+	char status[64];
+	unsigned long nvcsw;
+	int i;
+
+	snprintf(status, sizeof(status), "/proc/%d/status", pid);
+	for (i = 0; i < SWITCH_WAIT_ROUNDS; i++) {
+		ASSERT_EQ(read_nvcsw(status, &nvcsw), 0);
+		if (nvcsw > nvcsw_before)
+			return;
+		sched_yield();
+	}
+	ASSERT_GT(nvcsw, nvcsw_before);
+}
+
+/* Fork the child into the VE and return once it is off the CPU. */
+static pid_t spawn_cpuid_child(struct __test_metadata *_metadata,
+			       FIXTURE_DATA(ve_cpuid_override) * self,
+			       struct child_ctx *ctx)
+{
+	unsigned long nvcsw;
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(pipe(ctx->result_pipe), 0);
+	ASSERT_GE(enter_cgroup(self->cgv2_fd, self->ctid), 0);
+
+	pid = fork();
+	if (pid == 0)
+		_exit(cpuid_child(ctx));
+	if (pid > 0)
+		self->child = pid;
+
+	/* Leave the VE cgroup before any ASSERT, or destroy_ve() gets EBUSY. */
+	ASSERT_GE(enter_cgroup(self->cgv2_fd, 0), 0);
+	ASSERT_GE(pid, 0);
+	close(ctx->result_pipe[1]);
+
+	ASSERT_GE(waitpid(pid, &status, WUNTRACED), 0);
+	if (!WIFSTOPPED(status)) {
+		/* Reaped: the pid may be reused, keep it from teardown. */
+		self->child = 0;
+		if (WIFEXITED(status))
+			TH_LOG("child exited with %d", WEXITSTATUS(status));
+		if (WIFSIGNALED(status))
+			TH_LOG("child killed by signal %d", WTERMSIG(status));
+	}
+	ASSERT_TRUE(WIFSTOPPED(status));
+	ASSERT_EQ(read(ctx->result_pipe[0], &nvcsw, sizeof(nvcsw)),
+		  (int)sizeof(nvcsw));
+	wait_switched_out(_metadata, pid, nvcsw);
+	return pid;
+}
+
+static void resume_cpuid_child(struct __test_metadata *_metadata, pid_t pid)
+{
+	ASSERT_EQ(kill(pid, SIGCONT), 0);
+}
+
+static void collect_cpuid_child(struct __test_metadata *_metadata,
+				FIXTURE_DATA(ve_cpuid_override) * self,
+				struct child_ctx *ctx, struct cpuid_regs *r)
+{
+	int status, len;
+
+	len = read(ctx->result_pipe[0], r, sizeof(*r));
+	ASSERT_GE(waitpid(self->child, &status, 0), 0);
+	self->child = 0;
+	if (WIFSIGNALED(status))
+		TH_LOG("child killed by signal %d", WTERMSIG(status));
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), 0);
+	ASSERT_EQ(len, (int)sizeof(*r));
+
+	close(ctx->result_pipe[0]);
+}
+
+TEST_F(ve_cpuid_override, ve_sees_table_host_sees_hardware)
+{
+	struct child_ctx ctx;
+	struct cpuid_regs r;
+	pid_t pid;
+
+	ASSERT_EQ(write_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->table), 0);
+
+	pid = spawn_cpuid_child(_metadata, self, &ctx);
+	resume_cpuid_child(_metadata, pid);
+	collect_cpuid_child(_metadata, self, &ctx, &r);
+
+	EXPECT_EQ(r.eax, self->host.eax);
+	EXPECT_EQ(r.ebx, self->host.ebx);
+	EXPECT_EQ(r.ecx, self->host.ecx);
+	EXPECT_EQ(r.edx, self->host.edx & ~self->bit);
+
+	do_cpuid(CPUID_LEAF, &r);
+	EXPECT_NE(r.edx & self->bit, 0u);
+}
+
+TEST_F(ve_cpuid_override, flushed_table_falls_back_to_hardware)
+{
+	struct child_ctx ctx;
+	struct cpuid_regs r;
+	pid_t pid;
+
+	ASSERT_EQ(write_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->table), 0);
+
+	pid = spawn_cpuid_child(_metadata, self, &ctx);
+
+	ASSERT_EQ(write_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, "\n"), 0);
+	resume_cpuid_child(_metadata, pid);
+	collect_cpuid_child(_metadata, self, &ctx, &r);
+
+	EXPECT_NE(r.edx & self->bit, 0u);
+}
+
+TEST_HARNESS_MAIN

-- 
2.43.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 5%]

* Re: [Devel] [PATCH hci-8.1 v6 1/5] selftests/ve: check CPUID override emulation for VE tasks
  2026-10-07 11:57  5% ` [Devel] [PATCH hci-8.1 v6 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
@ 2026-10-07 12:04  3%   ` no-reply
  2026-10-07 13:16  0%     ` Konstantin Khorenko
  0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-07 12:04 UTC (permalink / raw)
  To: Konstantin Khorenko, Pavel Tikhomirov, Vasileios Almpanis; +Cc: devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] selftests/ve: arch gate runs $(CC) -dumpmachine before lib.mk sets CC
--

commit de465213924e8ab9123a95613dab45358f011917
Author: Konstantin Khorenko <khorenko@virtuozzo.com>

selftests/ve: check CPUID override emulation for VE tasks

This adds ve_cpuid_override_test, which writes a one-entry
/proc/vz/cpuid_override table and compares what a task in a fresh VE and
the host see for CPUID leaf 1. A second test flushes the table after the
task was armed and expects the hardware value back. The test is added to
the ve selftests Makefile for x86-64 builds only.

Link: https://virtuozzo.atlassian.net/browse/VSTOR-148843

> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
> index c6efe7c4b4fb..3f494a49270e 100644
> --- a/tools/testing/selftests/ve/Makefile
> +++ b/tools/testing/selftests/ve/Makefile
> @@ -6,4 +6,10 @@ TEST_GEN_PROGS += ve_ns_owner_test
>  TEST_GEN_PROGS += ve_perms_test
>  TEST_GEN_PROGS += ve_mount_accounting_test
>  
> +# CPUID faulting is x86-64 only. ARCH says "x86" for 32-bit too and lib.mk
> +# picks the clang target by its emptiness, so decide by the compiler.
> +ifneq ($(filter x86_64-%,$(shell $(CC) -dumpmachine 2>/dev/null)),)

[Severity: Low]
Is $(CC) the right compiler to query at this point? This ifneq is expanded
while the Makefile is being parsed, before the include of ../lib.mk, and
lib.mk is where the real compiler is assigned:

tools/testing/selftests/lib.mk:
    CC := $(CLANG) $(CLANG_FLAGS) -fintegrated-as    (LLVM builds)
    CC := $(CROSS_COMPILE)gcc                        (otherwise)

Until then $(CC) is make's built-in "cc", unless CC was set on the command
line or in the environment.

For example, with a cross build from an x86_64 host:

    make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-

"cc -dumpmachine" prints x86_64-linux-gnu, so the filter matches and
ve_cpuid_override_test is added to TEST_GEN_PROGS. It is then built with
the aarch64 compiler, where <cpuid.h> and __cpuid_count are not available,
so the whole ve selftest directory fails to build.

The reverse case also looks possible. On an arm64 host cross-building for
x86_64, the test would be skipped.

The comment above the check says it decides "by the compiler", and the
commit message says "The test is built on x86-64 only". Neither seems to
hold for cross builds.

Could the check be done after including lib.mk, or be based on ARCH,
SRCARCH or uname -m instead?

> +TEST_GEN_PROGS += ve_cpuid_override_test
> +endif
> +
>  include ../lib.mk

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261007-b4-prep-khorenko-vstor-148656-cpuid-v6-0-a8eb906e69b8@virtuozzo.com?part=1

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 v6 1/5] selftests/ve: check CPUID override emulation for VE tasks
  2026-10-07 12:04  3%   ` no-reply
@ 2026-10-07 13:16  0%     ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-10-07 13:16 UTC (permalink / raw)
  To: Pavel Tikhomirov, Vasileios Almpanis; +Cc: devel

On 10/7/26 14:04, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Low] selftests/ve: arch gate runs $(CC) -dumpmachine before lib.mk sets CC
> --
> 
> commit de465213924e8ab9123a95613dab45358f011917
> Author: Konstantin Khorenko <khorenko@virtuozzo.com>
> 
> selftests/ve: check CPUID override emulation for VE tasks
> 
> This adds ve_cpuid_override_test, which writes a one-entry
> /proc/vz/cpuid_override table and compares what a task in a fresh VE and
> the host see for CPUID leaf 1. A second test flushes the table after the
> task was armed and expects the hardware value back. The test is added to
> the ve selftests Makefile for x86-64 builds only.
> 
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-148843
> 
>> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
>> index c6efe7c4b4fb..3f494a49270e 100644
>> --- a/tools/testing/selftests/ve/Makefile
>> +++ b/tools/testing/selftests/ve/Makefile
>> @@ -6,4 +6,10 @@ TEST_GEN_PROGS += ve_ns_owner_test
>>  TEST_GEN_PROGS += ve_perms_test
>>  TEST_GEN_PROGS += ve_mount_accounting_test
>>  
>> +# CPUID faulting is x86-64 only. ARCH says "x86" for 32-bit too and lib.mk
>> +# picks the clang target by its emptiness, so decide by the compiler.
>> +ifneq ($(filter x86_64-%,$(shell $(CC) -dumpmachine 2>/dev/null)),)
> 
> [Severity: Low]
> Is $(CC) the right compiler to query at this point? This ifneq is expanded
> while the Makefile is being parsed, before the include of ../lib.mk, and
> lib.mk is where the real compiler is assigned:
> 
> tools/testing/selftests/lib.mk:
>     CC := $(CLANG) $(CLANG_FLAGS) -fintegrated-as    (LLVM builds)
>     CC := $(CROSS_COMPILE)gcc                        (otherwise)
> 
> Until then $(CC) is make's built-in "cc", unless CC was set on the command
> line or in the environment.
> 
> For example, with a cross build from an x86_64 host:
> 
>     make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
> 
> "cc -dumpmachine" prints x86_64-linux-gnu, so the filter matches and
> ve_cpuid_override_test is added to TEST_GEN_PROGS. It is then built with
> the aarch64 compiler, where <cpuid.h> and __cpuid_count are not available,
> so the whole ve selftest directory fails to build.
> 
> The reverse case also looks possible. On an arm64 host cross-building for
> x86_64, the test would be skipped.
> 
> The comment above the check says it decides "by the compiler", and the
> commit message says "The test is built on x86-64 only". Neither seems to
> hold for cross builds.
> 
> Could the check be done after including lib.mk, or be based on ARCH,
> SRCARCH or uname -m instead?

  The observation is right: at that point $(CC) is still make's built-in
  "cc", lib.mk assigns the real compiler only later. The check cannot move
  below the include, though. lib.mk derives its rules from TEST_GEN_PROGS
  while it is being parsed ("all: $(TEST_GEN_PROGS) ..." and the per-program
  rules), so anything added to TEST_GEN_PROGS after the include is simply
  not built.

  The alternatives do not work either:

  - ARCH cannot tell 32-bit from 64-bit x86: the top-level selftests
    Makefile maps both i386 and x86_64 to "x86". Assigning ARCH ourselves
    is out as well, lib.mk uses an empty ARCH to mean "the host target" for
    LLVM=1 builds, and a value it has no CLANG_TARGET_FLAGS_ entry for
    (e.g. "aarch64" from uname -m) makes it error out for the whole ve
    directory. That was the v2 version of this gate, and the review rightly
    rejected it.
  - SRCARCH has the same granularity as ARCH.
  - uname -m knows nothing about cross builds at all, so it is strictly
    worse than asking a compiler.

  What remains is a gap for cross builds only: the gate asks the host
  compiler, so cross-building these selftests from an x86-64 host for
  another architecture would add the test and fail on <cpuid.h>, and the
  reverse would skip it. We do not cross build the vz selftests, the
  feature under test exists on x86-64 only, and I would rather keep a
  three-line gate than grow it to cover a configuration nobody runs. If a
  cross build of tools/testing/selftests/ve ever becomes a thing, the gate
  can be revisited then.

> 
>> +TEST_GEN_PROGS += ve_cpuid_override_test
>> +endif
>> +
>>  include ../lib.mk
> 

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* Re: [Devel] [PATCH hci-8.1 v4 1/1] selftests: drv-net-hw: skip the tests that need a real interface
  @ 2026-10-07 14:55  3%   ` no-reply
  0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-07 14:55 UTC (permalink / raw)
  To: Eva Kurchatova, khorenko; +Cc: devel

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] selftests: skip_on_veth() misses a forwarding.config veth override under NETIF_FIND_DRIVER
- [Low] selftests: skip_on_veth() duplicates lib.sh interface selection and skips explicit veths
--

commit cb2e8721faec852dd04b82eca1ff441b1fede385
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>

selftests: drv-net-hw: skip the tests that need a real interface

This patch adds a skip_on_veth() helper. The helper is sourced by the
ethtool, ethtool_extended_state, ethtool_rmon, hw_stats_l3 and
hw_stats_l3_gre tests ahead of lib.sh. It exits with ksft_skip when the
first interface would be a veth, or when lib.sh would create one.

> diff --git a/tools/testing/selftests/drivers/net/hw/skip_on_veth.sh b/tools/testing/selftests/drivers/net/hw/skip_on_veth.sh
> new file mode 100644
> index 0000000000000..64f9c217d0fed
> --- /dev/null
> +++ b/tools/testing/selftests/drivers/net/hw/skip_on_veth.sh
> @@ -0,0 +1,42 @@

[ ... ]

> +# forwarding.config assigns by name into the array lib.sh declares
> +declare -A NETIFS
> +
> +if [[ -f $forwarding_dir/forwarding.config ]]; then
> +	source "$forwarding_dir/forwarding.config"
> +fi
> +
> +: "${NETIF_CREATE:=yes}"
> +: "${NETIF_TYPE:=veth}"
> +
> +# Call before sourcing lib.sh, exiting after it leaves the veth pair behind.
> +# Judges by the first interface, the one lib.sh creates the pair from,
> +# found the way lib.sh finds it: keep in step with lib.sh.
> +skip_on_veth()
> +{
> +	# lib.sh takes the interfaces from the command line ahead of anything
> +	local p1=${1:-}
> +	local kind
> +
> +	# Otherwise with NETIF_FIND_DRIVER it takes those of that driver, and
> +	# skips by itself when it finds none
> +	[[ -z $p1 && -n $find_driver ]] && return

[Severity: Medium]
Can this early return bypass the veth check when forwarding.config
overrides the discovered interface? In lib.sh, netif_find_driver runs
first when NETIF_FIND_DRIVER is set, and forwarding.config is sourced
after that:

    if [[ $NETIF_FIND_DRIVER ]]; then
        unset NETIFS
        declare -A NETIFS
        netif_find_driver
    fi
    ...
    if [[ -f $net_forwarding_dir/forwarding.config ]]; then
        source "$net_forwarding_dir/forwarding.config"

Suppose NETIF_FIND_DRIVER is set, no arguments are given, and
forwarding.config sets NETIFS[p1] to a veth. Suppose instead it sets
NETIFS[p1] to a missing name that lib.sh would create as a veth. In
both cases skip_on_veth() returns without looking at NETIFS[p1]. The
five tests then run on veth and fail, which is what this patch is meant
to prevent.

The comment above the function says the logic is "found the way lib.sh
finds it: keep in step with lib.sh", but this ordering is not mirrored.
Should the NETIFS[p1] value from forwarding.config be checked in this
case too?

> +	p1=${p1:-${NETIFS[p1]-veth0}}
> +	if ip link show dev "$p1" &> /dev/null; then
> +		kind=$(ip -j -d link show dev "$p1" | jq -r '.[].linkinfo.info_kind')

[Severity: Low]
This isn't a bug, but skip_on_veth() copies lib.sh's interface selection
by hand. That covers the NETIFS defaults, NETIF_FIND_DRIVER,
forwarding.config, command line arguments and NETIF_CREATE/NETIF_TYPE.
The two copies have to be kept in step manually.

forwarding.config is also sourced twice, once here and once in lib.sh,
so any side effects in the config run twice.

The check skips purely on info_kind being veth. A user who deliberately
passes or configures a pre-created veth pair is therefore skipped.
The message "set NETIFS in forwarding.config" is misleading when NETIFS
is already set.

Would it be cleaner to share the selection logic with lib.sh rather than
duplicating it?

> +	elif [[ $NETIF_CREATE == yes && $NETIF_TYPE == veth ]]; then
> +		kind=veth # what lib.sh creates when the interface is missing
> +	fi
> +	[[ $kind == veth ]] || return
> +	echo "SKIP: needs a real interface, set NETIFS in forwarding.config"
> +	exit 4 # ksft_skip, lib.sh is not sourced yet
> +}

-- 
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261007145044.2647274-1-eva.kurchatova@virtuozzo.com?part=1

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 3%]

* Re: [Devel] [PATCH hci-8.1 v7 2/2] selftests: ve: introduce vtty pair teardown stress test
  2026-10-06 22:42  0%     ` Eva Kurchatova (Virtuozzo)
@ 2026-10-07 17:47  0%       ` Konstantin Khorenko
  0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-10-07 17:47 UTC (permalink / raw)
  To: Eva Kurchatova (Virtuozzo); +Cc: devel

On 10/7/26 00:42, Eva Kurchatova (Virtuozzo) wrote:
> 
> On 10/7/26 01:25, no-reply@sashiko.virtuozzo.com wrote:
>> Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
>> - [Medium] selftests/ve: kmsg_check() fails on any unrelated kernel warning or log overrun
>> - [Low] selftests/ve: veid_in_use() relies on ENOKEY from an ioctl key marked unused
>> - [Low] selftests/ve: 30s default run has little margin under the 45s runner timeout
>> --
>>
>> commit cfcd5cbc2bcb13ed42052dd5ac26511475fe149f
>> Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>>
>> selftests: ve: introduce vtty pair teardown stress test
>>
>> This adds ve_vtty_stress, a selftest that opens and closes a vtty pair from
>> the master end (vzctl ioctl) and the slave end (inside a VE) at the same
>> time. It also scans /dev/kmsg for warnings and splats while the test runs.
>>
>> Link: https://virtuozzo.atlassian.net/browse/VSTOR-144771
>>
>>> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
>>> --- a/tools/testing/selftests/ve/Makefile
>>> +++ b/tools/testing/selftests/ve/Makefile
>> [ ... ]
>>
>>> TEST_GEN_PROGS += ve_vtty_stress
>> [Severity: Low]
>> The ve selftest directory has no settings file, so the kselftest runner's
>> default per-test timeout applies. I believe that default is 45 seconds,
>> but I have not checked it in this tree.
>>
>> The test runs for 30 seconds by default.
>>
>> After that it blocks reading the child's count and then runs destroy_ve(),
>> which may retry for about a second. Setup also happens before the run.
>>
>> Is there enough margin on a slow or loaded host? A timeout kill would be
>> reported as a failure that has nothing to do with vtty.
>>
>> A larger VE_VTTY_SECS or argv[1] value would exceed the default timeout
>> unless the runner is configured otherwise. The commit message says the
>> default stays "under the timeout of the suite". Could a settings file be
>> added to back that up?
> 
> Yes, the test run time is 30 seconds, which gives enough room to shut
> down before the 45 second timeout, and I am not sure why would we
> need a settings file to extend that. Whenever this test is invoked with
> altered duration, it is expected to be ran manually for bisecting 
> purposes, etc.


Timeout: agreed, no settings file. The run is a fixed wall-clock deadline, a slow kernel does not stretch it.


>>> diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
>>> new file mode 100644
>>> --- /dev/null
>>> +++ b/tools/testing/selftests/ve/ve_vtty_stress.c
>> [ ... ]
>>
>>> +/* Any key but the tty one makes ve_configure() look a running VE up */
>>> +static bool veid_in_use(int ctl_fd, unsigned int veid)
>>> +{
>>> +	struct vzctl_ve_configure c = {
>>> +		.veid = veid,
>>> +		.key = VE_CONFIGURE_OS_RELEASE,
>>> +	};
>>> +
>>> +	return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c) < 0 && errno == ENOKEY;
>>> +}
>> [Severity: Low]
>> VE_CONFIGURE_OS_RELEASE is defined as "2 /* unused */" in
>> include/uapi/linux/vzcalluser.h. In kernel/ve/vecalls.c, ve_configure()
>> behaves like this:
>>
>>      if (key == VE_CONFIGURE_OPEN_TTY)
>>              return vtty_open_master(veid, val);
>>
>>      ve = get_ve_by_id(veid);
>>      if (!ve)
>>              return -EINVAL;
>>
>>      put_ve(ve);
>>      return err;    /* err is -ENOKEY */
>>
>> So veid_in_use() treats ENOKEY as "a running VE holds this veid". Is it
>> safe to depend on the return code of a key that is documented as unused?
>>
>> If that key is later rejected earlier, for example with -ENOTTY or -EINVAL,
>> veid_in_use() will always return false. The only guard against taking over
>> a running container's vtty pair would then disappear without any notice.
>>
>> Also, a VE that has ve.veid set but is not started is not in ve_idr, so the
>> probe cannot see it. The commit message only mentions "one started later"
>> as the case that is not seen.
>>
>> [ ... ]
> 
> I would like to hear Konstantin Khorenko opinion, as I am not sure about 
> this ioctl() API stability or why it would be marked as unused. Either this patch 
> un-obsoletes that call, or a dedicated ioctl() for checking VEID state should be 
> implemented.

  No ioctl, old or new. Just do not check at all.

      name="vtty-$(uuidgen)"
      prlctl create "$name" --vmtype ct --ostemplate almalinux-10-x86_64
      prlctl start "$name"

  This is how a container gets a unique id in real life. Take a random
  veid in the test the same way and drop the probe.

> 
>>> +static int kmsg_check(int fd)
>>> +{
>>> +	/* How a warning, a splat or a sanitizer report starts in a record */
>>> +	static const char *const reports[] = {
>>> +		"WARNING: ", "Oops", ";BUG", "kernel BUG at", "UBSAN: ",
>>> +		"soft lockup - ", "general protection fault", "INFO: task ",
>>> +		"rcu: INFO: ",
>>> +	};
>> [ ... ]
>>
>>> +	for (;;) {
>>> +		r = read(fd, buf, sizeof(buf) - 1);
>>> +		if (r < 0) {
>>> +			/* The ring wrapped since the last read, records are gone */
>>> +			if (errno == EPIPE) {
>>> +				ksft_print_msg("kernel log overrun, records lost\n");
>>> +				found++;
>>> +				continue;
>>> +			}
>> [ ... ]
>>
>>> +		for (i = 0; i < ARRAY_SIZE(reports); i++) {
>>> +			if (!strstr(buf, reports[i]))
>>> +				continue;
>>> +			ksft_print_msg("kernel log: %s", buf);
>>> +			found++;
>>> +			break;
>>> +		}
>>> +	}
>>> +	return found;
>>> +}
>> [Severity: Medium]
>> Only the "vtty" checks in kmsg_check() are tied to vtty. The reports[]
>> patterns are matched with strstr() anywhere in a record. kmsg_open() seeks
>> to the end of /dev/kmsg, so every record from any subsystem is read for the
>> whole run.
>>
>> Can a warning or hung-task report from an unrelated subsystem, or from a
>> test running at the same time on a shared CI host, fail this test even when
>> the vtty code is correct?
>>
>> The EPIPE branch counts a ring overrun as a complaint. If other log traffic
>> overruns the ring, does the verdict then depend on host log volume instead
>> of on vtty behaviour?
>>
>> The log is drained only about once per second from the master loop. The
>> final message "kernel complained %d time(s)" does not say that the cause
>> may be unrelated to vtty.
>>
>> The commit message accepts the overrun rule deliberately. Could the
>> matching be narrowed, or the failure message made to say that the report
>> may be unrelated?
> 
> The test intentionally considers any symptom of a buggy kernel a problem,
> since memory corruption in the original issue can surface in various ways.
> Sure, this can report a failure due to some unrelated kernel bug, but that
> would be still a kernel bug that will be investigated and fixed. An overrun
> of the kernel kmsg ring during 1-second interval is an enormous amount
> of messages that can't appear under normal circumstances.

kmsg part is fine, leave it.

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 0%]

* [Devel] [PATCH hci-8.1 v8 1/2] selftests: ve: grant the directories the files sit in
  @ 2026-10-08  3:52 11% ` Eva Kurchatova
  0 siblings, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-10-08  3:52 UTC (permalink / raw)
  To: khorenko; +Cc: devel

The sysfs variant names nested leaves, kernel/vmcoreinfo and
kernel/notes, while the proc one names files at the top of the
filesystem. Both enforcement tests grant the leaves and nothing else,
so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
cannot traverse, and every open answers ENOENT:

  not ok 14 ve_perms.sysfs.enforce_allows
  not ok 15 ve_perms.sysfs.enforce_denies

Grant x on each directory along the way, taken from the path the
variant already gives. set_entry() gets a PATH_MAX buffer and fails on
truncation instead of writing a cut entry, as ve_open_rel() does.

Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")

https://virtuozzo.atlassian.net/browse/VSTOR-144955

Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
 tools/testing/selftests/ve/ve_perms_test.c | 34 ++++++++++++++++++++--
 1 file changed, 32 insertions(+), 2 deletions(-)

diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 25ffbd42c380..8096b1fa3b43 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -114,9 +114,11 @@ static int ve_open_rel(int cgv2_fd, int ctid, const char *prefix,
 static int set_entry(int cgv2_fd, const char *path, const char *entry,
 		     const char *mask)
 {
-	char line[256];
+	char line[PATH_MAX];
 
-	snprintf(line, sizeof(line), "%s %s", entry, mask);
+	if (snprintf(line, sizeof(line), "%s %s", entry, mask) >=
+	    (int)sizeof(line))
+		return -1;
 	return write_file_at(cgv2_fd, path, line);
 }
 
@@ -343,6 +345,32 @@ TEST_F(ve_perms, write_parser)
 	EXPECT_LT(write_file_at(self->cgv2_fd, path, file), 0);
 }
 
+/* A leaf is reachable only once every directory above it can be traversed */
+static int allow_path(int cgv2_fd, const char *path, const char *file)
+{
+	char dir[PATH_MAX];
+	const char *sep;
+
+	for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
+		if (snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file),
+			     file) >= (int)sizeof(dir))
+			return -1;
+		if (set_entry(cgv2_fd, path, dir, "x") < 0)
+			return -1;
+	}
+	return 0;
+}
+
+static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
+{
+	int i;
+
+	for (i = 0; i < NR_FILES; i++)
+		if (allow_path(cgv2_fd, path, files[i]) < 0)
+			return -1;
+	return 0;
+}
+
 /*
  * Enforcement, allowed side: from inside a VE, granting r lets each file be
  * read, and granting x on the directory lets it be traversed (the lookup then
@@ -358,6 +386,7 @@ TEST_F(ve_perms, enforce_allows)
 
 	ve_file(path, sizeof(path), self->ctid_a, variant->fs);
 
+	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
 	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
 	for (i = 0; i < NR_FILES; i++)
 		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
@@ -400,6 +429,7 @@ TEST_F(ve_perms, enforce_denies)
 				      O_RDONLY), 0);
 
 	/* Granted w only: visible but not readable. */
+	ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
 	ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
 	for (i = 0; i < NR_FILES; i++)
 		EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
-- 
2.56.0

_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel

^ permalink raw reply	[relevance 11%]

Results 201-400 of ~800   |  | reverse | sort options + mbox downloads above
-- links below jump to the message on this page --
2026-08-31 22:48     [Devel] [PATCH vz10 1/7] ms/vxlan: do not reuse cached ip_hdr() value after skb_tunnel_check_pmtu() Eva Kurchatova
2026-08-31 22:48     ` [Devel] [PATCH vz10 3/7] selftests: net: skip what this kernel and iproute2 do not have Eva Kurchatova
2026-09-21 14:51  0%   ` Vladimir Riabchun
2026-08-31 22:50     [Devel] [PATCH vz10 1/4] ms/selftests: net: Add python context manager for netns entering Eva Kurchatova
2026-08-31 22:50     ` [Devel] [PATCH vz10 4/4] selftests: drv-net-hw: skip the tests that need a real interface Eva Kurchatova
2026-09-24 13:23  3%   ` Konstantin Khorenko
2026-08-31 23:43     [Devel] [PATCH vz10 1/3] selftests: drv-net: let NetDrvEnv take nsim_test Eva Kurchatova
2026-08-31 23:43     ` [Devel] [PATCH vz10 3/3] selftests: drv-net: read the channel count over netlink Eva Kurchatova
2026-09-24 11:53  0%   ` Konstantin Khorenko
2026-09-10  3:10     [Devel] [PATCH vz10 v2] selftests: binderfs: skip the stress test without binderfs Eva Kurchatova
2026-09-23 18:03  0% ` Konstantin Khorenko
2026-09-10  3:17     [Devel] [PATCH vz10 v2 1/2] selftests: pstore: skip when no backend is registered Eva Kurchatova
2026-09-23 18:03  0% ` Konstantin Khorenko
2026-09-23 18:37  0% ` Konstantin Khorenko
2026-09-10  3:21     [Devel] [PATCH vz10 v2] selftests: pci_endpoint: skip when the test device is absent Eva Kurchatova
2026-09-23 18:03  0% ` Konstantin Khorenko
2026-09-15 17:22     [Devel] [PATCH VZ10 v9 0/8] Add per-VE failcount support Vladimir Riabchun
2026-09-15 17:22     ` [Devel] [PATCH VZ10 v9 6/8] ve: Introduce per-VE failcount Vladimir Riabchun
2026-09-25 16:02  0%   ` Konstantin Khorenko
2026-09-25 16:08  4%   ` Konstantin Khorenko
2026-09-15 17:22     ` [Devel] [PATCH VZ10 v9 8/8] selftests/ve: Add mount accounting selftest Vladimir Riabchun
2026-09-18 15:06  0%   ` Vladimir Riabchun
2026-09-25 16:04  0%   ` Konstantin Khorenko
2026-09-16 12:31     [Devel] [PATCH vz10 v2 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-09-16 12:31     ` [Devel] [PATCH vz10 v2 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-09-22 17:47  3%   ` Konstantin Khorenko
2026-09-22 17:47  6% ` [Devel] [PATCH vz10 v2 1/2] selftests: ve: grant the directories the files sit in Konstantin Khorenko
2026-09-21 15:44     [PATCH 0/2] UefiCpuPkg/MpInitLib: recover from a lost Start-up IPI #VSTOR-146639 Denis V. Lunev
2026-09-21 15:44  3% ` [PATCH 2/2] UefiCpuPkg/MpInitLib: resend the Start-up IPI when an AP misses it #VSTOR-146639 Denis V. Lunev
2026-09-23 13:30  0%   ` Andrey Drobyshev
2026-09-21 20:46     [Devel] [PATCH vz10 v2 0/3] selftests: timers: raw_skew fixes Eva Kurchatova
2026-09-21 20:46  4% ` [Devel] [PATCH vz10 v2 1/3] ms/selftests/posix_timers: Use CLOCK_THREAD_CPUTIME_ID for ITIMER_PROF measurements Eva Kurchatova
2026-09-22 12:02     [Devel] [PATCH vz10 v2 1/6] selftests: net: run the fdb and bind tests in their own netns Eva Kurchatova
2026-09-22 12:02  2% ` [Devel] [PATCH vz10 v2 2/6] selftests: net: skip what this kernel and iproute2 do not have Eva Kurchatova
2026-09-22 12:02 16% ` [Devel] [PATCH vz10 v2 6/6] selftests: net: let the bridged PMTU tests take the ICMP they ask for Eva Kurchatova
2026-09-22 14:27  0%   ` Konstantin Khorenko
2026-09-22 15:00     [Devel] [PATCH hci-8.1 v3 0/5] selftests: net: make the net tests pass on a vz10 host Konstantin Khorenko
2026-09-22 15:00  2% ` [Devel] [PATCH hci-8.1 v3 2/5] selftests: net: skip what this kernel and iproute2 do not have Konstantin Khorenko
2026-09-22 15:00 10% ` [Devel] [PATCH hci-8.1 v3 5/5] ms/tunnels: Drop stale dst when building an ICMP error for PMTUD Konstantin Khorenko
2026-09-24 12:13     [PATCH hci-8.0/hci-8.1 0/2] migration/block-dirty-bitmap: fix postcopy abort with bitmaps on several nodes #VSTOR-147066 Denis V. Lunev
2026-09-24 12:13 35% ` [PATCH hci-8.0/hci-8.1 1/2] " Denis V. Lunev
2026-09-24 15:01  3% [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-09-24 15:01  6% ` [Devel] [PATCH hci-8.1 v3 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-09-24 16:17  0% ` [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in Konstantin Khorenko
2026-09-24 16:41     [PATCH QEMU HCI-8.0 v4 00/12] usb-host: support CPR migration Andrey Drobyshev
2026-09-24 16:41 31% ` [PATCH QEMU HCI-8.0 v4 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 13% ` [PATCH QEMU HCI-8.0 v4 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 26% ` [PATCH QEMU HCI-8.0 v4 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 25% ` [PATCH QEMU HCI-8.0 v4 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 23% ` [PATCH QEMU HCI-8.0 v4 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 21% ` [PATCH QEMU HCI-8.0 v4 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 22% ` [PATCH QEMU HCI-8.0 v4 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 26% ` [PATCH QEMU HCI-8.0 v4 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 11% ` [PATCH QEMU HCI-8.0 v4 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 20% ` [PATCH QEMU HCI-8.0 v4 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
2026-09-25 12:47  3% [Devel] [PATCH VZ10 v4 0/3] Improvements for ve selftests Konstantin Khorenko
2026-09-25 12:47  4% ` [Devel] [PATCH VZ10 v4 1/3] ve_namespace: Allow entering non-running VE namespace Konstantin Khorenko
2026-09-25 12:47  6% ` [Devel] [PATCH VZ10 v4 2/3] selftests/ve: Add namespace management helpers Konstantin Khorenko
2026-09-25 12:47  5% ` [Devel] [PATCH VZ10 v4 3/3] selftests/ve: Add more helpers Konstantin Khorenko
2026-09-25 13:18     [Devel] [PATCH hci-8.1 v3 0/2] selftests: pstore: fix backend/frontend skip Eva Kurchatova
2026-09-25 13:18  4% ` [Devel] [PATCH hci-8.1 v3 1/2] selftests: pstore: skip when no backend is registered Eva Kurchatova
2026-09-25 14:11     [PATCH QEMU HCI-8.0 v5 00/13] usb-host: support CPR migration Andrey Drobyshev
2026-09-25 14:11 31% ` [PATCH QEMU HCI-8.0 v5 01/13] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 13% ` [PATCH QEMU HCI-8.0 v5 02/13] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 26% ` [PATCH QEMU HCI-8.0 v5 03/13] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 25% ` [PATCH QEMU HCI-8.0 v5 04/13] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 25% ` [PATCH QEMU HCI-8.0 v5 05/13] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 21% ` [PATCH QEMU HCI-8.0 v5 07/13] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 22% ` [PATCH QEMU HCI-8.0 v5 08/13] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 26% ` [PATCH QEMU HCI-8.0 v5 09/13] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 11% ` [PATCH QEMU HCI-8.0 v5 11/13] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 20% ` [PATCH QEMU HCI-8.0 v5 13/13] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
2026-09-25 16:53     [Devel] [PATCH hci-8.1 v10 00/13] Add per-VE failcount support Konstantin Khorenko
2026-09-25 16:53  3% ` [Devel] [PATCH hci-8.1 v10 02/13] ve/fs: count available mounts instead of used ones Konstantin Khorenko
2026-09-25 16:53  4% ` [Devel] [PATCH hci-8.1 v10 07/13] ve: introduce per-VE failcounter infrastructure Konstantin Khorenko
2026-09-25 16:53  4% ` [Devel] [PATCH hci-8.1 v10 09/13] ve: add mnt failcounter Konstantin Khorenko
2026-09-25 16:53  6% ` [Devel] [PATCH hci-8.1 v10 10/13] ve: add netif failcounter Konstantin Khorenko
2026-09-25 16:53  6% ` [Devel] [PATCH hci-8.1 v10 13/13] selftests/ve: add mount accounting selftest Konstantin Khorenko
2026-09-28 13:23 30% [QEMU HCI-7.4 PATCH] migration: set .vm_old_state in migration_stop_vm() #VSTOR-147066 Andrey Drobyshev
2026-09-28 16:51  0% ` Denis V. Lunev
2026-09-28 15:16     [Devel] [PATCH hci-8.1 v4 0/2] selftests: ve: fix ve_perms, introduce vtty_stress Eva Kurchatova
2026-09-28 15:16 11% ` [Devel] [PATCH hci-8.1 v4 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-09-28 15:16  6% ` [Devel] [PATCH hci-8.1 v4 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-09-28 15:24     [Devel] [PATCH hci-8.1 0/2] ve/fs: allow host to go over container mount limit Pavel Tikhomirov
2026-09-28 15:24  5% ` [Devel] [PATCH hci-8.1 2/2] selftests/ve: check that host can " Pavel Tikhomirov
2026-09-28 19:37     [Devel] [PATCH hci-8.1 v2 0/3] selftests: drv-net: bunch of fixes Eva Kurchatova
2026-09-28 19:37  4% ` [Devel] [PATCH hci-8.1 v2 3/3] selftests: drv-net: read the channel count over netlink Eva Kurchatova
2026-09-29 12:48  6% [Devel] [PATCH hci-8.1] ms/selftests: net: reduce txtimestamp deschedule flakes Eva Kurchatova
2026-09-29 12:52  0% ` no-reply
2026-09-30  5:44     [Devel] [PATCH hci-8.1 0/3] fs/fuse: rework close_wait handling Liu Kui
2026-09-30  5:44  2% ` [Devel] [PATCH hci-8.1 3/3] fs/fuse: use wait_var_event to wait for fuse file references Liu Kui
2026-09-30  5:51  0%   ` no-reply
2026-09-30  7:34     [Devel] [PATCH hci-8.1 0/3] fs/fuse kio: cut per-4k-page sendmsg/recvmsg calls in the sockio path Liu Kui
2026-09-30  7:34     ` [Devel] [PATCH hci-8.1 2/3] fs/fuse kio: refactor transmit path in pcs_sockio_send() Liu Kui
2026-09-30  7:42  3%   ` no-reply
2026-09-30 16:29  0%     ` Konstantin Khorenko
2026-09-30 11:17     [PATCH hci-8.0 0/9] block: cheaper zero handling in backup and commit #VSTOR-134085 Denis V. Lunev
2026-09-30 11:17 21% ` [PATCH hci-8.0 1/9] block/commit: pass BDRV_WANT_PRECISE to block-status #VSTOR-134085 Denis V. Lunev
2026-09-30 11:17  3% ` [PATCH hci-8.0 2/9] iotests/040: cover large and fragmented commit runs #VSTOR-134085 Denis V. Lunev
2026-09-30 11:17 18% ` [PATCH hci-8.0 3/9] block/commit: batch block-status queries #VSTOR-134085 Denis V. Lunev
2026-09-30 11:21  3%   ` no-reply
2026-09-30 11:17     ` [PATCH hci-8.0 4/9] iotests/124: cover backup of zero clusters and holes #VSTOR-134085 Denis V. Lunev
2026-09-30 11:31  5%   ` no-reply
2026-09-30 11:17 25% ` [PATCH hci-8.0 5/9] block/block-copy: don't reserve memory for zero tasks #VSTOR-134085 Denis V. Lunev
2026-09-30 11:17 23% ` [PATCH hci-8.0 6/9] block/block-copy: extract block_copy_set_task_method() #VSTOR-134085 Denis V. Lunev
2026-09-30 11:17 33% ` [PATCH hci-8.0 7/9] block/block-copy: track known-zero source clusters #VSTOR-134085 Denis V. Lunev
2026-09-30 11:30  6%   ` no-reply
2026-09-30 11:17 25% ` [PATCH hci-8.0 8/9] block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085 Denis V. Lunev
2026-09-30 11:33  5%   ` no-reply
2026-09-30 11:17 26% ` [PATCH hci-8.0 9/9] block/block-copy: coalesce write-zeroes tasks #VSTOR-134085 Denis V. Lunev
2026-09-30 11:34  3%   ` no-reply
2026-10-01  3:24     [Devel] [PATCH hci-8.1 v5 0/2] selftests: ve: fix ve_perms, introduce vtty_stress Eva Kurchatova
2026-10-01  3:24 11% ` [Devel] [PATCH hci-8.1 v5 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-10-01  3:24  6% ` [Devel] [PATCH hci-8.1 v5 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-10-01  3:34  5%   ` no-reply
2026-10-02  4:43     [Devel] [PATCH hci-8.1 v6 0/2] selftests: ve: fix ve_perms, introduce vtty_stress Eva Kurchatova
2026-10-02  4:43 11% ` [Devel] [PATCH hci-8.1 v6 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-10-02  4:43  6% ` [Devel] [PATCH hci-8.1 v6 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-10-02  4:54  4%   ` no-reply
2026-10-02 10:58  4% [PATCH hci-8.0 1/1] hw/scsi: avoid deadlock upon TMF request cancelling with VirtIO #VSTOR-148264 Denis V. Lunev
2026-10-02 11:14  0% ` Andrey Drobyshev
2026-10-02 16:48     [PATCH 0/3] chardev: fix qemu-update destroying VMs with a console log #VSTOR-148304 Denis V. Lunev
2026-10-02 16:48  7% ` [PATCH 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304 Denis V. Lunev
2026-10-02 16:48  3% ` [PATCH 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
2026-10-02 16:54  3%   ` no-reply
2026-10-02 16:48     ` [PATCH 3/3] iotests/migrate-cpr-exec: cover chardevs with logfile= #VSTOR-148304 Denis V. Lunev
2026-10-02 16:55  3%   ` no-reply
2026-10-02 21:26     [Devel] [PATCH hci-8.1 0/6] ve/printk: Fix logs leakage via ve_printk called from IRQ context Vladimir Riabchun
2026-10-02 21:26     ` [Devel] [PATCH hci-8.1 1/6] ve/printk: Add sanity checks in ve_printk functions Vladimir Riabchun
2026-10-02 21:31  4%   ` no-reply
2026-10-03 12:26  0%     ` Vladimir Riabchun
2026-10-02 21:26     ` [Devel] [PATCH hci-8.1 2/6] ve/printk: Report potentially leaking usage of " Vladimir Riabchun
2026-10-02 21:32  3%   ` no-reply
2026-10-03 12:26  0%     ` Vladimir Riabchun
2026-10-02 21:26 15% ` [Devel] [PATCH hci-8.1 3/6] ve/printk: Specify ve in printks Vladimir Riabchun
2026-10-02 21:36  4%   ` no-reply
2026-10-03 13:49  0%     ` Vladimir Riabchun
2026-10-02 21:26  3% ` [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
2026-10-02 21:37  3%   ` no-reply
2026-10-05  8:45  0%     ` Vladimir Riabchun
2026-10-02 21:26  8% ` [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
2026-10-02 21:38  5%   ` no-reply
2026-10-05  9:56  0%     ` Vladimir Riabchun
2026-10-02 21:26  4% ` [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
2026-10-02 21:40  3%   ` no-reply
2026-10-05 10:02  0%     ` Vladimir Riabchun
2026-10-05  9:26     [PATCH hci-8.0 v2 0/3] chardev: fix qemu-update destroying VMs with a console log #VSTOR-148304 Denis V. Lunev
2026-10-05  9:26  7% ` [PATCH hci-8.0 v2 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304 Denis V. Lunev
2026-10-05  9:26  3% ` [PATCH hci-8.0 v2 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
2026-10-05  9:30  3%   ` no-reply
2026-10-05 11:23     [Devel] [PATCH hci-8.1 v4 0/1] selftests: net: skip the veth GRO checks where they cannot run Eva Kurchatova
2026-10-05 11:23     ` [Devel] [PATCH hci-8.1 v4 1/1] " Eva Kurchatova
2026-10-05 11:29  3%   ` no-reply
2026-10-06 10:34     [PATCH QEMU HCI-8.0 v6 00/12] usb-host: support CPR migration Andrey Drobyshev
2026-10-06 10:34 31% ` [PATCH QEMU HCI-8.0 v6 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:37  7%   ` no-reply
2026-10-06 10:34 13% ` [PATCH QEMU HCI-8.0 v6 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:34 25% ` [PATCH QEMU HCI-8.0 v6 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:48  5%   ` no-reply
2026-10-06 10:34 23% ` [PATCH QEMU HCI-8.0 v6 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:46  6%   ` no-reply
2026-10-06 10:34 22% ` [PATCH QEMU HCI-8.0 v6 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:47  5%   ` no-reply
2026-10-06 10:34 29% ` [PATCH QEMU HCI-8.0 v6 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:54  6%   ` no-reply
2026-10-06 10:34     ` [PATCH QEMU HCI-8.0 v6 09/12] usb: migrate the interface altsetting #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:52  3%   ` no-reply
2026-10-06 10:34 10% ` [PATCH QEMU HCI-8.0 v6 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:57  6%   ` no-reply
2026-10-06 10:34     ` [PATCH QEMU HCI-8.0 v6 11/12] hcd-xhci: resync isochronous endpoints after CPR #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:57  3%   ` no-reply
2026-10-06 10:34 20% ` [PATCH QEMU HCI-8.0 v6 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
2026-10-06 14:56     [Devel] [PATCH hci-8.1 0/5] x86/ve: CPUID override follow-ups: arm at once, prefixes, selftests Konstantin Khorenko
2026-10-06 14:56  3% ` [Devel] [PATCH hci-8.1 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
2026-10-06 15:07  3%   ` no-reply
2026-10-06 14:56     ` [Devel] [PATCH hci-8.1 3/5] selftests/ve: check CPUID override right after entering a VE Konstantin Khorenko
2026-10-06 15:03  3%   ` no-reply
2026-10-06 15:35     [PATCH QEMU HCI-8.0 v7 00/14] usb-host: support CPR migration Andrey Drobyshev
2026-10-06 15:35 30% ` [PATCH QEMU HCI-8.0 v7 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:38  0%   ` no-reply
2026-10-06 15:35  6% ` [PATCH QEMU HCI-8.0 v7 02/14] usb-host: don't leak config descriptor on claim " Andrey Drobyshev
2026-10-06 15:35 13% ` [PATCH QEMU HCI-8.0 v7 03/14] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:35 25% ` [PATCH QEMU HCI-8.0 v7 05/14] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:45  4%   ` no-reply
2026-10-06 15:35 24% ` [PATCH QEMU HCI-8.0 v7 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:46  4%   ` no-reply
2026-10-06 15:35 22% ` [PATCH QEMU HCI-8.0 v7 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:47  5%   ` no-reply
2026-10-06 15:36 29% ` [PATCH QEMU HCI-8.0 v7 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:52  6%   ` no-reply
2026-10-06 15:36     ` [PATCH QEMU HCI-8.0 v7 11/14] usb: migrate the interface altsetting #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:51  3%   ` no-reply
2026-10-06 15:36 10% ` [PATCH QEMU HCI-8.0 v7 12/14] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:57  6%   ` no-reply
2026-10-06 15:36     ` [PATCH QEMU HCI-8.0 v7 13/14] hcd-xhci: resync isochronous endpoints after CPR #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:56  3%   ` no-reply
2026-10-06 15:36 20% ` [PATCH QEMU HCI-8.0 v7 14/14] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
2026-10-06 16:27     [PATCH QEMU HCI-8.0 v8 00/14] usb-host: support CPR migration Andrey Drobyshev
2026-10-06 16:27 30% ` [PATCH QEMU HCI-8.0 v8 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-10-06 16:31  6%   ` no-reply
2026-10-06 16:27  6% ` [PATCH QEMU HCI-8.0 v8 02/14] usb-host: don't leak config descriptor on claim " Andrey Drobyshev
2026-10-06 16:27 13% ` [PATCH QEMU HCI-8.0 v8 03/14] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
2026-10-06 16:27 26% ` [PATCH QEMU HCI-8.0 v8 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
2026-10-06 16:38  4%   ` no-reply
2026-10-06 16:27 25% ` [PATCH QEMU HCI-8.0 v8 05/14] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
2026-10-06 16:27 26% ` [PATCH QEMU HCI-8.0 v8 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
2026-10-06 16:38  5%   ` no-reply
2026-10-06 16:27 24% ` [PATCH QEMU HCI-8.0 v8 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
2026-10-06 16:44  3%   ` no-reply
2026-10-06 16:27 22% ` [PATCH QEMU HCI-8.0 v8 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-10-06 16:41  3%   ` no-reply
2026-10-06 16:28 29% ` [PATCH QEMU HCI-8.0 v8 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
2026-10-06 16:50  7%   ` no-reply
2026-10-06 16:28 10% ` [PATCH QEMU HCI-8.0 v8 12/14] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-10-06 16:52  6%   ` no-reply
2026-10-06 16:28     ` [PATCH QEMU HCI-8.0 v8 13/14] hcd-xhci: resync isochronous endpoints after CPR #VSTOR-137800 Andrey Drobyshev
2026-10-06 16:53  3%   ` no-reply
2026-10-06 16:28 20% ` [PATCH QEMU HCI-8.0 v8 14/14] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
2026-10-06 17:19     [Devel] [PATCH hci-8.1 v2 0/5] x86/ve: CPUID override follow-ups: arm at once, prefixes, selftests Konstantin Khorenko
2026-10-06 17:19  4% ` [Devel] [PATCH hci-8.1 v2 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
2026-10-06 17:29  3%   ` no-reply
2026-10-06 20:44     [Devel] [PATCH hci-8.1 v3 0/5] x86/ve: CPUID override follow-ups: arm at once, prefixes, selftests Konstantin Khorenko
2026-10-06 20:44  4% ` [Devel] [PATCH hci-8.1 v3 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
2026-10-06 20:52  3%   ` no-reply
2026-10-06 21:53     [Devel] [PATCH hci-8.1 v2 0/6] ve/printk: Fix logs leakage via ve_printk called from IRQ context Vladimir Riabchun
2026-10-06 21:53     ` [Devel] [PATCH hci-8.1 v2 1/6] ve/printk: Add sanity checks in ve_printk functions Vladimir Riabchun
2026-10-06 21:58  4%   ` no-reply
2026-10-06 21:53     ` [Devel] [PATCH hci-8.1 v2 2/6] ve/printk: Report potentially leaking usage of " Vladimir Riabchun
2026-10-06 21:59  3%   ` no-reply
2026-10-06 21:53 15% ` [Devel] [PATCH hci-8.1 v2 3/6] ve/printk: Specify ve in printks Vladimir Riabchun
2026-10-06 22:03  3%   ` no-reply
2026-10-06 21:53  4% ` [Devel] [PATCH hci-8.1 v2 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
2026-10-06 22:04  3%   ` no-reply
2026-10-06 21:53  8% ` [Devel] [PATCH hci-8.1 v2 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
2026-10-06 21:53  9% ` [Devel] [PATCH hci-8.1 v2 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
2026-10-06 22:06  4%   ` no-reply
2026-10-06 22:12     [Devel] [PATCH hci-8.1 v4 0/5] x86/ve: CPUID override follow-ups: arm at once, prefixes, selftests Konstantin Khorenko
2026-10-06 22:12  5% ` [Devel] [PATCH hci-8.1 v4 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
2026-10-06 22:22  3%   ` no-reply
2026-10-06 22:15     [Devel] [PATCH hci-8.1 v7 0/2] selftests: ve: fix ve_perms, introduce vtty_stress Eva Kurchatova
2026-10-06 22:15 11% ` [Devel] [PATCH hci-8.1 v7 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-10-06 22:15  6% ` [Devel] [PATCH hci-8.1 v7 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-10-06 22:25  4%   ` no-reply
2026-10-06 22:42  0%     ` Eva Kurchatova (Virtuozzo)
2026-10-07 17:47  0%       ` Konstantin Khorenko
2026-10-06 22:26     [Devel] [PATCH hci-8.1 v3 0/1] selftests: drv-net-hw: skip the tests that need a real interface Eva Kurchatova
2026-10-06 22:26     ` [Devel] [PATCH hci-8.1 v3 1/1] " Eva Kurchatova
2026-10-06 22:31  3%   ` no-reply
2026-10-06 22:54     [Devel] [PATCH hci-8.1 v5 0/5] x86/ve: CPUID override follow-ups: arm at once, prefixes, selftests Konstantin Khorenko
2026-10-06 22:54  5% ` [Devel] [PATCH hci-8.1 v5 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
2026-10-06 23:03  3%   ` no-reply
2026-10-06 22:54     ` [Devel] [PATCH hci-8.1 v5 5/5] selftests/ve: check CPUID override emulation for a prefixed CPUID Konstantin Khorenko
2026-10-06 23:03  3%   ` no-reply
2026-10-07 11:57     [Devel] [PATCH hci-8.1 v6 0/5] x86/ve: CPUID override follow-ups: arm at once, prefixes, selftests Konstantin Khorenko
2026-10-07 11:57  5% ` [Devel] [PATCH hci-8.1 v6 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
2026-10-07 12:04  3%   ` no-reply
2026-10-07 13:16  0%     ` Konstantin Khorenko
2026-10-07 14:50     [Devel] [PATCH hci-8.1 v4 0/1] drv-net-hw: skip the tests that need a real interface Eva Kurchatova
2026-10-07 14:50     ` [Devel] [PATCH hci-8.1 v4 1/1] selftests: " Eva Kurchatova
2026-10-07 14:55  3%   ` no-reply
2026-10-08  3:52     [Devel] [PATCH hci-8.1 v8 0/2] selftests: ve: fix ve_perms, introduce vtty_stress Eva Kurchatova
2026-10-08  3:52 11% ` [Devel] [PATCH hci-8.1 v8 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.