* Re: [Devel] [PATCH hci-8.1] selftests/bpf: run the xsk veth tests in a network namespace of their own
@ 2026-10-20 4:18 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-20 4:18 UTC (permalink / raw)
To: Eva Kurchatova, khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Low] selftests/bpf: test_xsk.sh passes a bare -f when max_skb_frags is unreadable
- [Low] selftests/bpf: xskxceiver -f parsing can wrap u32 and the calloc() size
- [Low] selftests/bpf: test_xsk.sh -f only changes the test's assumed max_skb_frags
- [Low] selftests/bpf: test_xsk.sh reports FAIL when the netns cannot be created
--
commit 035a16fff590cf5d4d3fde38f45f552e4cab308c
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
selftests/bpf: run the xsk veth tests in a network namespace of their own
This patch makes test_xsk.sh create its veth pair inside a private network
namespace, so host traffic does not reach the AF_XDP sockets under test. It
adds a -f option to both test_xsk.sh and xskxceiver so the max_skb_frags
value can be passed in from outside the namespace.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-150731
> diff --git a/tools/testing/selftests/bpf/test_xsk.sh b/tools/testing/selftests/bpf/test_xsk.sh
> --- a/tools/testing/selftests/bpf/test_xsk.sh
> +++ b/tools/testing/selftests/bpf/test_xsk.sh
[ ... ]
> @@ -83,6 +83,9 @@
> # Run a specific test from the test suite
> # sudo ./test_xsk.sh -t TEST_NAME
> #
> +# Run with a max_skb_frags limit other than the host's
> +# sudo ./test_xsk.sh -f FRAGS
> +#
[Severity: Low]
This isn't a regression in the mechanics, but is this description accurate?
Nothing writes net.core.max_skb_frags. The -f value is only forwarded to
xskxceiver, where testapp_too_many_frags() uses it to size the packet
stream, while the kernel keeps enforcing its real limit.
If FRAGS differs from the host's sysctl, can the too-many-frags test build
the wrong fragment count and fail or give misleading results? Should the
comment and the commit message wording say that -f only changes the value
the test assumes?
[ ... ]
> @@ -162,8 +166,21 @@ if [ ! -z $ETH ]; then
> VETH1=${ETH}
> else
> validate_root_exec
> - validate_veth_support ${VETH0}
> validate_ip_utility
> + # The host configures and talks on any new interface in its namespace
> + if [ -z "$XSK_NETNS" ]; then
> + XSK_NETNS=xsk${VETH0_POSTFIX}
> + ip netns add ${XSK_NETNS} || test_exit $ksft_fail
[Severity: Low]
When "ip netns add" fails (no CONFIG_NET_NS, a container without
/run/netns, or a collision on the random suffix), this now reports a
failure through test_exit $ksft_fail. Before the patch the test simply ran
in the current namespace.
The script uses ksft_skip for other missing prerequisites. Should this case
skip rather than fail?
> + trap "ip netns del ${XSK_NETNS}" EXIT
> + # The max_skb_frags sysctl has no file outside the initial namespace
> + [ -n "$FRAGS" ] || FRAGS=$(cat /proc/sys/net/core/max_skb_frags)
> + XSK_NETNS=${XSK_NETNS} ip netns exec ${XSK_NETNS} bash "$0" "$@" -f ${FRAGS}
[Severity: Low]
If /proc/sys/net/core/max_skb_frags is missing or unreadable (for example
on a kernel without the sysctl), the failed cat is not checked and FRAGS
stays empty.
The unquoted ${FRAGS} then expands to nothing, so the child is started
with a trailing "-f" and no argument. The child's getopts "vi:dm:lt:f:h"
prints "option requires an argument -- f". The case statement has no "?"
arm, so the run continues with FRAGS empty and xskxceiver falls back to 17.
Can the "-f ${FRAGS}" be appended only when FRAGS is non-empty?
> + retval=$?
> + # The pair stays up for a run by hand
> + [[ $debug -eq 1 ]] && trap - EXIT && echo "ip netns exec ${XSK_NETNS}"
In the -d case, the hint printed is only "ip netns exec ${XSK_NETNS}",
which is not a complete command. The inner script has already exited and
the namespace and veth pair are left behind. Could the hint include a
command to run, or the namespace name in a more useful form?
> + exit $retval
> + fi
> + validate_veth_support ${VETH0}
> setup_vethPairs
[ ... ]
> diff --git a/tools/testing/selftests/bpf/xskxceiver.c b/tools/testing/selftests/bpf/xskxceiver.c
> --- a/tools/testing/selftests/bpf/xskxceiver.c
> +++ b/tools/testing/selftests/bpf/xskxceiver.c
[ ... ]
> @@ -437,6 +440,12 @@ static void parse_command_line(struct ifobject *ifobj_tx, struct ifobject *ifobj
> if (errno)
> print_usage(argv);
> break;
> + case 'f':
> + errno = 0;
> + opt_max_skb_frags = strtoul(optarg, NULL, 0);
> + if (errno || !opt_max_skb_frags)
> + print_usage(argv);
> + break;
[Severity: Low]
Can this wrap opt_max_skb_frags? strtoul() returns an unsigned long that
is stored in a u32, and only errno and zero are checked. There is no upper
bound and no endptr check, so input such as "17abc" is accepted too.
> @@ -2271,7 +2280,7 @@ static int testapp_too_many_frags(struct test_spec *test)
> if (test->mode == TEST_MODE_ZC) {
> max_frags = test->ifobj_tx->xdp_zc_max_segs;
> } else {
> - max_frags = get_max_skb_frags();
> + max_frags = opt_max_skb_frags ? : get_max_skb_frags();
> if (!max_frags) {
> ksft_print_msg("Couldn't retrieve MAX_SKB_FRAGS from system, using default (17) value\n");
> max_frags = 17;
> }
> max_frags += 1;
Can this overflow the pkts[] allocation in testapp_too_many_frags()? With
-f 4294967295, max_frags += 1 wraps to 0. With a value around 0x80000000,
the size in calloc(2 * max_frags + 2, sizeof(struct pkt)) wraps to a tiny
value.
The later loops that fill pkts[i] up to 2 * max_frags + 1 would then write
far beyond the allocation. Before this patch the value came only from the
range-limited sysctl. test_xsk.sh -f forwards the value verbatim to
xskxceiver, so both entry points can reach this. Should -f be validated
with an upper bound and an endptr check?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261020041218.1659392-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%]
* [Devel] [PATCH hci-8.1 2/2] ms/kbuild: move GCOV flags to scripts/Makefile.gcov
@ 2026-10-19 18:26 10% ` Konstantin Khorenko
0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-10-19 18:26 UTC (permalink / raw)
To: devel, Konstantin Khorenko
* split the CFLAGS_GCOV block out of the top-level Makefile into
scripts/Makefile.gcov
* include it via include-$(CONFIG_GCOV_KERNEL) like neighbour
KASAN/KCOV/UBSAN makefiles do
* keep the include before scripts/Makefile.gcc-plugins: the try-run
must not get -fplugin= options, plugins do not exist on clean builds
CFLAGS_GCOV is no longer defined when CONFIG_GCOV_KERNEL is off:
* its user in scripts/Makefile.lib is guarded by the CONFIG_GCOV_KERNEL
* in i915/xe header an empty value is harmless
The try-run compiler invocations also stop running on builds without
GCOV.
Link: https://lore.kernel.org/20261001-b4-prep-gcov-v1-2-794792e06f24@virtuozzo.com
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Suggested-by: Peter Oberparleiter <oberpar@linux.ibm.com>
Cc: Nathan Chancellor <nathan@kernel.org>
Cc: Nicolas Schier <nsc@kernel.org>
Cc: Masahiro Yamada <masahiroy@kernel.org>
Cc: Arnd Bergmann <arnd@arndb.de>
Cc: Mikhail Zaslonko <zaslonko@linux.ibm.com>
(cherry picked from commit f541906e89f9d08ef29f058b6cef1db071cd8163)
Picked from the akpm/mm tree, branch mm-nonmm-stable: not in Linus's
tree yet.
Conflict in Makefile: the CFLAGS_GCOV block sits next to different
lines in this tree. Removed the block and kept the local context.
https://virtuozzo.atlassian.net/browse/VSTOR-148131
Feature: fix ms/gcov
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
MAINTAINERS | 1 +
Makefile | 22 +---------------------
scripts/Makefile.gcov | 21 +++++++++++++++++++++
3 files changed, 23 insertions(+), 21 deletions(-)
diff --git a/MAINTAINERS b/MAINTAINERS
index 9523da3c767cb..f0f955907fd7d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -9535,6 +9535,7 @@ M: Peter Oberparleiter <oberpar@linux.ibm.com>
S: Maintained
F: Documentation/dev-tools/gcov.rst
F: kernel/gcov/
+F: scripts/Makefile.gcov
GDB KERNEL DEBUGGING HELPER SCRIPTS
M: Jan Kiszka <jan.kiszka@siemens.com>
diff --git a/Makefile b/Makefile
index 3524889b5bb5f..7c8f8e9b9be99 100644
--- a/Makefile
+++ b/Makefile
@@ -1033,27 +1033,6 @@ ifdef CONFIG_CC_IS_GCC
KBUILD_CFLAGS += -fconserve-stack
endif
-CFLAGS_GCOV := -fprofile-arcs -ftest-coverage
-ifdef CONFIG_CC_IS_GCC
-CFLAGS_GCOV += -fno-tree-loop-im
-# Use atomic counter updates to avoid concurrent-access crashes in GCOV.
-# Only enable if -fprofile-update=prefer-atomic does not introduce new
-# undefined symbols (e.g. libatomic calls that the kernel cannot link).
-CFLAGS_GCOV += $(call try-run,\
- echo 'long long x; void f(void){x++;}' | \
- $(CC) $(KBUILD_CPPFLAGS) $(KBUILD_CFLAGS) -w $(CFLAGS_GCOV) \
- -x c - -c -o "$$TMP.base" && \
- echo 'long long x; void f(void){x++;}' | \
- $(CC) $(KBUILD_CPPFLAGS) $(KBUILD_CFLAGS) -w $(CFLAGS_GCOV) \
- -fprofile-update=prefer-atomic \
- -x c - -c -o "$$TMP" && \
- $(NM) "$$TMP.base" | grep ' U ' > "$$TMP.ubase" || true ; \
- $(NM) "$$TMP" | grep ' U ' > "$$TMP.utest" || true ; \
- cmp -s "$$TMP.ubase" "$$TMP.utest",\
- -fprofile-update=prefer-atomic)
-endif
-export CFLAGS_GCOV
-
# change __FILE__ to the relative path from the srctree
KBUILD_CPPFLAGS += $(call cc-option,-fmacro-prefix-map=$(srctree)/=)
@@ -1066,6 +1045,7 @@ include-$(CONFIG_KCSAN) += scripts/Makefile.kcsan
include-$(CONFIG_KMSAN) += scripts/Makefile.kmsan
include-$(CONFIG_UBSAN) += scripts/Makefile.ubsan
include-$(CONFIG_KCOV) += scripts/Makefile.kcov
+include-$(CONFIG_GCOV_KERNEL) += scripts/Makefile.gcov
include-$(CONFIG_RANDSTRUCT) += scripts/Makefile.randstruct
include-$(CONFIG_AUTOFDO_CLANG) += scripts/Makefile.autofdo
include-$(CONFIG_PROPELLER_CLANG) += scripts/Makefile.propeller
diff --git a/scripts/Makefile.gcov b/scripts/Makefile.gcov
new file mode 100644
index 0000000000000..16c5b876440c2
--- /dev/null
+++ b/scripts/Makefile.gcov
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: GPL-2.0-only
+CFLAGS_GCOV := -fprofile-arcs -ftest-coverage
+ifdef CONFIG_CC_IS_GCC
+CFLAGS_GCOV += -fno-tree-loop-im
+# Use atomic counter updates to avoid concurrent-access crashes in GCOV.
+# Only enable if -fprofile-update=prefer-atomic does not introduce new
+# undefined symbols (e.g. libatomic calls that the kernel cannot link).
+CFLAGS_GCOV += $(call try-run,\
+ echo 'long long x; void f(void){x++;}' | \
+ $(CC) $(KBUILD_CPPFLAGS) $(KBUILD_CFLAGS) -w $(CFLAGS_GCOV) \
+ -x c - -c -o "$$TMP.base" && \
+ echo 'long long x; void f(void){x++;}' | \
+ $(CC) $(KBUILD_CPPFLAGS) $(KBUILD_CFLAGS) -w $(CFLAGS_GCOV) \
+ -fprofile-update=prefer-atomic \
+ -x c - -c -o "$$TMP" && \
+ $(NM) "$$TMP.base" | grep ' U ' > "$$TMP.ubase" || true ; \
+ $(NM) "$$TMP" | grep ' U ' > "$$TMP.utest" || true ; \
+ cmp -s "$$TMP.ubase" "$$TMP.utest",\
+ -fprofile-update=prefer-atomic)
+endif
+export CFLAGS_GCOV
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 10%]
* Re: [Devel] [PATCH hci-8.1 v2 4/6] timens: freeze the offsets on the first join without the vDSO too
@ 2026-10-19 0:14 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-19 0:14 UTC (permalink / raw)
To: Eva Kurchatova, khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] timens: frozen_offsets published without release/acquire after vvar page setup
--
commit 594d8fa1aae41e5f90c67ea3a3e7ad325b6b1173
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
timens: freeze the offsets on the first join without the vDSO too
This patch moves the freezing of time namespace offsets out of the vDSO
page setup code and into timens_commit(). Kernels without a generic vDSO
(UML) now freeze the offsets on the first join as well. The vDSO page fill
and the vvar zap are now called from timens_commit() through
timens_vdso_setup_vvar_page() and timens_vdso_join().
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148112
> diff --git a/kernel/time/namespace.c b/kernel/time/namespace.c
> --- a/kernel/time/namespace.c
> +++ b/kernel/time/namespace.c
[ ... ]
> +/*
> + * The offsets are read without a lock once a task runs in the namespace,
> + * so the first task to join freezes them, and sets up the vDSO page from
> + * them where there is one.
> + */
> +static void timens_freeze_offsets(struct time_namespace *ns)
> +{
> + if (ns == &init_time_ns)
> + return;
> +
> + /* Fast-path, taken by every task in namespace except the first. */
> + if (likely(ns->frozen_offsets))
> + return;
> +
> + guard(mutex)(&timens_offset_lock);
> + if (ns->frozen_offsets)
> + return;
> +
> + timens_vdso_setup_vvar_page(ns);
> + ns->frozen_offsets = true;
> +}
[Severity: Low]
This is speculative, and I could not construct a concrete failing
interleaving. Is frozen_offsets published safely here?
The first joiner stores the vvar page contents in
timens_vdso_setup_vvar_page() and then does a plain store:
ns->frozen_offsets = true;
A second task in another thread group calling timens_commit() takes the
unlocked fast path:
if (likely(ns->frozen_offsets))
return;
The mutex only orders lock holders against each other, so this load is not
ordered against the vvar page stores (seq, clock_mode, offsets) made by the
first joiner.
The comment on timens_vdso_setup_vvar_page() says setup happens "before any
task reads", but no release/acquire pairing enforces that. On a weakly
ordered CPU, could the second joiner see frozen_offsets == true and then
have its userspace read a stale or zeroed vvar page?
timens_vdso_join() takes mmap_read_lock only after the flag load, and
find_timens_vvar_page() does not re-check the flag, so neither seems to
supply acquire semantics for the earlier load. In practice the window looks
very narrow, and x86 and UML are strongly ordered.
The flag is also read and written with plain accesses, which KCSAN could
report as a data race.
Would smp_store_release(&ns->frozen_offsets, true) in the slow path and
smp_load_acquire(&ns->frozen_offsets) in the fast path, with READ_ONCE() for
the check under the mutex, be appropriate?
[ ... ]
> diff --git a/kernel/time/namespace_vdso.c b/kernel/time/namespace_vdso.c
> --- a/kernel/time/namespace_vdso.c
> +++ b/kernel/time/namespace_vdso.c
[ ... ]
> +/* Called once per namespace, under timens_offset_lock, before any task reads */
> +void timens_vdso_setup_vvar_page(struct time_namespace *ns)
> {
> + struct vdso_time_data *vdata = page_address(ns->vvar_page);
> + struct vdso_clock *vc = vdata->clock_data;
> unsigned int i;
>
> for (i = 0; i < CS_BASES; i++)
> timens_setup_vdso_clock_data(&vc[i], ns);
> }
These are the vvar page stores that need to be visible before the flag is
observed by a fast-path joiner in timens_freeze_offsets().
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261019000649.1371025-1-eva.kurchatova@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 1/6] ms/vdso: Drop Kconfig GENERIC_VDSO_TIME_NS
@ 2026-10-19 0:12 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-19 0:12 UTC (permalink / raw)
To: khorenko, Eva Kurchatova; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] kconfig: stale CONFIG_GENERIC_VDSO_TIME_NS=y left in shipped x86_64 configs
--
commit 58fc0fc4b4942154ef41adeca15ad8a7447eb83c
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
ms/vdso: Drop Kconfig GENERIC_VDSO_TIME_NS
This commit is a cherry-pick of upstream bad53ae2dc42. It removes the
GENERIC_VDSO_TIME_NS Kconfig symbol and every select of it. TIME_NS now
depends on GENERIC_GETTIMEOFDAY instead.
Link: https://lore.kernel.org/all/20250826-vdso-cleanups-v1-10-d9b65750e49f@linutronix.de
> diff --git a/lib/vdso/Kconfig b/lib/vdso/Kconfig
> index 45df764b49ad6..a1cc90133cb9e 100644
> --- a/lib/vdso/Kconfig
> +++ b/lib/vdso/Kconfig
> @@ -24,12 +24,6 @@ config GENERIC_COMPAT_VDSO
> help
> This config option enables the compat VDSO layer.
>
> -config GENERIC_VDSO_TIME_NS
> - bool
> - help
> - Selected by architectures which support time namespaces in the
> - VDSO
> -
> config GENERIC_VDSO_OVERFLOW_PROTECT
> bool
> help
[Severity: Low]
This patch removes config GENERIC_VDSO_TIME_NS, but the shipped x86_64
configs still set it. Should those two entries be dropped too?
configs/kernel-6.12.0-x86_64.config:
CONFIG_GENERIC_VDSO_TIME_NS=y
configs/kernel-6.12.0-x86_64-debug.config:
CONFIG_GENERIC_VDSO_TIME_NS=y
A git grep at the end of the series still finds the symbol in both files.
The series does remove it from config.vz10.minimal, so only these two files
remain.
Kconfig ignores unknown symbols during oldconfig and olddefconfig, so I
don't expect a build or runtime effect. A config-sync or diff check may
report the two lines as stale.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261019000649.1371025-1-eva.kurchatova@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] ms/vdso/timens: Move functions to new file
2026-10-19 0:05 3% ` [Devel] [PATCH hci-8.1 v2 2/6] ms/vdso/timens: Move functions to new file Eva Kurchatova
@ 2026-10-19 0:12 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-19 0:12 UTC (permalink / raw)
To: Eva Kurchatova, khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] timens: stale extern offset_lock left in time_namespace.h after rename
--
commit 10270c644c766f4ad4754e8bd546e29088de7a79
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
ms/vdso/timens: Move functions to new file
This commit moves the glue functions between time namespaces and the vDSO
into a new file, kernel/time/namespace_vdso.c. It renames offset_lock to
timens_offset_lock, declared in a new private header, and converts the
mutex and mmap_read_lock() users to guard().
Link: https://patch.msgid.link/20260326-vdso-timens-decoupling-v2-1-c82693a7775f@linutronix.de
> diff --git a/include/linux/time_namespace.h b/include/linux/time_namespace.h
> index 3d3b529c5a98b..23384eafebfb5 100644
> --- a/include/linux/time_namespace.h
> +++ b/include/linux/time_namespace.h
> @@ -35,8 +35,6 @@ extern struct time_namespace init_time_ns;
> extern struct mutex offset_lock;
[Severity: Low]
This isn't a bug, but is this declaration now stale?
The mutex is renamed to timens_offset_lock, defined in
kernel/time/namespace.c:
DEFINE_MUTEX(timens_offset_lock);
and declared in the new private header namespace_internal.h:
extern struct mutex timens_offset_lock;
Nothing defines or references offset_lock any more, but this public
declaration is left in include/linux/time_namespace.h, while the
vdso_join_timens() declarations just below it were removed.
Should "extern struct mutex offset_lock;" be dropped from the header as well?
>
> #ifdef CONFIG_TIME_NS
> -extern int vdso_join_timens(struct task_struct *task,
> - struct time_namespace *ns);
> extern void timens_commit(struct task_struct *tsk, struct time_namespace *ns);
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261019000649.1371025-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%]
* [Devel] [PATCH hci-8.1 v2 2/6] ms/vdso/timens: Move functions to new file
@ 2026-10-19 0:05 3% ` Eva Kurchatova
2026-10-19 0:12 4% ` no-reply
2 siblings, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-10-19 0:05 UTC (permalink / raw)
To: khorenko; +Cc: devel
As a preparation of the untangling of time namespaces and the vDSO, move
the glue functions between those subsystems into a new file.
While at it, switch the mutex lock and mmap_read_lock() in the vDSO
namespace code to guard().
Signed-off-by: Thomas Weißschuh <thomas.weissschuh@linutronix.de>
Signed-off-by: Thomas Gleixner <tglx@kernel.org>
Link: https://patch.msgid.link/20260326-vdso-timens-decoupling-v2-1-c82693a7775f@linutronix.de
(cherry picked from commit 5dc9cf835aba73c882348aa4f99be83b6e45ad9b)
https://virtuozzo.atlassian.net/browse/VSTOR-148112
Feature: fix ms/timens
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
MAINTAINERS | 2 +
include/linux/time_namespace.h | 8 --
kernel/time/Makefile | 2 +-
kernel/time/namespace.c | 119 +-------------------------
kernel/time/namespace_internal.h | 13 +++
kernel/time/namespace_vdso.c | 141 +++++++++++++++++++++++++++++++
lib/vdso/datastore.c | 25 ------
7 files changed, 161 insertions(+), 149 deletions(-)
create mode 100644 kernel/time/namespace_internal.h
create mode 100644 kernel/time/namespace_vdso.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 9523da3c767c..ac649ee0c43c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -9683,6 +9683,7 @@ S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/tip/tip.git timers/vdso
F: include/asm-generic/vdso/vsyscall.h
F: include/vdso/
+F: kernel/time/namespace_vdso.c
F: kernel/time/vsyscall.c
F: lib/vdso/
F: tools/testing/selftests/vDSO/
@@ -18576,6 +18577,7 @@ F: include/trace/events/timer*
F: kernel/time/itimer.c
F: kernel/time/posix-*
F: kernel/time/namespace.c
+F: kernel/time/namespace_vdso.c
POWER MANAGEMENT CORE
M: "Rafael J. Wysocki" <rafael@kernel.org>
diff --git a/include/linux/time_namespace.h b/include/linux/time_namespace.h
index 3d3b529c5a98..23384eafebfb 100644
--- a/include/linux/time_namespace.h
+++ b/include/linux/time_namespace.h
@@ -35,8 +35,6 @@ extern struct time_namespace init_time_ns;
extern struct mutex offset_lock;
#ifdef CONFIG_TIME_NS
-extern int vdso_join_timens(struct task_struct *task,
- struct time_namespace *ns);
extern void timens_commit(struct task_struct *tsk, struct time_namespace *ns);
static inline struct time_namespace *get_time_ns(struct time_namespace *ns)
@@ -110,12 +108,6 @@ static inline ktime_t timens_ktime_to_host(clockid_t clockid, ktime_t tim)
}
#else
-static inline int vdso_join_timens(struct task_struct *task,
- struct time_namespace *ns)
-{
- return 0;
-}
-
static inline void timens_commit(struct task_struct *tsk,
struct time_namespace *ns)
{
diff --git a/kernel/time/Makefile b/kernel/time/Makefile
index fe0ae82124fe..494bd4e41781 100644
--- a/kernel/time/Makefile
+++ b/kernel/time/Makefile
@@ -23,6 +23,6 @@ endif
obj-$(CONFIG_HAVE_GENERIC_VDSO) += vsyscall.o
obj-$(CONFIG_DEBUG_FS) += timekeeping_debug.o
obj-$(CONFIG_TEST_UDELAY) += test_udelay.o
-obj-$(CONFIG_TIME_NS) += namespace.o
+obj-$(CONFIG_TIME_NS) += namespace.o namespace_vdso.o
obj-$(CONFIG_TEST_CLOCKSOURCE_WATCHDOG) += clocksource-wdtest.o
obj-$(CONFIG_TIME_KUNIT_TEST) += time_test.o
diff --git a/kernel/time/namespace.c b/kernel/time/namespace.c
index e85f947d4063..c82dc9a5d1de 100644
--- a/kernel/time/namespace.c
+++ b/kernel/time/namespace.c
@@ -18,7 +18,7 @@
#include <linux/err.h>
#include <linux/mm.h>
-#include <vdso/datapage.h>
+#include "namespace_internal.h"
ktime_t do_timens_ktime_to_host(clockid_t clockid, ktime_t tim,
struct timens_offsets *ns_offsets)
@@ -139,112 +139,7 @@ struct time_namespace *copy_time_ns(unsigned long flags,
return clone_time_ns(user_ns, old_ns);
}
-static struct timens_offset offset_from_ts(struct timespec64 off)
-{
- struct timens_offset ret;
-
- ret.sec = off.tv_sec;
- ret.nsec = off.tv_nsec;
-
- return ret;
-}
-
-/*
- * A time namespace VVAR page has the same layout as the VVAR page which
- * contains the system wide VDSO data.
- *
- * For a normal task the VVAR pages are installed in the normal ordering:
- * VVAR
- * PVCLOCK
- * HVCLOCK
- * TIMENS <- Not really required
- *
- * Now for a timens task the pages are installed in the following order:
- * TIMENS
- * PVCLOCK
- * HVCLOCK
- * VVAR
- *
- * The check for vdso_clock->clock_mode is in the unlikely path of
- * the seq begin magic. So for the non-timens case most of the time
- * 'seq' is even, so the branch is not taken.
- *
- * If 'seq' is odd, i.e. a concurrent update is in progress, the extra check
- * for vdso_clock->clock_mode is a non-issue. The task is spin waiting for the
- * update to finish and for 'seq' to become even anyway.
- *
- * Timens page has vdso_clock->clock_mode set to VDSO_CLOCKMODE_TIMENS which
- * enforces the time namespace handling path.
- */
-static void timens_setup_vdso_clock_data(struct vdso_clock *vc,
- struct time_namespace *ns)
-{
- struct timens_offset *offset = vc->offset;
- struct timens_offset monotonic = offset_from_ts(ns->offsets.monotonic);
- struct timens_offset boottime = offset_from_ts(ns->offsets.boottime);
-
- vc->seq = 1;
- vc->clock_mode = VDSO_CLOCKMODE_TIMENS;
- offset[CLOCK_MONOTONIC] = monotonic;
- offset[CLOCK_MONOTONIC_RAW] = monotonic;
- offset[CLOCK_MONOTONIC_COARSE] = monotonic;
- offset[CLOCK_BOOTTIME] = boottime;
- offset[CLOCK_BOOTTIME_ALARM] = boottime;
-}
-
-struct page *find_timens_vvar_page(struct vm_area_struct *vma)
-{
- if (likely(vma->vm_mm == current->mm))
- return current->nsproxy->time_ns->vvar_page;
-
- /*
- * VM_PFNMAP | VM_IO protect .fault() handler from being called
- * through interfaces like /proc/$pid/mem or
- * process_vm_{readv,writev}() as long as there's no .access()
- * in special_mapping_vmops().
- * For more details check_vma_flags() and __access_remote_vm()
- */
-
- WARN(1, "vvar_page accessed remotely");
-
- return NULL;
-}
-
-/*
- * Protects possibly multiple offsets writers racing each other
- * and tasks entering the namespace.
- */
-DEFINE_MUTEX(offset_lock);
-
-static void timens_set_vvar_page(struct task_struct *task,
- struct time_namespace *ns)
-{
- struct vdso_time_data *vdata;
- struct vdso_clock *vc;
- unsigned int i;
-
- if (ns == &init_time_ns)
- return;
-
- /* Fast-path, taken by every task in namespace except the first. */
- if (likely(ns->frozen_offsets))
- return;
-
- mutex_lock(&offset_lock);
- /* Nothing to-do: vvar_page has been already initialized. */
- if (ns->frozen_offsets)
- goto out;
-
- ns->frozen_offsets = true;
- vdata = page_address(ns->vvar_page);
- vc = vdata->clock_data;
-
- for (i = 0; i < CS_BASES; i++)
- timens_setup_vdso_clock_data(&vc[i], ns);
-
-out:
- mutex_unlock(&offset_lock);
-}
+DEFINE_MUTEX(timens_offset_lock);
void free_time_ns(struct time_namespace *ns)
{
@@ -297,12 +192,6 @@ static void timens_put(struct ns_common *ns)
put_time_ns(to_time_ns(ns));
}
-void timens_commit(struct task_struct *tsk, struct time_namespace *ns)
-{
- timens_set_vvar_page(tsk, ns);
- vdso_join_timens(tsk, ns);
-}
-
static int timens_install(struct nsset *nsset, struct ns_common *new)
{
struct nsproxy *nsproxy = nsset->nsproxy;
@@ -427,7 +316,7 @@ int proc_timens_set_offset(struct file *file, struct task_struct *p,
goto out;
}
- mutex_lock(&offset_lock);
+ mutex_lock(&timens_offset_lock);
if (time_ns->frozen_offsets) {
err = -EACCES;
goto out_unlock;
@@ -452,7 +341,7 @@ int proc_timens_set_offset(struct file *file, struct task_struct *p,
}
out_unlock:
- mutex_unlock(&offset_lock);
+ mutex_unlock(&timens_offset_lock);
out:
put_time_ns(time_ns);
diff --git a/kernel/time/namespace_internal.h b/kernel/time/namespace_internal.h
new file mode 100644
index 000000000000..e85da11abb4d
--- /dev/null
+++ b/kernel/time/namespace_internal.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _TIME_NAMESPACE_INTERNAL_H
+#define _TIME_NAMESPACE_INTERNAL_H
+
+#include <linux/mutex.h>
+
+/*
+ * Protects possibly multiple offsets writers racing each other
+ * and tasks entering the namespace.
+ */
+extern struct mutex timens_offset_lock;
+
+#endif /* _TIME_NAMESPACE_INTERNAL_H */
diff --git a/kernel/time/namespace_vdso.c b/kernel/time/namespace_vdso.c
new file mode 100644
index 000000000000..558bf5208588
--- /dev/null
+++ b/kernel/time/namespace_vdso.c
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Author: Andrei Vagin <avagin@openvz.org>
+ * Author: Dmitry Safonov <dima@arista.com>
+ */
+
+#include <linux/cleanup.h>
+#include <linux/mm.h>
+#include <linux/time_namespace.h>
+#include <linux/time.h>
+#include <linux/vdso_datastore.h>
+
+#include <vdso/clocksource.h>
+#include <vdso/datapage.h>
+
+#include "namespace_internal.h"
+
+static struct timens_offset offset_from_ts(struct timespec64 off)
+{
+ struct timens_offset ret;
+
+ ret.sec = off.tv_sec;
+ ret.nsec = off.tv_nsec;
+
+ return ret;
+}
+
+/*
+ * A time namespace VVAR page has the same layout as the VVAR page which
+ * contains the system wide VDSO data.
+ *
+ * For a normal task the VVAR pages are installed in the normal ordering:
+ * VVAR
+ * PVCLOCK
+ * HVCLOCK
+ * TIMENS <- Not really required
+ *
+ * Now for a timens task the pages are installed in the following order:
+ * TIMENS
+ * PVCLOCK
+ * HVCLOCK
+ * VVAR
+ *
+ * The check for vdso_clock->clock_mode is in the unlikely path of
+ * the seq begin magic. So for the non-timens case most of the time
+ * 'seq' is even, so the branch is not taken.
+ *
+ * If 'seq' is odd, i.e. a concurrent update is in progress, the extra check
+ * for vdso_clock->clock_mode is a non-issue. The task is spin waiting for the
+ * update to finish and for 'seq' to become even anyway.
+ *
+ * Timens page has vdso_clock->clock_mode set to VDSO_CLOCKMODE_TIMENS which
+ * enforces the time namespace handling path.
+ */
+static void timens_setup_vdso_clock_data(struct vdso_clock *vc,
+ struct time_namespace *ns)
+{
+ struct timens_offset *offset = vc->offset;
+ struct timens_offset monotonic = offset_from_ts(ns->offsets.monotonic);
+ struct timens_offset boottime = offset_from_ts(ns->offsets.boottime);
+
+ vc->seq = 1;
+ vc->clock_mode = VDSO_CLOCKMODE_TIMENS;
+ offset[CLOCK_MONOTONIC] = monotonic;
+ offset[CLOCK_MONOTONIC_RAW] = monotonic;
+ offset[CLOCK_MONOTONIC_COARSE] = monotonic;
+ offset[CLOCK_BOOTTIME] = boottime;
+ offset[CLOCK_BOOTTIME_ALARM] = boottime;
+}
+
+struct page *find_timens_vvar_page(struct vm_area_struct *vma)
+{
+ if (likely(vma->vm_mm == current->mm))
+ return current->nsproxy->time_ns->vvar_page;
+
+ /*
+ * VM_PFNMAP | VM_IO protect .fault() handler from being called
+ * through interfaces like /proc/$pid/mem or
+ * process_vm_{readv,writev}() as long as there's no .access()
+ * in special_mapping_vmops().
+ * For more details check_vma_flags() and __access_remote_vm()
+ */
+
+ WARN(1, "vvar_page accessed remotely");
+
+ return NULL;
+}
+
+static void timens_set_vvar_page(struct task_struct *task,
+ struct time_namespace *ns)
+{
+ struct vdso_time_data *vdata;
+ struct vdso_clock *vc;
+ unsigned int i;
+
+ if (ns == &init_time_ns)
+ return;
+
+ /* Fast-path, taken by every task in namespace except the first. */
+ if (likely(ns->frozen_offsets))
+ return;
+
+ guard(mutex)(&timens_offset_lock);
+ /* Nothing to-do: vvar_page has been already initialized. */
+ if (ns->frozen_offsets)
+ return;
+
+ ns->frozen_offsets = true;
+ vdata = page_address(ns->vvar_page);
+ vc = vdata->clock_data;
+
+ for (i = 0; i < CS_BASES; i++)
+ timens_setup_vdso_clock_data(&vc[i], ns);
+}
+
+/*
+ * The vvar page layout depends on whether a task belongs to the root or
+ * non-root time namespace. Whenever a task changes its namespace, the VVAR
+ * page tables are cleared and then they will be re-faulted with a
+ * corresponding layout.
+ * See also the comment near timens_setup_vdso_clock_data() for details.
+ */
+static int vdso_join_timens(struct task_struct *task, struct time_namespace *ns)
+{
+ struct mm_struct *mm = task->mm;
+ struct vm_area_struct *vma;
+ VMA_ITERATOR(vmi, mm, 0);
+
+ guard(mmap_read_lock)(mm);
+ for_each_vma(vmi, vma) {
+ if (vma_is_special_mapping(vma, &vdso_vvar_mapping))
+ zap_vma_pages(vma);
+ }
+ return 0;
+}
+
+void timens_commit(struct task_struct *tsk, struct time_namespace *ns)
+{
+ timens_set_vvar_page(tsk, ns);
+ vdso_join_timens(tsk, ns);
+}
diff --git a/lib/vdso/datastore.c b/lib/vdso/datastore.c
index 46192c1d8542..7713fd392dd0 100644
--- a/lib/vdso/datastore.c
+++ b/lib/vdso/datastore.c
@@ -102,28 +102,3 @@ struct vm_area_struct *vdso_install_vvar_mapping(struct mm_struct *mm, unsigned
VM_READ | VM_MAYREAD | VM_IO | VM_DONTDUMP | VM_PFNMAP,
&vdso_vvar_mapping);
}
-
-#ifdef CONFIG_TIME_NS
-/*
- * The vvar page layout depends on whether a task belongs to the root or
- * non-root time namespace. Whenever a task changes its namespace, the VVAR
- * page tables are cleared and then they will be re-faulted with a
- * corresponding layout.
- * See also the comment near timens_setup_vdso_clock_data() for details.
- */
-int vdso_join_timens(struct task_struct *task, struct time_namespace *ns)
-{
- struct mm_struct *mm = task->mm;
- struct vm_area_struct *vma;
- VMA_ITERATOR(vmi, mm, 0);
-
- mmap_read_lock(mm);
- for_each_vma(vmi, vma) {
- if (vma_is_special_mapping(vma, &vdso_vvar_mapping))
- zap_vma_pages(vma);
- }
- mmap_read_unlock(mm);
-
- return 0;
-}
-#endif
--
2.56.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 1/2] ms/checkpatch: add support for Assisted-by tag
@ 2026-10-16 21:37 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 21:37 UTC (permalink / raw)
To: Konstantin Khorenko, devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] checkpatch: Assisted-by block's unconditional next skips later per-line checks
--
commit 5a811b60fe8be1a0a89ee89ba84a4ed4223a05c3
Author: Sasha Levin <sashal@kernel.org>
ms/checkpatch: add support for Assisted-by tag
This commit teaches checkpatch.pl to recognize the Assisted-by tag. It adds
the tag to $signature_tags and skips email validation for Assisted-by lines.
It also warns when the value doesn't look like AGENT_NAME:MODEL_VERSION.
Link: https://lkml.kernel.org/r/20260311215818.518930-1-sashal@kernel.org
> diff --git a/scripts/checkpatch.pl b/scripts/checkpatch.pl
> --- a/scripts/checkpatch.pl
> +++ b/scripts/checkpatch.pl
[ ... ]
> @@ -3072,6 +3073,15 @@ sub process {
> }
> }
>
> + # Assisted-by uses AGENT_NAME:MODEL_VERSION format, not email
> + if ($sign_off =~ /^Assisted-by:/i) {
> + if ($email !~ /^\S+:\S+/) {
> + WARN("BAD_SIGN_OFF",
> + "Assisted-by expects 'AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2]' format\n" . $herecurr);
> + }
> + next;
> + }
> +
[Severity: Low]
Does the unconditional next; here skip checks that should still run for
Assisted-by lines?
In process(), this block sits inside the per-line loop. For a line such as
"Assisted-by: X", $sign_off is "Assisted-by:", so the new block ends with
next; and jumps to the next input line.
That skips the "# Check for duplicate signatures" code that records
$signatures{$sig_nospace}, so duplicate Assisted-by lines would never be
reported as "Duplicate signature". It also skips the other per-line checks
that follow the signature block, such as the repeated-word check.
The follow-up patch 7d898d23 ("ms/checkpatch.pl: adapt to new Assisted-by:
format") changes only the value check, to an empty-value check with
different warning text. It keeps the unconditional next;, so the final
series state still skips those checks.
Would an if/else around the email validation, instead of next;, avoid this?
> my ($email_name, $name_comment, $email_address, $comment) = parse_email($email);
> my $suggested_email = format_email(($email_name, $name_comment, $email_address, $comment));
> if ($suggested_email eq "") {
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-checkpatch-v1-0-3aceda8d9600@virtuozzo.com?part=1
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* [Devel] [PATCH hci-8.1 v2 2/2] selftests/mm: check MADV_DOEXEC on VFIO BARs and other PFN mappings
@ 2026-10-16 21:35 5% ` Konstantin Khorenko
0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-10-16 21:35 UTC (permalink / raw)
To: Andrey Drobyshev, Pavel Tikhomirov, Vasileios Almpanis; +Cc: devel
Add three cases to doexec:
- A shared mapping of /dev/mem is a PFN mapping of a driver other than
VFIO, and MADV_DOEXEC must refuse it.
- BAR 0 of the PCI device named by DOEXEC_VFIO_DEVICE, which has to be
bound to vfio-pci, is mapped through its VFIO device fd at an address
aligned to the mapped size, filled with a pattern and marked, and the
process execs. The new image must find the mapping at the same
address with the pfn and shared flags in smaps and the pattern in
place. It then execs again without marking, and that image must not
find the mapping.
- The same with the BAR mapped one page off that alignment. The aligned
mapping gets huge PFN entries, which the copy leaves to be faulted in
again in the new image; this one gets PTEs, which copy_page_range()
duplicates.
A group that has only a noiommu node is set up with the no-IOMMU
container. Without the variable both VFIO cases are skipped. The test
overwrites BAR 0, so the variable has to name a spare device whose BAR 0
is memory, such as an unused VGA frame buffer.
https://virtuozzo.atlassian.net/browse/VSTOR-150233
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 157 +++++++++++++++++++++++++++++++++++-
1 file changed, 155 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index 97e045ba678e8..87a9c9493fe35 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -4,6 +4,8 @@
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
+#include <limits.h>
+#include <linux/vfio.h>
#include <linux/vhost.h>
#include <poll.h>
#include <pthread.h>
@@ -585,6 +587,144 @@ static int doexec_noexec(void)
return ret;
}
+static bool smaps_has_flag(char *addr, const char *flag)
+{
+ FILE *file = fopen("/proc/self/smaps", "r");
+ char line[256], want[32];
+ bool in_vma = false, found = false;
+
+ if (!file)
+ return false;
+ snprintf(want, sizeof(want), "%lx-", (unsigned long)addr);
+ while (fgets(line, sizeof(line), file)) {
+ char *space = strchr(line, ' ');
+
+ if (!space)
+ continue;
+ if (memchr(line, '-', space - line) && !memchr(line, ':', space - line))
+ in_vma = !strncmp(line, want, strlen(want));
+ else if (in_vma && !strncmp(line, "VmFlags:", 8))
+ found = strstr(line, flag) != NULL;
+ }
+ fclose(file);
+ return found;
+}
+
+static int after_pfnmap_exec(char *bar, size_t size)
+{
+ size_t page = sysconf(_SC_PAGESIZE), i;
+ char addr_arg[32];
+
+ if (!smaps_has_flag(bar, " pf ") || !smaps_has_flag(bar, " sh "))
+ return KSFT_FAIL;
+ for (i = 0; i < size / page; i++)
+ if (__atomic_load_n((uint32_t *)(bar + i * page), __ATOMIC_RELAXED) !=
+ (0xd0e00000 | i))
+ return KSFT_FAIL;
+ /* The copy is not marked, so the next exec has to drop it. */
+ snprintf(addr_arg, sizeof(addr_arg), "%llu", (unsigned long long)(uintptr_t)bar);
+ execl("/proc/self/exe", "doexec", "--pfnmap-gone", addr_arg, NULL);
+ return KSFT_FAIL;
+}
+
+static int after_pfnmap_gone(char *bar)
+{
+ return smaps_has_flag(bar, " pf ") ? KSFT_FAIL : KSFT_PASS;
+}
+
+/* Only VFIO device mappings may be preserved; /dev/mem is a plain PFN map. */
+static int devmem_refused(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram;
+ int fd;
+
+ if (!preserved_ram(size))
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ /* Without FMODE_WRITE, do_mmap() drops VM_SHARED from the mapping. */
+ fd = open("/dev/mem", O_RDWR);
+ if (fd < 0)
+ return KSFT_SKIP;
+ ram = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
+ close(fd);
+ if (ram == MAP_FAILED || !smaps_has_flag(ram, " pf "))
+ return KSFT_SKIP;
+ return madvise(ram, size, MADV_DOEXEC) && errno == EINVAL ? KSFT_PASS : KSFT_FAIL;
+}
+
+static int vfio_device_fd(const char *bdf)
+{
+ char path[PATH_MAX], link[PATH_MAX], *group_id;
+ int container, group, noiommu;
+ ssize_t len;
+
+ snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/iommu_group", bdf);
+ len = readlink(path, link, sizeof(link) - 1);
+ if (len < 0)
+ return -1;
+ link[len] = 0;
+ group_id = strrchr(link, '/') + 1;
+ snprintf(path, sizeof(path), "/dev/vfio/%s", group_id);
+ noiommu = access(path, F_OK);
+ if (noiommu)
+ snprintf(path, sizeof(path), "/dev/vfio/noiommu-%s", group_id);
+ container = open("/dev/vfio/vfio", O_RDWR);
+ group = open(path, O_RDWR);
+ if (container < 0 || group < 0 ||
+ ioctl(group, VFIO_GROUP_SET_CONTAINER, &container) ||
+ ioctl(container, VFIO_SET_IOMMU, noiommu ? VFIO_NOIOMMU_IOMMU : VFIO_TYPE1v2_IOMMU))
+ return -1;
+ return ioctl(group, VFIO_GROUP_GET_DEVICE_FD, bdf);
+}
+
+/* Overwrites BAR 0 of DOEXEC_VFIO_DEVICE, so name a spare memory BAR. */
+static int vfio_bar_exec(bool ptes)
+{
+ struct vfio_region_info info = {
+ .argsz = sizeof(info),
+ .index = VFIO_PCI_BAR0_REGION_INDEX,
+ };
+ size_t page = sysconf(_SC_PAGESIZE), size, i;
+ const char *bdf = getenv("DOEXEC_VFIO_DEVICE");
+ char addr_arg[32], size_arg[32], *area, *bar;
+ uintptr_t aligned;
+ int dev;
+
+ if (!bdf || !preserved_ram(page))
+ return KSFT_SKIP;
+ dev = vfio_device_fd(bdf);
+ if (dev < 0 || ioctl(dev, VFIO_DEVICE_GET_REGION_INFO, &info)) {
+ ksft_print_msg("VFIO device %s: %s\n", bdf, strerror(errno));
+ return KSFT_FAIL;
+ }
+ if (!(info.flags & VFIO_REGION_INFO_FLAG_MMAP) || info.size < page) {
+ ksft_print_msg("VFIO device %s: BAR 0 cannot be mapped\n", bdf);
+ return KSFT_FAIL;
+ }
+ size = info.size < (4ul << 20) ? info.size : 4ul << 20;
+ /* Size-aligned: huge PFN entries, faulted after exec; a page off: PTEs, copied. */
+ area = mmap(NULL, 2 * size + page, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (area == MAP_FAILED)
+ return KSFT_FAIL;
+ aligned = (((uintptr_t)area + size - 1) & ~(size - 1)) + (ptes ? page : 0);
+ bar = mmap((void *)aligned, size, PROT_READ | PROT_WRITE,
+ MAP_SHARED | MAP_FIXED, dev, info.offset);
+ if (bar == MAP_FAILED)
+ return KSFT_FAIL;
+ for (i = 0; i < size / page; i++)
+ __atomic_store_n((uint32_t *)(bar + i * page), 0xd0e00000 | i,
+ __ATOMIC_RELAXED);
+ if (madvise(bar, size, MADV_DOEXEC)) {
+ ksft_print_msg("MADV_DOEXEC on a VFIO BAR: %s\n", strerror(errno));
+ return KSFT_FAIL;
+ }
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)bar);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ execl("/proc/self/exe", "doexec", "--pfnmap", addr_arg, size_arg, NULL);
+ return KSFT_FAIL;
+}
+
static void *exec_thread(void *arg)
{
check_exec(arg, sysconf(_SC_PAGESIZE), 1, false);
@@ -926,6 +1066,11 @@ int main(int argc, char **argv)
return KSFT_FAIL;
if (argc == 2 && !strcmp(argv[1], "--euid"))
return geteuid() ? KSFT_SKIP : KSFT_PASS;
+ if (argc == 3 && !strcmp(argv[1], "--pfnmap-gone"))
+ return after_pfnmap_gone((char *)(uintptr_t)strtoull(argv[2], NULL, 10));
+ if (argc == 4 && !strcmp(argv[1], "--pfnmap"))
+ return after_pfnmap_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
+ strtoull(argv[3], NULL, 10));
if (argc == 5 && !strcmp(argv[1], "--proc"))
return proc_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
strtoull(argv[3], NULL, 10), atoi(argv[4]));
@@ -945,7 +1090,7 @@ int main(int argc, char **argv)
return KSFT_PASS;
}
ksft_print_header();
- ksft_set_plan(NR_TESTS + 11);
+ ksft_set_plan(NR_TESTS + 14);
for (kind = 0; kind < NR_TESTS; kind++) {
fflush(stdout);
pid = fork();
@@ -968,7 +1113,7 @@ int main(int argc, char **argv)
ksft_test_result_skip("pre-existing COW: DOEXEC unavailable\n");
else
ksft_test_result(ret == KSFT_PASS, "pre-existing COW preserved\n");
- for (kind = 0; kind < 10; kind++) {
+ for (kind = 0; kind < 13; kind++) {
static const char * const extra_names[] = {
"CLONE_VM: recoverable EBUSY and retry",
"own threads: preserving exec allowed",
@@ -980,6 +1125,9 @@ int main(int argc, char **argv)
"setuid exec: preserving exec refused",
"private /dev/zero mapping: preserved across exec",
"marked range: not executable until unmarked",
+ "shared /dev/mem mapping: MADV_DOEXEC refused",
+ "VFIO BAR: preserved across exec",
+ "VFIO BAR mapped with PTEs: preserved across exec",
};
fflush(stdout);
@@ -1005,6 +1153,11 @@ int main(int argc, char **argv)
_exit(devzero_exec());
case 9:
_exit(doexec_noexec());
+ case 10:
+ _exit(devmem_refused());
+ case 11:
+ case 12:
+ _exit(vfio_bar_exec(kind == 12));
}
}
ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
--
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 2/2] ms/checkpatch.pl: adapt to new Assisted-by: format
@ 2026-10-16 21:33 3% ` Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-10-16 21:33 UTC (permalink / raw)
To: devel, Konstantin Khorenko
From: Jeff Layton <jlayton@kernel.org>
There is a new format for the 'Assisted-by:' tag. Change checkpatch.pl
to just check that it has a value rather than testing for a specific
format.
Assisted-by: LLM
Signed-off-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Jonathan Corbet <corbet@lwn.net>
Message-ID: <20260804-checkpatch-v1-1-27f29c79a46f@kernel.org>
(cherry picked from commit 2a8d68338ee77e200895f2697c56fd00094767f7)
Feature: fix ms/scripts
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
scripts/checkpatch.pl | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/scripts/checkpatch.pl b/scripts/checkpatch.pl
index c30ff25f70182..d731293db83c1 100755
--- a/scripts/checkpatch.pl
+++ b/scripts/checkpatch.pl
@@ -3073,11 +3073,12 @@ sub process {
}
}
- # Assisted-by uses AGENT_NAME:MODEL_VERSION format, not email
+ # Assisted-by uses a free-form value (e.g. "LLM"), not an
+ # email address, so skip the email format checks below.
if ($sign_off =~ /^Assisted-by:/i) {
- if ($email !~ /^\S+:\S+/) {
+ if ($email =~ /^\s*$/) {
WARN("BAD_SIGN_OFF",
- "Assisted-by expects 'AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2]' format\n" . $herecurr);
+ "Assisted-by requires a value\n" . $herecurr);
}
next;
}
--
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] ms/ipv4: validate checksum_start before completing checksum
2026-10-16 21:08 7% [Devel] [PATCH hci-8.1] ms/ipv4: validate checksum_start before completing checksum Konstantin Khorenko
@ 2026-10-16 21:13 0% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 21:13 UTC (permalink / raw)
To: devel, Konstantin Khorenko
> From: "Michael S. Tsirkin" <mst@redhat.com>
>
> commit 6785011f8b16abf40b1e9d8b2e332232b3e68822 upstream.
>
> If a packet with bad checksum metadata gets into the ipv4 stack,
> skb_checksum_help can corrupt the network header and cause a bunch of
> mischief.
>
> This was discovered and reported by Paulos, and has been reporoduced
> by others independently since.
>
> We really shouldn't allow such packets in, but as a defence
> in depth measure, let's also check before we complete the checksum.
>
> A more complete validation at input is forthcoming, but needs more work.
> [ ... ]
> https://virtuozzo.atlassian.net/browse/VSTOR-150637
>
> Feature: fix ms/net
> Assisted-by: LLM
> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-cve-v1-1-d244f4c79d4c@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] ms/ipv4: validate checksum_start before completing checksum
@ 2026-10-16 21:08 7% Konstantin Khorenko
2026-10-16 21:13 0% ` no-reply
0 siblings, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-10-16 21:08 UTC (permalink / raw)
To: devel, Konstantin Khorenko
From: "Michael S. Tsirkin" <mst@redhat.com>
commit 6785011f8b16abf40b1e9d8b2e332232b3e68822 upstream.
If a packet with bad checksum metadata gets into the ipv4 stack,
skb_checksum_help can corrupt the network header and cause a bunch of
mischief.
This was discovered and reported by Paulos, and has been reporoduced
by others independently since.
We really shouldn't allow such packets in, but as a defence
in depth measure, let's also check before we complete the checksum.
A more complete validation at input is forthcoming, but needs more work.
Cc: stable@vger.kernel.org
Fixes: f43798c27684 ("tun: Allow GSO using virtio_net_hdr")
Fixes: bfd5f4a3d605 ("packet: Add GSO/csum offload support.")
Reported-by: Paulos Yibelo <habte.yibelo@gmail.com>
Closes: https://lore.kernel.org/netdev/20260922030310.8684-2-habte.yibelo@gmail.com/
Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
Reviewed-by: Willem de Bruijn <willemb@google.com>
Link: https://patch.msgid.link/0a012b4923e189c4c593ef4f471e5ff0edbe9030.1791412497.git.mst@redhat.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
(cherry picked from commit 32a8b0adaa18e52aaa0b246b5ffe683075354dbb)
The patch has been backported from the stable linux-6.12.y branch.
CVE-2026-99999
https://virtuozzo.atlassian.net/browse/VSTOR-150637
Feature: fix ms/net
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
include/net/ip.h | 7 +++++++
net/bridge/netfilter/nf_conntrack_bridge.c | 10 +++++++---
net/ipv4/ip_output.c | 10 +++++++---
net/netfilter/nfnetlink_queue.c | 7 +++++++
net/netfilter/xt_CHECKSUM.c | 6 +++++-
net/xfrm/xfrm_output.c | 16 +++++++++++-----
6 files changed, 44 insertions(+), 12 deletions(-)
diff --git a/include/net/ip.h b/include/net/ip.h
index 7e7db511c9630..01c7ed96585ee 100644
--- a/include/net/ip.h
+++ b/include/net/ip.h
@@ -74,6 +74,13 @@ static inline unsigned int ip_hdrlen(const struct sk_buff *skb)
return ip_hdr(skb)->ihl * 4;
}
+static inline int ip_check_csum_start(const struct sk_buff *skb)
+{
+ if (unlikely(skb->csum_start < skb->network_header + ip_hdrlen(skb)))
+ return -EINVAL;
+ return 0;
+}
+
struct ipcm_cookie {
struct sockcm_cookie sockc;
__be32 addr;
diff --git a/net/bridge/netfilter/nf_conntrack_bridge.c b/net/bridge/netfilter/nf_conntrack_bridge.c
index 6482de4d87509..10127b7588efa 100644
--- a/net/bridge/netfilter/nf_conntrack_bridge.c
+++ b/net/bridge/netfilter/nf_conntrack_bridge.c
@@ -40,9 +40,13 @@ static int nf_br_ip_fragment(struct net *net, struct sock *sk,
int err = 0;
/* for offloaded checksums cleanup checksum before fragmentation */
- if (skb->ip_summed == CHECKSUM_PARTIAL &&
- (err = skb_checksum_help(skb)))
- goto blackhole;
+ if (skb->ip_summed == CHECKSUM_PARTIAL) {
+ err = ip_check_csum_start(skb);
+ if (!err)
+ err = skb_checksum_help(skb);
+ if (err)
+ goto blackhole;
+ }
iph = ip_hdr(skb);
diff --git a/net/ipv4/ip_output.c b/net/ipv4/ip_output.c
index f7f8048ba122a..7a9e11ab4ad96 100644
--- a/net/ipv4/ip_output.c
+++ b/net/ipv4/ip_output.c
@@ -779,9 +779,13 @@ int ip_do_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
int err = 0;
/* for offloaded checksums cleanup checksum before fragmentation */
- if (skb->ip_summed == CHECKSUM_PARTIAL &&
- (err = skb_checksum_help(skb)))
- goto fail;
+ if (skb->ip_summed == CHECKSUM_PARTIAL) {
+ err = ip_check_csum_start(skb);
+ if (!err)
+ err = skb_checksum_help(skb);
+ if (err)
+ goto fail;
+ }
/*
* Point into the IP datagram header.
diff --git a/net/netfilter/nfnetlink_queue.c b/net/netfilter/nfnetlink_queue.c
index b6ad12029fa9d..c3453d73564af 100644
--- a/net/netfilter/nfnetlink_queue.c
+++ b/net/netfilter/nfnetlink_queue.c
@@ -33,6 +33,7 @@
#include <linux/rhashtable.h>
#include <linux/jhash.h>
#include <net/gso.h>
+#include <net/ip.h>
#include <net/sock.h>
#include <net/tcp_states.h>
#include <net/netfilter/nf_queue.h>
@@ -607,6 +608,12 @@ static int nfqnl_put_bridge(struct nf_queue_entry *entry, struct sk_buff *skb)
static int nf_queue_checksum_help(struct sk_buff *entskb)
{
+ if (entskb->protocol == htons(ETH_P_IP)) {
+ if (!pskb_network_may_pull(entskb, sizeof(struct iphdr)) ||
+ ip_check_csum_start(entskb))
+ return -EINVAL;
+ }
+
if (skb_csum_is_sctp(entskb))
return skb_crc32c_csum_help(entskb);
diff --git a/net/netfilter/xt_CHECKSUM.c b/net/netfilter/xt_CHECKSUM.c
index 9d99f5a3d1764..1fb0f8118404d 100644
--- a/net/netfilter/xt_CHECKSUM.c
+++ b/net/netfilter/xt_CHECKSUM.c
@@ -15,6 +15,7 @@
#include <linux/netfilter_ipv4/ip_tables.h>
#include <linux/netfilter_ipv6/ip6_tables.h>
+#include <net/ip.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Michael S. Tsirkin <mst@redhat.com>");
@@ -25,8 +26,11 @@ MODULE_ALIAS("ip6t_CHECKSUM");
static unsigned int
checksum_tg(struct sk_buff *skb, const struct xt_action_param *par)
{
- if (skb->ip_summed == CHECKSUM_PARTIAL && !skb_is_gso(skb))
+ if (skb->ip_summed == CHECKSUM_PARTIAL && !skb_is_gso(skb)) {
+ if (xt_family(par) == NFPROTO_IPV4 && ip_check_csum_start(skb))
+ return NF_DROP;
skb_checksum_help(skb);
+ }
return XT_CONTINUE;
}
diff --git a/net/xfrm/xfrm_output.c b/net/xfrm/xfrm_output.c
index 3fc5eb3487135..ef72183dbba94 100644
--- a/net/xfrm/xfrm_output.c
+++ b/net/xfrm/xfrm_output.c
@@ -808,16 +808,22 @@ int xfrm_output(struct sock *sk, struct sk_buff *skb)
}
if (skb->ip_summed == CHECKSUM_PARTIAL) {
- err = skb_checksum_help(skb);
- if (err) {
- XFRM_INC_STATS(net, LINUX_MIB_XFRMOUTERROR);
- kfree_skb(skb);
- return err;
+ if (skb->protocol == htons(ETH_P_IP)) {
+ err = ip_check_csum_start(skb);
+ if (err)
+ goto error;
}
+ err = skb_checksum_help(skb);
+ if (err)
+ goto error;
}
out:
return xfrm_output2(net, sk, skb);
+error:
+ XFRM_INC_STATS(net, LINUX_MIB_XFRMOUTERROR);
+ kfree_skb(skb);
+ return err;
}
EXPORT_SYMBOL_GPL(xfrm_output);
---
base-commit: defa968cbe8c38f1ff0231042dee07f00d27b2c9
change-id: 20261016-cve-dd9e4c310885
Best regards,
--
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 7%]
* Re: [PATCH QEMU HCI-8.1 v3 27/28] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860
2026-10-16 15:26 6% ` [PATCH QEMU HCI-8.1 v3 27/28] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860 Andrey Drobyshev
@ 2026-10-16 16:02 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 16:02 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] vfio: vfio_device_attach() fails during migration/snapshot due to new blocker
--
commit c60b9a0f169a17999aea7d76e3213dbee63fd377
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860
This patch saves each VFIO BAR mapping in the CPR ramblock list. After
cpr-exec, new QEMU reuses the saved mapping instead of calling mmap()
again. It also adds a blocker so cpr-transfer is refused for VFIO devices.
> diff --git a/hw/vfio/device.c b/hw/vfio/device.c
> --- a/hw/vfio/device.c
> +++ b/hw/vfio/device.c
[ ... ]
> @@ -464,12 +464,32 @@ bool vfio_device_attach(char *name, VFIODevice *vbasedev,
> TYPE_VFIO_IOMMU_IOMMUFD :
> TYPE_VFIO_IOMMU_LEGACY;
>
> - return vfio_device_attach_by_iommu_type(iommu_type, name, vbasedev,
> - as, errp);
> + /*
> + * Only cpr-exec keeps the address space and so the region mappings,
> + * see vfio_region_mmap(). A cpr-transfer target would have to map the
> + * regions anew, which some drivers (NVIDIA vGPU) refuse.
> + */
> + error_setg(&vbasedev->cpr.mmap_blocker,
> + "cpr-transfer is not supported with VFIO devices; "
> + "use the cpr-exec mode");
> + if (migrate_add_blocker_modes(&vbasedev->cpr.mmap_blocker, errp,
> + MIG_MODE_CPR_TRANSFER, -1)) {
> + return false;
> + }
[Severity: Low]
Can this make vfio_device_attach() fail while a migration or savevm is
running? migrate_add_blocker_modes() calls is_busy() regardless of which
modes were requested:
migration/migration.c:is_busy() {
if (runstate_check(RUN_STATE_SAVE_VM) || migration_is_running()) {
error_propagate_prepend(errp, *reasonp, ...);
*reasonp = NULL;
return true;
}
}
migrate_add_blocker_modes() then returns -EBUSY, and vfio_device_attach()
returns false. So hotplugging a VFIO device during a migration or savevm
would now be refused, even though the new blocker only applies to
cpr-transfer. Before this patch, attach did not depend on migration state
for devices that needed no blocker.
Is that behaviour intended for this blocker?
> +
> + if (!vfio_device_attach_by_iommu_type(iommu_type, name, vbasedev,
> + as, errp)) {
> + migrate_del_blocker(&vbasedev->cpr.mmap_blocker);
> + return false;
> + }
> +
> + return true;
> }
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-vfio_v1-v3-0-47487cb5a8e5@virtuozzo.com?part=27
^ permalink raw reply [relevance 4%]
* Re: [PATCH QEMU HCI-8.1 v3 25/28] VZ: migration/cpr-exec: take the vhost-net devices back if exec fails
2026-10-16 15:26 10% ` [PATCH QEMU HCI-8.1 v3 25/28] VZ: migration/cpr-exec: take the vhost-net devices back if exec fails Andrey Drobyshev
@ 2026-10-16 15:58 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 15:58 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] vhost-net: cpr-exec failure path restores only SET_OWNER, not the vring err/call fds
- [Low] migration: cpr-exec calls vhost_net_cpr_set_owners() with no stub in vhost_net-stub.c
--
commit 4559492d0874959294013f874fff0e018375087a
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
VZ: migration/cpr-exec: take the vhost-net devices back if exec fails
When execvp() fails in cpr_exec_cb(), the old QEMU keeps running. This
patch records which vhost-net devices were released with
VHOST_RESET_OWNER in vhost_net_cpr_reset_owners(). It then takes them
back with VHOST_SET_OWNER through the new vhost_net_cpr_set_owners() on
the fail path.
> diff --git a/hw/net/vhost_net.c b/hw/net/vhost_net.c
> --- a/hw/net/vhost_net.c
> +++ b/hw/net/vhost_net.c
[ ... ]
> +/*
> + * Take the ownership back when the exec fails and the VM goes on in the
> + * old QEMU. VHOST_RESET_OWNER has reset the kernel device, but the devices
> + * are stopped, and vhost_net_start() sets everything up again when the VM
> + * resumes.
> + */
> +void vhost_net_cpr_set_owners(void)
> +{
> + NetClientState *nc;
> +
> + QTAILQ_FOREACH(nc, &net_clients, next) {
> + VHostNetState *net = get_vhost_net(nc);
> +
> + if (net && net->cpr_owner_reset) {
> + int r = vhost_dev_set_owner(&net->dev);
[Severity: Medium]
Does restoring only the owner here leave the vhost device without its
per-virtqueue error and call eventfds? vhost_dev_init() registers them
right after SET_OWNER, through vhost_virtqueue_init():
r = dev->vhost_ops->vhost_set_vring_call(dev, &file);
...
r = dev->vhost_ops->vhost_set_vring_err(dev, &file);
As far as I can tell, vhost_set_vring_err is called only from
vhost_virtqueue_init(), and that runs once from vhost_dev_init(). Neither
vhost_dev_start() nor vhost_virtqueue_start() calls it again. Would the
error fd therefore stay unregistered after VHOST_RESET_OWNER followed by
VHOST_SET_OWNER?
The call fd looks similar. In vhost_virtqueue_start() it is re-set only
when guest notifier masking is not in use:
if (!vdev->use_guest_notifier_mask) {
/* TODO: check and handle errors. */
vhost_virtqueue_mask(dev, vdev, idx, false);
}
With use_guest_notifier_mask and MSI-X, a vector that the guest keeps
masked seems to get no call fd after the reset. The initial setup had
pointed it at masked_notifier. Could a pending interrupt on such a vector
then be dropped, and go unseen by virtio_pci_vector_poll()?
I have not checked the kernel side. This assumes VHOST_RESET_OWNER clears
error_ctx and call_ctx, as vhost_dev_cleanup() does.
The comment above and the commit message say "vhost_net_start() sets up
the rest again when the VM resumes". Is that accurate for the vring err fd
and for the call fd of masked vectors?
> +
> + if (r < 0) {
> + warn_report("cpr: failed to set vhost-net owner for %s: %s",
> + nc->name, strerror(-r));
> + } else {
> + net->cpr_owner_reset = false;
> }
> }
> }
[ ... ]
> diff --git a/include/net/vhost_net.h b/include/net/vhost_net.h
> --- a/include/net/vhost_net.h
> +++ b/include/net/vhost_net.h
[ ... ]
> void vhost_net_cpr_reset_owners(void);
> +void vhost_net_cpr_set_owners(void);
[Severity: Low]
Is a stub needed for this function? hw/net/vhost_net-stub.c has no
vhost_net_cpr_reset_owners() or vhost_net_cpr_set_owners(). hw/net/meson.build
builds that stub file in place of vhost_net.c when vhost_net is not
available.
> diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
> --- a/migration/cpr-exec.c
> +++ b/migration/cpr-exec.c
[ ... ]
> + /* Still the same mm, take back the vhost-net devices we released */
> + vhost_net_cpr_set_owners();
[Severity: Low]
cpr_exec_cb() in migration/cpr-exec.c is always built and calls
vhost_net_cpr_set_owners() with no guard. In configurations that use
vhost_net-stub.c, would this give an undefined reference at link time?
The existing vhost_net_cpr_reset_owners() call in cpr_exec_cb() appears
to have the same problem, and this patch adds a second undefined symbol of
that kind.
>
> /* Note, we can go from state COMPLETED to FAILED */
> migration_call_notifiers(MIG_EVENT_PRECOPY_FAILED, NULL);
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-vfio_v1-v3-0-47487cb5a8e5@virtuozzo.com?part=25
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 v3 21/28] vfio/migration: don't migrate device state during CPR
@ 2026-10-16 15:55 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 15:55 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] vfio/migration: stale cached device_state when CPR starts from a paused VM
- [Medium] vfio/migration: CPR skips VFIO state using local-only checks, breaking old-source to new-target streams
- [Medium] vfio/migration: cached device_state not synced after CPR when guest was paused
--
commit 5d90a1a10c891a1bfb1b58a35b36f468c5d28350
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
vfio/migration: don't migrate device state during CPR
This patch skips VFIO device state save and load while a CPR migration
(cpr-transfer or cpr-exec) is in progress. It gates the save, load and
VM stop/start handlers on cpr_is_in_progress(). It also resets
multifd_transfer in vfio_multifd_cleanup().
> diff --git a/hw/vfio/migration.c b/hw/vfio/migration.c
> --- a/hw/vfio/migration.c
> +++ b/hw/vfio/migration.c
[ ... ]
> @@ -674,6 +674,16 @@ static void vfio_save_state(QEMUFile *f, void *opaque)
> Error *local_err = NULL;
> int ret;
>
> + /*
> + * .is_active doesn't cover the non-iterable stage, so the section is
> + * still sent: leave it empty, or new QEMU would load the config into
> + * the preserved device and re-program its MSI-X.
> + */
> + if (cpr_is_in_progress()) {
> + qemu_put_be64(f, VFIO_MIG_FLAG_END_OF_STATE);
> + return;
> + }
> +
[Severity: High]
Can the new QEMU end up with a stale cached device_state when CPR starts
from an already paused VM?
If the user stops the VM before CPR, vfio_vmstate_change(running=false)
runs while cpr_is_in_progress() is false. It moves the kernel device to
STOP and caches STOP in migration->device_state.
Once CPR starts, vfio_save_state() emits only END_OF_STATE, so no
device_state reaches the target. The new QEMU then runs
vfio_migration_init(), which does:
migration->device_state = VFIO_DEVICE_STATE_RUNNING;
It never queries the kernel. The preserved device is still in STOP.
The target stays paused, because process_incoming_migration_bh() calls
runstate_set(RUN_STATE_PAUSED) and then migration_incoming_state_destroy().
That clears the incoming mode, so cpr_is_in_progress() becomes false.
A later 'cont' then reaches vfio_vmstate_change(running=true), which
requests RUNNING. In vfio_migration_set_state() this hits:
if (new_state == migration->device_state) {
return 0;
}
No ioctl is issued, and the kernel device stays in STOP while the guest
runs. Could the cached state be synchronized with the preserved device in
this case?
The commit message says the device "is never stopped". Is that accurate
for a source that was paused before CPR?
[ ... ]
> @@ -810,6 +820,11 @@ static bool vfio_switchover_ack_needed(void *opaque)
> {
> VFIODevice *vbasedev = opaque;
>
> + /* No device state is sent during CPR, so no ACK to wait for */
> + if (cpr_is_in_progress()) {
> + return false;
> + }
> +
> return vfio_precopy_supported(vbasedev);
> }
[ ... ]
> +static bool vfio_is_active(void *opaque)
> +{
> + return !cpr_is_in_progress();
> +}
[Severity: Medium]
Is it a problem that the decision to send or load VFIO state is made only
from local runtime state?
Nothing in the stream tells the peer whether VFIO sections are present.
Consider a CPR incoming migration from a source that does not have this
patch:
vfio_is_active() returns false on the target, so loadvm_state_setup() skips
vfio_load_setup() and migration->data_fd stays -1. The old source still
sends VFIO_MIG_FLAG_DEV_DATA_STATE. vfio_load_state() has no CPR guard:
case VFIO_MIG_FLAG_DEV_DATA_STATE:
{
uint64_t data_size = qemu_get_be64(f);
if (data_size) {
ret = vfio_load_buffer(f, vbasedev, data_size);
Would that call qemu_file_get_to_fd(f, -1, size) and fail? The config
section would also still be loaded into the live device.
The reverse pair (new source, old target) looks similar. The old target
runs vfio_load_setup() and puts the live device into RESUMING. Its
vfio_switchover_ack_needed() has no CPR check, so it waits for an ACK that
the new source never sends.
docs/devel/migration/CPR.rst describes the incoming side as a new QEMU
binary, so mixed-version CPR seems to be a documented use. Is a stream flag
or a compat property needed here, or is mixed-version CPR intentionally
unsupported? I found no later patch in the series that adds a CPR guard in
vfio_load_state().
[ ... ]
> @@ -882,6 +914,10 @@ static void vfio_vmstate_change(void *opaque, bool running, RunState state)
> Error *local_err = NULL;
> int ret;
>
> + if (cpr_is_in_progress()) {
> + return;
> + }
> +
> if (running) {
> new_state = VFIO_DEVICE_STATE_RUNNING;
> } else {
[Severity: Medium]
Does this early return leave the cached migration->device_state out of sync
with the preserved device on the target?
On the target, vm_start() runs while the incoming mode is still set, so
this handler is skipped. For a guest paused before CPR, the kernel device
is in STOP, but vfio_migration_init() has cached RUNNING.
After the incoming migration completes, the later 'cont' calls
vfio_vmstate_change(running=true):
vfio_vmstate_change()
vfio_migration_set_state()
if (new_state == migration->device_state)
return 0;
The request is silently skipped, so the kernel device is never resumed.
Should the cache be resynchronized, or the real state queried, when the
device is inherited through CPR?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-vfio_v1-v3-0-47487cb5a8e5@virtuozzo.com?part=21
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 v3 22/28] vfio/listener: skip dirty page tracking during CPR
@ 2026-10-16 15:50 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 15:50 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] vfio: CPR skip in log_global_start() is never retried when another dirty-tracking flag is active
--
commit a7865175c300cb526642364e7a12bfbf13a29863
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
vfio/listener: skip dirty page tracking during CPR
This patch makes vfio_listener_log_global_start() return early without
starting VFIO dirty tracking while a CPR migration (cpr-transfer or
cpr-exec) is in progress. The gate is cpr_is_in_progress(), so snapshots
still track dirty pages. Stopping stays unconditional.
> diff --git a/hw/vfio/listener.c b/hw/vfio/listener.c
> index afcf518f567f..1270b9d5f3aa 100644
> --- a/hw/vfio/listener.c
> +++ b/hw/vfio/listener.c
[ ... ]
> @@ -1012,6 +1013,11 @@ static bool vfio_listener_log_global_start(MemoryListener *listener,
> listener);
> bool ret;
>
> + /* Preserved devices keep DMA-ing into preserved RAM, nothing to track */
> + if (cpr_is_in_progress()) {
> + return true;
> + }
> +
[Severity: High]
Can this early return leave VFIO dirty tracking permanently off for a later
normal migration?
The listener hook runs only when the first dirty-tracking flag is set:
system/memory.c:memory_global_dirty_log_start() {
old_flags = global_dirty_tracking;
global_dirty_tracking |= flags;
...
if (!old_flags) {
if (!memory_global_dirty_log_do_start(errp)) {
...
}
Consider this sequence:
1. A CPR migration starts as the first consumer.
memory_global_dirty_log_start(GLOBAL_DIRTY_MIGRATION) sees
old_flags == 0, so vfio_listener_log_global_start() runs. Because
cpr_is_in_progress() is true, it returns true without starting tracking.
2. Another consumer starts, for example calculate-dirty-rate
(GLOBAL_DIRTY_DIRTY_RATE) or the dirty limit (GLOBAL_DIRTY_LIMIT).
old_flags is now non-zero, so no listener is called.
3. The CPR migration fails or is cancelled. Only the migration flag is
cleared in memory_global_dirty_log_do_stop():
global_dirty_tracking &= ~flags;
...
if (!global_dirty_tracking) {
...
MEMORY_LISTENER_CALL_GLOBAL(log_global_stop, Reverse);
global_dirty_tracking stays non-zero, so no listener hook runs.
4. The user sets mode=normal and migrates.
memory_global_dirty_log_start(GLOBAL_DIRTY_MIGRATION) again sees
old_flags != 0 and skips the listeners, so VFIO tracking is never
started.
5. vfio_log_sync_needed() then returns false:
if (!vfio_container_dirty_tracking_is_started(bcontainer)) {
return false;
}
Both vbasedev->dirty_tracking and bcontainer->dirty_pages_started are
still false, so pages dirtied by device DMA are not synced.
The destination could then receive stale guest RAM during the later normal
migration. Before this patch, VFIO tracking always started with the first
consumer, so this state was not reachable.
Nothing later in the series touches hw/vfio/listener.c or system/memory.c,
so the series does not restart tracking in this case.
Could the skip be retried, or could VFIO tracking be started when a later
consumer or a later non-CPR migration needs it?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-vfio_v1-v3-0-47487cb5a8e5@virtuozzo.com?part=22
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 v3 20/28] migration/cpr: add cpr_is_in_progress()
@ 2026-10-16 15:46 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 15:46 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] migration/cpr: cpr_is_in_progress() misreports snapshots of a paused VM
--
commit 3d6e15a074763ee7756f891bee028184ab64eb6c
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
migration/cpr: add cpr_is_in_progress()
This adds cpr_is_in_progress(), which reports whether a CPR migration is
actually in progress. It returns true on the target until the incoming
migration is done, and on the source only while the outgoing migration
runs. On the source it tries to exclude snapshots by checking for
RUN_STATE_SAVE_VM.
> diff --git a/migration/cpr.c b/migration/cpr.c
> index a149e790c103..74acdfc4123e 100644
> --- a/migration/cpr.c
> +++ b/migration/cpr.c
[ ... ]
> @@ -549,6 +549,28 @@ bool cpr_exec_is_incoming(void)
> return incoming_mode == MIG_MODE_CPR_EXEC;
> }
>
> +bool cpr_is_in_progress(void)
> +{
[ ... ]
> + /*
> + * Source: the parameter alone means nothing, it may just have been set.
> + * But a running outgoing migration uses it. A snapshot runs as a
> + * migration too, and it is never a CPR one.
> + */
> + return migration_is_running() && !runstate_check(RUN_STATE_SAVE_VM);
> +}
[Severity: Medium]
Can this return true during an ordinary snapshot of a VM that is already
paused?
The snapshot exclusion relies on the runstate being RUN_STATE_SAVE_VM.
But save_snapshot() calls vm_stop(RUN_STATE_SAVE_VM), which reaches
do_vm_stop(), and that only changes the runstate when the VM was live:
system/cpus.c:do_vm_stop() {
...
if (runstate_is_live(oldstate)) {
vm_was_suspended = (oldstate == RUN_STATE_SUSPENDED);
runstate_set(state);
...
}
If the VM is paused or in prelaunch, the runstate never becomes
RUN_STATE_SAVE_VM, so !runstate_check(RUN_STATE_SAVE_VM) stays true.
Meanwhile qemu_savevm_state() calls migrate_init(), which puts
MigrationState into SETUP:
migration/savevm.c:qemu_savevm_state() {
...
ret = migrate_init(ms, errp);
...
}
migration_is_running() counts MIGRATION_STATUS_SETUP as running.
cpr_is_incoming() is false on the source.
So if migrate-set-parameters has set mode to cpr-transfer or cpr-exec and
savevm is then run on a paused VM, cpr_is_in_progress() returns true. That
contradicts the comment "A snapshot runs as a migration too, and it is
never a CPR one" and the commit message.
Would callers such as the VFIO save path then skip device state during
such a snapshot?
Would an explicit outgoing-CPR flag be a more robust discriminator? It
could be set in cpr_state_save() and cleared on completion, failure or
cancel.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-vfio_v1-v3-0-47487cb5a8e5@virtuozzo.com?part=20
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 v3 17/28] migration: Fix crash on second migration when cancel early
2026-10-16 15:26 23% ` [PATCH QEMU HCI-8.1 v3 17/28] migration: Fix crash on second migration when cancel early Andrey Drobyshev
@ 2026-10-16 15:44 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 15:44 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] migration: migration_cancel() asserts on mode that migrate-set-parameters can change
--
commit 1a45829d1f41fe592f4ff72499c683f6dac53858
Author: Peter Xu <peterx@redhat.com>
migration: Fix crash on second migration when cancel early
This commit changes migration_cancel() to call migration_cleanup() when
the cpr-transfer HUP gsource is still pending. The goal is to avoid a
crash on a second migration after an early cancel. It also clears
hup_source as soon as the HUP callback runs, and adds assertions.
Link: https://lore.kernel.org/r/20260417184742.293061-1-marcandre.lureau@redhat.com
Link: https://lore.kernel.org/r/20260421175820.302795-1-peterx@redhat.com
> diff --git a/migration/migration.c b/migration/migration.c
> --- a/migration/migration.c
> +++ b/migration/migration.c
[ ... ]
> @@ -1653,14 +1653,19 @@ void migration_cancel(void)
> }
>
> /*
> - * If qmp_migrate_finish has not been called, then there is no path that
> - * will complete the cancellation. Do it now.
> + * This is cpr-transfer specific processing.
> + *
> + * If this is true, it means cpr-transfer migration is waiting for the
> + * destination to send HUP event on CPR channel to continue the next
> + * phase. If so, do the cleanup proactively to avoid get stuck in
> + * CANCELLING state.
> */
> - if (setup && !s->to_dst_file) {
> - migrate_set_state(&s->state, MIGRATION_STATUS_CANCELLING,
> - MIGRATION_STATUS_CANCELLED);
> - cpr_state_close();
> - cpr_transfer_source_destroy(s);
> + if (cpr_transfer_source_active(s)) {
> + assert(migrate_mode() == MIG_MODE_CPR_TRANSFER);
[Severity: Medium]
Can this assert abort QEMU? migrate_mode() on the source falls back to
s->parameters.mode, because cpr_get_incoming_mode() returns
MIG_MODE_NONE there. That value can be changed while hup_source is still
set.
The sequence would be:
qmp_migrate() with mode=cpr-transfer
-> cpr_transfer_add_hup_watch()
(state stays SETUP, hup_source set until the destination closes
the CPR channel)
qmp_migrate_set_parameters() with mode=normal
-> migrate_params_check()
-> migrate_params_apply()
migrate_cancel, or migration_shutdown() at QEMU exit
-> migration_cancel()
-> cpr_transfer_source_active(s) is true
-> assert(migrate_mode() == MIG_MODE_CPR_TRANSFER) fails
As far as I can tell, neither migrate_params_check() nor the apply step
rejects a mode change while a migration is running or while hup_source
is set. The migration_is_running() check in options.c is in the
capabilities path, not the parameters path.
The wait for the destination to close the CPR channel has no upper bound,
so this window can stay open for a long time. Before this patch, the
cancel path in this window did not assert on the mode.
Would it make sense to reject a mode change while a migration is
running, or to drop the mode assertion here?
> + assert(setup && !s->to_dst_file);
> + migration_cleanup(s);
> + /* Now all things should have been released */
> + assert(!cpr_transfer_source_active(s));
> }
> }
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-vfio_v1-v3-0-47487cb5a8e5@virtuozzo.com?part=17
^ permalink raw reply [relevance 5%]
* Re: [PATCH QEMU HCI-8.1 v3 18/28] kvm: tolerate ioctls after kvm_close()
2026-10-16 15:26 23% ` [PATCH QEMU HCI-8.1 v3 18/28] kvm: tolerate ioctls after kvm_close() Andrey Drobyshev
@ 2026-10-16 15:44 7% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 15:44 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] kvm: late INTx via kvm_set_irq() still crashes after kvm_close()
- [Medium] kvm: kvm_set_ioeventfd_mmio() returns stale -errno on new -ENODEV path
- [Low] kvm: unsynchronized closed-state check in ioctl wrappers races with kvm_close()
--
commit 6d5d5f6faa5b401fc25d81071e7ee69ce2c8465b
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
kvm: tolerate ioctls after kvm_close()
This patch makes kvm_ioctl(), kvm_vm_ioctl() and kvm_vcpu_ioctl() return
-ENODEV once kvm_close() has run. It also stops kvm_send_msi() from
printing the "MSI lost" warning for that case. The goal is to stop the
retired cpr-transfer source from crashing on late interrupts.
> kvm: tolerate ioctls after kvm_close()
> Make the ioctl wrappers honour the closed state instead of assuming a
> live KVMState: kvm_ioctl(), kvm_vm_ioctl() and kvm_vcpu_ioctl() return
> -ENODEV when kvm_close() has already been called, so any late interrupt
> or query on the retired source fails gracefully.
The commit message says any late interrupt fails gracefully. Is that
accurate? Only the MSI path through kvm_send_msi() is fixed. The
findings below describe other late-interrupt paths that still crash.
> diff --git a/accel/kvm/kvm-all.c b/accel/kvm/kvm-all.c
> --- a/accel/kvm/kvm-all.c
> +++ b/accel/kvm/kvm-all.c
[ ... ]
> @@ -3389,6 +3394,11 @@ int kvm_vm_ioctl(KVMState *s, unsigned long type, ...)
> arg = va_arg(ap, void *);
> va_end(ap);
>
> + /* kvm_close() has been called: there is no VM to talk to any more */
> + if (!s || s->vmfd == -1) {
> + return -ENODEV;
> + }
> +
> trace_kvm_vm_ioctl(type, arg);
> accel_ioctl_begin();
> ret = ioctl(s->vmfd, type, arg);
[Severity: Medium]
Can a late INTx or GSI line interrupt still crash after kvm_close()?
kvm_close() sets kvm_state to NULL, but kvm_kernel_irqchip and
kvm_async_interrupts_allowed stay true. A late line interrupt then takes
this path:
kvm_ioapic_set_irq() / kvm_pic_set_irq()
kvm_set_irq(kvm_state = NULL, ...)
kvm_vm_ioctl(s, s->irq_set_ioctl, &event)
The argument s->irq_set_ioctl is evaluated in kvm_set_irq() before
kvm_vm_ioctl() runs. With s == NULL it faults there, so the new "!s"
check is never reached.
If s were non-NULL but closed, the new -ENODEV would make this code take
the error branch:
ret = kvm_vm_ioctl(s, s->irq_set_ioctl, &event);
if (ret < 0) {
perror("kvm_set_irq");
abort();
}
That would turn the late interrupt into an abort() instead.
kvm_irqchip_commit_routes() has the same problem:
ret = kvm_vm_ioctl(s, KVM_SET_GSI_ROUTING, s->irq_routes);
assert(ret == 0);
The new -ENODEV would trip the assert.
I compared the end of the series against this patch for accel/kvm and
hw/i386/kvm. The only change there is the coalesced MMIO ring handling in
kvm_close(). Nothing guards kvm_set_irq(), the IOAPIC and PIC callbacks,
or the irqchip route helpers. Should those callers be taught about the
closed state as well?
[Severity: Low]
Is the new closed-state check safe against a concurrent kvm_close()?
The check is a plain, unlocked load of s->vmfd:
if (!s || s->vmfd == -1) {
return -ENODEV;
}
...
accel_ioctl_begin();
ret = ioctl(s->vmfd, type, arg);
It is not atomic with the later use of s->vmfd. accel_ioctl_begin()
returns immediately when the BQL is held and only takes the lockcnt for
non-BQL callers. kvm_close() does not call kvm_ioctl_inhibit_begin().
kvm_close() also closes the fd before it stores -1:
close(kvm_state->vmfd);
kvm_state->vmfd = -1;
Can a non-BQL thread pass the -1 check and then call ioctl() on a closed
or recycled descriptor? The likely result is a spurious -EBADF. A
recycled fd number could refer to another file.
I did not find a specific non-BQL caller that runs during kvm_close().
The wrappers just don't exclude one. The BQL-held callers and the vCPU
threads (stopped by cpu_remove_sync()) are covered.
The same applies to kvm_vcpu_ioctl() and cpu->kvm_fd:
> @@ -3409,6 +3419,11 @@ int kvm_vcpu_ioctl(CPUState *cpu, unsigned long type, ...)
> arg = va_arg(ap, void *);
> va_end(ap);
>
> + /* kvm_close() has been called: the vCPU fds are gone */
> + if (cpu->kvm_fd == -1) {
> + return -ENODEV;
> + }
> +
> trace_kvm_vcpu_ioctl(cpu->cpu_index, type, arg);
> accel_cpu_ioctl_begin(cpu);
> ret = ioctl(cpu->kvm_fd, type, arg);
Similarly, kvm_send_msi() reads the global kvm_state without any
synchronization against kvm_close() setting it to NULL. The KVMState
object itself is never freed, so only the fd value is racy.
[Severity: Medium]
Can the new early return in kvm_vm_ioctl() give kvm_set_ioeventfd_mmio()
a stale errno?
kvm_vm_ioctl() already returns a negative errno value. The new -ENODEV
return never calls ioctl(), so errno is not set. kvm_set_ioeventfd_mmio()
ignores the returned value and re-reads errno:
ret = kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &iofd);
if (ret < 0) {
return -errno;
}
After kvm_close() this returns whatever errno an earlier failed syscall
left behind. If errno is 0, the failure is reported as success. If errno
is non-zero, the caller reports a misleading error.
The callers kvm_mem_ioeventfd_add() and kvm_mem_ioeventfd_del() abort()
on any negative return:
r = kvm_set_ioeventfd_mmio(fd, section->offset_within_address_space,
data, true, int128_get64(section->size),
match_data);
if (r < 0) {
fprintf(stderr, "%s: error adding ioeventfd: %s (%d)\n",
__func__, strerror(-r), -r);
abort();
}
Should kvm_set_ioeventfd_mmio() return ret instead of -errno? The
sibling kvm_set_ioeventfd_pio() already returns the kvm_vm_ioctl() value
directly.
I did not confirm that kvm_enabled() stays true after kvm_close(). I also
did not find a path that deletes an ioeventfd after kvm_close(), such as
device unrealize during qemu_cleanup. So I'm not sure this can be
triggered today.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-vfio_v1-v3-0-47487cb5a8e5@virtuozzo.com?part=18
^ permalink raw reply [relevance 7%]
* Re: [PATCH QEMU HCI-8.1 v3 14/28] migration: yank: Move register instance earlier
2026-10-16 15:26 22% ` [PATCH QEMU HCI-8.1 v3 14/28] migration: yank: Move register instance earlier Andrey Drobyshev
@ 2026-10-16 15:41 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 15:41 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] migration: stale incoming_postcopy_state makes migrate_prepare() refuse all later outgoing migrations
- [Medium] migration: migrate_prepare() registers yank with &error_abort, aborting QEMU if cleanup is pending
--
Commit ce0aafa8f0f4f6ebc0a21db9c39794339fc1b277
Author: Fabiano Rosas <farosas@suse.de>
migration: yank: Move register instance earlier
This commit moves yank_register_instance() into migrate_prepare() so it
pairs with the unregister in migration_cleanup(). It also adds a guard
that refuses a non-resume migrate while an incoming postcopy migration is
in progress, and reworks the error-path unregister logic in
qmp_migrate() and qmp_migrate_finish().
Link: https://lore.kernel.org/qemu-devel/20260123141656.6765-11-farosas@suse.de
> diff --git a/migration/migration.c b/migration/migration.c
> index 2f8ef20c0369..0b8309565323 100644
> --- a/migration/migration.c
> +++ b/migration/migration.c
[ ... ]
> @@ -2174,6 +2174,15 @@ static bool migrate_prepare(MigrationState *s, bool resume, Error **errp)
> return false;
> }
>
> + /*
> + * The guest already runs during incoming postcopy, but the incoming
> + * migration still holds the yank instance registered below.
> + */
> + if (migration_incoming_postcopy_advised()) {
> + error_setg(errp, "Incoming postcopy migration is still in progress");
> + return false;
> + }
> +
[Severity: High]
Can this guard reject every later outgoing migrate on a VM whose incoming
migration has already finished?
migration_incoming_postcopy_advised() evaluates:
PostcopyState ps = postcopy_state_get();
return ps >= POSTCOPY_INCOMING_ADVISE && ps < POSTCOPY_INCOMING_END;
Only the postcopy listen thread ever sets POSTCOPY_INCOMING_END. Consider
this sequence on the destination with postcopy-ram enabled:
loadvm_postcopy_handle_advise()
postcopy_state_set(POSTCOPY_INCOMING_ADVISE);
(the len == 0 case with !migrate_postcopy_ram() also returns 0
without resetting the state)
Precopy then completes without postcopy starting, so LISTEN and the
listen thread never run:
process_incoming_migration_co()
if (ps == POSTCOPY_INCOMING_ADVISE) {
postcopy_ram_incoming_cleanup(mis);
}
...
process_incoming_migration_bh()
migration_incoming_state_destroy() <- yank instance released here
postcopy_ram_incoming_cleanup() does not reset the state, and none of the
other postcopy_state_set() callers run on this path or on the ret < 0
fail: path. The state therefore stays at ADVISE (or DISCARD, LISTENING or
RUNNING after some failures).
After that, migration_incoming_postcopy_advised() stays true, and every
non-resume QMP migrate returns "Incoming postcopy migration is still in
progress", even though the incoming side no longer holds the yank
instance. Chained migration (A->B->C) looks like a realistic trigger.
The refusal appears to last until a new incoming migration starts or QEMU
is restarted.
I did not find anything later in the series that resets the state or
narrows this check.
Should the guard test whether the incoming side still holds the yank
instance, or should the postcopy state be reset when incoming migration
finishes or fails?
[ ... ]
> @@ -2235,11 +2244,12 @@ static bool migrate_prepare(MigrationState *s, bool resume, Error **errp)
> return false;
> }
>
> + yank_register_instance(MIGRATION_YANK_INSTANCE, &error_abort);
> +
> return true;
> }
[Severity: Medium]
Can this abort QEMU instead of returning a QMP error when the previous
outgoing migration's cleanup has not run yet?
The unregister is the last step of migration_cleanup(), which runs only
from the bottom half scheduled in migration_iteration_finish():
migration_thread()
s->state set to FAILED (or COMPLETED)
migration_iteration_finish()
bql_lock();
...
migration_bh_schedule(migration_cleanup_bh, s);
bql_unlock();
Between the terminal state change and the bottom half running, the main
loop can process a QMP migrate command. In that window
migration_is_running() returns false, since FAILED, CANCELLED and
COMPLETED fall into its default case:
case MIGRATION_STATUS_CANCELLING:
case MIGRATION_STATUS_COLO:
return true;
default:
return false;
The RUN_STATE_POSTMIGRATE check does not catch it either, as that is set
later in migration_iteration_finish(). migrate_init() does not look at a
pending cleanup. So migrate_prepare() can reach:
yank_register_instance(MIGRATION_YANK_INSTANCE, &error_abort);
while the old instance is still registered. yank_register_instance() then
does error_setg(errp, "duplicate yank instance"), which with &error_abort
calls abort().
Before this patch the registration in qmp_migrate_finish() used the
caller's errp, so the same race produced a QMP error.
The commit message adds a guard for the incoming postcopy case for this
reason, but nothing similar covers the outgoing cleanup-pending window.
The later patches through the series end do not change this.
Could this use a non-fatal errp and return an error from migrate_prepare()
instead, or could migrate_prepare() reject the request while cleanup is
still pending?
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-vfio_v1-v3-0-47487cb5a8e5@virtuozzo.com?part=14
^ permalink raw reply [relevance 5%]
* Re: [PATCH QEMU HCI-8.1 v3 13/28] migration: vmstate_save_state_v: fix double error_setg
2026-10-16 15:26 11% ` [PATCH QEMU HCI-8.1 v3 13/28] migration: vmstate_save_state_v: fix double error_setg Andrey Drobyshev
@ 2026-10-16 15:38 6% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 15:38 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] migration: vmstate_save_state_v() sets Error only for ret < 0 but prepends for any ret != 0
--
commit e68e95a3b4262b7b5d21222eb858ca4b5d64cc07
Author: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
migration: vmstate_save_state_v: fix double error_setg
This patch avoids calling error_setg() twice on the same errp in
vmstate_save_state_v(). It adds error_setg() after a failing put()
callback, and it changes the later "Save of field" error_setg() to
error_prepend().
Link: https://lore.kernel.org/qemu-devel/20260304212303.667141-2-vsementsov@yandex-team.ru
> diff --git a/migration/vmstate.c b/migration/vmstate.c
> index aea41c099463..04903131257b 100644
> --- a/migration/vmstate.c
> +++ b/migration/vmstate.c
[ ... ]
> @@ -523,6 +523,9 @@ int vmstate_save_state_v(QEMUFile *f, const VMStateDescription *vmsd,
> } else {
> ret = inner_field->info->put(f, curr_elem, size,
> inner_field, vmdesc_loop);
> + if (ret < 0) {
> + error_setg(errp, "put failed");
> + }
> }
>
> written_bytes = qemu_file_transferred(f) - old_offset;
> @@ -535,8 +538,8 @@ int vmstate_save_state_v(QEMUFile *f, const VMStateDescription *vmsd,
> }
>
> if (ret) {
> - error_setg(errp, "Save of field %s/%s failed",
> - vmsd->name, field->name);
> + error_prepend(errp, "Save of field %s/%s failed: ",
> + vmsd->name, field->name);
> if (vmsd->post_save) {
> vmsd->post_save(opaque);
> }
[Severity: Medium]
Can error_prepend() dereference a NULL *errp here?
The new error_setg(errp, "put failed") after put() is guarded by
"ret < 0", but the failure branch below is guarded by "if (ret)". For a
positive non-zero ret, nothing sets *errp before:
if (ret) {
error_prepend(errp, "Save of field %s/%s failed: ", ...);
and error_vprepend() only checks !errp before using the message:
util/error.c:error_vprepend() {
...
if (!errp) {
return;
}
newmsg = g_string_new(NULL);
g_string_vprintf(newmsg, fmt, ap);
g_string_append(newmsg, (*errp)->msg);
...
}
Before this patch every non-zero ret went through error_setg(), so this
asymmetry is new.
A positive value could reach this point through the wrapper callbacks
that return the nested vmstate_save_state() result unchanged while
passing a local Error instead of the outer errp, for example:
migration/vmstate-types.c:put_tmp() {
...
ret = vmstate_save_state(f, vmsd, tmp, vmdesc, &local_err);
if (ret) {
error_report_err(local_err);
}
g_free(tmp);
return ret;
}
put_gtree() has the same shape and returns capsule.ret. The nested
vmstate_save_state_v() returns any non-zero pre_save or post_save result:
ret = vmsd->pre_save(opaque);
...
if (ret) {
error_setg(errp, "pre-save failed: %s", vmsd->name);
return ret;
}
so a VMStateDescription whose pre_save or post_save returns a positive
value would leave the outer *errp unset and crash in error_prepend().
The same crash would also occur for a negative ret if errp were
&error_warn, since error_handle() leaves *errp NULL in that case. I did
not find a caller that passes error_warn.
I did not find an in-tree put() callback that returns a positive value
today, so this looks speculative, but I did not audit every pre_save,
post_save or target/ put callback. The later patches in the series do
not change migration/vmstate.c or migration/vmstate-types.c.
Would it make sense to make the two guards consistent, for example by
calling error_setg() for any non-zero ret, or by checking
"if (ret && !*errp)" before error_setg()? Using error_propagate_prepend()
semantics is another option.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-vfio_v1-v3-0-47487cb5a8e5@virtuozzo.com?part=13
^ permalink raw reply [relevance 6%]
* Re: [PATCH QEMU HCI-8.1 v3 12/28] migration: Fix regression of passing error_fatal into vmstate_load_state()
2026-10-16 15:26 12% ` [PATCH QEMU HCI-8.1 v3 12/28] migration: Fix regression of passing error_fatal into vmstate_load_state() Andrey Drobyshev
@ 2026-10-16 15:35 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 15:35 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] migration: Error leaked when vmstate hooks return positive ret (ret < 0 check)
- [Low] migration: void vmstate save wrappers only report errors and leave QEMUFile unmarked
--
commit e28996c9e3a0b9aff33ccbdf09f181f304ec549c
Author: Arun Menon <armenon@redhat.com>
migration: Fix regression of passing error_fatal into vmstate_load_state()
This commit replaces error_fatal with a local Error object in the callers of
vmstate_load_state() and vmstate_save_state(). Failures are now reported with
error_report_err() and, where the caller can return a value, propagated.
Link: https://lore.kernel.org/r/20251028-solve_error_fatal_regression-v2-1-dab24c808a28@redhat.com
> diff --git a/hw/pci/pci.c b/hw/pci/pci.c
> --- a/hw/pci/pci.c
> +++ b/hw/pci/pci.c
[ ... ]
> void pci_device_save(PCIDevice *s, QEMUFile *f)
> {
> + Error *local_err = NULL;
> + int ret;
> +
> /* Clear interrupt status bit: it is implicit
> * in irq_state which we are saving.
> * This makes us compatible with old devices
> * which never set or clear this bit. */
> s->config[PCI_STATUS] &= ~PCI_STATUS_INTERRUPT;
> - vmstate_save_state(f, &vmstate_pci_device, s, NULL, &error_fatal);
> + ret = vmstate_save_state(f, &vmstate_pci_device, s, NULL, &local_err);
> + if (ret < 0) {
> + error_report_err(local_err);
> + }
> /* Restore the interrupt status bit. */
> pci_update_irq_status(s);
> }
[Severity: Low]
pci_device_save() is a void function. It only prints the error and carries
on to pci_update_irq_status().
vmstate_save_state_v() does not call qemu_file_set_error() on failure, so
the QEMUFile is not marked as failed here.
Can a failed nested save (a nonzero pre_save, put or post_save return) in
pci_device_save() leave a truncated device section in the stream while the
save still looks successful?
The same pattern is present in virtio_ccw_save_config(),
virtio_mmio_save_extra_state() and virtio_pci_save_extra_state().
[ ... ]
> diff --git a/hw/scsi/spapr_vscsi.c b/hw/scsi/spapr_vscsi.c
> --- a/hw/scsi/spapr_vscsi.c
> +++ b/hw/scsi/spapr_vscsi.c
[ ... ]
> + rc = vmstate_save_state(f, &vmstate_spapr_vscsi_req, req, NULL, &local_err);
> + if (rc < 0) {
> + error_report_err(local_err);
> + return;
> + }
[Severity: Low]
In vscsi_save_request(), the error is printed and then dropped. The
QEMUFile is not marked failed, and the caller cannot tell that the request
was not saved.
Should the failure be recorded on the stream, for example with
qemu_file_set_error(), so that this and the other void save helpers do not
hide it?
The commit message says the change is meant to keep "either propagating the
error to the caller or reporting it". That holds for the load paths,
virtio_gpu_save() and virtio_save(), but not for these void helpers.
[ ... ]
> diff --git a/hw/virtio/virtio.c b/hw/virtio/virtio.c
> --- a/hw/virtio/virtio.c
> +++ b/hw/virtio/virtio.c
[ ... ]
> /* Subsections */
> - return vmstate_save_state(f, &vmstate_virtio, vdev, NULL, &error_fatal);
> + ret = vmstate_save_state(f, &vmstate_virtio, vdev, NULL, &local_err);
> + if (ret < 0) {
> + error_report_err(local_err);
> + }
> + return ret;
> }
[Severity: Low]
The earlier vdc->vmsd call in virtio_save() tests "if (ret)". This new call
tests "ret < 0".
vmstate_save_state_v() calls error_setg(errp, ...) for any nonzero return
from pre_save, and vmstate_load_state() does the same for any nonzero
pre_load return. The load side also returns early on a nonzero
vmstate_subsection_load() result.
If one of those hooks returns a positive value, local_err is set but never
passed to error_report_err(). Can that leak the Error and leave the failure
unreported?
The same "ret < 0" check is used in pci_device_save(), pci_device_load(),
virtio_ccw_save_config(), virtio_ccw_load_config(),
virtio_mmio_save_extra_state(), virtio_mmio_load_extra_state(),
virtio_pci_save_extra_state(), virtio_pci_load_extra_state(),
virtio_gpu_save(), virtio_gpu_load() and vscsi_save_request().
I did not find an in-tree hook that returns a positive value. This looks
like a conditional problem, because hooks conventionally return negative
errno. Would testing "if (ret)" here, as the vdc->vmsd call does, be more
consistent?
The follow-up commit e68e95a3b426 ("migration: vmstate_save_state_v: fix
double error_setg") does not change this. The nonzero-return error_setg()
calls in vmstate.c are unchanged, and the call sites still use "ret < 0".
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-vfio_v1-v3-0-47487cb5a8e5@virtuozzo.com?part=12
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 v3 10/28] q35: Fix migration of SMRAM state
2026-10-16 15:26 4% ` [PATCH QEMU HCI-8.1 v3 10/28] q35: Fix migration of SMRAM state Andrey Drobyshev
@ 2026-10-16 15:33 0% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 15:33 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev
> From: Igor Mammedov <imammedo@redhat.com>
>
> When migrating, dst QEMU by default has SMRAM unlocked,
> and since wmask is not migrated, the migrated value of
> MCH_HOST_BRIDGE_F_SMBASE in config space fall to prey of
>
> mch_update_smbase_smram()
> ...
> if (pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] == 0xff) {
> *reg = 0x00;
>
> and is getting cleared and leads to unlocked smram
> on dst even if on source it's been locked.
>
> As Andrey has pointed out [1], we should derive wmask
> [ ... ]
> Reported-by: Andrey Ryabinin <arbn@yandex-team.com>
> Signed-off-by: Igor Mammedov <imammedo@redhat.com>
> Reviewed-by: Andrey Ryabinin <arbn@yandex-team.com>
> Reviewed-by: Michael S. Tsirkin <mst@redhat.com>
> Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-vfio_v1-v3-0-47487cb5a8e5@virtuozzo.com?part=10
^ permalink raw reply [relevance 0%]
* Re: [PATCH QEMU HCI-8.1 v3 04/28] migration: Plug memory leaks after migrate_set_error()
2026-10-16 15:26 13% ` [PATCH QEMU HCI-8.1 v3 04/28] migration: Plug memory leaks after migrate_set_error() Andrey Drobyshev
@ 2026-10-16 15:29 0% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 15:29 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev
> From: Markus Armbruster <armbru@redhat.com>
>
> migrate_set_error(s, err) stores a copy of @err in @s. The original
> @err is not freed. Most callers free it immediately. Some callers
> free it later, or pass it on. And some leak it. Fix those.
>
> Perhaps migrate_set_error(s, err) should take ownership of @err. The
> callers that free it immediately would become simpler, and avoid a
> copy and a deallocation. The others would have to pass
> error_copy(err).
>
> andrey.drobyshev:
> This commit is cherry-picked as-is, with no modifications, from the
> upstream commit 93817ec39656.
>
> Signed-off-by: Markus Armbruster <armbru@redhat.com>
> Link: https://lore.kernel.org/r/20251115083500.2753895-2-armbru@redhat.com
> Signed-off-by: Peter Xu <peterx@redhat.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-vfio_v1-v3-0-47487cb5a8e5@virtuozzo.com?part=4
^ permalink raw reply [relevance 0%]
* [PATCH QEMU HCI-8.1 v3 26/28] VZ: migration/cpr-exec: don't exec if a vhost-net device can't be released
` (16 preceding siblings ...)
2026-10-16 15:26 10% ` [PATCH QEMU HCI-8.1 v3 25/28] VZ: migration/cpr-exec: take the vhost-net devices back if exec fails Andrey Drobyshev
@ 2026-10-16 15:26 16% ` Andrey Drobyshev
2026-10-16 15:26 6% ` [PATCH QEMU HCI-8.1 v3 27/28] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860 Andrey Drobyshev
18 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
vhost_net_cpr_reset_owners() only warns when VHOST_RESET_OWNER fails for
a device, and the exec goes on. But the new QEMU can't take such a
device: it is still owned by the old mm, so VHOST_SET_OWNER fails with
EBUSY, and the guest is left without network after the update.
Stop at the first device that can't be released and abort the cpr-exec
instead. The VM goes on in the old QEMU, with the devices released so
far taken back. vhost-vsock already does that in its pre_save.
Fixes: 1278e4830052 ("VZ: migration/cpr-exec: vhost-net: send RESET_OWNER before cpr-exec")
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/net/vhost_net.c | 19 +++++++++++++------
include/net/vhost_net.h | 2 +-
migration/cpr-exec.c | 7 ++++++-
3 files changed, 20 insertions(+), 8 deletions(-)
diff --git a/hw/net/vhost_net.c b/hw/net/vhost_net.c
index ae98574fcfe..a556b41f5f7 100644
--- a/hw/net/vhost_net.c
+++ b/hw/net/vhost_net.c
@@ -579,8 +579,12 @@ void vhost_net_config_mask(VHostNetState *net, VirtIODevice *dev, bool mask)
* EBUSY. Called right before execve(), when the VM is stopped and the
* devices have already been stopped, so nothing is in flight. The device is
* fully re-initialized from the migrated state by the new QEMU.
+ *
+ * Stops at the first device that can't be released, since the new QEMU
+ * couldn't take it. The caller then takes back the ones already released
+ * with vhost_net_cpr_set_owners().
*/
-void vhost_net_cpr_reset_owners(void)
+void vhost_net_cpr_reset_owners(Error **errp)
{
NetClientState *nc;
@@ -591,12 +595,15 @@ void vhost_net_cpr_reset_owners(void)
net->dev.vhost_ops->backend_type == VHOST_BACKEND_TYPE_KERNEL) {
int r = vhost_dev_reset_owner(&net->dev);
- if (r < 0 && r != -ENOSYS) {
- warn_report("cpr: failed to reset vhost-net owner for %s: %s",
- nc->name, strerror(-r));
- } else if (r == 0) {
- net->cpr_owner_reset = true;
+ if (r == -ENOSYS) {
+ continue;
+ }
+ if (r < 0) {
+ error_setg_errno(errp, -r, "cpr: failed to reset vhost-net "
+ "owner for %s", nc->name);
+ return;
}
+ net->cpr_owner_reset = true;
}
}
}
diff --git a/include/net/vhost_net.h b/include/net/vhost_net.h
index cdfefa81500..679edefa1d3 100644
--- a/include/net/vhost_net.h
+++ b/include/net/vhost_net.h
@@ -89,6 +89,6 @@ int vhost_net_virtqueue_restart(VirtIODevice *vdev, NetClientState *nc,
void vhost_net_save_acked_features(NetClientState *nc);
-void vhost_net_cpr_reset_owners(void);
+void vhost_net_cpr_reset_owners(Error **errp);
void vhost_net_cpr_set_owners(void);
#endif
diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
index 21caa614a13..a206ca44a4b 100644
--- a/migration/cpr-exec.c
+++ b/migration/cpr-exec.c
@@ -228,7 +228,10 @@ static void cpr_exec_cb(void *opaque)
* Hand the preserved vhost-net devices over to the new QEMU: they are
* still owned by this mm, which is about to be replaced by execve().
*/
- vhost_net_cpr_reset_owners();
+ vhost_net_cpr_reset_owners(&err);
+ if (err) {
+ goto fail_with_err;
+ }
trace_cpr_exec();
execvp(argv[0], argv);
@@ -238,6 +241,8 @@ static void cpr_exec_cb(void *opaque)
* or the system is very short on resources.
*/
error_setg_errno(&err, errno, "execvp %s failed", argv[0]);
+
+fail_with_err:
error_report_err(error_copy(err));
migrate_set_state(&s->state, s->state, MIGRATION_STATUS_FAILED);
migrate_set_error(s, err);
--
2.47.1
^ permalink raw reply [relevance 16%]
* [PATCH QEMU HCI-8.1 v3 25/28] VZ: migration/cpr-exec: take the vhost-net devices back if exec fails
` (15 preceding siblings ...)
2026-10-16 15:26 6% ` [PATCH QEMU HCI-8.1 v3 24/28] VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved Andrey Drobyshev
@ 2026-10-16 15:26 10% ` Andrey Drobyshev
2026-10-16 15:58 3% ` no-reply
2026-10-16 15:26 16% ` [PATCH QEMU HCI-8.1 v3 26/28] VZ: migration/cpr-exec: don't exec if a vhost-net device can't be released Andrey Drobyshev
2026-10-16 15:26 6% ` [PATCH QEMU HCI-8.1 v3 27/28] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860 Andrey Drobyshev
18 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
Right before execve(), cpr_exec_cb() releases the kernel vhost-net
devices with VHOST_RESET_OWNER, so that the new QEMU can take them. If
execve() fails, the VM goes on in the old QEMU, but nobody takes the
devices back: the kernel refuses the ioctls of the next
vhost_net_start(), and the guest is left without network.
Take them back with VHOST_SET_OWNER when the exec fails. The devices
are stopped at that point, and vhost_net_start() sets up the rest again
when the VM resumes. Only the devices that were actually released are
taken back: vhost_net_cpr_reset_owners() marks them. vhost-vsock does
the same in its CPR notifier.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/net/vhost_net.c | 28 ++++++++++++++++++++++++++++
include/hw/virtio/vhost.h | 2 ++
include/net/vhost_net.h | 1 +
migration/cpr-exec.c | 2 ++
4 files changed, 33 insertions(+)
diff --git a/hw/net/vhost_net.c b/hw/net/vhost_net.c
index 56c697f4bcd..ae98574fcfe 100644
--- a/hw/net/vhost_net.c
+++ b/hw/net/vhost_net.c
@@ -594,6 +594,34 @@ void vhost_net_cpr_reset_owners(void)
if (r < 0 && r != -ENOSYS) {
warn_report("cpr: failed to reset vhost-net owner for %s: %s",
nc->name, strerror(-r));
+ } else if (r == 0) {
+ net->cpr_owner_reset = true;
+ }
+ }
+ }
+}
+
+/*
+ * Take the ownership back when the exec fails and the VM goes on in the
+ * old QEMU. VHOST_RESET_OWNER has reset the kernel device, but the devices
+ * are stopped, and vhost_net_start() sets everything up again when the VM
+ * resumes.
+ */
+void vhost_net_cpr_set_owners(void)
+{
+ NetClientState *nc;
+
+ QTAILQ_FOREACH(nc, &net_clients, next) {
+ VHostNetState *net = get_vhost_net(nc);
+
+ if (net && net->cpr_owner_reset) {
+ int r = vhost_dev_set_owner(&net->dev);
+
+ if (r < 0) {
+ warn_report("cpr: failed to set vhost-net owner for %s: %s",
+ nc->name, strerror(-r));
+ } else {
+ net->cpr_owner_reset = false;
}
}
}
diff --git a/include/hw/virtio/vhost.h b/include/hw/virtio/vhost.h
index a92439d3443..421f7139cfb 100644
--- a/include/hw/virtio/vhost.h
+++ b/include/hw/virtio/vhost.h
@@ -151,6 +151,8 @@ struct vhost_net {
SaveAcketFeatures *save_acked_features;
bool is_vhost_user;
NetClientState *nc;
+ /* released for cpr-exec, take back if it fails */
+ bool cpr_owner_reset;
};
/**
diff --git a/include/net/vhost_net.h b/include/net/vhost_net.h
index 39ce7ad0e3d..cdfefa81500 100644
--- a/include/net/vhost_net.h
+++ b/include/net/vhost_net.h
@@ -90,4 +90,5 @@ int vhost_net_virtqueue_restart(VirtIODevice *vdev, NetClientState *nc,
void vhost_net_save_acked_features(NetClientState *nc);
void vhost_net_cpr_reset_owners(void);
+void vhost_net_cpr_set_owners(void);
#endif
diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
index dd2ff9b26ff..21caa614a13 100644
--- a/migration/cpr-exec.c
+++ b/migration/cpr-exec.c
@@ -251,6 +251,8 @@ fail:
if (cpr_ram_rdonly) {
cpr_ram_restore_rw();
}
+ /* Still the same mm, take back the vhost-net devices we released */
+ vhost_net_cpr_set_owners();
/* Note, we can go from state COMPLETED to FAILED */
migration_call_notifiers(MIG_EVENT_PRECOPY_FAILED, NULL);
--
2.47.1
^ permalink raw reply [relevance 10%]
* [PATCH QEMU HCI-8.1 v3 27/28] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860
` (17 preceding siblings ...)
2026-10-16 15:26 16% ` [PATCH QEMU HCI-8.1 v3 26/28] VZ: migration/cpr-exec: don't exec if a vhost-net device can't be released Andrey Drobyshev
@ 2026-10-16 15:26 6% ` Andrey Drobyshev
2026-10-16 16:02 4% ` no-reply
18 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
After cpr-exec, new QEMU mmaps the device BARs again on the preserved
device fd. Some drivers don't allow that: the NVIDIA vGPU VFIO driver
refuses to map a VF BAR that is still recorded as mapped by old QEMU,
and that record is dropped only when the device fd is closed. So BAR1
and BAR3 of the vGPU can't be mapped, and the guest loses the device.
Keep the mappings instead. Save each BAR mapping in the CPR ramblock
list, so cpr_exec_cb() marks it with MADV_DOEXEC together with guest RAM,
and new QEMU finds it there and reuses it instead of calling mmap().
Same find-or-create pattern as for other preserved objects, e.g. vector
eventfds in vfio_notifier_init(). No saved mapping means old QEMU didn't
map the region, e.g. its mmap failed or the region is mapped lazily, so
new QEMU maps it afresh.
The ID is "<device name> region <index> mmaps[<index>]". It doesn't
depend on the device path, which is gone when the display region is
finalized, so the entry is dropped together with the mapping.
The saved addresses only make sense for cpr-exec, which keeps the
address space. A cpr-transfer target is another process and would have
to map the regions anew, which the drivers above refuse while the old
mappings exist. So block cpr-transfer for VFIO devices.
This needs a kernel which supports MADV_DOEXEC on shared PFN mappings of
device memory. Without it cpr-exec of a VM with VFIO devices fails.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/vfio/device.c | 24 ++++++++++++++++++++++--
hw/vfio/region.c | 45 +++++++++++++++++++++++++++++++++++++++++++++
include/hw/vfio/vfio-cpr.h | 1 +
3 files changed, 68 insertions(+), 2 deletions(-)
diff --git a/hw/vfio/device.c b/hw/vfio/device.c
index 5a14a7d5c0a..2a7073ccec8 100644
--- a/hw/vfio/device.c
+++ b/hw/vfio/device.c
@@ -464,12 +464,32 @@ bool vfio_device_attach(char *name, VFIODevice *vbasedev,
TYPE_VFIO_IOMMU_IOMMUFD :
TYPE_VFIO_IOMMU_LEGACY;
- return vfio_device_attach_by_iommu_type(iommu_type, name, vbasedev,
- as, errp);
+ /*
+ * Only cpr-exec keeps the address space and so the region mappings,
+ * see vfio_region_mmap(). A cpr-transfer target would have to map the
+ * regions anew, which some drivers (NVIDIA vGPU) refuse.
+ */
+ error_setg(&vbasedev->cpr.mmap_blocker,
+ "cpr-transfer is not supported with VFIO devices; "
+ "use the cpr-exec mode");
+ if (migrate_add_blocker_modes(&vbasedev->cpr.mmap_blocker, errp,
+ MIG_MODE_CPR_TRANSFER, -1)) {
+ return false;
+ }
+
+ if (!vfio_device_attach_by_iommu_type(iommu_type, name, vbasedev,
+ as, errp)) {
+ migrate_del_blocker(&vbasedev->cpr.mmap_blocker);
+ return false;
+ }
+
+ return true;
}
void vfio_device_detach(VFIODevice *vbasedev)
{
+ migrate_del_blocker(&vbasedev->cpr.mmap_blocker);
+
if (!vbasedev->bcontainer) {
return;
}
diff --git a/hw/vfio/region.c b/hw/vfio/region.c
index b0332f27fcd..4d58794b3cf 100644
--- a/hw/vfio/region.c
+++ b/hw/vfio/region.c
@@ -30,6 +30,7 @@
#include "qemu/units.h"
#include "monitor/monitor.h"
#include "system/ramblock.h"
+#include "migration/cpr.h"
#include "vfio-helpers.h"
/*
@@ -227,8 +228,43 @@ int vfio_region_setup(Object *obj, VFIODevice *vbasedev, VFIORegion *region,
return 0;
}
+static char *vfio_region_cpr_name(VFIORegion *region, int index)
+{
+ return g_strdup_printf("%s region %d mmaps[%d]", region->vbasedev->name,
+ region->nr, index);
+}
+
+/*
+ * A saved mapping is one preserved by cpr-exec, reuse it. None means old
+ * QEMU didn't map the region, e.g. its mmap failed or it's mapped lazily,
+ * so map it afresh.
+ */
+static void *vfio_region_cpr_find(VFIORegion *region, int index)
+{
+ g_autofree char *name = vfio_region_cpr_name(region, index);
+ cpr_ramblock_t ram = cpr_find_ramblock(name);
+
+ if (ram.addr) {
+ assert(ram.length == region->mmaps[index].size);
+ }
+
+ return (void *)(uintptr_t)ram.addr;
+}
+
+static void vfio_region_cpr_save(VFIORegion *region, int index)
+{
+ g_autofree char *name = vfio_region_cpr_name(region, index);
+ void *addr = region->mmaps[index].mmap;
+ size_t size = region->mmaps[index].size;
+
+ cpr_save_ramblock(name, (cpr_ramblock_t) { (uintptr_t)addr, size });
+}
+
static void vfio_subregion_unmap(VFIORegion *region, int index)
{
+ g_autofree char *cpr_name = vfio_region_cpr_name(region, index);
+
+ cpr_delete_ramblock(cpr_name);
trace_vfio_region_unmap(memory_region_name(®ion->mmaps[index].mem),
region->mmaps[index].offset,
region->mmaps[index].offset +
@@ -314,6 +350,11 @@ int vfio_region_mmap(VFIORegion *region)
size_t align = MIN(1ULL << ctz64(region->mmaps[i].size), 1 * GiB);
void *map_base, *map_align;
+ region->mmaps[i].mmap = vfio_region_cpr_find(region, i);
+ if (region->mmaps[i].mmap) {
+ goto mapped;
+ }
+
/*
* Align the mmap for more efficient mapping in the kernel. Ideally
* we'd know the PMD and PUD mapping sizes to use as discrete alignment
@@ -347,7 +388,9 @@ int vfio_region_mmap(VFIORegion *region)
ret = -errno;
goto no_mmap;
}
+ vfio_region_cpr_save(region, i);
+mapped:
name = g_strdup_printf("%s mmaps[%d]",
memory_region_name(region->mem), i);
memory_region_init_ram_device_ptr(®ion->mmaps[i].mem,
@@ -427,6 +470,8 @@ void vfio_region_finalize(VFIORegion *region)
for (i = 0; i < region->nr_mmaps; i++) {
if (region->mmaps[i].mmap) {
+ g_autofree char *cpr_name = vfio_region_cpr_name(region, i);
+ cpr_delete_ramblock(cpr_name);
munmap(region->mmaps[i].mmap, region->mmaps[i].size);
}
}
diff --git a/include/hw/vfio/vfio-cpr.h b/include/hw/vfio/vfio-cpr.h
index 4606da500a7..fa1699857ca 100644
--- a/include/hw/vfio/vfio-cpr.h
+++ b/include/hw/vfio/vfio-cpr.h
@@ -34,6 +34,7 @@ typedef struct VFIOContainerCPR {
typedef struct VFIODeviceCPR {
Error *mdev_blocker;
Error *id_blocker;
+ Error *mmap_blocker;
uint32_t hwpt_id;
uint32_t ioas_id;
} VFIODeviceCPR;
--
2.47.1
^ permalink raw reply [relevance 6%]
* [PATCH QEMU HCI-8.1 v3 24/28] VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved
` (14 preceding siblings ...)
@ 2026-10-16 15:26 6% ` Andrey Drobyshev
2026-10-16 15:26 10% ` [PATCH QEMU HCI-8.1 v3 25/28] VZ: migration/cpr-exec: take the vhost-net devices back if exec fails Andrey Drobyshev
` (2 subsequent siblings)
18 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
cpr_exec_cb() marks all saved ramblocks with MADV_DOEXEC before exec, but
ignores the result. If madvise() fails for some block, the walk stops
there and the remaining blocks are not marked, yet execvp() still runs.
The new QEMU then reuses addresses of guest RAM that wasn't preserved,
and the guest is lost.
Treat that as a failed cpr-exec: go to the same failure path as when
execvp() fails, so the VM keeps running in the old QEMU.
On that path also clear MADV_DOEXEC from all saved ramblocks again, just
like the preserved fds get close-on-exec back.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
migration/cpr-exec.c | 32 +++++++++++++++++++++++++-------
1 file changed, 25 insertions(+), 7 deletions(-)
diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
index d37293baec5..dd2ff9b26ff 100644
--- a/migration/cpr-exec.c
+++ b/migration/cpr-exec.c
@@ -155,6 +155,19 @@ static bool preserve_ram(cpr_ramblock_t ram, char *name)
return true;
}
+static bool unpreserve_ram(cpr_ramblock_t ram, char *name)
+{
+#ifdef MADV_DONTEXEC
+ qemu_madvise((void *) ram.addr, ram.length, MADV_DONTEXEC);
+#endif
+ return true;
+}
+
+static void cpr_exec_unpreserve_ram(void)
+{
+ cpr_walk_ramblock(unpreserve_ram);
+}
+
static bool mark_ram_rdonly(cpr_ramblock_t ram, char *name)
{
/*
@@ -203,7 +216,10 @@ static void cpr_exec_cb(void *opaque)
*/
cpr_exec_preserve_fds();
- cpr_walk_ramblock(preserve_ram);
+ if (!cpr_walk_ramblock(preserve_ram)) {
+ /* preserve_ram() has already reported the error and failed migration */
+ goto fail;
+ }
if (cpr_ram_rdonly) {
cpr_walk_ramblock(mark_ram_rdonly);
}
@@ -222,18 +238,20 @@ static void cpr_exec_cb(void *opaque)
* or the system is very short on resources.
*/
error_setg_errno(&err, errno, "execvp %s failed", argv[0]);
- g_clear_pointer(&argv, g_strfreev);
- cpr_exec_unpreserve_fds();
- if (cpr_ram_rdonly) {
- cpr_ram_restore_rw();
- }
-
error_report_err(error_copy(err));
migrate_set_state(&s->state, s->state, MIGRATION_STATUS_FAILED);
migrate_set_error(s, err);
error_free(err);
err = NULL;
+fail:
+ g_clear_pointer(&argv, g_strfreev);
+ cpr_exec_unpreserve_fds();
+ cpr_exec_unpreserve_ram();
+ if (cpr_ram_rdonly) {
+ cpr_ram_restore_rw();
+ }
+
/* Note, we can go from state COMPLETED to FAILED */
migration_call_notifiers(MIG_EVENT_PRECOPY_FAILED, NULL);
--
2.47.1
^ permalink raw reply [relevance 6%]
* [PATCH QEMU HCI-8.1 v3 17/28] migration: Fix crash on second migration when cancel early
` (9 preceding siblings ...)
2026-10-16 15:26 28% ` [PATCH QEMU HCI-8.1 v3 16/28] migration: Move CPR HUP watch to cpr-transfer.c Andrey Drobyshev
@ 2026-10-16 15:26 23% ` Andrey Drobyshev
2026-10-16 15:44 5% ` no-reply
2026-10-16 15:26 23% ` [PATCH QEMU HCI-8.1 v3 18/28] kvm: tolerate ioctls after kvm_close() Andrey Drobyshev
` (7 subsequent siblings)
18 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Peter Xu <peterx@redhat.com>
Marc-André reported an issue on QEMU crash when retrying a cancelled
migration during early setup phase, see "Link:" for more information, and
also easy way to reproduce.
This patch is a replacement of the prior fix proposed by not only switching
to migration_cleanup(), but also fixing it from CPR side, so that we track
hup_source properly to know if src QEMU is waiting or the HUP signal.
To put it simple: this chunk of special casing in migration_cancel() should
not affect normal migration, but only cpr-transfer migration to cover the
small window when the src QEMU is waiting for a HUP signal on cpr
channel (so that src QEMU can continue the migration on the main channel).
To achieve that, we'll also need to remember to detach the hup_source
whenenver invoked: after that point, we should always be able to cleanup
the migration.
It's not a generic operation to explicitly detach a gsource from its
context while in its dispatch() function. But it should be safe, because
gsource disptch() will only happen with a boosted refcount for the
dispatcher so that the gsource will not be freed until the callback
completes. It's also safe to return G_SOURCE_REMOVE after the gsource is
detached, as glib will simply ignore the G_SOURCE_REMOVE.
One can refer to latest 2.86.5 glib code in g_main_dispatch() for that:
https://github.com/GNOME/glib/blob/2.86.5/glib/gmain.c#L3592
When at this, add a bunch of assertions to make sure nothing surprises us.
After this patch applied, the 2nd migration will not crash QEMU, instead
it'll be in CANCELLING until the socket connection times out (it will take
~2min on my Fedora default kernel). During this process no 2nd migration
will be allowed, and after it timed out migration can be restarted.
It's because so far we don't have control over socket_connect_outgoing(),
or anything yet managed by a task executed in qio_task_run_in_thread().
Speeding up the cancellation to be left for future.
I also tested cpr-transfer by only providing cpr channel not the main
channel (with -incoming defer), kickoff migration on source, then cancel it
on source directly without providing the main channel. It keeps working.
I wanted to add an unit test for that but it'll need to refactor current
cpr-transfer tests first; let's leave it for later.
andrey.drobyshev:
This commit was cherry-picked from the upstream commit 4f08b870f8f0,
adapted for the hci-dev base, which predates the upstream qmp_migrate()
rework: the HUP callback is still qmp_migrate_finish_cb() rather than
migration_connect_outgoing_cb(), so the early
cpr_transfer_source_destroy() call lands there, ahead of
qmp_migrate_finish() and the cpr_state_close() the callback keeps
doing itself.
Link: https://lore.kernel.org/r/20260417184742.293061-1-marcandre.lureau@redhat.com
Reported-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Tested-by: Fabiano Rosas <farosas@suse.de>
Reviewed-by: Fabiano Rosas <farosas@suse.de>
Link: https://lore.kernel.org/r/20260421175820.302795-1-peterx@redhat.com
Signed-off-by: Peter Xu <peterx@redhat.com>
---
include/migration/cpr.h | 1 +
migration/cpr-transfer.c | 10 ++++++++++
migration/migration.c | 32 ++++++++++++++++++++++++--------
migration/migration.h | 5 +++++
4 files changed, 40 insertions(+), 8 deletions(-)
diff --git a/include/migration/cpr.h b/include/migration/cpr.h
index ae441322ff5..4dd397a7c13 100644
--- a/include/migration/cpr.h
+++ b/include/migration/cpr.h
@@ -80,6 +80,7 @@ QEMUFile *cpr_transfer_input(MigrationChannel *channel, Error **errp);
void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
void *opaque);
void cpr_transfer_source_destroy(MigrationState *s);
+bool cpr_transfer_source_active(MigrationState *s);
void cpr_exec_init(void);
QEMUFile *cpr_exec_output(Error **errp);
diff --git a/migration/cpr-transfer.c b/migration/cpr-transfer.c
index 61d5c9dce2b..9defe7bad78 100644
--- a/migration/cpr-transfer.c
+++ b/migration/cpr-transfer.c
@@ -6,6 +6,7 @@
*/
#include "qemu/osdep.h"
+#include "qemu/main-loop.h"
#include "qapi/clone-visitor.h"
#include "qapi/error.h"
#include "qapi/qapi-visit-migration.h"
@@ -79,6 +80,7 @@ QEMUFile *cpr_transfer_input(MigrationChannel *channel, Error **errp)
void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
void *opaque)
{
+ assert(bql_locked());
s->hup_source = qio_channel_create_watch(cpr_state_ioc(), G_IO_HUP);
g_source_set_callback(s->hup_source,
(GSourceFunc)func,
@@ -89,9 +91,17 @@ void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
void cpr_transfer_source_destroy(MigrationState *s)
{
+ assert(bql_locked());
if (s->hup_source) {
g_source_destroy(s->hup_source);
g_source_unref(s->hup_source);
s->hup_source = NULL;
}
}
+
+bool cpr_transfer_source_active(MigrationState *s)
+{
+ /* Whenever the HUP gsource is available, it's active. */
+ assert(bql_locked());
+ return s->hup_source;
+}
diff --git a/migration/migration.c b/migration/migration.c
index c138fa40e10..f48e75e8fad 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -1653,14 +1653,19 @@ void migration_cancel(void)
}
/*
- * If qmp_migrate_finish has not been called, then there is no path that
- * will complete the cancellation. Do it now.
+ * This is cpr-transfer specific processing.
+ *
+ * If this is true, it means cpr-transfer migration is waiting for the
+ * destination to send HUP event on CPR channel to continue the next
+ * phase. If so, do the cleanup proactively to avoid get stuck in
+ * CANCELLING state.
*/
- if (setup && !s->to_dst_file) {
- migrate_set_state(&s->state, MIGRATION_STATUS_CANCELLING,
- MIGRATION_STATUS_CANCELLED);
- cpr_state_close();
- cpr_transfer_source_destroy(s);
+ if (cpr_transfer_source_active(s)) {
+ assert(migrate_mode() == MIG_MODE_CPR_TRANSFER);
+ assert(setup && !s->to_dst_file);
+ migration_cleanup(s);
+ /* Now all things should have been released */
+ assert(!cpr_transfer_source_active(s));
}
}
@@ -2255,11 +2260,22 @@ static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
void *opaque)
{
MigrationAddress *addr = opaque;
+ MigrationState *s = migrate_get_current();
+
+ /*
+ * Detach and release the GSource right after use. We rely on this to
+ * detect this small cpr-transfer window of "waiting for HUP event".
+ */
+ cpr_transfer_source_destroy(s);
qmp_migrate_finish(addr, NULL);
cpr_state_close();
- cpr_transfer_source_destroy(migrate_get_current());
+
+ /*
+ * This is redundant as we do cpr_transfer_source_destroy() at the
+ * entry, but it's benign; glib will just skip the detach.
+ */
return G_SOURCE_REMOVE;
}
diff --git a/migration/migration.h b/migration/migration.h
index a7ea43e925d..ee15ca88461 100644
--- a/migration/migration.h
+++ b/migration/migration.h
@@ -509,6 +509,11 @@ struct MigrationState {
/* Is this a rdma migration */
bool rdma_migration;
+ /*
+ * When set, it means cpr-transfer is waiting for the HUP signal from
+ * destination to continue the 2nd step of migration via the main
+ * channel.
+ */
GSource *hup_source;
};
--
2.47.1
^ permalink raw reply [relevance 23%]
* [PATCH QEMU HCI-8.1 v3 18/28] kvm: tolerate ioctls after kvm_close()
` (10 preceding siblings ...)
2026-10-16 15:26 23% ` [PATCH QEMU HCI-8.1 v3 17/28] migration: Fix crash on second migration when cancel early Andrey Drobyshev
@ 2026-10-16 15:26 23% ` Andrey Drobyshev
2026-10-16 15:44 7% ` no-reply
` (6 subsequent siblings)
18 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
Since commit 7ed0919119b0 ("migration: close kvm after cpr") a source
QEMU that has handed its VFIO devices over via cpr-transfer (or
cpr-exec) calls kvm_close() once the migration completes, so that the
destination can register its irq bypass consumers. kvm_close() closes
the VM and vCPU descriptors and sets kvm_state to NULL, but the source
process keeps living with all its device models intact until the
management layer terminates it.
Nothing prevents those device models from raising interrupts in the
meantime, and the regular exit path even guarantees one: net_cleanup()
deletes the netdev behind an emulated virtio-net, the NIC sees the link
go down and calls virtio_notify_config(), MSI-X delivers through the
in-kernel APIC, and kvm_irqchip_send_msi() dereferences the NULL
kvm_state:
#0 kvm_vm_ioctl (s=0x0, type=KVM_SIGNAL_MSI) accel/kvm/kvm-all.c
#1 kvm_irqchip_send_msi (s=0x0)
#2 kvm_send_msi hw/i386/kvm/apic.c
#3 kvm_apic_mem_write
...
#9 pci_msi_trigger
#11 msix_notify
#12 virtio_pci_notify
#14 virtio_notify_config
#15 virtio_net_set_link_status
#16 qemu_del_net_client
#17 net_cleanup
#18 qemu_cleanup
Every retired cpr-transfer source with a VFIO device thus dies with
SIGSEGV on SIGTERM/quit and leaves a core dump behind. Without a VFIO
device kvm_close() never runs and the same exit is harmless.
Make the ioctl wrappers honour the closed state instead of assuming a
live KVMState: kvm_ioctl(), kvm_vm_ioctl() and kvm_vcpu_ioctl() return
-ENODEV when kvm_close() has already been called, so any late interrupt
or query on the retired source fails gracefully. Teach kvm_send_msi()
that -ENODEV in this situation is expected and not worth the "MSI lost"
warning on every such interrupt.
Fixes: 7ed0919119b0 ("migration: close kvm after cpr")
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
accel/kvm/kvm-all.c | 15 +++++++++++++++
hw/i386/kvm/apic.c | 6 +++++-
2 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/accel/kvm/kvm-all.c b/accel/kvm/kvm-all.c
index a39c1ba260a..ea027c37829 100644
--- a/accel/kvm/kvm-all.c
+++ b/accel/kvm/kvm-all.c
@@ -3371,6 +3371,11 @@ int kvm_ioctl(KVMState *s, unsigned long type, ...)
arg = va_arg(ap, void *);
va_end(ap);
+ /* kvm_close() has been called: there is no KVM to talk to any more */
+ if (!s || s->fd == -1) {
+ return -ENODEV;
+ }
+
trace_kvm_ioctl(type, arg);
ret = ioctl(s->fd, type, arg);
if (ret == -1) {
@@ -3389,6 +3394,11 @@ int kvm_vm_ioctl(KVMState *s, unsigned long type, ...)
arg = va_arg(ap, void *);
va_end(ap);
+ /* kvm_close() has been called: there is no VM to talk to any more */
+ if (!s || s->vmfd == -1) {
+ return -ENODEV;
+ }
+
trace_kvm_vm_ioctl(type, arg);
accel_ioctl_begin();
ret = ioctl(s->vmfd, type, arg);
@@ -3409,6 +3419,11 @@ int kvm_vcpu_ioctl(CPUState *cpu, unsigned long type, ...)
arg = va_arg(ap, void *);
va_end(ap);
+ /* kvm_close() has been called: the vCPU fds are gone */
+ if (cpu->kvm_fd == -1) {
+ return -ENODEV;
+ }
+
trace_kvm_vcpu_ioctl(cpu->cpu_index, type, arg);
accel_cpu_ioctl_begin(cpu);
ret = ioctl(cpu->kvm_fd, type, arg);
diff --git a/hw/i386/kvm/apic.c b/hw/i386/kvm/apic.c
index 1be9bfe36e9..0c9f7759b6e 100644
--- a/hw/i386/kvm/apic.c
+++ b/hw/i386/kvm/apic.c
@@ -196,7 +196,11 @@ static void kvm_send_msi(MSIMessage *msg)
msg->address = kvm_swizzle_msi_ext_dest_id(msg->address);
ret = kvm_irqchip_send_msi(kvm_state, *msg);
- if (ret < 0) {
+ /*
+ * -ENODEV means kvm_close() has already run (the VM was handed over
+ * via CPR); the interrupt is meaningless here and not worth a warning.
+ */
+ if (ret < 0 && ret != -ENODEV) {
fprintf(stderr, "KVM: injection failed, MSI lost (%s)\n",
strerror(-ret));
}
--
2.47.1
^ permalink raw reply [relevance 23%]
* [PATCH QEMU HCI-8.1 v3 16/28] migration: Move CPR HUP watch to cpr-transfer.c
` (8 preceding siblings ...)
2026-10-16 15:26 19% ` [PATCH QEMU HCI-8.1 v3 15/28] migration: Free cpr-transfer MigrationAddress along with gsource Andrey Drobyshev
@ 2026-10-16 15:26 28% ` Andrey Drobyshev
2026-10-16 15:26 23% ` [PATCH QEMU HCI-8.1 v3 17/28] migration: Fix crash on second migration when cancel early Andrey Drobyshev
` (8 subsequent siblings)
18 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Fabiano Rosas <farosas@suse.de>
Move this CPR-specific code into a cpr file. While here, give the
functions more significant names.
This makes the new idea (after cpr-transfer) of having two parts to
qmp_migrate slightly more obvious: either wait for the hangup or
continue directly.
andrey.drobyshev:
This commit was cherry-picked from the upstream commit f523e5aa5f56,
adapted for the hci-dev base, which predates the upstream qmp_migrate()
rework around MigrationChannel: the watch is still armed with the
plain MigrationAddress from qmp_migrate() rather than main_ch->addr,
and qmp_migrate_finish_cb() keeps closing the cpr state and destroying
the HUP source itself.
Reviewed-by: Peter Xu <peterx@redhat.com>
Reviewed-by: Prasad Pandit <pjp@fedoraproject.org>
Link: https://lore.kernel.org/qemu-devel/20260123141656.6765-24-farosas@suse.de
Signed-off-by: Fabiano Rosas <farosas@suse.de>
---
include/migration/cpr.h | 5 +++++
migration/cpr-transfer.c | 23 +++++++++++++++++++++++
migration/migration.c | 29 ++++-------------------------
3 files changed, 32 insertions(+), 25 deletions(-)
diff --git a/include/migration/cpr.h b/include/migration/cpr.h
index 8fb79eb0a28..ae441322ff5 100644
--- a/include/migration/cpr.h
+++ b/include/migration/cpr.h
@@ -9,6 +9,7 @@
#define MIGRATION_CPR_H
#include "qapi/qapi-types-migration.h"
+#include "io/channel.h"
#include "qemu/queue.h"
#define MIG_MODE_NONE -1
@@ -76,6 +77,10 @@ int cpr_get_fd_param(const char *name, const char *fdname, int index, bool cpr,
QEMUFile *cpr_transfer_output(MigrationChannel *channel, Error **errp);
QEMUFile *cpr_transfer_input(MigrationChannel *channel, Error **errp);
+void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
+ void *opaque);
+void cpr_transfer_source_destroy(MigrationState *s);
+
void cpr_exec_init(void);
QEMUFile *cpr_exec_output(Error **errp);
QEMUFile *cpr_exec_input(Error **errp);
diff --git a/migration/cpr-transfer.c b/migration/cpr-transfer.c
index 00371d17c3a..61d5c9dce2b 100644
--- a/migration/cpr-transfer.c
+++ b/migration/cpr-transfer.c
@@ -6,7 +6,10 @@
*/
#include "qemu/osdep.h"
+#include "qapi/clone-visitor.h"
#include "qapi/error.h"
+#include "qapi/qapi-visit-migration.h"
+#include "io/channel.h"
#include "io/channel-file.h"
#include "io/channel-socket.h"
#include "io/net-listener.h"
@@ -72,3 +75,23 @@ QEMUFile *cpr_transfer_input(MigrationChannel *channel, Error **errp)
return NULL;
}
}
+
+void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
+ void *opaque)
+{
+ s->hup_source = qio_channel_create_watch(cpr_state_ioc(), G_IO_HUP);
+ g_source_set_callback(s->hup_source,
+ (GSourceFunc)func,
+ QAPI_CLONE(MigrationAddress, opaque),
+ (GDestroyNotify)qapi_free_MigrationAddress);
+ g_source_attach(s->hup_source, NULL);
+}
+
+void cpr_transfer_source_destroy(MigrationState *s)
+{
+ if (s->hup_source) {
+ g_source_destroy(s->hup_source);
+ g_source_unref(s->hup_source);
+ s->hup_source = NULL;
+ }
+}
diff --git a/migration/migration.c b/migration/migration.c
index 9ac11d57d5b..c138fa40e10 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -110,7 +110,6 @@ static bool migration_object_check(MigrationState *ms, Error **errp);
static bool migration_switchover_start(MigrationState *s, Error **errp);
static bool close_return_path_on_source(MigrationState *s);
static void migration_completion_end(MigrationState *s);
-static void migrate_hup_delete(MigrationState *s);
static void migration_downtime_start(MigrationState *s)
{
@@ -1500,7 +1499,7 @@ static void migration_cleanup(MigrationState *s)
qemu_savevm_state_cleanup();
cpr_state_close();
- migrate_hup_delete(s);
+ cpr_transfer_source_destroy(s);
close_return_path_on_source(s);
@@ -1661,7 +1660,7 @@ void migration_cancel(void)
migrate_set_state(&s->state, MIGRATION_STATUS_CANCELLING,
MIGRATION_STATUS_CANCELLED);
cpr_state_close();
- migrate_hup_delete(s);
+ cpr_transfer_source_destroy(s);
}
}
@@ -2251,25 +2250,6 @@ static bool migrate_prepare(MigrationState *s, bool resume, Error **errp)
static void qmp_migrate_finish(MigrationAddress *addr, Error **errp);
-static void migrate_hup_add(MigrationState *s, QIOChannel *ioc, GSourceFunc cb,
- void *opaque)
-{
- s->hup_source = qio_channel_create_watch(ioc, G_IO_HUP);
- g_source_set_callback(s->hup_source, cb,
- QAPI_CLONE(MigrationAddress, opaque),
- (GDestroyNotify)qapi_free_MigrationAddress);
- g_source_attach(s->hup_source, NULL);
-}
-
-static void migrate_hup_delete(MigrationState *s)
-{
- if (s->hup_source) {
- g_source_destroy(s->hup_source);
- g_source_unref(s->hup_source);
- s->hup_source = NULL;
- }
-}
-
static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
GIOCondition cond,
void *opaque)
@@ -2279,7 +2259,7 @@ static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
qmp_migrate_finish(addr, NULL);
cpr_state_close();
- migrate_hup_delete(migrate_get_current());
+ cpr_transfer_source_destroy(migrate_get_current());
return G_SOURCE_REMOVE;
}
@@ -2359,8 +2339,7 @@ void qmp_migrate(const char *uri, bool has_channels,
* connection, so qmp_migrate_finish will fail to connect, and then recover.
*/
if (migrate_mode() == MIG_MODE_CPR_TRANSFER) {
- migrate_hup_add(s, cpr_state_ioc(), (GSourceFunc)qmp_migrate_finish_cb,
- addr);
+ cpr_transfer_add_hup_watch(s, qmp_migrate_finish_cb, addr);
} else {
qmp_migrate_finish(addr, errp);
--
2.47.1
^ permalink raw reply [relevance 28%]
* [PATCH QEMU HCI-8.1 v3 14/28] migration: yank: Move register instance earlier
` (6 preceding siblings ...)
2026-10-16 15:26 11% ` [PATCH QEMU HCI-8.1 v3 13/28] migration: vmstate_save_state_v: fix double error_setg Andrey Drobyshev
@ 2026-10-16 15:26 22% ` Andrey Drobyshev
2026-10-16 15:41 5% ` no-reply
2026-10-16 15:26 19% ` [PATCH QEMU HCI-8.1 v3 15/28] migration: Free cpr-transfer MigrationAddress along with gsource Andrey Drobyshev
` (10 subsequent siblings)
18 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Fabiano Rosas <farosas@suse.de>
Move the register_instance call to migrate_prepare() so it can be
paired with the unregister_instance at migration_cleanup(). Otherwise,
the cleanup cannot be run when cpr_state_save() fails because the
instance is registered only after it.
When resuming from a paused postcopy migration, migrate_prepare()
returns early, but migration_cleanup() doesn't run, so the yank will
remain paired.
andrey.drobyshev:
This commit was cherry-picked from the upstream commit 7e714392437c,
adapted for the hci-dev base, which predates the upstream
migration_connect_set_error() -> migration_connect_error_propagate()
rename (77c82edadbe0): the error propagation calls in qmp_migrate()
and qmp_migrate_finish() keep the older spelling.
Also fixed here: upstream left the yank instance registered when
qmp_migrate_finish() fails before connecting (nothing calls
migration_cleanup() then), so the next migrate command aborted on
yank_register_instance(&error_abort); and the unregister at the out:
label ran on a resume, which never registered the instance. Upstream
fixed both in 6b587af5ec9d ("migration: Fold migration_cleanup() into
migration_connect_error_propagate()"), which needs the above rework;
this is the equivalent for our base.
Also refuse migrate while an incoming postcopy migration is still in
progress: the guest already runs then, but the incoming side still
holds the yank instance, so registering it with &error_abort would
abort QEMU.
Reviewed-by: Peter Xu <peterx@redhat.com>
Reviewed-by: Prasad Pandit <pjp@fedoraproject.org>
Link: https://lore.kernel.org/qemu-devel/20260123141656.6765-11-farosas@suse.de
Signed-off-by: Fabiano Rosas <farosas@suse.de>
---
migration/migration.c | 40 ++++++++++++++++++++++++----------------
1 file changed, 24 insertions(+), 16 deletions(-)
diff --git a/migration/migration.c b/migration/migration.c
index 2f8ef20c036..0b830956532 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -2174,6 +2174,15 @@ static bool migrate_prepare(MigrationState *s, bool resume, Error **errp)
return false;
}
+ /*
+ * The guest already runs during incoming postcopy, but the incoming
+ * migration still holds the yank instance registered below.
+ */
+ if (migration_incoming_postcopy_advised()) {
+ error_setg(errp, "Incoming postcopy migration is still in progress");
+ return false;
+ }
+
if (runstate_check(RUN_STATE_POSTMIGRATE)) {
error_setg(errp, "Can't migrate the vm that was paused due to "
"previous migration");
@@ -2235,11 +2244,12 @@ static bool migrate_prepare(MigrationState *s, bool resume, Error **errp)
return false;
}
+ yank_register_instance(MIGRATION_YANK_INSTANCE, &error_abort);
+
return true;
}
-static void qmp_migrate_finish(MigrationAddress *addr, bool resume_requested,
- Error **errp);
+static void qmp_migrate_finish(MigrationAddress *addr, Error **errp);
static void migrate_hup_add(MigrationState *s, QIOChannel *ioc, GSourceFunc cb,
void *opaque)
@@ -2264,7 +2274,7 @@ static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
{
MigrationAddress *addr = opaque;
- qmp_migrate_finish(addr, false, NULL);
+ qmp_migrate_finish(addr, NULL);
cpr_state_close();
migrate_hup_delete(migrate_get_current());
@@ -2276,7 +2286,6 @@ void qmp_migrate(const char *uri, bool has_channels,
MigrationChannelList *channels, bool has_detach, bool detach,
bool has_resume, bool resume, Error **errp)
{
- bool resume_requested;
Error *local_err = NULL;
MigrationState *s = migrate_get_current();
g_autoptr(MigrationChannel) channel = NULL;
@@ -2329,8 +2338,7 @@ void qmp_migrate(const char *uri, bool has_channels,
return;
}
- resume_requested = has_resume && resume;
- if (!migrate_prepare(s, resume_requested, errp)) {
+ if (!migrate_prepare(s, has_resume && resume, errp)) {
/* Error detected, put into errp */
return;
}
@@ -2354,27 +2362,26 @@ void qmp_migrate(const char *uri, bool has_channels,
QAPI_CLONE(MigrationAddress, addr));
} else {
- qmp_migrate_finish(addr, resume_requested, errp);
+ qmp_migrate_finish(addr, errp);
}
out:
if (local_err) {
+ /* A resume didn't register it, see migrate_prepare() */
+ if (s->state != MIGRATION_STATUS_POSTCOPY_RECOVER_SETUP) {
+ yank_unregister_instance(MIGRATION_YANK_INSTANCE);
+ }
migration_connect_set_error(s, local_err);
error_propagate(errp, local_err);
}
}
-static void qmp_migrate_finish(MigrationAddress *addr, bool resume_requested,
- Error **errp)
+static void qmp_migrate_finish(MigrationAddress *addr, Error **errp)
{
MigrationState *s = migrate_get_current();
Error *local_err = NULL;
-
- if (!resume_requested) {
- if (!yank_register_instance(MIGRATION_YANK_INSTANCE, errp)) {
- return;
- }
- }
+ /* Before the start functions below change the state */
+ bool resume = s->state == MIGRATION_STATUS_POSTCOPY_RECOVER_SETUP;
if (addr->transport == MIGRATION_ADDRESS_TYPE_SOCKET) {
SocketAddress *saddr = &addr->u.socket;
@@ -2401,7 +2408,8 @@ static void qmp_migrate_finish(MigrationAddress *addr, bool resume_requested,
}
if (local_err) {
- if (!resume_requested) {
+ /* Nothing connected: migration_cleanup() won't unregister it */
+ if (!resume) {
yank_unregister_instance(MIGRATION_YANK_INSTANCE);
}
migration_connect_set_error(s, local_err);
--
2.47.1
^ permalink raw reply [relevance 22%]
* [PATCH QEMU HCI-8.1 v3 15/28] migration: Free cpr-transfer MigrationAddress along with gsource
` (7 preceding siblings ...)
2026-10-16 15:26 22% ` [PATCH QEMU HCI-8.1 v3 14/28] migration: yank: Move register instance earlier Andrey Drobyshev
@ 2026-10-16 15:26 19% ` Andrey Drobyshev
2026-10-16 15:26 28% ` [PATCH QEMU HCI-8.1 v3 16/28] migration: Move CPR HUP watch to cpr-transfer.c Andrey Drobyshev
` (9 subsequent siblings)
18 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Fabiano Rosas <farosas@suse.de>
When setting a callback on a Glib source and giving it a data pointer,
it's natural to also provide the destructor for the data in question.
Since migrate_hup_add() already needs to clone the MigrationAddress
when setting the qmp_migrate_finish_cb callback, also pass the
qapi_free_MigrationAddress as the GDestroyNotify callback.
With this the address doesn't need to be freed at the callback body,
making the management of that memory slightly simpler.
andrey.drobyshev:
This commit was cherry-picked from the upstream commit 6a887119ce18,
adapted for the hci-dev base: qmp_migrate_finish_cb() here still
closes the cpr state and deletes the HUP source itself (upstream had
already moved that into migration_cleanup()), so only the explicit
qapi_free_MigrationAddress() call goes away from it.
Cc: Mark Kanda <mark.kanda@oracle.com>
Cc: Ben Chaney <bchaney@akamai.com>
Reviewed-by: Peter Xu <peterx@redhat.com>
Reviewed-by: Prasad Pandit <pjp@fedoraproject.org>
Link: https://lore.kernel.org/qemu-devel/20260123141656.6765-23-farosas@suse.de
Signed-off-by: Fabiano Rosas <farosas@suse.de>
---
migration/migration.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/migration/migration.c b/migration/migration.c
index 0b830956532..9ac11d57d5b 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -2254,9 +2254,11 @@ static void qmp_migrate_finish(MigrationAddress *addr, Error **errp);
static void migrate_hup_add(MigrationState *s, QIOChannel *ioc, GSourceFunc cb,
void *opaque)
{
- s->hup_source = qio_channel_create_watch(ioc, G_IO_HUP);
- g_source_set_callback(s->hup_source, cb, opaque, NULL);
- g_source_attach(s->hup_source, NULL);
+ s->hup_source = qio_channel_create_watch(ioc, G_IO_HUP);
+ g_source_set_callback(s->hup_source, cb,
+ QAPI_CLONE(MigrationAddress, opaque),
+ (GDestroyNotify)qapi_free_MigrationAddress);
+ g_source_attach(s->hup_source, NULL);
}
static void migrate_hup_delete(MigrationState *s)
@@ -2278,7 +2280,6 @@ static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
cpr_state_close();
migrate_hup_delete(migrate_get_current());
- qapi_free_MigrationAddress(addr);
return G_SOURCE_REMOVE;
}
@@ -2359,7 +2360,7 @@ void qmp_migrate(const char *uri, bool has_channels,
*/
if (migrate_mode() == MIG_MODE_CPR_TRANSFER) {
migrate_hup_add(s, cpr_state_ioc(), (GSourceFunc)qmp_migrate_finish_cb,
- QAPI_CLONE(MigrationAddress, addr));
+ addr);
} else {
qmp_migrate_finish(addr, errp);
--
2.47.1
^ permalink raw reply [relevance 19%]
* [PATCH QEMU HCI-8.1 v3 12/28] migration: Fix regression of passing error_fatal into vmstate_load_state()
` (4 preceding siblings ...)
2026-10-16 15:26 4% ` [PATCH QEMU HCI-8.1 v3 10/28] q35: Fix migration of SMRAM state Andrey Drobyshev
@ 2026-10-16 15:26 12% ` Andrey Drobyshev
2026-10-16 15:35 3% ` no-reply
2026-10-16 15:26 11% ` [PATCH QEMU HCI-8.1 v3 13/28] migration: vmstate_save_state_v: fix double error_setg Andrey Drobyshev
` (12 subsequent siblings)
18 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Arun Menon <armenon@redhat.com>
error_fatal is passed to vmstate_load_state() and vmstate_save_state()
functions. This was introduced in commit c632ffbd74. This would exit(1)
on error, and therefore does not allow to propagate the error back to
the caller.
To maintain consistency with prior error handling i.e. either propagating
the error to the caller or reporting it, we must set the error within a
local Error object instead of using error_fatal.
Reviewed-by: Cornelia Huck <cohuck@redhat.com>
Signed-off-by: Arun Menon <armenon@redhat.com>
Reviewed-by: Akihiko Odaki <odaki@rsg.ci.i.u-tokyo.ac.jp>
Link: https://lore.kernel.org/r/20251028-solve_error_fatal_regression-v2-1-dab24c808a28@redhat.com
[peterx: always uninit var ret, per Akihiko]
[peterx: touchups on line ordering, spacings etc.]
andrey.drobyshev:
This commit was cherry-picked from the upstream commit 986c3292c6b8,
adapted for the hci-dev base, which predates the upstream commit
bcb536cabe10 ("error: Kill @error_warn"): virtio_gpu_load() has no
'err' local there yet, so it is declared here.
Signed-off-by: Peter Xu <peterx@redhat.com>
---
hw/display/virtio-gpu.c | 22 +++++++++++++++-------
hw/pci/pci.c | 15 +++++++++++++--
hw/s390x/virtio-ccw.c | 17 +++++++++++++++--
hw/scsi/spapr_vscsi.c | 10 ++++++++--
hw/virtio/virtio-mmio.c | 15 +++++++++++++--
hw/virtio/virtio-pci.c | 15 +++++++++++++--
hw/virtio/virtio.c | 10 +++++++---
7 files changed, 84 insertions(+), 20 deletions(-)
diff --git a/hw/display/virtio-gpu.c b/hw/display/virtio-gpu.c
index 477ec700a13..4bc8c778508 100644
--- a/hw/display/virtio-gpu.c
+++ b/hw/display/virtio-gpu.c
@@ -1223,7 +1223,8 @@ static int virtio_gpu_save(QEMUFile *f, void *opaque, size_t size,
{
VirtIOGPU *g = opaque;
struct virtio_gpu_simple_resource *res;
- int i;
+ Error *err = NULL;
+ int i, ret;
/* in 2d mode we should never find unprocessed commands here */
assert(QTAILQ_EMPTY(&g->cmdq));
@@ -1246,8 +1247,12 @@ static int virtio_gpu_save(QEMUFile *f, void *opaque, size_t size,
}
qemu_put_be32(f, 0); /* end of list */
- return vmstate_save_state(f, &vmstate_virtio_gpu_scanouts, g, NULL,
- &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_virtio_gpu_scanouts, g, NULL,
+ &err);
+ if (ret < 0) {
+ error_report_err(err);
+ }
+ return ret;
}
static bool virtio_gpu_load_restore_mapping(VirtIOGPU *g,
@@ -1283,9 +1288,10 @@ static int virtio_gpu_load(QEMUFile *f, void *opaque, size_t size,
const VMStateField *field)
{
VirtIOGPU *g = opaque;
+ Error *err = NULL;
struct virtio_gpu_simple_resource *res;
uint32_t resource_id, pformat;
- int i;
+ int i, ret;
g->hostmem = 0;
@@ -1344,9 +1350,11 @@ static int virtio_gpu_load(QEMUFile *f, void *opaque, size_t size,
}
/* load & apply scanout state */
- vmstate_load_state(f, &vmstate_virtio_gpu_scanouts, g, 1, &error_fatal);
-
- return 0;
+ ret = vmstate_load_state(f, &vmstate_virtio_gpu_scanouts, g, 1, &err);
+ if (ret < 0) {
+ error_report_err(err);
+ }
+ return ret;
}
static int virtio_gpu_blob_save(QEMUFile *f, void *opaque, size_t size,
diff --git a/hw/pci/pci.c b/hw/pci/pci.c
index 5d7d17f6529..50f062045a9 100644
--- a/hw/pci/pci.c
+++ b/hw/pci/pci.c
@@ -926,21 +926,32 @@ const VMStateDescription vmstate_pci_device = {
void pci_device_save(PCIDevice *s, QEMUFile *f)
{
+ Error *local_err = NULL;
+ int ret;
+
/* Clear interrupt status bit: it is implicit
* in irq_state which we are saving.
* This makes us compatible with old devices
* which never set or clear this bit. */
s->config[PCI_STATUS] &= ~PCI_STATUS_INTERRUPT;
- vmstate_save_state(f, &vmstate_pci_device, s, NULL, &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_pci_device, s, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
/* Restore the interrupt status bit. */
pci_update_irq_status(s);
}
int pci_device_load(PCIDevice *s, QEMUFile *f)
{
+ Error *local_err = NULL;
int ret;
+
ret = vmstate_load_state(f, &vmstate_pci_device, s, s->version_id,
- &error_fatal);
+ &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
/* Restore the interrupt status bit. */
pci_update_irq_status(s);
return ret;
diff --git a/hw/s390x/virtio-ccw.c b/hw/s390x/virtio-ccw.c
index 4cb1ced001a..4a3ffb84f8f 100644
--- a/hw/s390x/virtio-ccw.c
+++ b/hw/s390x/virtio-ccw.c
@@ -1130,13 +1130,26 @@ static int virtio_ccw_load_queue(DeviceState *d, int n, QEMUFile *f)
static void virtio_ccw_save_config(DeviceState *d, QEMUFile *f)
{
VirtioCcwDevice *dev = VIRTIO_CCW_DEVICE(d);
- vmstate_save_state(f, &vmstate_virtio_ccw_dev, dev, NULL, &error_fatal);
+ Error *local_err = NULL;
+ int ret;
+
+ ret = vmstate_save_state(f, &vmstate_virtio_ccw_dev, dev, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
}
static int virtio_ccw_load_config(DeviceState *d, QEMUFile *f)
{
VirtioCcwDevice *dev = VIRTIO_CCW_DEVICE(d);
- return vmstate_load_state(f, &vmstate_virtio_ccw_dev, dev, 1, &error_fatal);
+ Error *local_err = NULL;
+ int ret;
+
+ ret = vmstate_load_state(f, &vmstate_virtio_ccw_dev, dev, 1, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
+ return ret;
}
static void virtio_ccw_pre_plugged(DeviceState *d, Error **errp)
diff --git a/hw/scsi/spapr_vscsi.c b/hw/scsi/spapr_vscsi.c
index f0a7dd2b882..a6591319db7 100644
--- a/hw/scsi/spapr_vscsi.c
+++ b/hw/scsi/spapr_vscsi.c
@@ -628,10 +628,16 @@ static const VMStateDescription vmstate_spapr_vscsi_req = {
static void vscsi_save_request(QEMUFile *f, SCSIRequest *sreq)
{
vscsi_req *req = sreq->hba_private;
- assert(req->active);
+ Error *local_err = NULL;
+ int rc;
- vmstate_save_state(f, &vmstate_spapr_vscsi_req, req, NULL, &error_fatal);
+ assert(req->active);
+ rc = vmstate_save_state(f, &vmstate_spapr_vscsi_req, req, NULL, &local_err);
+ if (rc < 0) {
+ error_report_err(local_err);
+ return;
+ }
trace_spapr_vscsi_save_request(req->qtag, req->cur_desc_num,
req->cur_desc_offset);
}
diff --git a/hw/virtio/virtio-mmio.c b/hw/virtio/virtio-mmio.c
index fb58c364527..c05c00bcd4a 100644
--- a/hw/virtio/virtio-mmio.c
+++ b/hw/virtio/virtio-mmio.c
@@ -612,15 +612,26 @@ static const VMStateDescription vmstate_virtio_mmio = {
static void virtio_mmio_save_extra_state(DeviceState *opaque, QEMUFile *f)
{
VirtIOMMIOProxy *proxy = VIRTIO_MMIO(opaque);
+ Error *local_err = NULL;
+ int ret;
- vmstate_save_state(f, &vmstate_virtio_mmio, proxy, NULL, &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_virtio_mmio, proxy, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
}
static int virtio_mmio_load_extra_state(DeviceState *opaque, QEMUFile *f)
{
VirtIOMMIOProxy *proxy = VIRTIO_MMIO(opaque);
+ Error *local_err = NULL;
+ int ret;
- return vmstate_load_state(f, &vmstate_virtio_mmio, proxy, 1, &error_fatal);
+ ret = vmstate_load_state(f, &vmstate_virtio_mmio, proxy, 1, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
+ return ret;
}
static bool virtio_mmio_has_extra_state(DeviceState *opaque)
diff --git a/hw/virtio/virtio-pci.c b/hw/virtio/virtio-pci.c
index 7273da4c018..c276fbb017c 100644
--- a/hw/virtio/virtio-pci.c
+++ b/hw/virtio/virtio-pci.c
@@ -189,15 +189,26 @@ static bool virtio_pci_has_extra_state(DeviceState *d)
static void virtio_pci_save_extra_state(DeviceState *d, QEMUFile *f)
{
VirtIOPCIProxy *proxy = to_virtio_pci_proxy(d);
+ Error *local_err = NULL;
+ int ret;
- vmstate_save_state(f, &vmstate_virtio_pci, proxy, NULL, &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_virtio_pci, proxy, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
}
static int virtio_pci_load_extra_state(DeviceState *d, QEMUFile *f)
{
VirtIOPCIProxy *proxy = to_virtio_pci_proxy(d);
+ Error *local_err = NULL;
+ int ret;
- return vmstate_load_state(f, &vmstate_virtio_pci, proxy, 1, &error_fatal);
+ ret = vmstate_load_state(f, &vmstate_virtio_pci, proxy, 1, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
+ return ret;
}
static void virtio_pci_save_queue(DeviceState *d, int n, QEMUFile *f)
diff --git a/hw/virtio/virtio.c b/hw/virtio/virtio.c
index a7063d5555e..d4b2e67c274 100644
--- a/hw/virtio/virtio.c
+++ b/hw/virtio/virtio.c
@@ -3016,7 +3016,7 @@ int virtio_save(VirtIODevice *vdev, QEMUFile *f)
VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
VirtioDeviceClass *vdc = VIRTIO_DEVICE_GET_CLASS(vdev);
uint32_t guest_features_lo = (vdev->guest_features & 0xffffffff);
- int i;
+ int i, ret;
Error *local_err = NULL;
if (k->save_config) {
@@ -3061,7 +3061,7 @@ int virtio_save(VirtIODevice *vdev, QEMUFile *f)
}
if (vdc->vmsd) {
- int ret = vmstate_save_state(f, vdc->vmsd, vdev, NULL, &local_err);
+ ret = vmstate_save_state(f, vdc->vmsd, vdev, NULL, &local_err);
if (ret) {
error_report_err(local_err);
return ret;
@@ -3069,7 +3069,11 @@ int virtio_save(VirtIODevice *vdev, QEMUFile *f)
}
/* Subsections */
- return vmstate_save_state(f, &vmstate_virtio, vdev, NULL, &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_virtio, vdev, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
+ return ret;
}
/* A wrapper for use as a VMState .put function */
--
2.47.1
^ permalink raw reply [relevance 12%]
* [PATCH QEMU HCI-8.1 v3 13/28] migration: vmstate_save_state_v: fix double error_setg
` (5 preceding siblings ...)
2026-10-16 15:26 12% ` [PATCH QEMU HCI-8.1 v3 12/28] migration: Fix regression of passing error_fatal into vmstate_load_state() Andrey Drobyshev
@ 2026-10-16 15:26 11% ` Andrey Drobyshev
2026-10-16 15:38 6% ` no-reply
2026-10-16 15:26 22% ` [PATCH QEMU HCI-8.1 v3 14/28] migration: yank: Move register instance earlier Andrey Drobyshev
` (11 subsequent siblings)
18 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
We may call error_setg twice on same errp if inner
vmstate_save_state_v() or vmstate_save_state() call fails. Next we will
crash on assertion in error_setv().
andrey.drobyshev:
This commit is cherry-picked as-is, with no modifications, from the
upstream commit d41ce10d0f5a.
Fixes: 848a0503422d043 "migration: Update error description outside migration.c"
Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Fabiano Rosas <farosas@suse.de>
Reviewed-by: Peter Xu <peterx@redhat.com>
Link: https://lore.kernel.org/qemu-devel/20260304212303.667141-2-vsementsov@yandex-team.ru
Signed-off-by: Fabiano Rosas <farosas@suse.de>
---
migration/vmstate.c | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/migration/vmstate.c b/migration/vmstate.c
index aea41c09946..04903131257 100644
--- a/migration/vmstate.c
+++ b/migration/vmstate.c
@@ -523,6 +523,9 @@ int vmstate_save_state_v(QEMUFile *f, const VMStateDescription *vmsd,
} else {
ret = inner_field->info->put(f, curr_elem, size,
inner_field, vmdesc_loop);
+ if (ret < 0) {
+ error_setg(errp, "put failed");
+ }
}
written_bytes = qemu_file_transferred(f) - old_offset;
@@ -535,8 +538,8 @@ int vmstate_save_state_v(QEMUFile *f, const VMStateDescription *vmsd,
}
if (ret) {
- error_setg(errp, "Save of field %s/%s failed",
- vmsd->name, field->name);
+ error_prepend(errp, "Save of field %s/%s failed: ",
+ vmsd->name, field->name);
if (vmsd->post_save) {
vmsd->post_save(opaque);
}
--
2.47.1
^ permalink raw reply [relevance 11%]
* [PATCH QEMU HCI-8.1 v3 10/28] q35: Fix migration of SMRAM state
` (3 preceding siblings ...)
2026-10-16 15:26 5% ` [PATCH QEMU HCI-8.1 v3 08/28] vfio/pci: Initialize rom_read_failed in vfio_pci_load_rom() Andrey Drobyshev
@ 2026-10-16 15:26 4% ` Andrey Drobyshev
2026-10-16 15:33 0% ` no-reply
2026-10-16 15:26 12% ` [PATCH QEMU HCI-8.1 v3 12/28] migration: Fix regression of passing error_fatal into vmstate_load_state() Andrey Drobyshev
` (13 subsequent siblings)
18 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Igor Mammedov <imammedo@redhat.com>
When migrating, dst QEMU by default has SMRAM unlocked,
and since wmask is not migrated, the migrated value of
MCH_HOST_BRIDGE_F_SMBASE in config space fall to prey of
mch_update_smbase_smram()
...
if (pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] == 0xff) {
*reg = 0x00;
and is getting cleared and leads to unlocked smram
on dst even if on source it's been locked.
As Andrey has pointed out [1], we should derive wmask
from config and not other way around.
Drop offending chunk and resync wmask based on MCH_HOST_BRIDGE_F_SMBASE
register value. That would preserve the register during
migration and set smram regions into corresponding state.
What that changes is:
that it would let guest write junk values in register
(with no apparent effect) until it's stumbles upon
reserved 0x1 [|] 0x2 values, at which point it
would be only possible to lock register and trigger
switch to SMRAM blackhole in CPU AS.
While at it, fix up test by removing junk discard before negotiation hunk.
PS2:
Instead of adding a dedicated post_load handler for it,
reuse mch_update->mch_update_smbase_smram call chain
that is called on write/reset/post_load to be consistent
with how we handle mch registers.
PS3:
for prosterity here is erro message Andrey got due to this bug:
qemu: vfio_container_dma_map(0x..., 0x0, 0xa0000, 0x....) = -22 (Invalid argument)
qemu: hardware error: vfio: DMA mapping failed, unable to continue
1) https://patchew.org/QEMU/20251203180851.6390-1-arbn@yandex-team.com/
andrey.drobyshev:
This commit is cherry-picked as-is, with no modifications, from the
upstream commit 66cf169e29b0.
Fixes: f404220e279c ("q35: implement 128K SMRAM at default SMBASE address")
Reported-by: Andrey Ryabinin <arbn@yandex-team.com>
Signed-off-by: Igor Mammedov <imammedo@redhat.com>
Reviewed-by: Andrey Ryabinin <arbn@yandex-team.com>
Reviewed-by: Michael S. Tsirkin <mst@redhat.com>
Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
Message-Id: <20251211165454.288476-1-imammedo@redhat.com>
---
hw/pci-host/q35.c | 25 +++++++++++--------------
tests/qtest/q35-test.c | 6 ------
2 files changed, 11 insertions(+), 20 deletions(-)
diff --git a/hw/pci-host/q35.c b/hw/pci-host/q35.c
index a708758d361..946342ba585 100644
--- a/hw/pci-host/q35.c
+++ b/hw/pci-host/q35.c
@@ -432,30 +432,27 @@ static void mch_update_smbase_smram(MCHPCIState *mch)
}
if (*reg == MCH_HOST_BRIDGE_F_SMBASE_QUERY) {
- pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] =
- MCH_HOST_BRIDGE_F_SMBASE_LCK;
+ pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = MCH_HOST_BRIDGE_F_SMBASE_LCK;
*reg = MCH_HOST_BRIDGE_F_SMBASE_IN_RAM;
return;
}
/*
- * default/reset state, discard written value
- * which will disable SMRAM balackhole at SMBASE
+ * reg value can come from register write/reset/migration source,
+ * update wmask to be in sync with it regardless of source
*/
- if (pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] == 0xff) {
- *reg = 0x00;
+ if (*reg == MCH_HOST_BRIDGE_F_SMBASE_IN_RAM) {
+ pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = MCH_HOST_BRIDGE_F_SMBASE_LCK;
+ return;
}
-
- memory_region_transaction_begin();
if (*reg & MCH_HOST_BRIDGE_F_SMBASE_LCK) {
- /* disable all writes */
- pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] &=
- ~MCH_HOST_BRIDGE_F_SMBASE_LCK;
+ /* lock register at 0x2 and disable all writes */
+ pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = 0;
*reg = MCH_HOST_BRIDGE_F_SMBASE_LCK;
- lck = true;
- } else {
- lck = false;
}
+
+ lck = *reg & MCH_HOST_BRIDGE_F_SMBASE_LCK;
+ memory_region_transaction_begin();
memory_region_set_enabled(&mch->smbase_blackhole, lck);
memory_region_set_enabled(&mch->smbase_window, lck);
memory_region_transaction_commit();
diff --git a/tests/qtest/q35-test.c b/tests/qtest/q35-test.c
index 62fff49fc89..4e3a4457f62 100644
--- a/tests/qtest/q35-test.c
+++ b/tests/qtest/q35-test.c
@@ -206,12 +206,6 @@ static void test_smram_smbase_lock(void)
qtest_writeb(qts, SMBASE, SMRAM_TEST_PATTERN);
g_assert_cmpint(qtest_readb(qts, SMBASE), ==, SMRAM_TEST_PATTERN);
- /* check that writing junk to 0x9c before before negotiating is ignored */
- for (i = 0; i < 0xff; i++) {
- qpci_config_writeb(pcidev, MCH_HOST_BRIDGE_F_SMBASE, i);
- g_assert(qpci_config_readb(pcidev, MCH_HOST_BRIDGE_F_SMBASE) == 0);
- }
-
/* enable SMRAM at SMBASE */
qpci_config_writeb(pcidev, MCH_HOST_BRIDGE_F_SMBASE, 0xff);
g_assert(qpci_config_readb(pcidev, MCH_HOST_BRIDGE_F_SMBASE) == 0x01);
--
2.47.1
^ permalink raw reply [relevance 4%]
* [PATCH QEMU HCI-8.1 v3 08/28] vfio/pci: Initialize rom_read_failed in vfio_pci_load_rom()
` (2 preceding siblings ...)
2026-10-16 15:26 13% ` [PATCH QEMU HCI-8.1 v3 04/28] migration: Plug memory leaks after migrate_set_error() Andrey Drobyshev
@ 2026-10-16 15:26 5% ` Andrey Drobyshev
2026-10-16 15:26 4% ` [PATCH QEMU HCI-8.1 v3 10/28] q35: Fix migration of SMRAM state Andrey Drobyshev
` (14 subsequent siblings)
18 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Mario Casquero <mcasquer@redhat.com>
When vfio_device_get_region_info() fails in vfio_pci_load_rom(),
the function returns without setting vdev->rom_read_failed to true,
and without allocating vdev->rom. This leaves vdev->rom as NULL.
andrey.drobyshev:
This commit is cherry-picked as-is, with no modifications, from the
upstream commit c822ecd80661.
Signed-off-by: Mario Casquero <mcasquer@redhat.com>
Reviewed-by: Cédric Le Goater <clg@redhat.com>
Link: https://lore.kernel.org/qemu-devel/20260602070857.356526-1-mcasquer@redhat.com
Signed-off-by: Cédric Le Goater <clg@redhat.com>
---
hw/vfio/pci.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/hw/vfio/pci.c b/hw/vfio/pci.c
index fa2ca0d779d..bea478d521f 100644
--- a/hw/vfio/pci.c
+++ b/hw/vfio/pci.c
@@ -1039,7 +1039,7 @@ static void vfio_update_msi(VFIOPCIDevice *vdev)
}
}
-static void vfio_pci_load_rom(VFIOPCIDevice *vdev)
+static bool vfio_pci_load_rom(VFIOPCIDevice *vdev)
{
VFIODevice *vbasedev = &vdev->vbasedev;
struct vfio_region_info *reg_info = NULL;
@@ -1053,7 +1053,7 @@ static void vfio_pci_load_rom(VFIOPCIDevice *vdev)
if (ret != 0) {
error_report("vfio: Error getting ROM info: %s", strerror(-ret));
- return;
+ return false;
}
trace_vfio_pci_load_rom(vbasedev->name, (unsigned long)reg_info->size,
@@ -1064,12 +1064,11 @@ static void vfio_pci_load_rom(VFIOPCIDevice *vdev)
vdev->rom_offset = reg_info->offset;
if (!vdev->rom_size) {
- vdev->rom_read_failed = true;
error_report("vfio-pci: Cannot read device rom at %s", vbasedev->name);
error_printf("Device option ROM contents are probably invalid "
"(check dmesg).\nSkip option ROM probe with rombar=0, "
"or load from file with romfile=\n");
- return;
+ return false;
}
vdev->rom = g_malloc(size);
@@ -1125,6 +1124,8 @@ static void vfio_pci_load_rom(VFIOPCIDevice *vdev)
data[6] = -csum;
}
}
+
+ return true;
}
/* "Raw" read of underlying config space. */
@@ -1158,7 +1159,7 @@ static uint64_t vfio_rom_read(void *opaque, hwaddr addr, unsigned size)
/* Load the ROM lazily when the guest tries to read it */
if (unlikely(!vdev->rom && !vdev->rom_read_failed)) {
- vfio_pci_load_rom(vdev);
+ vdev->rom_read_failed = !vfio_pci_load_rom(vdev);
}
memcpy(&val, vdev->rom + addr,
--
2.47.1
^ permalink raw reply [relevance 5%]
* [PATCH QEMU HCI-8.1 v3 04/28] migration: Plug memory leaks after migrate_set_error()
2026-10-16 15:25 6% ` [PATCH QEMU HCI-8.1 v3 01/28] VZ: migration/cpr-exec: fix Error leak in preserve_ram() Andrey Drobyshev
2026-10-16 15:26 6% ` [PATCH QEMU HCI-8.1 v3 02/28] migration/cpr-exec: restore RAM write access if exec fails Andrey Drobyshev
@ 2026-10-16 15:26 13% ` Andrey Drobyshev
2026-10-16 15:29 0% ` no-reply
2026-10-16 15:26 5% ` [PATCH QEMU HCI-8.1 v3 08/28] vfio/pci: Initialize rom_read_failed in vfio_pci_load_rom() Andrey Drobyshev
` (15 subsequent siblings)
18 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Markus Armbruster <armbru@redhat.com>
migrate_set_error(s, err) stores a copy of @err in @s. The original
@err is not freed. Most callers free it immediately. Some callers
free it later, or pass it on. And some leak it. Fix those.
Perhaps migrate_set_error(s, err) should take ownership of @err. The
callers that free it immediately would become simpler, and avoid a
copy and a deallocation. The others would have to pass
error_copy(err).
andrey.drobyshev:
This commit is cherry-picked as-is, with no modifications, from the
upstream commit 93817ec39656.
Signed-off-by: Markus Armbruster <armbru@redhat.com>
Link: https://lore.kernel.org/r/20251115083500.2753895-2-armbru@redhat.com
Signed-off-by: Peter Xu <peterx@redhat.com>
---
migration/cpr-exec.c | 3 ++-
migration/multifd.c | 2 ++
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
index c4bdf2cb99f..d37293baec5 100644
--- a/migration/cpr-exec.c
+++ b/migration/cpr-exec.c
@@ -231,11 +231,12 @@ static void cpr_exec_cb(void *opaque)
error_report_err(error_copy(err));
migrate_set_state(&s->state, s->state, MIGRATION_STATUS_FAILED);
migrate_set_error(s, err);
+ error_free(err);
+ err = NULL;
/* Note, we can go from state COMPLETED to FAILED */
migration_call_notifiers(MIG_EVENT_PRECOPY_FAILED, NULL);
- err = NULL;
if (!migration_block_activate(&err)) {
/* error was already reported */
error_free(err);
diff --git a/migration/multifd.c b/migration/multifd.c
index 5a723ae25d2..e369421b3ff 100644
--- a/migration/multifd.c
+++ b/migration/multifd.c
@@ -966,6 +966,7 @@ bool multifd_send_setup(void)
if (!multifd_new_send_channel_create(p, &local_err)) {
migrate_set_error(s, local_err);
+ error_free(local_err);
ret = -1;
}
}
@@ -990,6 +991,7 @@ bool multifd_send_setup(void)
ret = multifd_send_state->ops->send_setup(p, &local_err);
if (ret) {
migrate_set_error(s, local_err);
+ error_free(local_err);
goto err;
}
assert(p->iov);
--
2.47.1
^ permalink raw reply [relevance 13%]
* [PATCH QEMU HCI-8.1 v3 02/28] migration/cpr-exec: restore RAM write access if exec fails
2026-10-16 15:25 6% ` [PATCH QEMU HCI-8.1 v3 01/28] VZ: migration/cpr-exec: fix Error leak in preserve_ram() Andrey Drobyshev
@ 2026-10-16 15:26 6% ` Andrey Drobyshev
2026-10-16 15:26 13% ` [PATCH QEMU HCI-8.1 v3 04/28] migration: Plug memory leaks after migrate_set_error() Andrey Drobyshev
` (16 subsequent siblings)
18 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
With -cpr-ram-rdonly the RAM blocks are made read-only right before
execve(). If execve() fails, the VM resumes in the old QEMU with
read-only RAM and crashes on the first write. Make it writable again.
Fixes: 91da25f20471 ("migration/cpr-exec: add '-cpr-ram-rdonly' option to mark RAM blocks as RO #VSTOR-101579")
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
migration/cpr-exec.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
index 9fd3515753a..c4bdf2cb99f 100644
--- a/migration/cpr-exec.c
+++ b/migration/cpr-exec.c
@@ -224,6 +224,9 @@ static void cpr_exec_cb(void *opaque)
error_setg_errno(&err, errno, "execvp %s failed", argv[0]);
g_clear_pointer(&argv, g_strfreev);
cpr_exec_unpreserve_fds();
+ if (cpr_ram_rdonly) {
+ cpr_ram_restore_rw();
+ }
error_report_err(error_copy(err));
migrate_set_state(&s->state, s->state, MIGRATION_STATUS_FAILED);
--
2.47.1
^ permalink raw reply [relevance 6%]
* [PATCH QEMU HCI-8.1 v3 01/28] VZ: migration/cpr-exec: fix Error leak in preserve_ram()
@ 2026-10-16 15:25 6% ` Andrey Drobyshev
2026-10-16 15:26 6% ` [PATCH QEMU HCI-8.1 v3 02/28] migration/cpr-exec: restore RAM write access if exec fails Andrey Drobyshev
` (17 subsequent siblings)
18 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-16 15:25 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
migrate_set_error() stores its own copy of the error, so free ours.
Fixes: a3745506daf9 ("VZ: migration/cpr-exec: mark saved RAM blocks with MADV_DOEXEC before execve")
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
migration/cpr-exec.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
index edace3f0bd8..9fd3515753a 100644
--- a/migration/cpr-exec.c
+++ b/migration/cpr-exec.c
@@ -148,6 +148,7 @@ static bool preserve_ram(cpr_ramblock_t ram, char *name)
error_report_err(error_copy(err));
migrate_set_state(&s->state, s->state, MIGRATION_STATUS_FAILED);
migrate_set_error(s, err);
+ error_free(err);
return false;
}
--
2.47.1
^ permalink raw reply [relevance 6%]
* Re: [Devel] [PATCH hci-8.1] block/blk-cbt: account a write request also when dm starts it
@ 2026-10-16 14:41 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-16 14:41 UTC (permalink / raw)
To: Pavel Tikhomirov, Konstantin Khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] block: cbt_page_alloc() GFP_ATOMIC failure sets CBT_ERROR on dm dispatch path
--
commit 07126a82d2afc4a47285774d1cb334f3398a6e89
Author: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
block/blk-cbt: account a write request also when dm starts it
This patch adds blk_cbt_rq_start(), called from dm_start_request(), so a
write is accounted in CBT a second time when request based dm starts it.
A missing bitmap page is allocated with GFP_ATOMIC in this path, and
CBT_ERROR is set if that allocation fails.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-149268
> diff --git a/block/blk-cbt.c b/block/blk-cbt.c
> --- a/block/blk-cbt.c
> +++ b/block/blk-cbt.c
[ ... ]
> @@ -129,11 +137,15 @@ static int cbt_page_alloc(struct cbt_info **cbt_pp, unsigned long idx,
> }
> }
> spin_unlock_irq(&cbt->lock);
> - if (in_rcu)
> - rcu_read_unlock();
> - page = alloc_page(GFP_NOIO|__GFP_ZERO);
> - if (in_rcu)
> - rcu_read_lock();
> + if (alloc == CBT_ALLOC_ATOMIC) {
> + page = alloc_page(GFP_ATOMIC | __GFP_ZERO | __GFP_NOWARN);
[Severity: Medium]
Can this GFP_ATOMIC allocation fail often enough to matter?
Under memory pressure alloc_page(GFP_ATOMIC) has no direct reclaim and
can fail. When it does, a few lines further down in cbt_page_alloc():
if (unlikely(!page)) {
set_bit(CBT_ERROR, &cbt->flags);
spin_unlock_irq(&cbt->lock);
return -ENOMEM;
}
CBT_ERROR is a sticky flag, and __GFP_NOWARN makes the failure silent.
The call chain is:
dm_mq_queue_rq()
dm_start_request()
blk_cbt_rq_start()
blk_cbt_add(..., CBT_ALLOC_ATOMIC)
__blk_cbt_set()
cbt_page_alloc()
Neither blk_cbt_rq_start() nor blk_cbt_add() checks the return value.
After the failure, blk_cbt_add() returns early for this CBT on every
later write:
if (unlikely(test_bit(CBT_ERROR, &cbt->flags)))
return;
cbt_ioc_get() and cbt_ioc_set() then return -EIO.
So a transient atomic allocation failure permanently invalidates the CBT,
and with it the incremental backup chain, until the CBT is re-created.
The old submission time path used GFP_NOIO with the RCU lock dropped,
which does not fail in practice.
Is there a reason not to retry from a sleepable context, or to avoid
setting CBT_ERROR in the atomic case?
[ ... ]
> @@ -248,7 +261,7 @@ static void blk_cbt_add(struct cbt_info *cbt, blkcnt_t start, blkcnt_t len,
> put_cpu_ptr(cbt->cache);
>
> if (likely(old.len))
> - __blk_cbt_set(cbt, old.start, old.len, 1, 1, NULL, NULL);
> + __blk_cbt_set(cbt, old.start, old.len, alloc, 1, NULL, NULL);
[Severity: Medium]
Is the commit message claim correct that a page can be missing here only
for requests submitted before BLKCBTSTART?
The commit message says:
"This only happens for requests submitted before BLKCBTSTART: any later
write has its page allocated already by its submission time accounting."
On the normal (cached) path, blk_cbt_add() only records the extent in the
per-CPU cache. It calls __blk_cbt_set(), and so allocates pages, only
for the evicted old extent.
That means a write submitted after BLKCBTSTART can still have no bitmap
page when dm_start_request() runs.
In blk_cbt_rq_start(), re-adding a range that is not contiguous with the
cached extent evicts the previous cached extent, which is often an
unrelated earlier write. That extent is flushed with CBT_ALLOC_ATOMIC.
So GFP_ATOMIC page allocation looks possible on ordinary write-heavy
workloads, not only in the narrow pre-BLKCBTSTART window.
Also, re-adding the same range from blk_cbt_rq_start() never coalesces,
since ex->start + ex->len != start for it. Each started request
therefore causes an extra __blk_cbt_set() call and bitmap page lock.
Could the commit message and the allocation strategy be revisited in
light of this?
[ ... ]
> +void blk_cbt_rq_start(struct request *rq)
> +{
[ ... ]
> + blk_cbt_add(cbt, bio->bi_iter.bi_sector << 9,
> + bio->bi_iter.bi_size, CBT_ALLOC_ATOMIC);
[Severity: Medium]
This is the same issue as above, seen from the caller. Errors from
blk_cbt_add() are dropped here, so a CBT_ERROR set by a failed
GFP_ATOMIC allocation in blk_cbt_rq_start() is not reported to the dm
dispatch path. Is that intended?
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261016-ptikhomirov-vstor-149268-cbt-account-at-rq-start-v1-1-1f20b151431d@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 8/8] selftests/mm: test memory preservation across exec
2026-10-14 17:11 4% ` [Devel] [PATCH hci-8.1 8/8] selftests/mm: test memory preservation across exec Konstantin Khorenko
@ 2026-10-14 17:33 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-14 17:33 UTC (permalink / raw)
To: Konstantin Khorenko, Pavel Tikhomirov, Andrey Drobyshev,
Vasileios Almpanis
Cc: devel
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Low] selftests/mm: doexec child diagnostics are lost by _exit()/exec
- [Low] selftests/mm: doexec ignores write() return values
- [Low] selftests/mm: doexec treats any SIGSEGV in allocation sweep as expected
--
commit eee32e73516b760bb3007e1b97633a4999581fec
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
selftests/mm: test memory preservation across exec
This commit adds doexec, a selftest for MADV_DOEXEC. It checks that memory
backing long-term GUP pins and other mappings survives repeated exec and
fork. It also covers CLONE_VM handling, threads, /proc readers and an
optional allocation-failure sweep. The test is wired into run_vmtests.sh.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-150231
> diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
> new file mode 100644
> --- /dev/null
> +++ b/tools/testing/selftests/mm/doexec.c
[ ... ]
> +static void exec_stage(int fd, char *ram, size_t size, int kind, int stage)
> +{
> + char fd_arg[32], addr_arg[32], size_arg[32], kind_arg[32], stage_arg[32];
> +
> [ ... ]
> + execl("/proc/self/exe", "doexec", "--exec", fd_arg, addr_arg, size_arg,
> + kind_arg, stage_arg, NULL);
> + ksft_print_msg("exec: %s\n", strerror(errno));
> + _exit(KSFT_FAIL);
> +}
[Severity: Low]
Can the diagnostics printed by the forked children get lost here?
ksft_print_msg() writes through buffered stdio. When stdout is a pipe or a
file, as under run_vmtests.sh or a CI harness, the "exec: ..." message is
still in the buffer when _exit(KSFT_FAIL) runs, and _exit() does not flush
it.
The same applies to the other messages printed from the per-test children,
for example:
before_exec() -> "pin: ...", "MADV_DOEXEC: ..."
check_pin() -> "read pin: ...", "CPU/pin mismatch at byte N"
allocation_failures() -> "fail-nth N: ..."
In main(), the fflush(stdout) calls run in the parent before fork(). They
prevent duplicated output but do not flush what the child prints itself.
Messages printed before the execl() into the --exec stage are likewise
discarded by the exec.
A failing run would then show only a bare "not ok" line. Would an
fflush(stdout) before each _exit() and execl() in the children be
appropriate?
> +static int shared_mm_exec(bool own_thread)
> +{
[ ... ]
> + if (own_thread) {
> + if (pthread_create(&thread, NULL, blocked_thread, &pipe_fd[0]))
> + return KSFT_FAIL;
> + check_exec(ram, size, 1, false);
> + write(pipe_fd[1], "x", 1);
> + pthread_join(thread, NULL);
> + return KSFT_FAIL;
> + }
[ ... ]
> + write(pipe_fd[1], "x", 1);
> + if (wait_child(pid))
> + ret = KSFT_FAIL;
[Severity: Low]
Is it intended that the write() results are ignored here?
write() is warn_unused_result under glibc _FORTIFY_SOURCE, so this can
produce -Wunused-result warnings.
If this write ever failed, the CLONE_VM child or thread would stay blocked
in read(), and wait_child() or pthread_join() would hang because the test
has no timeout.
> +static int clone_race_child(void *arg)
> +{
> + struct clone_race *race = arg;
> + unsigned int i;
> +
> + for (i = 0; i < 8; i++) {
> + if (__atomic_load_n(race->ram, __ATOMIC_RELAXED) != 0x31) {
> + write(race->report, "x", 1);
> + return KSFT_FAIL;
> + }
> + usleep(1000);
> + }
> + return KSFT_PASS;
> +}
[Severity: Low]
Should the result of this write() be checked as well?
This write is the only signal that the shared memory was stolen. If it fails,
clone_exec_race() sees EOF on the report pipe and passes despite the detected
failure. clone_race_thread() and the --check handler in main() also drop
write() results.
> + if (argc == 5 && !strcmp(argv[1], "--check")) {
> + char *ram = (char *)(uintptr_t)strtoull(argv[2], NULL, 10);
> + size_t size = strtoull(argv[3], NULL, 10);
> + size_t stride = strtoull(argv[4], NULL, 10), i;
> + int fd = open("/proc/self/fail-nth", O_WRONLY);
> +
> + if (fd >= 0) {
> + write(fd, "0", 1);
> + close(fd);
> + }
> + for (i = 0; i < size; i += stride)
> + if (ram[i] != 0x31)
> + return KSFT_FAIL;
> + return KSFT_PASS;
> + }
[ ... ]
> + if (WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV)
> + fatal++;
[ ... ]
> + /* A sweep that injected no recoverable/fatal errors provides no coverage. */
> + return early && fatal ? KSFT_PASS : KSFT_SKIP;
> +}
[Severity: Low]
Can the allocation_failures() sweep pass even when the preserved mapping has
been lost?
do_execveat_common() deliberately raises SIGSEGV when exec fails after the
point of no return:
if (bprm->point_of_no_return && !fatal_signal_pending(current))
force_fatal_sig(SIGSEGV);
So SIGSEGV is a legitimate outcome of the sweep. However, the test cannot
tell that kernel-forced SIGSEGV apart from a user-space fault.
After a successful preserving exec, the --check image dereferences ram[i]
in main(). If a kernel bug dropped or unmapped the preserved range, that load
would also raise SIGSEGV. allocation_failures() would count it as fatal++.
The final check is "early && fatal ? KSFT_PASS : KSFT_SKIP", and it requires
at least one SIGSEGV. A regression that loses the mapping can therefore help
the test pass.
Is there a way to distinguish the two cases? One option is to have the --check
image report a lost mapping without faulting, for example by probing the range
with mincore() or msync() before reading it.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-0-36f1120f9b57@virtuozzo.com?part=8
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH hci-8.1 6/8] oracle/mm: zap the preserved ranges of the old mm after copying them
@ 2026-10-14 17:24 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-14 17:24 UTC (permalink / raw)
To: Vasileios Almpanis, Konstantin Khorenko, Pavel Tikhomirov,
Andrey Drobyshev
Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] mm: zap_vma_mt() clears old_mm PTEs without vma_start_write()
--
commit f34df816bcbe51e2402a6b8ae419dea8ef679f49
Author: Konstantin Khorenko <khorenko@virtuozzo.com>
oracle/mm: zap the preserved ranges of the old mm after copying them
After the VM_EXEC_KEEP ranges are copied into the new mm, this patch zaps
them from the old mm under its write lock. This is meant to leave the
copies as the only mappings of the preserved pages. A new zap_vma_mt()
helper spreads the zap of an anonymous VMA over several threads.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-150231
> diff --git a/fs/exec.c b/fs/exec.c
> --- a/fs/exec.c
> +++ b/fs/exec.c
[ ... ]
> @@ -997,7 +997,15 @@ static int vma_dup_some(struct mm_struct *old_mm, struct mm_struct *new_mm)
> }
>
> mmap_write_unlock(new_mm);
> - return ret;
> + if (ret)
> + return ret;
> +
> + /* Whoever keeps old_mm alive past the exec must not reach the pages through it. */
> + vma_iter_set(&vmi, 0);
> + for_each_vma(vmi, vma)
> + if (vma->vm_flags & VM_EXEC_KEEP)
> + zap_vma_mt(vma);
> + return 0;
> }
[Severity: Medium]
Can this zap race with a per-VMA lock holder on old_mm?
The loop calls zap_vma_mt() on old_mm VMAs without calling
vma_start_write(vma) first:
vma_dup_some()
for_each_vma(vmi, vma)
zap_vma_mt(vma)
mmap_write_lock() alone does not drain holders that already took the
per-VMA read lock through lock_vma_under_rcu().
One such holder is UFFDIO_MOVE:
uffd_move_lock()
*src_vmap = lock_vma_under_rcu(mm, src_start);
This path never takes mmap_lock. A task that owns a userfaultfd context
bound to old_mm has mm_users on old_mm, and its own t->mm is a different
mm. reserve_preserved_mem() only rejects tasks whose t->mm == mm, so such
a task is not excluded.
It could therefore run folio_move_anon_rmap() or a fault in old_mm while
the zap clears the same PTEs. That would reintroduce the problems the
commit message describes, since the preserved pages still carry
PageAnonExclusive in both mms.
Would it make sense to call vma_start_write(vma) before zapping each
VM_EXEC_KEEP VMA, either in vma_dup_some() or at the start of
zap_vma_mt()?
> diff --git a/mm/memory.c b/mm/memory.c
> --- a/mm/memory.c
> +++ b/mm/memory.c
[ ... ]
> +void zap_vma_mt(struct vm_area_struct *vma)
> +{
> +#ifdef CONFIG_PADATA
> + struct padata_mt_job job = {
[ ... ]
> + mmap_assert_write_locked(vma->vm_mm);
> + if (vma_is_anonymous(vma)) {
> + lru_add_drain();
> + mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma->vm_mm,
> + vma->vm_start, vma->vm_end);
> + update_hiwater_rss(vma->vm_mm);
> + mmu_notifier_invalidate_range_start(&range);
> + padata_do_multithreaded(&job);
> + mmu_notifier_invalidate_range_end(&range);
> + return;
> + }
> +#endif
> + zap_page_range_single(vma, vma->vm_start, vma->vm_end - vma->vm_start,
> + NULL);
> +}
[Severity: Medium]
The same concern applies here. zap_vma_mt() only asserts the mmap write
lock with mmap_assert_write_locked() before clearing PTEs, either in the
padata worker threads through zap_vma_chunk() or in
zap_page_range_single().
It never calls vma_start_write(vma).
For the anonymous case, can PTEs be cleared while a per-VMA locked fault
or UFFDIO_MOVE is still operating on the same VMA? Both the padata path
and the zap_page_range_single() path appear to be exposed.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-0-36f1120f9b57@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 7/8] exec: protect preserved memory against surviving shared-mm tasks
@ 2026-10-14 17:24 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-14 17:24 UTC (permalink / raw)
To: Pavel Tikhomirov, Vasileios Almpanis, Andrey Drobyshev,
Konstantin Khorenko
Cc: devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] fork: copy_mm()/copy_process() take page_table_lock twice per shared-mm clone
- [Low] exec: VM_EXEC_KEEP scan in reserve_preserved_mem() is stale by exec_mmap()
--
commit 3fae5b8a82902f91e53a5e8e729322cb7b034228
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
exec: protect preserved memory against surviving shared-mm tasks
This patch makes an exec that accepts preserved memory fail with EBUSY
when a task outside the thread group still uses the mm and the mm has a
VM_EXEC_KEEP VMA. copy_mm() refuses and counts CLONE_VM, non-thread clones
while the mm is marked. exec_mmap() takes the preserve path only when the
mm was marked.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-150231
> diff --git a/fs/exec.c b/fs/exec.c
> --- a/fs/exec.c
> +++ b/fs/exec.c
[ ... ]
> +static int reserve_preserved_mem(struct linux_binprm *bprm)
> +{
> + struct mm_struct *mm = current->mm;
[ ... ]
> + mmap_read_unlock(mm);
> + if (!preserve)
> + return 0;
[Severity: Low]
Can the result of this scan be stale by the time exec_mmap() runs?
When no VM_EXEC_KEEP VMA is found here, nothing is reserved:
exec_keep_active and bprm->preserving_mem both stay unset. Sibling
threads have not yet been removed by de_thread(), and copy_mm() only
refuses clones while exec_keep_active is set. So a sibling thread or a
CLONE_VM task can still call madvise(MADV_DOEXEC) after mmap_read_unlock()
and set VM_EXEC_KEEP on a range.
> @@ -1040,7 +1112,7 @@ static int exec_mmap(struct linux_binprm *bprm)
> up_write(&tsk->signal->exec_update_lock);
> return ret;
> }
> - if (unlikely(bprm->accepts_preserved_mem)) {
> + if (unlikely(bprm->preserving_mem)) {
Here the preserve path (write lock plus vma_dup_some()) is now selected by
the early sample rather than by the VMAs present under the lock at copy
time. A VM_EXEC_KEEP VMA created after the scan in
reserve_preserved_mem() would not be copied, and the range would be lost
with the old mm without any error.
Before this patch, accepts_preserved_mem alone selected the path, and
vma_dup_some() copied any VM_EXEC_KEEP VMA present under
mmap_write_lock. Should the decision be revalidated, or the mark taken
before the scan, so that a range marked during begin_new_exec() is not
silently dropped?
[ ... ]
> diff --git a/kernel/fork.c b/kernel/fork.c
> --- a/kernel/fork.c
> +++ b/kernel/fork.c
[ ... ]
> +/* Keep unpublished CLONE_VM tasks visible to preserving-exec admission. */
> +static int exec_keep_clone_begin(struct mm_struct *mm)
> +{
> + int ret = 0;
> +
> + spin_lock(&mm->page_table_lock);
> + if (mm->exec_keep_active)
> + ret = -EBUSY;
> + else
> + mm->exec_keep_clones++;
> + spin_unlock(&mm->page_table_lock);
> + return ret;
> +}
> +
> +static void exec_keep_clone_end(struct mm_struct *mm)
> +{
> + spin_lock(&mm->page_table_lock);
> + if (!WARN_ON_ONCE(!mm->exec_keep_clones))
> + mm->exec_keep_clones--;
> + spin_unlock(&mm->page_table_lock);
> +}
[Severity: Low]
This isn't a correctness problem, but is the cost of this accounting
intended for every spawn?
exec_keep_clone_begin() in copy_mm() and exec_keep_clone_end() in
copy_process() each take and release the parent's mm->page_table_lock.
This happens on every CLONE_VM clone that is not a thread (vfork(),
posix_spawn(), clone(CLONE_VM)), whether or not the mm has any
VM_EXEC_KEEP VMA and whether or not an exec is in progress.
page_table_lock also protects page table operations, so a multithreaded
parent taking page faults can contend with it.
Would an atomic counter, or skipping the accounting until the mm has ever
had MADV_DOEXEC applied, avoid the two lock round trips per spawn?
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261014-b4-prep-khorenko-vstor-149847-2-doexec-pinned-memory-v1-0-36f1120f9b57@virtuozzo.com?part=7
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* [Devel] [PATCH hci-8.1 7/7] selftests/mm: say why a doexec case fails
2026-10-14 17:14 10% ` [Devel] [PATCH hci-8.1 1/7] selftests/mm: cover the MADV_DOEXEC interface and VMA attributes Konstantin Khorenko
2026-10-14 17:15 4% ` [Devel] [PATCH hci-8.1 2/7] selftests/mm: cover the MADV_DOEXEC opt-in note and the new stack Konstantin Khorenko
@ 2026-10-14 17:15 7% ` Konstantin Khorenko
2 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-10-14 17:15 UTC (permalink / raw)
To: Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis; +Cc: devel
Many failure paths of doexec return KSFT_FAIL without a word, so a
failing case shows nothing but "not ok". Print the call that failed
with its errno, or what the check found, before failing.
https://virtuozzo.atlassian.net/browse/VSTOR-150234
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 223 ++++++++++++++++++++++--------------
1 file changed, 136 insertions(+), 87 deletions(-)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index 1b33bc489eb71..f74471abe13e2 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -103,6 +103,8 @@ static int wait_child(pid_t pid)
do {
ret = waitpid(pid, &status, 0);
} while (ret < 0 && errno == EINTR);
+ if (ret >= 0 && WIFSIGNALED(status))
+ ksft_print_msg("child killed by signal %d\n", WTERMSIG(status));
if (ret < 0 || !WIFEXITED(status))
return KSFT_FAIL;
return WEXITSTATUS(status);
@@ -171,6 +173,13 @@ static int fail(const char *what)
return KSFT_FAIL;
}
+static int fail_errno(const char *what)
+{
+ ksft_print_msg("%s: %s\n", what, strerror(errno));
+ fflush(stdout);
+ return KSFT_FAIL;
+}
+
static int check_contents(char *ram, size_t size, unsigned char value)
{
return check_bytes(ram, size, value) ? fail("contents lost") : KSFT_PASS;
@@ -179,14 +188,14 @@ static int check_contents(char *ram, size_t size, unsigned char value)
static int after_cow_exec(char *ram, size_t size, pid_t sibling, int pipe_fd)
{
char ready = 1;
- int ret = check_bytes(ram, size, 0x31);
+ int ret = check_bytes(ram, size, 0x31) ? fail("contents lost") : KSFT_PASS;
memset(ram, 0x42, size);
if (write(pipe_fd, &ready, 1) != 1)
- ret = KSFT_FAIL;
+ ret = fail("lost the COW sibling");
close(pipe_fd);
if (wait_child(sibling))
- ret = KSFT_FAIL;
+ ret = fail("COW sibling saw the new image's writes");
return ret;
}
@@ -202,20 +211,22 @@ static int before_cow_exec(void)
ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ram == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
memset(ram, 0x31, size);
if (madvise(ram, size, MADV_DOEXEC))
return unsupported();
if (pipe(pipe_fd))
- return KSFT_FAIL;
+ return fail_errno("pipe");
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
close(pipe_fd[1]);
if (read(pipe_fd[0], &ready, 1) != 1)
_exit(KSFT_FAIL);
- _exit(check_bytes(ram, size, 0x31));
+ if (check_bytes(ram, size, 0x31))
+ _exit(fail("COW sibling lost its contents"));
+ _exit(KSFT_PASS);
}
close(pipe_fd[0]);
snprintf(addr_arg, sizeof(addr_arg), "%llu",
@@ -225,6 +236,7 @@ static int before_cow_exec(void)
snprintf(fd_arg, sizeof(fd_arg), "%d", pipe_fd[1]);
execl("/proc/self/exe", "doexec", "--cow", addr_arg, size_arg,
pid_arg, fd_arg, NULL);
+ fail_errno("exec");
close(pipe_fd[1]);
wait_child(pid);
return KSFT_FAIL;
@@ -242,8 +254,7 @@ static void exec_stage(int fd, char *ram, size_t size, int kind, int stage)
snprintf(stage_arg, sizeof(stage_arg), "%d", stage);
execl("/proc/self/exe", "doexec", "--exec", fd_arg, addr_arg, size_arg,
kind_arg, stage_arg, NULL);
- ksft_print_msg("exec: %s\n", strerror(errno));
- _exit(KSFT_FAIL);
+ _exit(fail_errno("exec"));
}
static int after_exec(int fd, char *ram, size_t size, int kind, int stage)
@@ -255,33 +266,35 @@ static int after_exec(int fd, char *ram, size_t size, int kind, int stage)
if (check_pin(fd, ram, size, before))
return KSFT_FAIL;
if (kind == PROTECTED && mprotect(ram, size, PROT_READ | PROT_WRITE))
- return KSFT_FAIL;
+ return fail_errno("mprotect");
memset(ram, after, size);
if (check_pin(fd, ram, size, after))
return KSFT_FAIL;
if (stage == 1) {
/* VM_EXEC_KEEP is consumed, so mark again for the second exec. */
if (madvise(ram, size, MADV_DOEXEC))
- return KSFT_FAIL;
+ return fail_errno("MADV_DOEXEC");
if (kind == PROTECTED && mprotect(ram, size, PROT_READ))
- return KSFT_FAIL;
+ return fail_errno("mprotect");
exec_stage(fd, ram, size, kind, 2);
}
/* Fork must copy into the child and retain the parent's pinned pages. */
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
memset(ram, 0x64, size);
_exit(KSFT_PASS);
}
- if (wait_child(pid) || check_pin(fd, ram, size, after))
+ if (wait_child(pid))
+ return fail("fork child of the new image failed");
+ if (check_pin(fd, ram, size, after))
return KSFT_FAIL;
memset(ram, 0x75, size);
if (check_pin(fd, ram, size, 0x75))
return KSFT_FAIL;
- return ioctl(fd, PIN_LONGTERM_TEST_STOP) ? KSFT_FAIL : KSFT_PASS;
+ return ioctl(fd, PIN_LONGTERM_TEST_STOP) ? fail_errno("PIN_LONGTERM_TEST_STOP") : KSFT_PASS;
}
static size_t huge_page_size(void)
@@ -316,7 +329,7 @@ static int before_exec(int kind)
mapping = mmap(NULL, 2 * size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mapping == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
aligned = ((uintptr_t)mapping + size - 1) & ~(size - 1);
ram = (char *)aligned;
if (ram != mapping)
@@ -325,7 +338,7 @@ static int before_exec(int kind)
munmap(ram + size, mapping + 2 * size - (ram + size));
if (madvise(ram, size, kind == HUGE ? MADV_HUGEPAGE : MADV_NOHUGEPAGE) &&
(kind == HUGE || errno != EINVAL))
- return KSFT_FAIL;
+ return fail_errno("THP madvise");
memset(ram, 0x31, size);
if (kind == HUGE && madvise(ram, size, MADV_COLLAPSE))
return KSFT_SKIP;
@@ -345,7 +358,7 @@ static int before_exec(int kind)
return errno == ENOTTY ? KSFT_SKIP : KSFT_FAIL;
}
if (kind == PROTECTED && mprotect(ram, size, PROT_READ))
- return KSFT_FAIL;
+ return fail_errno("mprotect");
exec_stage(fd, ram, size, kind, 1);
return KSFT_FAIL;
}
@@ -366,7 +379,13 @@ static int exec_path(const char *exe, char *ram, size_t size, size_t stride,
static int check_exec(char *ram, size_t size, size_t stride, bool reject)
{
- return exec_path("/proc/self/exe", ram, size, stride, reject);
+ int err = exec_path("/proc/self/exe", ram, size, stride, reject);
+
+ if (!reject) {
+ errno = err;
+ fail_errno("exec");
+ }
+ return err;
}
static char *preserved_ram(size_t size)
@@ -417,10 +436,10 @@ static int shared_mm_exec(bool own_thread)
if (!ram)
return unsupported();
if (pipe(pipe_fd))
- return KSFT_FAIL;
+ return fail_errno("pipe");
if (own_thread) {
if (pthread_create(&thread, NULL, blocked_thread, &pipe_fd[0]))
- return KSFT_FAIL;
+ return fail("pthread_create failed");
check_exec(ram, size, 1, false);
write(pipe_fd[1], "x", 1);
pthread_join(thread, NULL);
@@ -429,16 +448,18 @@ static int shared_mm_exec(bool own_thread)
stack = mmap(NULL, 65536, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
if (stack == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
pid = clone(blocked_clone, stack + 65536, CLONE_VM | SIGCHLD, pipe_fd);
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("clone");
ret = check_exec(ram, size, 1, true);
/* Exec must return EBUSY with the old mapping intact. */
- if (ret != EBUSY || check_bytes(ram, size, 0x31))
- ret = KSFT_FAIL;
- else
- ret = KSFT_PASS;
+ if (ret != EBUSY) {
+ errno = ret;
+ ret = fail_errno("exec with a CLONE_VM task not refused with EBUSY");
+ } else {
+ ret = check_bytes(ram, size, 0x31) ? fail("contents lost") : KSFT_PASS;
+ }
write(pipe_fd[1], "x", 1);
if (wait_child(pid))
ret = KSFT_FAIL;
@@ -464,7 +485,7 @@ static int vhost_owner_exec(void)
if (ioctl(fd, VHOST_SET_OWNER))
return KSFT_SKIP;
if (ioctl(fd, VHOST_RESET_OWNER))
- return KSFT_FAIL;
+ return fail_errno("VHOST_RESET_OWNER");
close(fd);
check_exec(ram, size, 1, false);
return KSFT_FAIL;
@@ -520,7 +541,7 @@ static int unprivileged_exec(const char *path, bool preserve)
int err;
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (pid)
return wait_child(pid);
if (setgroups(0, NULL) || setresgid(65534, 65534, 65534) ||
@@ -535,12 +556,16 @@ static int unprivileged_exec(const char *path, bool preserve)
_exit(unsupported());
err = exec_path(path, ram, size, 1, true);
/* Exec must return EPERM with the old mapping intact. */
- if (err != EPERM || check_bytes(ram, size, 0x31))
- _exit(KSFT_FAIL);
+ if (err != EPERM) {
+ errno = err;
+ _exit(fail_errno("setuid exec not refused with EPERM"));
+ }
+ if (check_bytes(ram, size, 0x31))
+ _exit(fail("contents lost by a refused setuid exec"));
if (madvise(ram, size, MADV_DONTEXEC))
- _exit(KSFT_FAIL);
+ _exit(fail_errno("MADV_DONTEXEC"));
execl(path, "doexec", "--euid", NULL);
- _exit(KSFT_FAIL);
+ _exit(fail_errno("exec of the setuid copy"));
}
/* The ELF opt-in must not pull memory into a more privileged image. */
@@ -575,7 +600,7 @@ static int devzero_exec(void)
ram = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
close(fd);
if (ram == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
memset(ram, 0x31, size);
if (madvise(ram, size, MADV_DOEXEC))
return unsupported();
@@ -655,6 +680,8 @@ static int noexec_dontexec(void)
}
if (!madvise(ram, size, MADV_DONTEXEC) && exec_refused(ram, size))
ret = KSFT_PASS;
+ else
+ ret = fail("MADV_DONTEXEC made a noexec mapping executable");
out:
umount2(dir, MNT_DETACH);
rmdir(dir);
@@ -670,9 +697,13 @@ static int doexec_noexec(void)
if (!ram)
return unsupported();
- if (!exec_refused(ram, size) || madvise(ram, size, MADV_DONTEXEC) ||
- mprotect(ram, size, PROT_READ | PROT_EXEC))
- return KSFT_FAIL;
+ if (!exec_refused(ram, size))
+ return fail("marked range can be made executable");
+ if (madvise(ram, size, MADV_DONTEXEC) || !smaps_has_flag(ram, " me "))
+ return fail("MADV_DONTEXEC did not give VM_MAYEXEC back");
+ /* SELinux execmem or MDWE may still refuse it. */
+ if (mprotect(ram, size, PROT_READ | PROT_EXEC))
+ return KSFT_SKIP;
ret = noexec_dontexec();
if (ret == KSFT_SKIP)
ksft_print_msg("noexec mount part skipped\n");
@@ -684,11 +715,11 @@ static int after_pfnmap_exec(char *bar, size_t size)
size_t page = sysconf(_SC_PAGESIZE), i;
if (!smaps_has_flag(bar, " pf ") || !smaps_has_flag(bar, " sh "))
- return KSFT_FAIL;
+ return fail("BAR not a shared PFN map after exec");
for (i = 0; i < size / page; i++)
if (__atomic_load_n((uint32_t *)(bar + i * page), __ATOMIC_RELAXED) !=
(0xd0e00000 | i))
- return KSFT_FAIL;
+ return fail("BAR contents changed across exec");
return munmap(bar, size) ? KSFT_FAIL : KSFT_PASS;
}
@@ -709,7 +740,9 @@ static int devmem_refused(void)
close(fd);
if (ram == MAP_FAILED || !smaps_has_flag(ram, " pf "))
return KSFT_SKIP;
- return madvise(ram, size, MADV_DOEXEC) && errno == EINVAL ? KSFT_PASS : KSFT_FAIL;
+ if (madvise(ram, size, MADV_DOEXEC) && errno == EINVAL)
+ return KSFT_PASS;
+ return fail("MADV_DOEXEC on a shared /dev/mem mapping not refused");
}
static int vfio_device_fd(const char *bdf)
@@ -764,12 +797,12 @@ static int vfio_bar_exec(void)
/* Align to the size so that the BAR can be mapped with huge PFN entries. */
area = mmap(NULL, 2 * size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (area == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
aligned = ((uintptr_t)area + size - 1) & ~(size - 1);
bar = mmap((void *)aligned, size, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_FIXED, dev, info.offset);
if (bar == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
for (i = 0; i < size / page; i++)
__atomic_store_n((uint32_t *)(bar + i * page), 0xd0e00000 | i,
__ATOMIC_RELAXED);
@@ -781,7 +814,7 @@ static int vfio_bar_exec(void)
(unsigned long long)(uintptr_t)bar);
snprintf(size_arg, sizeof(size_arg), "%zu", size);
execl("/proc/self/exe", "doexec", "--pfnmap", addr_arg, size_arg, NULL);
- return KSFT_FAIL;
+ return fail_errno("exec");
}
static void *exec_thread(void *arg)
@@ -798,7 +831,7 @@ static int nonleader_exec(void)
if (!ram)
return unsupported();
if (pthread_create(&thread, NULL, exec_thread, ram))
- return KSFT_FAIL;
+ return fail("pthread_create failed");
pthread_join(thread, NULL);
return KSFT_FAIL;
}
@@ -808,18 +841,18 @@ static int proc_exec(char *ram, size_t size, int stage)
char addr_arg[32], size_arg[32], stage_arg[32];
if (check_bytes(ram, size, 0x31))
- return KSFT_FAIL;
+ return fail("contents lost in a repeated exec");
if (stage == 32)
return KSFT_PASS;
if (madvise(ram, size, MADV_DOEXEC))
- return KSFT_FAIL;
+ return fail_errno("MADV_DOEXEC");
snprintf(addr_arg, sizeof(addr_arg), "%llu",
(unsigned long long)(uintptr_t)ram);
snprintf(size_arg, sizeof(size_arg), "%zu", size);
snprintf(stage_arg, sizeof(stage_arg), "%d", stage + 1);
execl("/proc/self/exe", "doexec", "--proc", addr_arg, size_arg,
stage_arg, NULL);
- return KSFT_FAIL;
+ return fail_errno("exec");
}
/* Read both pre-opened and freshly opened proc files during repeated exec. */
@@ -833,10 +866,10 @@ static int proc_readers_exec(void)
int i;
if (pipe(ready) || pipe(go))
- return KSFT_FAIL;
+ return fail_errno("pipe");
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
size_t size = 16 * sysconf(_SC_PAGESIZE);
char *ram = preserved_ram(size);
@@ -874,7 +907,8 @@ static int proc_readers_exec(void)
for (;;) {
waited = waitpid(pid, &status, WNOHANG);
if (waited == pid) {
- ret = WIFEXITED(status) ? WEXITSTATUS(status) : KSFT_FAIL;
+ ret = WIFEXITED(status) ? WEXITSTATUS(status) :
+ fail("repeated exec killed by a signal");
break;
}
if (waited < 0 && errno != EINTR)
@@ -968,13 +1002,14 @@ static int clone_exec_race(void)
pid_t pid;
if (pipe(report))
- return KSFT_FAIL;
+ return fail_errno("pipe");
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
size_t size = sysconf(_SC_PAGESIZE);
struct clone_race race = { .report = report[1] };
+ const char *why = "exec refused with EBUSY every time";
pthread_t thread;
unsigned int attempt;
@@ -983,25 +1018,31 @@ static int clone_exec_race(void)
if (!race.ram)
_exit(unsupported());
if (pthread_create(&thread, NULL, clone_race_thread, &race))
- _exit(KSFT_FAIL);
+ _exit(fail("pthread_create failed"));
while (atomic_load(&race.rounds) < 4)
usleep(1000);
for (attempt = 0; attempt < 500; attempt++) {
- if (check_exec(race.ram, size, 1, false) != EBUSY)
+ errno = exec_path("/proc/self/exe", race.ram, size, 1, false);
+ if (errno != EBUSY) {
+ why = NULL;
+ fail_errno("exec");
break;
- if (check_bytes(race.ram, size, 0x31))
+ }
+ if (check_bytes(race.ram, size, 0x31)) {
+ why = "contents changed while exec was refused";
break;
+ }
usleep(1000);
}
atomic_store(&race.stop, true);
pthread_join(thread, NULL);
- _exit(KSFT_FAIL);
+ _exit(why ? fail(why) : KSFT_FAIL);
}
close(report[1]);
ret = wait_child(pid);
pfd = (struct pollfd) { .fd = report[0], .events = POLLIN | POLLHUP };
if (poll(&pfd, 1, 3000) <= 0 || read(report[0], &failed, 1) != 0)
- ret = KSFT_FAIL;
+ ret = fail("a racing CLONE_VM clone failed");
close(report[0]);
return ret;
}
@@ -1041,7 +1082,7 @@ static int allocation_failures(void)
for (nth = 1; nth <= 256; nth++) {
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
char *ram, number[32];
size_t i;
@@ -1052,7 +1093,7 @@ static int allocation_failures(void)
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ram == MAP_FAILED ||
(madvise(ram, size, MADV_NOHUGEPAGE) && errno != EINVAL))
- _exit(KSFT_FAIL);
+ _exit(fail_errno("mmap or MADV_NOHUGEPAGE"));
for (i = 0; i < size; i += stride)
ram[i] = 0x31;
if (madvise(ram, size, MADV_DOEXEC))
@@ -1062,13 +1103,13 @@ static int allocation_failures(void)
fd = open("/proc/self/fail-nth", O_WRONLY | O_CLOEXEC);
len = snprintf(number, sizeof(number), "%u", nth);
if (exe < 0 || fd < 0 || write(fd, number, len) != len)
- _exit(KSFT_FAIL);
+ _exit(fail_errno("fail-nth setup"));
err = check_exec_fd(exe, ram, size, stride);
/* Disarm before checking mappings or exiting. */
pwrite(fd, "0", 1, 0);
for (i = 0; i < size; i += stride)
if (ram[i] != 0x31)
- _exit(KSFT_FAIL);
+ _exit(fail("contents lost by a failed exec"));
/*
* Failed allocations also surface as E2BIG from
* copy_strings(), and as ELIBBAD or EIO from reading the
@@ -1084,7 +1125,7 @@ static int allocation_failures(void)
waited = waitpid(pid, &status, 0);
} while (waited < 0 && errno == EINTR);
if (waited < 0)
- return KSFT_FAIL;
+ return fail_errno("waitpid");
if (WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV)
fatal++;
else if (WIFEXITED(status) && WEXITSTATUS(status) == 10)
@@ -1141,9 +1182,11 @@ static int sysctl_flag(void)
static bool doexec_refused(char *addr, size_t size, const char *what)
{
if (addr == MAP_FAILED) {
- ksft_print_msg("%s: mmap: %s\n", what, strerror(errno));
/* A security policy may forbid the mapping itself. */
- return errno == EACCES || errno == EPERM;
+ if (errno == EACCES || errno == EPERM)
+ return true;
+ ksft_print_msg("%s: mmap: %s\n", what, strerror(errno));
+ return false;
}
if (madvise(addr, size, MADV_DOEXEC) && errno == EINVAL)
return true;
@@ -1227,7 +1270,7 @@ static int partial_exec(void)
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ram == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
memset(ram, 0x31, 3 * page);
if (madvise(ram + page, page, MADV_DOEXEC))
return unsupported();
@@ -1334,11 +1377,13 @@ static int after_fork_flag(char *ram, size_t size, const char *flag, bool wipe)
return fail("contents lost");
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
- if (wipe)
- _exit(range_mapped(ram, size) ? check_bytes(ram, size, 0) : KSFT_FAIL);
- _exit(range_mapped(ram, size) ? KSFT_FAIL : KSFT_PASS);
+ if (wipe && (!range_mapped(ram, size) || check_bytes(ram, size, 0)))
+ _exit(fail("child of the new image did not get a wiped range"));
+ if (!wipe && range_mapped(ram, size))
+ _exit(fail("child of the new image got a DONTFORK range"));
+ _exit(KSFT_PASS);
}
return wait_child(pid);
}
@@ -1581,7 +1626,7 @@ static int copy_control(void)
int exe;
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
ram = preserved_ram(size);
if (!ram)
@@ -1604,7 +1649,7 @@ static int no_note_exec(void)
err = copy_control();
if (err)
- return err == KSFT_SKIP ? KSFT_SKIP : fail("unpatched copy lost the memory");
+ return err == KSFT_SKIP ? KSFT_SKIP : fail("unpatched copy did not keep the memory");
ram = preserved_ram(size);
if (!ram)
return unsupported();
@@ -1732,7 +1777,7 @@ static int stack_overlap_exec(void)
/* ...within the 128K the stack is grown by, it kills the task. */
status = stack_exec(16);
if (status < 0)
- return KSFT_FAIL;
+ return fail_errno("fork or waitpid");
if (WIFEXITED(status))
return WEXITSTATUS(status) == KSFT_SKIP ? KSFT_SKIP :
fail("exec did not kill the task");
@@ -1757,7 +1802,7 @@ static int sparse_exec(void)
return KSFT_SKIP;
/* EINVAL: no THP support, the range is mapped with base pages anyway. */
if (madvise(ram, size, MADV_NOHUGEPAGE) && errno != EINVAL)
- return KSFT_FAIL;
+ return fail_errno("MADV_NOHUGEPAGE");
for (i = 0; i < size; i += stride)
ram[i] = 0x31;
if (madvise(ram, size, MADV_DOEXEC))
@@ -1790,7 +1835,7 @@ static int swapped_exec(void)
if (ram == MAP_FAILED ||
(madvise(ram, size, MADV_NOHUGEPAGE) && errno != EINVAL))
- return KSFT_FAIL;
+ return fail_errno("mmap or MADV_NOHUGEPAGE");
memset(ram, 0x31, size);
if (madvise(ram, size, MADV_PAGEOUT))
return KSFT_SKIP;
@@ -1824,7 +1869,7 @@ static int thp_half_exec(void)
area = mmap(NULL, 2 * size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (area == MAP_FAILED)
- return KSFT_FAIL;
+ return fail_errno("mmap");
ram = (char *)(((uintptr_t)area + size - 1) & ~(size - 1));
if (madvise(ram, size, MADV_HUGEPAGE))
return KSFT_SKIP;
@@ -1873,7 +1918,7 @@ static int after_old_mm(char *ram, size_t size, int done, int check)
char c;
if (write(done, "x", 1) != 1 || read(check, &c, 1) != 1)
- return KSFT_FAIL;
+ return fail("lost the parent of the old mm write case");
if (check_bytes(ram, size, 0x31))
return fail("a write through the old mm reached the new image");
return KSFT_PASS;
@@ -1920,11 +1965,11 @@ static int old_mm_write_exec(void)
reg.range.len = size;
if (w.src == MAP_FAILED || fill == MAP_FAILED || ioctl(uffd, UFFDIO_API, &api) ||
ioctl(uffd, UFFDIO_REGISTER, ®) || pipe(to_child) || pipe(to_parent))
- return KSFT_FAIL;
+ return fail_errno("userfaultfd setup");
memset(fill, 0x77, size);
pid = fork();
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (!pid) {
char *ram = preserved_ram(size), args[4][32];
@@ -1942,7 +1987,7 @@ static int old_mm_write_exec(void)
snprintf(args[3], sizeof(args[3]), "%d", to_child[0]);
execl("/proc/self/exe", "doexec", "--old-mm", args[0], args[1], args[2],
args[3], NULL);
- _exit(KSFT_FAIL);
+ _exit(fail_errno("exec"));
}
close(to_child[0]);
close(to_parent[1]);
@@ -1957,8 +2002,10 @@ static int old_mm_write_exec(void)
ret = KSFT_SKIP;
goto out;
}
- if (pthread_create(&thread, NULL, mem_writer, &w))
+ if (pthread_create(&thread, NULL, mem_writer, &w)) {
+ ret = fail("pthread_create failed");
goto out;
+ }
/* The writer holds the child's mm while it faults on its source. */
pfd.fd = uffd;
pfd.events = POLLIN;
@@ -2009,7 +2056,7 @@ static int vhost_kthread_exec(void)
if (err != EBUSY || check_bytes(ram, size, 0x31))
return fail("preserving exec with a vhost kthread not refused");
if (ioctl(fd, VHOST_RESET_OWNER))
- return KSFT_FAIL;
+ return fail_errno("VHOST_RESET_OWNER");
close(fd);
check_exec(ram, size, 1, false);
return KSFT_FAIL;
@@ -2044,10 +2091,10 @@ static int vfork_exec(void)
_exit(errno == EBUSY ? REFUSED : KSFT_FAIL);
}
if (pid < 0)
- return KSFT_FAIL;
+ return fail_errno("fork");
if (wait_child(pid) != REFUSED)
return fail("preserving exec from a vfork() child not refused");
- return check_bytes(ram, size, 0x31) ? KSFT_FAIL : KSFT_PASS;
+ return check_bytes(ram, size, 0x31) ? fail("contents lost by a refused exec") : KSFT_PASS;
}
static const struct more_case more_cases[] = {
@@ -2083,7 +2130,7 @@ int main(int argc, char **argv)
/* The setuid copy only reports that it runs as root. */
if (geteuid() != getuid())
return argc == 2 && !strcmp(argv[1], "--euid") && !geteuid() ?
- KSFT_PASS : KSFT_FAIL;
+ KSFT_PASS : fail("setuid copy started in another mode");
if (argc == 7 && !strcmp(argv[1], "--exec"))
return after_exec(atoi(argv[2]),
(char *)(uintptr_t)strtoull(argv[3], NULL, 10),
@@ -2092,7 +2139,7 @@ int main(int argc, char **argv)
return after_cow_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
strtoull(argv[3], NULL, 10), atoi(argv[4]), atoi(argv[5]));
if (argc == 5 && !strcmp(argv[1], "--reject"))
- return KSFT_FAIL;
+ return fail("exec went through instead of failing");
for (kind = 0; argc == 4 && kind < (int)ARRAY_SIZE(after_modes); kind++)
if (!strcmp(argv[1], after_modes[kind].mode))
return after_modes[kind].fn((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
@@ -2121,8 +2168,10 @@ int main(int argc, char **argv)
if (!range_mapped(ram, size))
return fail("range not preserved");
for (i = 0; i < size; i += stride)
- if (ram[i] != 0x31)
- return KSFT_FAIL;
+ if (ram[i] != 0x31) {
+ ksft_print_msg("byte %zu: %#x\n", i, (unsigned char)ram[i]);
+ return fail("preserved contents changed");
+ }
return KSFT_PASS;
}
ksft_print_header();
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 7%]
* [Devel] [PATCH hci-8.1 1/7] selftests/mm: cover the MADV_DOEXEC interface and VMA attributes
@ 2026-10-14 17:14 10% ` Konstantin Khorenko
2026-10-14 17:15 4% ` [Devel] [PATCH hci-8.1 2/7] selftests/mm: cover the MADV_DOEXEC opt-in note and the new stack Konstantin Khorenko
2026-10-14 17:15 7% ` [Devel] [PATCH hci-8.1 7/7] selftests/mm: say why a doexec case fails Konstantin Khorenko
2 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-10-14 17:14 UTC (permalink / raw)
To: Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis; +Cc: devel
Add cases for what madvise accepts and what the copy of a preserved VMA
keeps:
- /proc/sys/vm/madv_doexec_flag reports MADV_DOEXEC, and a kernel
without the sysctl does not accept MADV_DOEXEC either.
- Shared anonymous, private file, executable, MAP_GROWSDOWN and stack
mappings are refused with EINVAL, and so is a hugetlb mapping when
huge pages are available.
- A range unmarked with MADV_DONTEXEC (twice, the second a no-op) is
not preserved.
- Marking the middle page of a mapping preserves that page only.
- An mlocked range is preserved without VM_LOCKED.
- MADV_DONTFORK and MADV_WIPEONFORK carry over, so a fork in the new
image leaves the range out or wipes it.
- A userfaultfd registration does not carry over.
- The memory policy of the range carries over.
A flag expected to disappear is checked in smaps before the exec as
well, so that a failed lookup cannot pass for its absence. EINVAL from
MADV_DOEXEC fails a case on a 64-bit kernel with the madv_doexec_flag
sysctl, in the existing cases too; the cases skip only when something
they need is missing: the feature, free huge pages, the mlock limit,
userfaultfd, mbind. A failing case prints what failed, and the
children running the cases, old and new, exit with exit(), so that the
message reaches a pipe.
New cases go to a table, and the checks run in the new image to a
second one, so that main() stays small.
https://virtuozzo.atlassian.net/browse/VSTOR-150234
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 424 +++++++++++++++++++++++++++++++++---
1 file changed, 392 insertions(+), 32 deletions(-)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index a89fe2f067df5..850b498e3d1dc 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -1,10 +1,12 @@
// SPDX-License-Identifier: GPL-2.0-only
-/* Verify that exec preserves the memory backing long-term DMA pins. */
+/* Verify what MADV_DOEXEC preserves across exec, long-term DMA pins first. */
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <limits.h>
+#include <linux/mempolicy.h>
+#include <linux/userfaultfd.h>
#include <linux/vfio.h>
#include <linux/vhost.h>
#include <poll.h>
@@ -25,12 +27,16 @@
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/sysmacros.h>
+#include <sys/utsname.h>
#include <sys/wait.h>
#include <unistd.h>
#include "../../../../mm/gup_test.h"
#include "../kselftest.h"
+#ifndef UFFD_USER_MODE_ONLY
+#define UFFD_USER_MODE_ONLY 1
+#endif
#ifndef MADV_DOEXEC
#define MADV_DOEXEC 201
#endif
@@ -40,6 +46,9 @@
#ifndef MADV_COLLAPSE
#define MADV_COLLAPSE 25
#endif
+#ifndef MADV_WIPEONFORK
+#define MADV_WIPEONFORK 18
+#endif
/* The downstream ELF loader requires this opt-in in a PT_NOTE segment. */
asm(".pushsection .note.doexec,\"a\",%note\n"
@@ -117,6 +126,38 @@ static int check_bytes(char *ram, size_t size, unsigned char value)
return KSFT_PASS;
}
+/*
+ * EINVAL from MADV_DOEXEC means no feature only on a kernel without the
+ * sysctl, or on a 32-bit one, which has no VM_EXEC_KEEP.
+ */
+static int unsupported(void)
+{
+ int err = errno;
+ struct utsname uts;
+
+ if (err == EINVAL &&
+ (access("/proc/sys/vm/madv_doexec_flag", F_OK) ||
+ (!uname(&uts) && !strstr(uts.machine, "64") &&
+ strcmp(uts.machine, "s390x") && strcmp(uts.machine, "alpha"))))
+ return KSFT_SKIP;
+ ksft_print_msg("MADV_DOEXEC: %s\n", strerror(err));
+ fflush(stdout);
+ errno = err;
+ return KSFT_FAIL;
+}
+
+static int fail(const char *what)
+{
+ ksft_print_msg("%s\n", what);
+ fflush(stdout);
+ return KSFT_FAIL;
+}
+
+static int check_contents(char *ram, size_t size, unsigned char value)
+{
+ return check_bytes(ram, size, value) ? fail("contents lost") : KSFT_PASS;
+}
+
static int after_cow_exec(char *ram, size_t size, pid_t sibling, int pipe_fd)
{
char ready = 1;
@@ -146,7 +187,7 @@ static int before_cow_exec(void)
return KSFT_FAIL;
memset(ram, 0x31, size);
if (madvise(ram, size, MADV_DOEXEC))
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
if (pipe(pipe_fd))
return KSFT_FAIL;
pid = fork();
@@ -272,10 +313,8 @@ static int before_exec(int kind)
return KSFT_SKIP;
if (kind == LOCKED && mlock(ram, size))
return KSFT_SKIP;
- if (madvise(ram, size, MADV_DOEXEC)) {
- ksft_print_msg("MADV_DOEXEC: %s\n", strerror(errno));
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
- }
+ if (madvise(ram, size, MADV_DOEXEC))
+ return unsupported();
pin = (struct pin_longterm_test) {
.addr = (uintptr_t)ram,
.size = size,
@@ -358,7 +397,7 @@ static int shared_mm_exec(bool own_thread)
pid_t pid;
if (!ram)
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
if (pipe(pipe_fd))
return KSFT_FAIL;
if (own_thread) {
@@ -403,7 +442,7 @@ static int vhost_owner_exec(void)
return KSFT_SKIP;
ram = preserved_ram(size);
if (!ram)
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
if (ioctl(fd, VHOST_SET_OWNER))
return KSFT_SKIP;
if (ioctl(fd, VHOST_RESET_OWNER))
@@ -473,7 +512,7 @@ static int unprivileged_exec(const char *path, bool preserve)
}
ram = preserved_ram(size);
if (!ram)
- _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ _exit(unsupported());
err = exec_path(path, ram, size, 1, true);
/* Exec must return EPERM with the old mapping intact. */
if (err != EPERM || check_bytes(ram, size, 0x31))
@@ -520,7 +559,7 @@ static int devzero_exec(void)
return KSFT_FAIL;
memset(ram, 0x31, size);
if (madvise(ram, size, MADV_DOEXEC))
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
check_exec(ram, size, 1, false);
return KSFT_FAIL;
}
@@ -558,7 +597,7 @@ static int noexec_dontexec(void)
goto out;
}
if (madvise(ram, size, MADV_DOEXEC)) {
- ret = errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ ret = unsupported();
goto out;
}
if (!madvise(ram, size, MADV_DONTEXEC) && exec_refused(ram, size))
@@ -577,7 +616,7 @@ static int doexec_noexec(void)
int ret;
if (!ram)
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
if (!exec_refused(ram, size) || madvise(ram, size, MADV_DONTEXEC) ||
mprotect(ram, size, PROT_READ | PROT_EXEC))
return KSFT_FAIL;
@@ -631,7 +670,7 @@ static int devmem_refused(void)
int fd;
if (!preserved_ram(size))
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
/* Without FMODE_WRITE, do_mmap() drops VM_SHARED from the mapping. */
fd = open("/dev/mem", O_RDWR);
if (fd < 0)
@@ -681,8 +720,10 @@ static int vfio_bar_exec(void)
uintptr_t aligned;
int dev;
- if (!bdf || !preserved_ram(page))
+ if (!bdf)
return KSFT_SKIP;
+ if (!preserved_ram(page))
+ return unsupported();
dev = vfio_device_fd(bdf);
if (dev < 0 || ioctl(dev, VFIO_DEVICE_GET_REGION_INFO, &info) ||
!(info.flags & VFIO_REGION_INFO_FLAG_MMAP) || info.size < page) {
@@ -725,7 +766,7 @@ static int nonleader_exec(void)
pthread_t thread;
if (!ram)
- return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ return unsupported();
if (pthread_create(&thread, NULL, exec_thread, ram))
return KSFT_FAIL;
pthread_join(thread, NULL);
@@ -773,7 +814,7 @@ static int proc_readers_exec(void)
close(ready[0]);
close(go[1]);
if (!ram)
- _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ _exit(unsupported());
if (write(ready[1], "x", 1) != 1 || read(go[0], buffer, 1) != 1)
_exit(KSFT_FAIL);
close(ready[1]);
@@ -910,7 +951,7 @@ static int clone_exec_race(void)
close(report[0]);
race.ram = preserved_ram(size);
if (!race.ram)
- _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ _exit(unsupported());
if (pthread_create(&thread, NULL, clone_race_thread, &race))
_exit(KSFT_FAIL);
while (atomic_load(&race.rounds) < 4)
@@ -985,7 +1026,7 @@ static int allocation_failures(void)
for (i = 0; i < size; i += stride)
ram[i] = 0x31;
if (madvise(ram, size, MADV_DOEXEC))
- _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ _exit(unsupported());
/* Keep the path lookup, which /proc fails with ENOENT, out of the sweep. */
exe = open("/proc/self/exe", O_RDONLY | O_CLOEXEC);
fd = open("/proc/self/fail-nth", O_WRONLY | O_CLOEXEC);
@@ -1034,6 +1075,308 @@ static int allocation_failures(void)
return early && fatal ? KSFT_PASS : KSFT_SKIP;
}
+static void exec_mode(const char *exe, const char *mode, void *addr, size_t size)
+{
+ char addr_arg[32], size_arg[32];
+
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)addr);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ execl(exe, "doexec", mode, addr_arg, size_arg, NULL);
+ ksft_print_msg("exec: %s\n", strerror(errno));
+ fflush(stdout);
+}
+
+static bool range_mapped(char *addr, size_t size)
+{
+ return !msync(addr, size, MS_ASYNC);
+}
+
+static int sysctl_flag(void)
+{
+ FILE *file = fopen("/proc/sys/vm/madv_doexec_flag", "r");
+ int flag = -1;
+
+ if (!file) {
+ if (access("/proc/sys/vm", F_OK) || !preserved_ram(sysconf(_SC_PAGESIZE)))
+ return KSFT_SKIP;
+ return fail("MADV_DOEXEC works without the sysctl");
+ }
+ if (fscanf(file, "%d", &flag) != 1)
+ flag = -1;
+ fclose(file);
+ return flag == MADV_DOEXEC ? KSFT_PASS : fail("unexpected madv_doexec_flag");
+}
+
+static bool doexec_refused(char *addr, size_t size, const char *what)
+{
+ if (addr == MAP_FAILED) {
+ ksft_print_msg("%s: mmap: %s\n", what, strerror(errno));
+ /* A security policy may forbid the mapping itself. */
+ return errno == EACCES || errno == EPERM;
+ }
+ if (madvise(addr, size, MADV_DOEXEC) && errno == EINVAL)
+ return true;
+ ksft_print_msg("%s not refused\n", what);
+ return false;
+}
+
+/* Apart from VFIO PFN maps, only private non-stack anonymous memory can be marked. */
+static int refused_mappings(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ int fd = open("/proc/self/exe", O_RDONLY);
+ char stack_var = 0;
+ bool ok = true;
+
+ if (!preserved_ram(size))
+ return unsupported();
+ ok &= doexec_refused(mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_SHARED | MAP_ANONYMOUS, -1, 0), size, "shared anon");
+ ok &= doexec_refused(mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0), size,
+ "private file");
+ ok &= doexec_refused(mmap(NULL, size, PROT_READ | PROT_EXEC,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0), size, "executable");
+ ok &= doexec_refused(mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN, -1, 0), size,
+ "MAP_GROWSDOWN");
+ ok &= doexec_refused((char *)((uintptr_t)&stack_var & ~(size - 1)), size, "stack");
+ close(fd);
+ return ok ? KSFT_PASS : KSFT_FAIL;
+}
+
+static int hugetlb_refused(void)
+{
+ size_t size = 2ul << 20;
+ char *addr;
+
+ if (!preserved_ram(sysconf(_SC_PAGESIZE)))
+ return unsupported();
+ addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);
+ if (addr == MAP_FAILED)
+ return KSFT_SKIP;
+ return doexec_refused(addr, size, "hugetlb") ? KSFT_PASS : KSFT_FAIL;
+}
+
+static int unmarked_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+
+ if (!ram)
+ return unsupported();
+ if (madvise(ram, size, MADV_DONTEXEC) || madvise(ram, size, MADV_DONTEXEC))
+ return fail("MADV_DONTEXEC failed");
+ exec_mode("/proc/self/exe", "--absent", ram, size);
+ return KSFT_FAIL;
+}
+
+static int after_absent(char *ram, size_t size)
+{
+ return range_mapped(ram, size) ? fail("range still mapped") : KSFT_PASS;
+}
+
+/* Marking the middle page splits the VMA; only that page carries over. */
+static int partial_exec(void)
+{
+ size_t page = sysconf(_SC_PAGESIZE);
+ char *ram = mmap(NULL, 3 * page, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+
+ if (ram == MAP_FAILED)
+ return KSFT_FAIL;
+ memset(ram, 0x31, 3 * page);
+ if (madvise(ram + page, page, MADV_DOEXEC))
+ return unsupported();
+ exec_mode("/proc/self/exe", "--partial", ram, 3 * page);
+ return KSFT_FAIL;
+}
+
+static int after_partial(char *ram, size_t size)
+{
+ size_t page = size / 3;
+
+ if (range_mapped(ram, page) || range_mapped(ram + 2 * page, page))
+ return fail("unmarked page preserved");
+ if (!range_mapped(ram + page, page))
+ return fail("marked page not preserved");
+ return check_contents(ram + page, page, 0x31);
+}
+
+/* The flag is checked before exec too, so that its absence after is meaningful. */
+static int exec_with_flag(char *ram, size_t size, const char *mode, const char *flag)
+{
+ if (!smaps_has_flag(ram, flag))
+ return fail("flag missing before exec");
+ exec_mode("/proc/self/exe", mode, ram, size);
+ return KSFT_FAIL;
+}
+
+static int flagged_exec(const char *mode, int advice, const char *flag)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+
+ if (!ram)
+ return unsupported();
+ if (madvise(ram, size, advice))
+ return fail("madvise failed");
+ return exec_with_flag(ram, size, mode, flag);
+}
+
+static int locked_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+
+ if (!ram)
+ return unsupported();
+ if (mlock(ram, size))
+ return KSFT_SKIP;
+ return exec_with_flag(ram, size, "--unlocked", " lo ");
+}
+
+static int after_unlocked(char *ram, size_t size)
+{
+ if (smaps_has_flag(ram, " lo "))
+ return fail("VM_LOCKED preserved");
+ return check_contents(ram, size, 0x31);
+}
+
+static int dontfork_exec(void)
+{
+ return flagged_exec("--dontfork", MADV_DONTFORK, " dc ");
+}
+
+static int wipeonfork_exec(void)
+{
+ return flagged_exec("--wipeonfork", MADV_WIPEONFORK, " wf ");
+}
+
+/* The fork flags carry over: a child of the new image sees what they say. */
+static int after_fork_flag(char *ram, size_t size, const char *flag, bool wipe)
+{
+ pid_t pid;
+
+ if (!smaps_has_flag(ram, flag))
+ return fail("fork flag lost");
+ if (check_bytes(ram, size, 0x31))
+ return fail("contents lost");
+ pid = fork();
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ if (wipe)
+ _exit(range_mapped(ram, size) ? check_bytes(ram, size, 0) : KSFT_FAIL);
+ _exit(range_mapped(ram, size) ? KSFT_FAIL : KSFT_PASS);
+ }
+ return wait_child(pid);
+}
+
+static int after_dontfork(char *ram, size_t size)
+{
+ return after_fork_flag(ram, size, " dc ", false);
+}
+
+static int after_wipeonfork(char *ram, size_t size)
+{
+ return after_fork_flag(ram, size, " wf ", true);
+}
+
+/* The userfaultfd context belongs to the old mm and must not carry over. */
+static int uffd_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+ struct uffdio_api api = { .api = UFFD_API };
+ struct uffdio_register reg = {
+ .mode = UFFDIO_REGISTER_MODE_MISSING,
+ };
+ int uffd;
+
+ if (!ram)
+ return unsupported();
+ uffd = syscall(__NR_userfaultfd, O_CLOEXEC | UFFD_USER_MODE_ONLY);
+ if (uffd < 0 || ioctl(uffd, UFFDIO_API, &api))
+ return KSFT_SKIP;
+ reg.range.start = (uintptr_t)ram;
+ reg.range.len = size;
+ if (ioctl(uffd, UFFDIO_REGISTER, ®))
+ return KSFT_SKIP;
+ if (!smaps_has_flag(ram, " um "))
+ return fail("userfaultfd registration not in smaps");
+ exec_mode("/proc/self/exe", "--no-uffd", ram, size);
+ return KSFT_FAIL;
+}
+
+static int after_no_uffd(char *ram, size_t size)
+{
+ if (smaps_has_flag(ram, " um "))
+ return fail("userfaultfd registration preserved");
+ return check_contents(ram, size, 0x31);
+}
+
+/* vma_dup_policy() carries the memory policy of the range over. */
+static int mempolicy_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+ unsigned long nodes = 1;
+
+ if (!ram)
+ return unsupported();
+ if (syscall(__NR_mbind, ram, size, MPOL_PREFERRED, &nodes, 2, 0))
+ return KSFT_SKIP;
+ exec_mode("/proc/self/exe", "--mempolicy", ram, size);
+ return KSFT_FAIL;
+}
+
+static int after_mempolicy(char *ram, size_t size)
+{
+ unsigned long nodes[16] = { 0 };
+ int mode = -1;
+
+ if (syscall(__NR_get_mempolicy, &mode, nodes, 8 * sizeof(nodes), ram, MPOL_F_ADDR))
+ return fail("get_mempolicy failed");
+ if (mode != MPOL_PREFERRED || nodes[0] != 1)
+ return fail("memory policy lost");
+ return check_contents(ram, size, 0x31);
+}
+
+struct after_mode {
+ const char *mode;
+ int (*fn)(char *ram, size_t size);
+};
+
+static const struct after_mode after_modes[] = {
+ { "--absent", after_absent },
+ { "--partial", after_partial },
+ { "--unlocked", after_unlocked },
+ { "--dontfork", after_dontfork },
+ { "--wipeonfork", after_wipeonfork },
+ { "--no-uffd", after_no_uffd },
+ { "--mempolicy", after_mempolicy },
+};
+
+struct more_case {
+ const char *name;
+ int (*fn)(void);
+};
+
+static const struct more_case more_cases[] = {
+ { "madv_doexec_flag sysctl matches MADV_DOEXEC", sysctl_flag },
+ { "shared, file, executable and stack mappings refused", refused_mappings },
+ { "hugetlb mapping refused", hugetlb_refused },
+ { "MADV_DONTEXEC: range not preserved", unmarked_exec },
+ { "partially marked VMA: only the marked part preserved", partial_exec },
+ { "mlocked range: preserved unlocked", locked_exec },
+ { "MADV_DONTFORK range: preserved, not given to a child", dontfork_exec },
+ { "MADV_WIPEONFORK range: preserved, wiped in a child", wipeonfork_exec },
+ { "userfaultfd registration: not preserved", uffd_exec },
+ { "memory policy: preserved", mempolicy_exec },
+};
+
int main(int argc, char **argv)
{
pid_t pid;
@@ -1052,6 +1395,10 @@ int main(int argc, char **argv)
strtoull(argv[3], NULL, 10), atoi(argv[4]), atoi(argv[5]));
if (argc == 5 && !strcmp(argv[1], "--reject"))
return KSFT_FAIL;
+ for (kind = 0; argc == 4 && kind < (int)ARRAY_SIZE(after_modes); kind++)
+ if (!strcmp(argv[1], after_modes[kind].mode))
+ return after_modes[kind].fn((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
+ strtoull(argv[3], NULL, 10));
if (argc == 2 && !strcmp(argv[1], "--euid"))
return geteuid() ? KSFT_SKIP : KSFT_PASS;
if (argc == 4 && !strcmp(argv[1], "--pfnmap"))
@@ -1070,18 +1417,20 @@ int main(int argc, char **argv)
write(fd, "0", 1);
close(fd);
}
+ if (!range_mapped(ram, size))
+ return fail("range not preserved");
for (i = 0; i < size; i += stride)
if (ram[i] != 0x31)
return KSFT_FAIL;
return KSFT_PASS;
}
ksft_print_header();
- ksft_set_plan(NR_TESTS + 13);
+ ksft_set_plan(NR_TESTS + 13 + ARRAY_SIZE(more_cases));
for (kind = 0; kind < NR_TESTS; kind++) {
fflush(stdout);
pid = fork();
if (!pid)
- _exit(before_exec(kind));
+ exit(before_exec(kind));
ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
if (ret == KSFT_SKIP)
ksft_test_result_skip("%s: DOEXEC, GUP test or mapping unavailable\n",
@@ -1093,7 +1442,7 @@ int main(int argc, char **argv)
fflush(stdout);
pid = fork();
if (!pid)
- _exit(before_cow_exec());
+ exit(before_cow_exec());
ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
if (ret == KSFT_SKIP)
ksft_test_result_skip("pre-existing COW: DOEXEC unavailable\n");
@@ -1121,27 +1470,27 @@ int main(int argc, char **argv)
switch (kind) {
case 0:
case 1:
- _exit(shared_mm_exec(kind));
+ exit(shared_mm_exec(kind));
case 2:
- _exit(vhost_owner_exec());
+ exit(vhost_owner_exec());
case 3:
- _exit(allocation_failures());
+ exit(allocation_failures());
case 4:
- _exit(nonleader_exec());
+ exit(nonleader_exec());
case 5:
- _exit(proc_readers_exec());
+ exit(proc_readers_exec());
case 6:
- _exit(clone_exec_race());
+ exit(clone_exec_race());
case 7:
- _exit(setuid_exec());
+ exit(setuid_exec());
case 8:
- _exit(devzero_exec());
+ exit(devzero_exec());
case 9:
- _exit(doexec_noexec());
+ exit(doexec_noexec());
case 10:
- _exit(devmem_refused());
+ exit(devmem_refused());
case 11:
- _exit(vfio_bar_exec());
+ exit(vfio_bar_exec());
}
}
ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
@@ -1150,5 +1499,16 @@ int main(int argc, char **argv)
else
ksft_test_result(ret == KSFT_PASS, "%s\n", extra_names[kind]);
}
+ for (kind = 0; kind < (int)ARRAY_SIZE(more_cases); kind++) {
+ fflush(stdout);
+ pid = fork();
+ if (!pid)
+ exit(more_cases[kind].fn());
+ ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
+ if (ret == KSFT_SKIP)
+ ksft_test_result_skip("%s: unavailable\n", more_cases[kind].name);
+ else
+ ksft_test_result(ret == KSFT_PASS, "%s\n", more_cases[kind].name);
+ }
ksft_finished();
}
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 10%]
* [Devel] [PATCH hci-8.1 2/7] selftests/mm: cover the MADV_DOEXEC opt-in note and the new stack
2026-10-14 17:14 10% ` [Devel] [PATCH hci-8.1 1/7] selftests/mm: cover the MADV_DOEXEC interface and VMA attributes Konstantin Khorenko
@ 2026-10-14 17:15 4% ` Konstantin Khorenko
2026-10-14 17:15 7% ` [Devel] [PATCH hci-8.1 7/7] selftests/mm: say why a doexec case fails Konstantin Khorenko
2 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-10-14 17:15 UTC (permalink / raw)
To: Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis; +Cc: devel
Add cases for the exec side:
- A copy of the test binary whose note type is changed carries no
opt-in, and the marked range is absent in the new image, while an
unchanged copy run the same way keeps it.
- A copy whose note has the opt-in type but another name, in the last
PT_NOTE segment, fails exec with ENOEXEC before the point of no
return, and the marked range is still there. The copy is started in a
mode that fails if the exec goes through. The same note in another
segment is skipped: the exec goes through without the memory.
- A copy whose PT_NOTE holding the opt-in comes before another PT_NOTE
in the program headers still gets the memory: the loader has to look
at every note segment, not only the last one. The test binary also
carries an 8-aligned note, which ld.bfd and lld never put in the
PT_NOTE of the 4-aligned opt-in, so there are two to reorder.
- With ADDR_NO_RANDOMIZE the new stack starts at STACK_TOP and grows by
128K below the argument pages, plus the stack guard gap; a range
preserved there makes setup_arg_pages() fail the exec, which kills
the task with SIGSEGV. The place is computed from the size of the
environment, and the same exec with the page far below the stack must
succeed first. x86_64 only, as the address comes from its STACK_TOP;
skipped when randomization is off, since the page then falls on the
stack of the test itself.
The note is found through the PT_NOTE program headers and patched in the
byte order of the machine; a missing note fails the cases. The copies
are written to the first of the current directory, /var/tmp and /tmp
that is not mounted noexec, unlinked and executed through their fd.
https://virtuozzo.atlassian.net/browse/VSTOR-150234
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 373 +++++++++++++++++++++++++++++++++++-
1 file changed, 370 insertions(+), 3 deletions(-)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index 850b498e3d1dc..5eaf752b8413c 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -1,10 +1,12 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Verify what MADV_DOEXEC preserves across exec, long-term DMA pins first. */
#define _GNU_SOURCE
+#include <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <limits.h>
+#include <link.h>
#include <linux/mempolicy.h>
#include <linux/userfaultfd.h>
#include <linux/vfio.h>
@@ -22,9 +24,11 @@
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/mount.h>
+#include <sys/personality.h>
#include <sys/resource.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
+#include <sys/statvfs.h>
#include <sys/syscall.h>
#include <sys/sysmacros.h>
#include <sys/utsname.h>
@@ -34,6 +38,9 @@
#include "../../../../mm/gup_test.h"
#include "../kselftest.h"
+#ifndef MAP_FIXED_NOREPLACE
+#define MAP_FIXED_NOREPLACE 0x100000
+#endif
#ifndef UFFD_USER_MODE_ONLY
#define UFFD_USER_MODE_ONLY 1
#endif
@@ -58,6 +65,14 @@ asm(".pushsection .note.doexec,\"a\",%note\n"
".balign 4\n"
".popsection\n");
+/* An 8-aligned note cannot share a PT_NOTE with the 4-aligned ones. */
+asm(".pushsection .note.doexec8,\"a\",%note\n"
+ ".balign 8\n"
+ ".long 4, 0, 1\n"
+ ".asciz \"DOE\"\n"
+ ".balign 8\n"
+ ".popsection\n");
+
enum test_kind {
BASE,
HUGE,
@@ -452,6 +467,8 @@ static int vhost_owner_exec(void)
return KSFT_FAIL;
}
+static const char * const copy_dirs[] = { ".", "/var/tmp", "/tmp" };
+
static void remove_setuid_copy(char *path)
{
unlink(path);
@@ -526,14 +543,13 @@ static int unprivileged_exec(const char *path, bool preserve)
/* The ELF opt-in must not pull memory into a more privileged image. */
static int setuid_exec(void)
{
- static const char * const dirs[] = { ".", "/var/tmp", "/tmp" };
char path[96];
int i, ret = KSFT_SKIP;
if (geteuid())
return KSFT_SKIP;
- for (i = 0; i < 3 && ret == KSFT_SKIP; i++) {
- if (make_setuid_copy(dirs[i], path, sizeof(path)))
+ for (i = 0; i < (int)ARRAY_SIZE(copy_dirs) && ret == KSFT_SKIP; i++) {
+ if (make_setuid_copy(copy_dirs[i], path, sizeof(path)))
continue;
/* A nosuid mount or a private directory makes the copy useless. */
ret = unprivileged_exec(path, false);
@@ -1172,6 +1188,18 @@ static int unmarked_exec(void)
return KSFT_FAIL;
}
+static int after_present(char *ram, size_t size)
+{
+ if (!range_mapped(ram, size))
+ return fail("range not preserved");
+ return check_contents(ram, size, 0x31);
+}
+
+static int after_never(char *ram, size_t size)
+{
+ return fail("exec was expected to fail");
+}
+
static int after_absent(char *ram, size_t size)
{
return range_mapped(ram, size) ? fail("range still mapped") : KSFT_PASS;
@@ -1344,6 +1372,338 @@ static int after_mempolicy(char *ram, size_t size)
return check_contents(ram, size, 0x31);
}
+#define DOEXEC_NOTE_TYPE 0x07c1feed
+#define ELF_EHDR ElfW(Ehdr)
+#define ELF_PHDR ElfW(Phdr)
+#define ELF_NHDR ElfW(Nhdr)
+
+static char *read_self(size_t *size)
+{
+ int fd = open("/proc/self/exe", O_RDONLY);
+ char *image = NULL;
+ struct stat st;
+
+ if (fd < 0)
+ return NULL;
+ if (!fstat(fd, &st))
+ image = malloc(st.st_size);
+ if (image && read(fd, image, st.st_size) != st.st_size) {
+ free(image);
+ image = NULL;
+ }
+ close(fd);
+ if (image)
+ *size = st.st_size;
+ return image;
+}
+
+static ELF_NHDR *find_opt_in_note(char *image, size_t size, int *seg)
+{
+ ELF_EHDR *ehdr = (ELF_EHDR *)image;
+ ELF_PHDR *phdr;
+ size_t off, end;
+ int i;
+
+ if (size < sizeof(*ehdr) || memcmp(ehdr->e_ident, ELFMAG, SELFMAG))
+ return NULL;
+ for (i = 0; i < ehdr->e_phnum; i++) {
+ phdr = (ELF_PHDR *)(image + ehdr->e_phoff) + i;
+ if (phdr->p_type != PT_NOTE || phdr->p_offset + phdr->p_filesz > size)
+ continue;
+ end = phdr->p_offset + phdr->p_filesz;
+ for (off = phdr->p_offset; off + sizeof(ELF_NHDR) <= end;) {
+ ELF_NHDR *note = (ELF_NHDR *)(image + off);
+
+ if (note->n_type == DOEXEC_NOTE_TYPE &&
+ note->n_namesz == sizeof("preserved-mem-ok") &&
+ !memcmp(note + 1, "preserved-mem-ok", sizeof("preserved-mem-ok"))) {
+ *seg = i;
+ return note;
+ }
+ off += sizeof(*note) + ((note->n_namesz + 3) & ~3u) +
+ ((note->n_descsz + 3) & ~3u);
+ }
+ }
+ return NULL;
+}
+
+enum note_patch { NOTE_KEEP, NOTE_RETYPE, NOTE_RENAME, NOTE_NOT_LAST, NOTE_RENAME_NOT_LAST };
+
+/* Move the segment holding the opt-in before another PT_NOTE if it is the last. */
+static bool note_not_last(char *image, int seg)
+{
+ ELF_EHDR *ehdr = (ELF_EHDR *)image;
+ ELF_PHDR *phdr = (ELF_PHDR *)(image + ehdr->e_phoff);
+ ELF_PHDR tmp;
+ int i, other = -1;
+
+ for (i = 0; i < ehdr->e_phnum; i++) {
+ if (phdr[i].p_type != PT_NOTE || i == seg)
+ continue;
+ if (i > seg)
+ return true;
+ if (other < 0)
+ other = i;
+ }
+ if (other < 0)
+ return false;
+ tmp = phdr[other];
+ phdr[other] = phdr[seg];
+ phdr[seg] = tmp;
+ return true;
+}
+
+/* Make the segment holding the opt-in the last PT_NOTE. */
+static void note_last(char *image, int seg)
+{
+ ELF_EHDR *ehdr = (ELF_EHDR *)image;
+ ELF_PHDR *phdr = (ELF_PHDR *)(image + ehdr->e_phoff);
+ ELF_PHDR tmp;
+ int i, last = seg;
+
+ for (i = 0; i < ehdr->e_phnum; i++)
+ if (phdr[i].p_type == PT_NOTE)
+ last = i;
+ tmp = phdr[last];
+ phdr[last] = phdr[seg];
+ phdr[seg] = tmp;
+}
+
+/* An fd of a patched copy of this binary; -2: no opt-in note, -3: one PT_NOTE. */
+static int patched_copy(enum note_patch how)
+{
+ char path[64], *image;
+ ELF_NHDR *note;
+ struct statvfs vfs;
+ int fd = -1, i, seg;
+ size_t size;
+
+ image = read_self(&size);
+ if (!image)
+ return -1;
+ note = find_opt_in_note(image, size, &seg);
+ if (!note) {
+ free(image);
+ return -2;
+ }
+ if (how == NOTE_RETYPE)
+ note->n_type = DOEXEC_NOTE_TYPE - 1;
+ else if (how == NOTE_RENAME) {
+ memcpy(note + 1, "preserved-mem-no", 16);
+ note_last(image, seg);
+ } else if ((how == NOTE_NOT_LAST || how == NOTE_RENAME_NOT_LAST) &&
+ !note_not_last(image, seg)) {
+ free(image);
+ return -3;
+ }
+ if (how == NOTE_RENAME_NOT_LAST)
+ memcpy(note + 1, "preserved-mem-no", 16);
+ for (i = 0; i < (int)ARRAY_SIZE(copy_dirs) && fd < 0; i++) {
+ if (statvfs(copy_dirs[i], &vfs) || (vfs.f_flag & ST_NOEXEC))
+ continue;
+ snprintf(path, sizeof(path), "%s/doexec-note.XXXXXX", copy_dirs[i]);
+ fd = mkstemp(path);
+ if (fd < 0)
+ continue;
+ if (write(fd, image, size) != (ssize_t)size || fchmod(fd, 0755)) {
+ close(fd);
+ unlink(path);
+ fd = -1;
+ continue;
+ }
+ close(fd);
+ fd = open(path, O_RDONLY | O_CLOEXEC);
+ unlink(path);
+ }
+ free(image);
+ return fd;
+}
+
+static int exec_fd_mode(int exe, const char *mode, char *ram, size_t size)
+{
+ char addr_arg[32], size_arg[32];
+ char *args[] = { "doexec", (char *)mode, addr_arg, size_arg, NULL };
+
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)ram);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ syscall(__NR_execveat, exe, "", args, environ, AT_EMPTY_PATH);
+ return errno;
+}
+
+/* An unpatched copy run the same way keeps the memory. */
+static int copy_control(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ pid_t pid = fork();
+ char *ram;
+ int exe;
+
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ ram = preserved_ram(size);
+ if (!ram)
+ exit(unsupported());
+ exe = patched_copy(NOTE_KEEP);
+ if (exe == -2)
+ exit(fail("opt-in note not found in the test binary"));
+ if (exe < 0)
+ exit(KSFT_SKIP);
+ exit(exec_fd_mode(exe, "--present", ram, size) == EACCES ? KSFT_SKIP : KSFT_FAIL);
+ }
+ return wait_child(pid);
+}
+
+static int no_note_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram;
+ int exe, err;
+
+ err = copy_control();
+ if (err)
+ return err == KSFT_SKIP ? KSFT_SKIP : fail("unpatched copy lost the memory");
+ ram = preserved_ram(size);
+ if (!ram)
+ return unsupported();
+ exe = patched_copy(NOTE_RETYPE);
+ if (exe == -2)
+ return fail("opt-in note not found in the test binary");
+ if (exe < 0)
+ return KSFT_SKIP;
+ err = exec_fd_mode(exe, "--absent", ram, size);
+ if (err == EACCES)
+ return KSFT_SKIP;
+ ksft_print_msg("exec: %s\n", strerror(err));
+ return KSFT_FAIL;
+}
+
+static int note_not_last_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+ int exe, err;
+
+ if (!ram)
+ return unsupported();
+ exe = patched_copy(NOTE_NOT_LAST);
+ if (exe == -2)
+ return fail("opt-in note not found in the test binary");
+ if (exe < 0)
+ return KSFT_SKIP;
+ err = exec_fd_mode(exe, "--present", ram, size);
+ if (err == EACCES)
+ return KSFT_SKIP;
+ ksft_print_msg("exec: %s\n", strerror(err));
+ return KSFT_FAIL;
+}
+
+/* Outside the last PT_NOTE a malformed opt-in is skipped, not refused. */
+static int bad_note_not_last_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+ int exe, err;
+
+ if (!ram)
+ return unsupported();
+ exe = patched_copy(NOTE_RENAME_NOT_LAST);
+ if (exe == -2)
+ return fail("opt-in note not found in the test binary");
+ if (exe < 0)
+ return KSFT_SKIP;
+ err = exec_fd_mode(exe, "--absent", ram, size);
+ if (err == EACCES)
+ return KSFT_SKIP;
+ ksft_print_msg("exec: %s\n", strerror(err));
+ return KSFT_FAIL;
+}
+
+static int bad_note_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+ int exe, err;
+
+ if (!ram)
+ return unsupported();
+ exe = patched_copy(NOTE_RENAME);
+ if (exe == -2)
+ return fail("opt-in note not found in the test binary");
+ if (exe < 0)
+ return KSFT_SKIP;
+ err = exec_fd_mode(exe, "--never", ram, size);
+ if (err == EACCES)
+ return KSFT_SKIP;
+ if (err != ENOEXEC) {
+ ksft_print_msg("exec: %s\n", strerror(err));
+ return fail("malformed note not refused with ENOEXEC");
+ }
+ return check_contents(ram, size, 0x31);
+}
+
+#if defined(__x86_64__) && !defined(__ILP32__)
+/* With @below pages under the argument pages, exec the page preserved there. */
+static int stack_exec(size_t below)
+{
+ size_t size = sysconf(_SC_PAGESIZE), args = 2 * size;
+ struct rlimit no_core = { 0, 0 };
+ char *top = (char *)((1ul << 47) - size), *ram, **env;
+ int status;
+ pid_t pid;
+
+ for (env = environ; *env; env++)
+ args += strlen(*env) + 1;
+ args = (args + size - 1) & ~(size - 1);
+ pid = fork();
+ if (pid < 0)
+ return -1;
+ if (!pid) {
+ setrlimit(RLIMIT_CORE, &no_core);
+ if (personality(ADDR_NO_RANDOMIZE) < 0)
+ _exit(KSFT_SKIP);
+ ram = mmap(top - args - below * size, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
+ if (ram == MAP_FAILED)
+ _exit(KSFT_SKIP);
+ memset(ram, 0x31, size);
+ if (madvise(ram, size, MADV_DOEXEC))
+ _exit(unsupported());
+ exec_mode("/proc/self/exe", "--present", ram, size);
+ _exit(KSFT_FAIL);
+ }
+ if (waitpid(pid, &status, 0) != pid)
+ return -1;
+ return status;
+}
+
+static int stack_overlap_exec(void)
+{
+ int status;
+
+ /* Far below the stack, past the default guard gap, the same exec works... */
+ status = stack_exec(4096);
+ if (status < 0 || !WIFEXITED(status))
+ return fail("control exec failed");
+ if (WEXITSTATUS(status) != KSFT_PASS)
+ return WEXITSTATUS(status) == KSFT_SKIP ? KSFT_SKIP : fail("control exec failed");
+ /* ...within the 128K the stack is grown by, it kills the task. */
+ status = stack_exec(16);
+ if (status < 0)
+ return KSFT_FAIL;
+ if (WIFEXITED(status))
+ return WEXITSTATUS(status) == KSFT_SKIP ? KSFT_SKIP :
+ fail("exec did not kill the task");
+ return WTERMSIG(status) == SIGSEGV ? KSFT_PASS : fail("killed by another signal");
+}
+#else
+static int stack_overlap_exec(void)
+{
+ return KSFT_SKIP;
+}
+#endif
+
struct after_mode {
const char *mode;
int (*fn)(char *ram, size_t size);
@@ -1351,6 +1711,8 @@ struct after_mode {
static const struct after_mode after_modes[] = {
{ "--absent", after_absent },
+ { "--never", after_never },
+ { "--present", after_present },
{ "--partial", after_partial },
{ "--unlocked", after_unlocked },
{ "--dontfork", after_dontfork },
@@ -1375,6 +1737,11 @@ static const struct more_case more_cases[] = {
{ "MADV_WIPEONFORK range: preserved, wiped in a child", wipeonfork_exec },
{ "userfaultfd registration: not preserved", uffd_exec },
{ "memory policy: preserved", mempolicy_exec },
+ { "binary without the opt-in note: nothing preserved", no_note_exec },
+ { "malformed opt-in note: ENOEXEC, memory intact", bad_note_exec },
+ { "opt-in note not in the last PT_NOTE: preserved", note_not_last_exec },
+ { "malformed opt-in note not in the last PT_NOTE: skipped", bad_note_not_last_exec },
+ { "preserved range under the new stack: exec kills the task", stack_overlap_exec },
};
int main(int argc, char **argv)
--
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 2/2] selftests/mm: check MADV_DOEXEC on VFIO BARs and other PFN mappings
@ 2026-10-14 17:14 5% ` Konstantin Khorenko
0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-10-14 17:14 UTC (permalink / raw)
To: Andrey Drobyshev, Vasileios Almpanis, Pavel Tikhomirov; +Cc: devel
Add two cases to doexec:
- A shared mapping of /dev/mem is a PFN mapping of a driver other than
VFIO, and MADV_DOEXEC must refuse it.
- BAR 0 of the PCI device named by DOEXEC_VFIO_DEVICE, which has to be
bound to vfio-pci, is mapped through its VFIO device fd at an address
aligned to the mapped size, so that huge PFN entries can be used. The
test fills it with a pattern, marks it and execs. The new image must
find the mapping at the same address with the pfn and shared flags in
smaps and the pattern in place. A group that has only a noiommu node
is set up with the no-IOMMU container. Without the variable the case
is skipped. The test overwrites BAR 0, so the variable has to name a
spare device whose BAR 0 is memory, such as an unused VGA frame
buffer.
https://virtuozzo.atlassian.net/browse/VSTOR-150233
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 141 +++++++++++++++++++++++++++++++++++-
1 file changed, 139 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index 97e045ba678e8..a89fe2f067df5 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -4,6 +4,8 @@
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
+#include <limits.h>
+#include <linux/vfio.h>
#include <linux/vhost.h>
#include <poll.h>
#include <pthread.h>
@@ -585,6 +587,132 @@ static int doexec_noexec(void)
return ret;
}
+static bool smaps_has_flag(char *addr, const char *flag)
+{
+ FILE *file = fopen("/proc/self/smaps", "r");
+ char line[256], want[32];
+ bool in_vma = false, found = false;
+
+ if (!file)
+ return false;
+ snprintf(want, sizeof(want), "%lx-", (unsigned long)addr);
+ while (fgets(line, sizeof(line), file)) {
+ char *space = strchr(line, ' ');
+
+ if (!space)
+ continue;
+ if (memchr(line, '-', space - line) && !memchr(line, ':', space - line))
+ in_vma = !strncmp(line, want, strlen(want));
+ else if (in_vma && !strncmp(line, "VmFlags:", 8))
+ found = strstr(line, flag) != NULL;
+ }
+ fclose(file);
+ return found;
+}
+
+static int after_pfnmap_exec(char *bar, size_t size)
+{
+ size_t page = sysconf(_SC_PAGESIZE), i;
+
+ if (!smaps_has_flag(bar, " pf ") || !smaps_has_flag(bar, " sh "))
+ return KSFT_FAIL;
+ for (i = 0; i < size / page; i++)
+ if (__atomic_load_n((uint32_t *)(bar + i * page), __ATOMIC_RELAXED) !=
+ (0xd0e00000 | i))
+ return KSFT_FAIL;
+ return munmap(bar, size) ? KSFT_FAIL : KSFT_PASS;
+}
+
+/* Only VFIO device mappings may be preserved; /dev/mem is a plain PFN map. */
+static int devmem_refused(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram;
+ int fd;
+
+ if (!preserved_ram(size))
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ /* Without FMODE_WRITE, do_mmap() drops VM_SHARED from the mapping. */
+ fd = open("/dev/mem", O_RDWR);
+ if (fd < 0)
+ return KSFT_SKIP;
+ ram = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
+ close(fd);
+ if (ram == MAP_FAILED || !smaps_has_flag(ram, " pf "))
+ return KSFT_SKIP;
+ return madvise(ram, size, MADV_DOEXEC) && errno == EINVAL ? KSFT_PASS : KSFT_FAIL;
+}
+
+static int vfio_device_fd(const char *bdf)
+{
+ char path[PATH_MAX], link[PATH_MAX], *group_id;
+ int container, group, noiommu;
+ ssize_t len;
+
+ snprintf(path, sizeof(path), "/sys/bus/pci/devices/%s/iommu_group", bdf);
+ len = readlink(path, link, sizeof(link) - 1);
+ if (len < 0)
+ return -1;
+ link[len] = 0;
+ group_id = strrchr(link, '/') + 1;
+ snprintf(path, sizeof(path), "/dev/vfio/%s", group_id);
+ noiommu = access(path, F_OK);
+ if (noiommu)
+ snprintf(path, sizeof(path), "/dev/vfio/noiommu-%s", group_id);
+ container = open("/dev/vfio/vfio", O_RDWR);
+ group = open(path, O_RDWR);
+ if (container < 0 || group < 0 ||
+ ioctl(group, VFIO_GROUP_SET_CONTAINER, &container) ||
+ ioctl(container, VFIO_SET_IOMMU, noiommu ? VFIO_NOIOMMU_IOMMU : VFIO_TYPE1v2_IOMMU))
+ return -1;
+ return ioctl(group, VFIO_GROUP_GET_DEVICE_FD, bdf);
+}
+
+/* Overwrites BAR 0 of DOEXEC_VFIO_DEVICE, so name a spare memory BAR. */
+static int vfio_bar_exec(void)
+{
+ struct vfio_region_info info = {
+ .argsz = sizeof(info),
+ .index = VFIO_PCI_BAR0_REGION_INDEX,
+ };
+ size_t page = sysconf(_SC_PAGESIZE), size, i;
+ const char *bdf = getenv("DOEXEC_VFIO_DEVICE");
+ char addr_arg[32], size_arg[32], *area, *bar;
+ uintptr_t aligned;
+ int dev;
+
+ if (!bdf || !preserved_ram(page))
+ return KSFT_SKIP;
+ dev = vfio_device_fd(bdf);
+ if (dev < 0 || ioctl(dev, VFIO_DEVICE_GET_REGION_INFO, &info) ||
+ !(info.flags & VFIO_REGION_INFO_FLAG_MMAP) || info.size < page) {
+ ksft_print_msg("VFIO device %s: %s\n", bdf, strerror(errno));
+ return KSFT_FAIL;
+ }
+ size = info.size < (4ul << 20) ? info.size : 4ul << 20;
+ /* Align to the size so that the BAR can be mapped with huge PFN entries. */
+ area = mmap(NULL, 2 * size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (area == MAP_FAILED)
+ return KSFT_FAIL;
+ aligned = ((uintptr_t)area + size - 1) & ~(size - 1);
+ bar = mmap((void *)aligned, size, PROT_READ | PROT_WRITE,
+ MAP_SHARED | MAP_FIXED, dev, info.offset);
+ if (bar == MAP_FAILED)
+ return KSFT_FAIL;
+ for (i = 0; i < size / page; i++)
+ __atomic_store_n((uint32_t *)(bar + i * page), 0xd0e00000 | i,
+ __ATOMIC_RELAXED);
+ if (madvise(bar, size, MADV_DOEXEC)) {
+ ksft_print_msg("MADV_DOEXEC on a VFIO BAR: %s\n", strerror(errno));
+ return KSFT_FAIL;
+ }
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)bar);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ execl("/proc/self/exe", "doexec", "--pfnmap", addr_arg, size_arg, NULL);
+ return KSFT_FAIL;
+}
+
static void *exec_thread(void *arg)
{
check_exec(arg, sysconf(_SC_PAGESIZE), 1, false);
@@ -926,6 +1054,9 @@ int main(int argc, char **argv)
return KSFT_FAIL;
if (argc == 2 && !strcmp(argv[1], "--euid"))
return geteuid() ? KSFT_SKIP : KSFT_PASS;
+ if (argc == 4 && !strcmp(argv[1], "--pfnmap"))
+ return after_pfnmap_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
+ strtoull(argv[3], NULL, 10));
if (argc == 5 && !strcmp(argv[1], "--proc"))
return proc_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
strtoull(argv[3], NULL, 10), atoi(argv[4]));
@@ -945,7 +1076,7 @@ int main(int argc, char **argv)
return KSFT_PASS;
}
ksft_print_header();
- ksft_set_plan(NR_TESTS + 11);
+ ksft_set_plan(NR_TESTS + 13);
for (kind = 0; kind < NR_TESTS; kind++) {
fflush(stdout);
pid = fork();
@@ -968,7 +1099,7 @@ int main(int argc, char **argv)
ksft_test_result_skip("pre-existing COW: DOEXEC unavailable\n");
else
ksft_test_result(ret == KSFT_PASS, "pre-existing COW preserved\n");
- for (kind = 0; kind < 10; kind++) {
+ for (kind = 0; kind < 12; kind++) {
static const char * const extra_names[] = {
"CLONE_VM: recoverable EBUSY and retry",
"own threads: preserving exec allowed",
@@ -980,6 +1111,8 @@ int main(int argc, char **argv)
"setuid exec: preserving exec refused",
"private /dev/zero mapping: preserved across exec",
"marked range: not executable until unmarked",
+ "shared /dev/mem mapping: MADV_DOEXEC refused",
+ "VFIO BAR: preserved across exec",
};
fflush(stdout);
@@ -1005,6 +1138,10 @@ int main(int argc, char **argv)
_exit(devzero_exec());
case 9:
_exit(doexec_noexec());
+ case 10:
+ _exit(devmem_refused());
+ case 11:
+ _exit(vfio_bar_exec());
}
}
ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
--
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 7/7] selftests/mm: check MADV_DOEXEC on /dev/zero and exec permission
2026-10-14 17:12 2% ` [Devel] [PATCH hci-8.1 6/7] selftests/mm: check that a setuid exec refuses preserved memory Konstantin Khorenko
@ 2026-10-14 17:12 2% ` Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-10-14 17:12 UTC (permalink / raw)
To: Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis; +Cc: devel
Add two cases to doexec:
- A private /dev/zero mapping is marked and preserved across exec. The
mapping is anonymous but holds a reference to the file; a copy that
does not take its own shows up as an fput() underflow at teardown.
- A marked range cannot be made executable until MADV_DONTEXEC, and
MADV_DONTEXEC does not make a /dev/zero mapping from a noexec tmpfs
executable. The noexec part needs a private mount namespace, a tmpfs
mount and a /dev/zero node on it; the case is reported as skipped
when it cannot get them.
https://virtuozzo.atlassian.net/browse/VSTOR-150232
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 94 ++++++++++++++++++++++++++++++++++++-
1 file changed, 92 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index dfa3da2c43934..97e045ba678e8 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -17,10 +17,12 @@
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
+#include <sys/mount.h>
#include <sys/resource.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/syscall.h>
+#include <sys/sysmacros.h>
#include <sys/wait.h>
#include <unistd.h>
@@ -501,6 +503,88 @@ static int setuid_exec(void)
return ret;
}
+/* A private /dev/zero mapping is anonymous but holds a file reference. */
+static int devzero_exec(void)
+{
+ size_t size = 16 * sysconf(_SC_PAGESIZE);
+ int fd = open("/dev/zero", O_RDWR);
+ char *ram;
+
+ if (fd < 0)
+ return KSFT_SKIP;
+ ram = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
+ close(fd);
+ if (ram == MAP_FAILED)
+ return KSFT_FAIL;
+ memset(ram, 0x31, size);
+ if (madvise(ram, size, MADV_DOEXEC))
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ check_exec(ram, size, 1, false);
+ return KSFT_FAIL;
+}
+
+static bool exec_refused(char *ram, size_t size)
+{
+ return mprotect(ram, size, PROT_READ | PROT_EXEC) && errno == EACCES;
+}
+
+/* MADV_DONTEXEC must not grant more than mmap() did on a noexec mount. */
+static int noexec_dontexec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char dir[] = "/tmp/doexec-noexec.XXXXXX", node[64];
+ int fd, ret = KSFT_FAIL;
+ char *ram;
+
+ if (unshare(CLONE_NEWNS) || mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL) ||
+ !mkdtemp(dir))
+ return KSFT_SKIP;
+ if (mount("tmpfs", dir, "tmpfs", MS_NOEXEC, NULL)) {
+ rmdir(dir);
+ return KSFT_SKIP;
+ }
+ snprintf(node, sizeof(node), "%s/zero", dir);
+ fd = mknod(node, S_IFCHR | 0600, makedev(1, 5)) ? -1 : open(node, O_RDWR);
+ if (fd < 0) {
+ ret = KSFT_SKIP;
+ goto out;
+ }
+ ram = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
+ close(fd);
+ if (ram == MAP_FAILED || !exec_refused(ram, size)) {
+ ret = KSFT_SKIP;
+ goto out;
+ }
+ if (madvise(ram, size, MADV_DOEXEC)) {
+ ret = errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ goto out;
+ }
+ if (!madvise(ram, size, MADV_DONTEXEC) && exec_refused(ram, size))
+ ret = KSFT_PASS;
+out:
+ umount2(dir, MNT_DETACH);
+ rmdir(dir);
+ return ret;
+}
+
+/* A marked range cannot become executable until it is unmarked. */
+static int doexec_noexec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+ int ret;
+
+ if (!ram)
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ if (!exec_refused(ram, size) || madvise(ram, size, MADV_DONTEXEC) ||
+ mprotect(ram, size, PROT_READ | PROT_EXEC))
+ return KSFT_FAIL;
+ ret = noexec_dontexec();
+ if (ret == KSFT_SKIP)
+ ksft_print_msg("noexec mount part skipped\n");
+ return ret;
+}
+
static void *exec_thread(void *arg)
{
check_exec(arg, sysconf(_SC_PAGESIZE), 1, false);
@@ -861,7 +945,7 @@ int main(int argc, char **argv)
return KSFT_PASS;
}
ksft_print_header();
- ksft_set_plan(NR_TESTS + 9);
+ ksft_set_plan(NR_TESTS + 11);
for (kind = 0; kind < NR_TESTS; kind++) {
fflush(stdout);
pid = fork();
@@ -884,7 +968,7 @@ int main(int argc, char **argv)
ksft_test_result_skip("pre-existing COW: DOEXEC unavailable\n");
else
ksft_test_result(ret == KSFT_PASS, "pre-existing COW preserved\n");
- for (kind = 0; kind < 8; kind++) {
+ for (kind = 0; kind < 10; kind++) {
static const char * const extra_names[] = {
"CLONE_VM: recoverable EBUSY and retry",
"own threads: preserving exec allowed",
@@ -894,6 +978,8 @@ int main(int argc, char **argv)
"proc readers: repeated preserving exec allowed",
"concurrent CLONE_VM: shared memory is not stolen",
"setuid exec: preserving exec refused",
+ "private /dev/zero mapping: preserved across exec",
+ "marked range: not executable until unmarked",
};
fflush(stdout);
@@ -915,6 +1001,10 @@ int main(int argc, char **argv)
_exit(clone_exec_race());
case 7:
_exit(setuid_exec());
+ case 8:
+ _exit(devzero_exec());
+ case 9:
+ _exit(doexec_noexec());
}
}
ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
--
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 6/7] selftests/mm: check that a setuid exec refuses preserved memory
@ 2026-10-14 17:12 2% ` Konstantin Khorenko
2026-10-14 17:12 2% ` [Devel] [PATCH hci-8.1 7/7] selftests/mm: check MADV_DOEXEC on /dev/zero and exec permission Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-10-14 17:12 UTC (permalink / raw)
To: Pavel Tikhomirov, Andrey Drobyshev, Vasileios Almpanis; +Cc: devel
Copy the test binary, make it setuid root and exec it from an
unprivileged child with a MADV_DOEXEC range: exec must fail with EPERM
and leave the mapping intact, and must succeed once the range is
unmarked with MADV_DONTEXEC. The case needs root and a directory that
is neither private nor mounted nosuid; the current directory, /var/tmp
and /tmp are tried in that order and the case is skipped if none fits.
The copy goes into a new directory that others can enter but not list,
and run setuid it does nothing but report its euid.
https://virtuozzo.atlassian.net/browse/VSTOR-150232
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/doexec.c | 121 ++++++++++++++++++++++++++++++++++--
1 file changed, 117 insertions(+), 4 deletions(-)
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
index 33529aa41367b..dfa3da2c43934 100644
--- a/tools/testing/selftests/mm/doexec.c
+++ b/tools/testing/selftests/mm/doexec.c
@@ -3,6 +3,7 @@
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
+#include <grp.h>
#include <linux/vhost.h>
#include <poll.h>
#include <pthread.h>
@@ -17,6 +18,8 @@
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/resource.h>
+#include <sys/sendfile.h>
+#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
@@ -27,6 +30,9 @@
#ifndef MADV_DOEXEC
#define MADV_DOEXEC 201
#endif
+#ifndef MADV_DONTEXEC
+#define MADV_DONTEXEC 202
+#endif
#ifndef MADV_COLLAPSE
#define MADV_COLLAPSE 25
#endif
@@ -283,7 +289,8 @@ static int before_exec(int kind)
return KSFT_FAIL;
}
-static int check_exec(char *ram, size_t size, size_t stride, bool reject)
+static int exec_path(const char *exe, char *ram, size_t size, size_t stride,
+ bool reject)
{
char addr_arg[32], size_arg[32], stride_arg[32];
@@ -291,11 +298,16 @@ static int check_exec(char *ram, size_t size, size_t stride, bool reject)
(unsigned long long)(uintptr_t)ram);
snprintf(size_arg, sizeof(size_arg), "%zu", size);
snprintf(stride_arg, sizeof(stride_arg), "%zu", stride);
- execl("/proc/self/exe", "doexec", reject ? "--reject" : "--check", addr_arg, size_arg,
+ execl(exe, "doexec", reject ? "--reject" : "--check", addr_arg, size_arg,
stride_arg, NULL);
return errno;
}
+static int check_exec(char *ram, size_t size, size_t stride, bool reject)
+{
+ return exec_path("/proc/self/exe", ram, size, stride, reject);
+}
+
static char *preserved_ram(size_t size)
{
char *ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
@@ -397,6 +409,98 @@ static int vhost_owner_exec(void)
return KSFT_FAIL;
}
+static void remove_setuid_copy(char *path)
+{
+ unlink(path);
+ *strrchr(path, '/') = '\0';
+ rmdir(path);
+}
+
+static int make_setuid_copy(const char *dir, char *path, size_t len)
+{
+ ssize_t copied = -1;
+ int in, out;
+ size_t n;
+
+ snprintf(path, len, "%s/doexec-setuid.XXXXXX", dir);
+ /* Others may run the copy but cannot find it. */
+ if (!mkdtemp(path))
+ return -1;
+ if (chmod(path, 0711)) {
+ rmdir(path);
+ return -1;
+ }
+ n = strlen(path);
+ snprintf(path + n, len - n, "/doexec.XXXXXX");
+ out = mkstemp(path);
+ in = open("/proc/self/exe", O_RDONLY);
+ if (in >= 0 && out >= 0)
+ while ((copied = sendfile(out, in, NULL, 1 << 20)) > 0)
+ ;
+ if (in >= 0)
+ close(in);
+ if (out >= 0 && !copied && !fchmod(out, S_ISUID | 0755)) {
+ close(out);
+ return 0;
+ }
+ if (out >= 0)
+ close(out);
+ remove_setuid_copy(path);
+ return -1;
+}
+
+static int unprivileged_exec(const char *path, bool preserve)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ pid_t pid = fork();
+ char *ram;
+ int err;
+
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (pid)
+ return wait_child(pid);
+ if (setgroups(0, NULL) || setresgid(65534, 65534, 65534) ||
+ setresuid(65534, 65534, 65534))
+ _exit(KSFT_SKIP);
+ if (!preserve) {
+ execl(path, "doexec", "--euid", NULL);
+ _exit(KSFT_SKIP);
+ }
+ ram = preserved_ram(size);
+ if (!ram)
+ _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ err = exec_path(path, ram, size, 1, true);
+ /* Exec must return EPERM with the old mapping intact. */
+ if (err != EPERM || check_bytes(ram, size, 0x31))
+ _exit(KSFT_FAIL);
+ if (madvise(ram, size, MADV_DONTEXEC))
+ _exit(KSFT_FAIL);
+ execl(path, "doexec", "--euid", NULL);
+ _exit(KSFT_FAIL);
+}
+
+/* The ELF opt-in must not pull memory into a more privileged image. */
+static int setuid_exec(void)
+{
+ static const char * const dirs[] = { ".", "/var/tmp", "/tmp" };
+ char path[96];
+ int i, ret = KSFT_SKIP;
+
+ if (geteuid())
+ return KSFT_SKIP;
+ for (i = 0; i < 3 && ret == KSFT_SKIP; i++) {
+ if (make_setuid_copy(dirs[i], path, sizeof(path)))
+ continue;
+ /* A nosuid mount or a private directory makes the copy useless. */
+ ret = unprivileged_exec(path, false);
+ if (ret == KSFT_PASS)
+ ret = unprivileged_exec(path, true);
+ remove_setuid_copy(path);
+ }
+ return ret;
+}
+
static void *exec_thread(void *arg)
{
check_exec(arg, sysconf(_SC_PAGESIZE), 1, false);
@@ -723,6 +827,10 @@ int main(int argc, char **argv)
pid_t pid;
int kind, ret;
+ /* The setuid copy only reports that it runs as root. */
+ if (geteuid() != getuid())
+ return argc == 2 && !strcmp(argv[1], "--euid") && !geteuid() ?
+ KSFT_PASS : KSFT_FAIL;
if (argc == 7 && !strcmp(argv[1], "--exec"))
return after_exec(atoi(argv[2]),
(char *)(uintptr_t)strtoull(argv[3], NULL, 10),
@@ -732,6 +840,8 @@ int main(int argc, char **argv)
strtoull(argv[3], NULL, 10), atoi(argv[4]), atoi(argv[5]));
if (argc == 5 && !strcmp(argv[1], "--reject"))
return KSFT_FAIL;
+ if (argc == 2 && !strcmp(argv[1], "--euid"))
+ return geteuid() ? KSFT_SKIP : KSFT_PASS;
if (argc == 5 && !strcmp(argv[1], "--proc"))
return proc_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
strtoull(argv[3], NULL, 10), atoi(argv[4]));
@@ -751,7 +861,7 @@ int main(int argc, char **argv)
return KSFT_PASS;
}
ksft_print_header();
- ksft_set_plan(NR_TESTS + 8);
+ ksft_set_plan(NR_TESTS + 9);
for (kind = 0; kind < NR_TESTS; kind++) {
fflush(stdout);
pid = fork();
@@ -774,7 +884,7 @@ int main(int argc, char **argv)
ksft_test_result_skip("pre-existing COW: DOEXEC unavailable\n");
else
ksft_test_result(ret == KSFT_PASS, "pre-existing COW preserved\n");
- for (kind = 0; kind < 7; kind++) {
+ for (kind = 0; kind < 8; kind++) {
static const char * const extra_names[] = {
"CLONE_VM: recoverable EBUSY and retry",
"own threads: preserving exec allowed",
@@ -783,6 +893,7 @@ int main(int argc, char **argv)
"non-leader thread: preserving exec allowed",
"proc readers: repeated preserving exec allowed",
"concurrent CLONE_VM: shared memory is not stolen",
+ "setuid exec: preserving exec refused",
};
fflush(stdout);
@@ -802,6 +913,8 @@ int main(int argc, char **argv)
_exit(proc_readers_exec());
case 6:
_exit(clone_exec_race());
+ case 7:
+ _exit(setuid_exec());
}
}
ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
--
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 8/8] selftests/mm: test memory preservation across exec
@ 2026-10-14 17:11 4% ` Konstantin Khorenko
2026-10-14 17:33 3% ` no-reply
2 siblings, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-10-14 17:11 UTC (permalink / raw)
To: Vasileios Almpanis, Andrey Drobyshev, Pavel Tikhomirov; +Cc: devel
From: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Add doexec, a MADV_DOEXEC test with the ELF opt-in note, and wire it
into run_vmtests.sh. The pin cases need CONFIG_GUP_TEST.
What it checks:
- CPU writes match what a long-term pin held through gup_test reads
back, across two execs and a later fork, for base pages, PMD huge
pages, mlocked pages, a read-only mapping, read-only pins and fast
GUP pins.
- A COW mapping shared with a child forked before exec stays intact on
both sides.
- exec is refused with EBUSY while a CLONE_VM task outside the thread
group has the mm, and succeeds once that task is gone.
- Own threads, a non-leader thread calling exec, /proc readers during
repeated exec and a vhost device whose owner was reset before exec do
not block it.
- A CLONE_VM clone racing with exec does not end up sharing the
preserved memory.
- Optionally, a sweep over task-local allocation failures.
https://virtuozzo.atlassian.net/browse/VSTOR-150231
Feature: oracle/mm: MADV_DOEXEC madvise() flag
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
[khorenko: exec through an fd opened before fail-nth is armed, as /proc
fails the path lookup with ENOENT; accept E2BIG, ELIBBAD and EIO, which
exec returns for some failed allocations; report an unexpected child
status; add the binary to .gitignore; use %note, which every
architecture's assembler accepts; accept EINVAL from MADV_NOHUGEPAGE
on kernels without THP; run it in a doexec category of its own; let
the blocked CLONE_VM child drop its copy of the pipe write end]
Assisted-by: LLM
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/mm/.gitignore | 1 +
tools/testing/selftests/mm/Makefile | 1 +
tools/testing/selftests/mm/doexec.c | 814 ++++++++++++++++++++++++++++++
tools/testing/selftests/mm/run_vmtests.sh | 4 +
4 files changed, 820 insertions(+)
diff --git a/tools/testing/selftests/mm/.gitignore b/tools/testing/selftests/mm/.gitignore
index 8f01f4da1c0de..2bdaebdbb87ea 100644
--- a/tools/testing/selftests/mm/.gitignore
+++ b/tools/testing/selftests/mm/.gitignore
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
cow
+doexec
hugepage-mmap
hugepage-mremap
hugepage-shm
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index c837b0549c892..7f9efb0e0a386 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -74,6 +74,7 @@ endif
TEST_GEN_FILES = cow
TEST_GEN_FILES += compaction_test
+TEST_GEN_FILES += doexec
TEST_GEN_FILES += gup_longterm
TEST_GEN_FILES += gup_test
TEST_GEN_FILES += hmm-tests
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
new file mode 100644
index 0000000000000..33529aa41367b
--- /dev/null
+++ b/tools/testing/selftests/mm/doexec.c
@@ -0,0 +1,814 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Verify that exec preserves the memory backing long-term DMA pins. */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/vhost.h>
+#include <poll.h>
+#include <pthread.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/resource.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../../../../mm/gup_test.h"
+#include "../kselftest.h"
+
+#ifndef MADV_DOEXEC
+#define MADV_DOEXEC 201
+#endif
+#ifndef MADV_COLLAPSE
+#define MADV_COLLAPSE 25
+#endif
+
+/* The downstream ELF loader requires this opt-in in a PT_NOTE segment. */
+asm(".pushsection .note.doexec,\"a\",%note\n"
+ ".balign 4\n"
+ ".long 17, 0, 0x07c1feed\n"
+ ".asciz \"preserved-mem-ok\"\n"
+ ".balign 4\n"
+ ".popsection\n");
+
+enum test_kind {
+ BASE,
+ HUGE,
+ LOCKED,
+ PROTECTED,
+ READ_PIN,
+ FAST_PIN,
+ NR_TESTS,
+};
+
+static const char * const names[] = {
+ "base pages",
+ "PMD huge pages",
+ "mlocked pages",
+ "read-only mapping",
+ "read-only pins",
+ "fast GUP pins",
+};
+
+static int wait_child(pid_t pid)
+{
+ int status;
+ pid_t ret;
+
+ do {
+ ret = waitpid(pid, &status, 0);
+ } while (ret < 0 && errno == EINTR);
+ if (ret < 0 || !WIFEXITED(status))
+ return KSFT_FAIL;
+ return WEXITSTATUS(status);
+}
+
+static int check_pin(int fd, char *ram, size_t size, unsigned char value)
+{
+ char *copy = malloc(size);
+ uint64_t addr = (uintptr_t)copy;
+ size_t i;
+ int ret = KSFT_FAIL;
+
+ if (!copy)
+ return ret;
+ if (ioctl(fd, PIN_LONGTERM_TEST_READ, &addr)) {
+ ksft_print_msg("read pin: %s\n", strerror(errno));
+ goto out;
+ }
+ for (i = 0; i < size; i++) {
+ if ((unsigned char)ram[i] != value ||
+ (unsigned char)copy[i] != value) {
+ ksft_print_msg("CPU/pin mismatch at byte %zu\n", i);
+ goto out;
+ }
+ }
+ ret = KSFT_PASS;
+out:
+ free(copy);
+ return ret;
+}
+
+static int check_bytes(char *ram, size_t size, unsigned char value)
+{
+ size_t i;
+
+ for (i = 0; i < size; i++)
+ if ((unsigned char)ram[i] != value)
+ return KSFT_FAIL;
+ return KSFT_PASS;
+}
+
+static int after_cow_exec(char *ram, size_t size, pid_t sibling, int pipe_fd)
+{
+ char ready = 1;
+ int ret = check_bytes(ram, size, 0x31);
+
+ memset(ram, 0x42, size);
+ if (write(pipe_fd, &ready, 1) != 1)
+ ret = KSFT_FAIL;
+ close(pipe_fd);
+ if (wait_child(sibling))
+ ret = KSFT_FAIL;
+ return ret;
+}
+
+/* Pages shared with a separate mm must retain their pre-existing COW state. */
+static int before_cow_exec(void)
+{
+ size_t size = 16 * sysconf(_SC_PAGESIZE);
+ char addr_arg[32], size_arg[32], pid_arg[32], fd_arg[32], ready;
+ int pipe_fd[2];
+ char *ram;
+ pid_t pid;
+
+ ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (ram == MAP_FAILED)
+ return KSFT_FAIL;
+ memset(ram, 0x31, size);
+ if (madvise(ram, size, MADV_DOEXEC))
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ if (pipe(pipe_fd))
+ return KSFT_FAIL;
+ pid = fork();
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ close(pipe_fd[1]);
+ if (read(pipe_fd[0], &ready, 1) != 1)
+ _exit(KSFT_FAIL);
+ _exit(check_bytes(ram, size, 0x31));
+ }
+ close(pipe_fd[0]);
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)ram);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ snprintf(pid_arg, sizeof(pid_arg), "%d", pid);
+ snprintf(fd_arg, sizeof(fd_arg), "%d", pipe_fd[1]);
+ execl("/proc/self/exe", "doexec", "--cow", addr_arg, size_arg,
+ pid_arg, fd_arg, NULL);
+ close(pipe_fd[1]);
+ wait_child(pid);
+ return KSFT_FAIL;
+}
+
+static void exec_stage(int fd, char *ram, size_t size, int kind, int stage)
+{
+ char fd_arg[32], addr_arg[32], size_arg[32], kind_arg[32], stage_arg[32];
+
+ snprintf(fd_arg, sizeof(fd_arg), "%d", fd);
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)ram);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ snprintf(kind_arg, sizeof(kind_arg), "%d", kind);
+ snprintf(stage_arg, sizeof(stage_arg), "%d", stage);
+ execl("/proc/self/exe", "doexec", "--exec", fd_arg, addr_arg, size_arg,
+ kind_arg, stage_arg, NULL);
+ ksft_print_msg("exec: %s\n", strerror(errno));
+ _exit(KSFT_FAIL);
+}
+
+static int after_exec(int fd, char *ram, size_t size, int kind, int stage)
+{
+ unsigned char before = stage == 1 ? 0x31 : 0x42;
+ unsigned char after = stage == 1 ? 0x42 : 0x53;
+ pid_t pid;
+
+ if (check_pin(fd, ram, size, before))
+ return KSFT_FAIL;
+ if (kind == PROTECTED && mprotect(ram, size, PROT_READ | PROT_WRITE))
+ return KSFT_FAIL;
+ memset(ram, after, size);
+ if (check_pin(fd, ram, size, after))
+ return KSFT_FAIL;
+ if (stage == 1) {
+ /* VM_EXEC_KEEP is consumed, so mark again for the second exec. */
+ if (madvise(ram, size, MADV_DOEXEC))
+ return KSFT_FAIL;
+ if (kind == PROTECTED && mprotect(ram, size, PROT_READ))
+ return KSFT_FAIL;
+ exec_stage(fd, ram, size, kind, 2);
+ }
+
+ /* Fork must copy into the child and retain the parent's pinned pages. */
+ pid = fork();
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ memset(ram, 0x64, size);
+ _exit(KSFT_PASS);
+ }
+ if (wait_child(pid) || check_pin(fd, ram, size, after))
+ return KSFT_FAIL;
+ memset(ram, 0x75, size);
+ if (check_pin(fd, ram, size, 0x75))
+ return KSFT_FAIL;
+ return ioctl(fd, PIN_LONGTERM_TEST_STOP) ? KSFT_FAIL : KSFT_PASS;
+}
+
+static size_t huge_page_size(void)
+{
+ FILE *file = fopen("/sys/kernel/mm/transparent_hugepage/hpage_pmd_size", "r");
+ size_t size = 0;
+
+ if (file) {
+ if (fscanf(file, "%zu", &size) != 1)
+ size = 0;
+ fclose(file);
+ }
+ return size;
+}
+
+static int before_exec(int kind)
+{
+ size_t size = 16 * sysconf(_SC_PAGESIZE);
+ struct pin_longterm_test pin;
+ char *ram, *mapping;
+ uintptr_t aligned;
+ int fd;
+
+ if (kind == HUGE) {
+ size = huge_page_size();
+ if (!size)
+ return KSFT_SKIP;
+ }
+ fd = open("/sys/kernel/debug/gup_test", O_RDWR);
+ if (fd < 0)
+ return KSFT_SKIP;
+ mapping = mmap(NULL, 2 * size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (mapping == MAP_FAILED)
+ return KSFT_FAIL;
+ aligned = ((uintptr_t)mapping + size - 1) & ~(size - 1);
+ ram = (char *)aligned;
+ if (ram != mapping)
+ munmap(mapping, ram - mapping);
+ if (ram + size != mapping + 2 * size)
+ munmap(ram + size, mapping + 2 * size - (ram + size));
+ if (madvise(ram, size, kind == HUGE ? MADV_HUGEPAGE : MADV_NOHUGEPAGE) &&
+ (kind == HUGE || errno != EINVAL))
+ return KSFT_FAIL;
+ memset(ram, 0x31, size);
+ if (kind == HUGE && madvise(ram, size, MADV_COLLAPSE))
+ return KSFT_SKIP;
+ if (kind == LOCKED && mlock(ram, size))
+ return KSFT_SKIP;
+ if (madvise(ram, size, MADV_DOEXEC)) {
+ ksft_print_msg("MADV_DOEXEC: %s\n", strerror(errno));
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ }
+ pin = (struct pin_longterm_test) {
+ .addr = (uintptr_t)ram,
+ .size = size,
+ .flags = kind == READ_PIN ? 0 : PIN_LONGTERM_TEST_FLAG_USE_WRITE,
+ };
+ if (kind == FAST_PIN)
+ pin.flags |= PIN_LONGTERM_TEST_FLAG_USE_FAST;
+ if (ioctl(fd, PIN_LONGTERM_TEST_START, &pin)) {
+ ksft_print_msg("pin: %s\n", strerror(errno));
+ return errno == ENOTTY ? KSFT_SKIP : KSFT_FAIL;
+ }
+ if (kind == PROTECTED && mprotect(ram, size, PROT_READ))
+ return KSFT_FAIL;
+ exec_stage(fd, ram, size, kind, 1);
+ return KSFT_FAIL;
+}
+
+static int check_exec(char *ram, size_t size, size_t stride, bool reject)
+{
+ char addr_arg[32], size_arg[32], stride_arg[32];
+
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)ram);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ snprintf(stride_arg, sizeof(stride_arg), "%zu", stride);
+ execl("/proc/self/exe", "doexec", reject ? "--reject" : "--check", addr_arg, size_arg,
+ stride_arg, NULL);
+ return errno;
+}
+
+static char *preserved_ram(size_t size)
+{
+ char *ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+
+ if (ram == MAP_FAILED)
+ return NULL;
+ memset(ram, 0x31, size);
+ if (madvise(ram, size, MADV_DOEXEC)) {
+ munmap(ram, size);
+ return NULL;
+ }
+ return ram;
+}
+
+static int blocked_task(void *arg)
+{
+ int fd = *(int *)arg;
+ char ready;
+
+ return read(fd, &ready, 1) == 1 ? KSFT_PASS : KSFT_FAIL;
+}
+
+static void *blocked_thread(void *arg)
+{
+ blocked_task(arg);
+ return NULL;
+}
+
+/* Without its own copy of the write end, the clone ends with its parent. */
+static int blocked_clone(void *arg)
+{
+ close(((int *)arg)[1]);
+ return blocked_task(arg);
+}
+
+/* Own threads may be killed by exec; a separate CLONE_VM task may not. */
+static int shared_mm_exec(bool own_thread)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size), *stack;
+ int pipe_fd[2], ret;
+ pthread_t thread;
+ pid_t pid;
+
+ if (!ram)
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ if (pipe(pipe_fd))
+ return KSFT_FAIL;
+ if (own_thread) {
+ if (pthread_create(&thread, NULL, blocked_thread, &pipe_fd[0]))
+ return KSFT_FAIL;
+ check_exec(ram, size, 1, false);
+ write(pipe_fd[1], "x", 1);
+ pthread_join(thread, NULL);
+ return KSFT_FAIL;
+ }
+ stack = mmap(NULL, 65536, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
+ if (stack == MAP_FAILED)
+ return KSFT_FAIL;
+ pid = clone(blocked_clone, stack + 65536, CLONE_VM | SIGCHLD, pipe_fd);
+ if (pid < 0)
+ return KSFT_FAIL;
+ ret = check_exec(ram, size, 1, true);
+ /* Exec must return EBUSY with the old mapping intact. */
+ if (ret != EBUSY || check_bytes(ram, size, 0x31))
+ ret = KSFT_FAIL;
+ else
+ ret = KSFT_PASS;
+ write(pipe_fd[1], "x", 1);
+ if (wait_child(pid))
+ ret = KSFT_FAIL;
+ if (ret)
+ return ret;
+ /* Releasing the outside user must allow a retry. */
+ check_exec(ram, size, 1, false);
+ return KSFT_FAIL;
+}
+
+/* The caller must release its backend owner before preserving exec. */
+static int vhost_owner_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram;
+ int fd = open("/dev/vhost-net", O_RDWR);
+
+ if (fd < 0)
+ return KSFT_SKIP;
+ ram = preserved_ram(size);
+ if (!ram)
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ if (ioctl(fd, VHOST_SET_OWNER))
+ return KSFT_SKIP;
+ if (ioctl(fd, VHOST_RESET_OWNER))
+ return KSFT_FAIL;
+ close(fd);
+ check_exec(ram, size, 1, false);
+ return KSFT_FAIL;
+}
+
+static void *exec_thread(void *arg)
+{
+ check_exec(arg, sysconf(_SC_PAGESIZE), 1, false);
+ return NULL;
+}
+
+static int nonleader_exec(void)
+{
+ char *ram = preserved_ram(sysconf(_SC_PAGESIZE));
+ pthread_t thread;
+
+ if (!ram)
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ if (pthread_create(&thread, NULL, exec_thread, ram))
+ return KSFT_FAIL;
+ pthread_join(thread, NULL);
+ return KSFT_FAIL;
+}
+
+static int proc_exec(char *ram, size_t size, int stage)
+{
+ char addr_arg[32], size_arg[32], stage_arg[32];
+
+ if (check_bytes(ram, size, 0x31))
+ return KSFT_FAIL;
+ if (stage == 32)
+ return KSFT_PASS;
+ if (madvise(ram, size, MADV_DOEXEC))
+ return KSFT_FAIL;
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)ram);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ snprintf(stage_arg, sizeof(stage_arg), "%d", stage + 1);
+ execl("/proc/self/exe", "doexec", "--proc", addr_arg, size_arg,
+ stage_arg, NULL);
+ return KSFT_FAIL;
+}
+
+/* Read both pre-opened and freshly opened proc files during repeated exec. */
+static int proc_readers_exec(void)
+{
+ static const char * const files[] = { "maps", "smaps_rollup", "pagemap" };
+ int ready[2], go[2], fds[3] = { -1, -1, -1 }, status, ret = KSFT_FAIL;
+ char paths[3][80], buffer[4096];
+ unsigned int round = 0;
+ pid_t pid, waited;
+ int i;
+
+ if (pipe(ready) || pipe(go))
+ return KSFT_FAIL;
+ pid = fork();
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ size_t size = 16 * sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+
+ close(ready[0]);
+ close(go[1]);
+ if (!ram)
+ _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ if (write(ready[1], "x", 1) != 1 || read(go[0], buffer, 1) != 1)
+ _exit(KSFT_FAIL);
+ close(ready[1]);
+ close(go[0]);
+ _exit(proc_exec(ram, size, 0));
+ }
+ close(ready[1]);
+ close(go[0]);
+ if (read(ready[0], buffer, 1) != 1) {
+ close(ready[0]);
+ close(go[1]);
+ return wait_child(pid);
+ }
+ close(ready[0]);
+ for (i = 0; i < 3; i++) {
+ snprintf(paths[i], sizeof(paths[i]), "/proc/%d/%s", pid, files[i]);
+ fds[i] = open(paths[i], O_RDONLY);
+ if (fds[i] < 0) {
+ ret = KSFT_SKIP;
+ goto stop;
+ }
+ }
+ if (write(go[1], "x", 1) != 1)
+ goto stop;
+ close(go[1]);
+ go[1] = -1;
+ for (;;) {
+ waited = waitpid(pid, &status, WNOHANG);
+ if (waited == pid) {
+ ret = WIFEXITED(status) ? WEXITSTATUS(status) : KSFT_FAIL;
+ break;
+ }
+ if (waited < 0 && errno != EINTR)
+ break;
+ for (i = 0; i < 3; i++) {
+ lseek(fds[i], 0, SEEK_SET);
+ if (read(fds[i], buffer, sizeof(buffer)) < 0 &&
+ errno != EINTR && errno != ESRCH)
+ goto stop;
+ }
+ /* Keep two old handles while refreshing the third. */
+ i = round++ % 3;
+ close(fds[i]);
+ fds[i] = open(paths[i], O_RDONLY);
+ if (fds[i] < 0) {
+ ret = wait_child(pid);
+ break;
+ }
+ }
+ for (i = 0; i < 3; i++)
+ if (fds[i] >= 0)
+ close(fds[i]);
+ return ret;
+stop:
+ if (go[1] >= 0)
+ close(go[1]);
+ kill(pid, SIGKILL);
+ wait_child(pid);
+ for (i = 0; i < 3; i++)
+ if (fds[i] >= 0)
+ close(fds[i]);
+ return ret;
+}
+
+struct clone_race {
+ char *ram;
+ int report;
+ atomic_bool stop;
+ atomic_uint rounds;
+};
+
+static int clone_race_child(void *arg)
+{
+ struct clone_race *race = arg;
+ unsigned int i;
+
+ for (i = 0; i < 8; i++) {
+ if (__atomic_load_n(race->ram, __ATOMIC_RELAXED) != 0x31) {
+ write(race->report, "x", 1);
+ return KSFT_FAIL;
+ }
+ usleep(1000);
+ }
+ return KSFT_PASS;
+}
+
+static void *clone_race_thread(void *arg)
+{
+ struct clone_race *race = arg;
+ char *stack = mmap(NULL, 65536, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
+ pid_t pid;
+
+ if (stack == MAP_FAILED) {
+ write(race->report, "x", 1);
+ atomic_store(&race->rounds, 4);
+ return NULL;
+ }
+ while (!atomic_load(&race->stop)) {
+ pid = clone(clone_race_child, stack + 65536,
+ CLONE_VM | SIGCHLD, race);
+ if (pid >= 0) {
+ if (wait_child(pid))
+ write(race->report, "x", 1);
+ } else if (errno != EBUSY && errno != EINTR) {
+ write(race->report, "x", 1);
+ }
+ atomic_fetch_add(&race->rounds, 1);
+ usleep(1000);
+ }
+ munmap(stack, 65536);
+ return NULL;
+}
+
+/* A supervisor collects errors from any shared-mm child surviving exec. */
+static int clone_exec_race(void)
+{
+ int report[2], ret;
+ struct pollfd pfd;
+ char failed;
+ pid_t pid;
+
+ if (pipe(report))
+ return KSFT_FAIL;
+ pid = fork();
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ size_t size = sysconf(_SC_PAGESIZE);
+ struct clone_race race = { .report = report[1] };
+ pthread_t thread;
+ unsigned int attempt;
+
+ close(report[0]);
+ race.ram = preserved_ram(size);
+ if (!race.ram)
+ _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ if (pthread_create(&thread, NULL, clone_race_thread, &race))
+ _exit(KSFT_FAIL);
+ while (atomic_load(&race.rounds) < 4)
+ usleep(1000);
+ for (attempt = 0; attempt < 500; attempt++) {
+ if (check_exec(race.ram, size, 1, false) != EBUSY)
+ break;
+ if (check_bytes(race.ram, size, 0x31))
+ break;
+ usleep(1000);
+ }
+ atomic_store(&race.stop, true);
+ pthread_join(thread, NULL);
+ _exit(KSFT_FAIL);
+ }
+ close(report[1]);
+ ret = wait_child(pid);
+ pfd = (struct pollfd) { .fd = report[0], .events = POLLIN | POLLHUP };
+ if (poll(&pfd, 1, 3000) <= 0 || read(report[0], &failed, 1) != 0)
+ ret = KSFT_FAIL;
+ close(report[0]);
+ return ret;
+}
+
+static int check_exec_fd(int exe, char *ram, size_t size, size_t stride)
+{
+ char addr_arg[32], size_arg[32], stride_arg[32];
+ char *args[] = { "doexec", "--check", addr_arg, size_arg, stride_arg, NULL };
+
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)ram);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ snprintf(stride_arg, sizeof(stride_arg), "%zu", stride);
+ syscall(__NR_execveat, exe, "", args, environ, AT_EMPTY_PATH);
+ return errno;
+}
+
+static int unexpected_status(unsigned int nth, int status)
+{
+ ksft_print_msg("fail-nth %u: wait status %#x\n", nth, status);
+ return KSFT_FAIL;
+}
+
+/* Opt-in sweep of allocation failures, using only the child's fail-nth. */
+static int allocation_failures(void)
+{
+ const size_t size = 256ul << 20, stride = 2ul << 20;
+ struct rlimit no_core = { 0, 0 };
+ unsigned int early = 0, fatal = 0, success = 0, loader = 0;
+ unsigned int nth;
+ pid_t pid;
+ int status;
+ pid_t waited;
+
+ if (!getenv("DOEXEC_FAULT_INJECTION") || access("/proc/self/fail-nth", W_OK))
+ return KSFT_SKIP;
+ for (nth = 1; nth <= 256; nth++) {
+ pid = fork();
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ char *ram, number[32];
+ size_t i;
+ int fd, len, err, exe;
+
+ setrlimit(RLIMIT_CORE, &no_core);
+ ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (ram == MAP_FAILED ||
+ (madvise(ram, size, MADV_NOHUGEPAGE) && errno != EINVAL))
+ _exit(KSFT_FAIL);
+ for (i = 0; i < size; i += stride)
+ ram[i] = 0x31;
+ if (madvise(ram, size, MADV_DOEXEC))
+ _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ /* Keep the path lookup, which /proc fails with ENOENT, out of the sweep. */
+ exe = open("/proc/self/exe", O_RDONLY | O_CLOEXEC);
+ fd = open("/proc/self/fail-nth", O_WRONLY | O_CLOEXEC);
+ len = snprintf(number, sizeof(number), "%u", nth);
+ if (exe < 0 || fd < 0 || write(fd, number, len) != len)
+ _exit(KSFT_FAIL);
+ err = check_exec_fd(exe, ram, size, stride);
+ /* Disarm before checking mappings or exiting. */
+ pwrite(fd, "0", 1, 0);
+ for (i = 0; i < size; i += stride)
+ if (ram[i] != 0x31)
+ _exit(KSFT_FAIL);
+ /*
+ * Failed allocations also surface as E2BIG from
+ * copy_strings(), and as ELIBBAD or EIO from reading the
+ * ELF interpreter.
+ */
+ if (err == ENOMEM || err == EFAULT || err == EAGAIN ||
+ err == E2BIG || err == ELIBBAD || err == EIO)
+ _exit(10);
+ ksft_print_msg("fail-nth %u: exec: %s\n", nth, strerror(err));
+ _exit(KSFT_FAIL);
+ }
+ do {
+ waited = waitpid(pid, &status, 0);
+ } while (waited < 0 && errno == EINTR);
+ if (waited < 0)
+ return KSFT_FAIL;
+ if (WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV)
+ fatal++;
+ else if (WIFEXITED(status) && WEXITSTATUS(status) == 10)
+ early++;
+ else if (WIFEXITED(status) && WEXITSTATUS(status) == KSFT_PASS)
+ success++;
+ /* A dynamic loader may exit when a post-exec allocation fails. */
+ else if (WIFEXITED(status) && WEXITSTATUS(status) == 127)
+ loader++;
+ else if (WIFEXITED(status) && WEXITSTATUS(status) == KSFT_SKIP)
+ return KSFT_SKIP;
+ else
+ return unexpected_status(nth, status);
+ }
+ ksft_print_msg("faults: %u recoverable, %u SIGSEGV, %u success, %u loader exits\n",
+ early, fatal, success, loader);
+ /* A sweep that injected no recoverable/fatal errors provides no coverage. */
+ return early && fatal ? KSFT_PASS : KSFT_SKIP;
+}
+
+int main(int argc, char **argv)
+{
+ pid_t pid;
+ int kind, ret;
+
+ if (argc == 7 && !strcmp(argv[1], "--exec"))
+ return after_exec(atoi(argv[2]),
+ (char *)(uintptr_t)strtoull(argv[3], NULL, 10),
+ strtoull(argv[4], NULL, 10), atoi(argv[5]), atoi(argv[6]));
+ if (argc == 6 && !strcmp(argv[1], "--cow"))
+ return after_cow_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
+ strtoull(argv[3], NULL, 10), atoi(argv[4]), atoi(argv[5]));
+ if (argc == 5 && !strcmp(argv[1], "--reject"))
+ return KSFT_FAIL;
+ if (argc == 5 && !strcmp(argv[1], "--proc"))
+ return proc_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
+ strtoull(argv[3], NULL, 10), atoi(argv[4]));
+ if (argc == 5 && !strcmp(argv[1], "--check")) {
+ char *ram = (char *)(uintptr_t)strtoull(argv[2], NULL, 10);
+ size_t size = strtoull(argv[3], NULL, 10);
+ size_t stride = strtoull(argv[4], NULL, 10), i;
+ int fd = open("/proc/self/fail-nth", O_WRONLY);
+
+ if (fd >= 0) {
+ write(fd, "0", 1);
+ close(fd);
+ }
+ for (i = 0; i < size; i += stride)
+ if (ram[i] != 0x31)
+ return KSFT_FAIL;
+ return KSFT_PASS;
+ }
+ ksft_print_header();
+ ksft_set_plan(NR_TESTS + 8);
+ for (kind = 0; kind < NR_TESTS; kind++) {
+ fflush(stdout);
+ pid = fork();
+ if (!pid)
+ _exit(before_exec(kind));
+ ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
+ if (ret == KSFT_SKIP)
+ ksft_test_result_skip("%s: DOEXEC, GUP test or mapping unavailable\n",
+ names[kind]);
+ else
+ ksft_test_result(ret == KSFT_PASS, "%s: repeated exec and fork\n",
+ names[kind]);
+ }
+ fflush(stdout);
+ pid = fork();
+ if (!pid)
+ _exit(before_cow_exec());
+ ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
+ if (ret == KSFT_SKIP)
+ ksft_test_result_skip("pre-existing COW: DOEXEC unavailable\n");
+ else
+ ksft_test_result(ret == KSFT_PASS, "pre-existing COW preserved\n");
+ for (kind = 0; kind < 7; kind++) {
+ static const char * const extra_names[] = {
+ "CLONE_VM: recoverable EBUSY and retry",
+ "own threads: preserving exec allowed",
+ "vhost owner: reset before preserving exec",
+ "allocation failure sweep (opt-in)",
+ "non-leader thread: preserving exec allowed",
+ "proc readers: repeated preserving exec allowed",
+ "concurrent CLONE_VM: shared memory is not stolen",
+ };
+
+ fflush(stdout);
+ pid = fork();
+ if (!pid) {
+ switch (kind) {
+ case 0:
+ case 1:
+ _exit(shared_mm_exec(kind));
+ case 2:
+ _exit(vhost_owner_exec());
+ case 3:
+ _exit(allocation_failures());
+ case 4:
+ _exit(nonleader_exec());
+ case 5:
+ _exit(proc_readers_exec());
+ case 6:
+ _exit(clone_exec_race());
+ }
+ }
+ ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
+ if (ret == KSFT_SKIP)
+ ksft_test_result_skip("%s: unavailable or disabled\n", extra_names[kind]);
+ else
+ ksft_test_result(ret == KSFT_PASS, "%s\n", extra_names[kind]);
+ }
+ ksft_finished();
+}
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 6ac8ba7e24471..5604fc8939a33 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -77,6 +77,8 @@ to the -t argument containing one or more of the following categories
test prctl(PR_SET_MDWE, ...)
- page_frag
test handling of page fragment allocation and freeing
+- doexec
+ test memory preserved across exec with MADV_DOEXEC
example: ./run_vmtests.sh -t "hmm mmap ksm"
EOF
@@ -297,6 +299,8 @@ fi
CATEGORY="gup_test" run_test ./gup_test -ct -F 0x1 0 19 0x1000
CATEGORY="gup_test" run_test ./gup_longterm
+CATEGORY="doexec" run_test ./doexec
+
CATEGORY="userfaultfd" run_test ./uffd-unit-tests
uffd_stress_bin=./uffd-stress
CATEGORY="userfaultfd" run_test ${uffd_stress_bin} anon 20 16
--
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 3/4] sched: account sleep and block time of a task group entity
2026-10-14 11:28 6% ` no-reply
@ 2026-10-14 11:36 0% ` Vasileios Almpanis
0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-10-14 11:36 UTC (permalink / raw)
To: no-reply, Pavel Tikhomirov, Konstantin Khorenko; +Cc: devel
On 10/14/26 1:28 PM, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Medium] sched: group iowait/idle classification uses non-hierarchical cfs_rq->nr_iowait
> --
>
> commit b026bcc4d4882c5ba1716b8736a28e5f90a38798
> Author: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
>
> sched: account sleep and block time of a task group entity
>
> This commit makes a group sched_entity start a sleep or block period when
> it is dequeued with DEQUEUE_SLEEP. It chooses block if the group's cfs_rq
> has iowait sleepers, and sleep otherwise. It also starts the period from
> tg_unthrottle_up() when an unthrottled group has nothing to run.
>
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-147823
>
>> diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
>> --- a/kernel/sched/fair.c
>> +++ b/kernel/sched/fair.c
> [ ... ]
>
>> +/* The group goes idle, blocked if it has iowait sleepers */
>> +static inline void
>> +update_stats_group_sleep_start(struct cfs_rq *cfs_rq, struct sched_entity *se)
>> +{
>> + struct sched_statistics *stats;
>> +
>> + stats = __schedstats_from_se(se);
>> + if (group_cfs_rq(se)->nr_iowait)
>> + __schedstat_set(stats->block_start, rq_clock(rq_of(cfs_rq)));
>> + else
>> + __schedstat_set(stats->sleep_start, rq_clock(rq_of(cfs_rq)));
>> +}
> [Severity: Medium]
> Is the classification here hierarchical? group_cfs_rq(se)->nr_iowait is
> the counter of that group's own cfs_rq only.
>
> dequeue_entity() increments nr_iowait only on the task's immediate cfs_rq:
>
> if ((flags & DEQUEUE_SLEEP) && entity_is_task(se)) {
> struct task_struct *tsk = task_of(se);
>
> if (tsk->in_iowait)
> cfs_rq->nr_iowait++;
>
> Consider a task with in_iowait set that sleeps in a sub-cgroup S of a
> container CT:
>
> dequeue_entity(task)
> S->cfs_rq->nr_iowait++
> dequeue_entity(S group entity, DEQUEUE_SLEEP)
> update_stats_group_sleep_start()
> S->nr_iowait != 0, so block_start is set
> dequeue_entity(CT group entity, DEQUEUE_SLEEP)
> update_stats_group_sleep_start()
> CT->nr_iowait == 0, so sleep_start is set
>
> cpu_cgroup_update_stat() then reads iowait_sum and sum_sleep_runtime from
> the CT group entity, so the time appears as idle and not as iowait.
>
> The follow-up patch "sched: switch a sleeping task group entity between
> idle and iowait" (5de73903cb61) adds nr_iowait_update_group(), called from
> nr_iowait_inc_fair() and nr_iowait_dec_fair(). It re-evaluates only
> p->se.parent, the immediate parent group entity, and the increments and
> decrements still stay on the task's direct cfs_rq.
>
> So even with the whole series applied, it looks like the CT entity stays
> misclassified when the iowait sleepers are in nested cgroups, and the
> stated goal is met only for a flat hierarchy. The same non-hierarchical
> counter is read per task_group in cpuacct.c (tg_nr_iowait).
>
> Should the iowait state be propagated to, or evaluated across, all
> ancestor group entities?
Yes, it is not hierarchical and I have already written about it in the
cover letter. cfs_rq:nr_iowait counts only the tasks sleeping on that
cfs_rq, so with nested cpu cgroups inside the CT the CT group entity is
accounted as idle instead of iowait.
Its the same with vz7. This series restores that behavior, it doesn't
change the previous model. If we want to make it hierarchical I would
rather do it in a separate ticket if there is a real need for it.
--
Best regards, Vasileios Almpanis
Software 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/4] sched: account sleep and block time of a task group entity
@ 2026-10-14 11:28 6% ` no-reply
2026-10-14 11:36 0% ` Vasileios Almpanis
0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-14 11:28 UTC (permalink / raw)
To: Pavel Tikhomirov, Konstantin Khorenko, Vasileios Almpanis; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] sched: group iowait/idle classification uses non-hierarchical cfs_rq->nr_iowait
--
commit b026bcc4d4882c5ba1716b8736a28e5f90a38798
Author: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
sched: account sleep and block time of a task group entity
This commit makes a group sched_entity start a sleep or block period when
it is dequeued with DEQUEUE_SLEEP. It chooses block if the group's cfs_rq
has iowait sleepers, and sleep otherwise. It also starts the period from
tg_unthrottle_up() when an unthrottled group has nothing to run.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-147823
> diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
> --- a/kernel/sched/fair.c
> +++ b/kernel/sched/fair.c
[ ... ]
> +/* The group goes idle, blocked if it has iowait sleepers */
> +static inline void
> +update_stats_group_sleep_start(struct cfs_rq *cfs_rq, struct sched_entity *se)
> +{
> + struct sched_statistics *stats;
> +
> + stats = __schedstats_from_se(se);
> + if (group_cfs_rq(se)->nr_iowait)
> + __schedstat_set(stats->block_start, rq_clock(rq_of(cfs_rq)));
> + else
> + __schedstat_set(stats->sleep_start, rq_clock(rq_of(cfs_rq)));
> +}
[Severity: Medium]
Is the classification here hierarchical? group_cfs_rq(se)->nr_iowait is
the counter of that group's own cfs_rq only.
dequeue_entity() increments nr_iowait only on the task's immediate cfs_rq:
if ((flags & DEQUEUE_SLEEP) && entity_is_task(se)) {
struct task_struct *tsk = task_of(se);
if (tsk->in_iowait)
cfs_rq->nr_iowait++;
Consider a task with in_iowait set that sleeps in a sub-cgroup S of a
container CT:
dequeue_entity(task)
S->cfs_rq->nr_iowait++
dequeue_entity(S group entity, DEQUEUE_SLEEP)
update_stats_group_sleep_start()
S->nr_iowait != 0, so block_start is set
dequeue_entity(CT group entity, DEQUEUE_SLEEP)
update_stats_group_sleep_start()
CT->nr_iowait == 0, so sleep_start is set
cpu_cgroup_update_stat() then reads iowait_sum and sum_sleep_runtime from
the CT group entity, so the time appears as idle and not as iowait.
The follow-up patch "sched: switch a sleeping task group entity between
idle and iowait" (5de73903cb61) adds nr_iowait_update_group(), called from
nr_iowait_inc_fair() and nr_iowait_dec_fair(). It re-evaluates only
p->se.parent, the immediate parent group entity, and the increments and
decrements still stay on the task's direct cfs_rq.
So even with the whole series applied, it looks like the CT entity stays
misclassified when the iowait sleepers are in nested cgroups, and the
stated goal is met only for a flat hierarchy. The same non-hierarchical
counter is read per task_group in cpuacct.c (tg_nr_iowait).
Should the iowait state be propagated to, or evaluated across, all
ancestor group entities?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261014-ct-iowait-v1-0-9d65fe37cf89@virtuozzo.com?part=3
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 6%]
* Re: [PATCH QEMU HCI-8.1 v2 23/24] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860
2026-10-13 15:28 8% ` [PATCH QEMU HCI-8.1 v2 23/24] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860 Andrey Drobyshev
@ 2026-10-13 16:00 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 16:00 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] vfio: vfio_region_cpr_find() exits QEMU for lazily mmapped display region after incoming mode reset
- [Medium] vfio: vfio_region_mmap() adds cpr-transfer blocker with error_abort
- [Medium] vfio: cpr-exec new QEMU exit(1)s when a BAR was not mmapped by old QEMU
- [Medium] vfio: display region CPR entry leaked, vfio_region_exit() runs after unparent
- [Low] vfio: cpr-transfer blocker added before mmap and kept after failure
- [Low] vfio: cpr-transfer is blocked for every VFIO device with mmapped regions
--
commit 715bce8fddbc1bc88d530d34be1f40b7da065453
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860
This patch saves each VFIO BAR mmap in the CPR ramblock list, so that the
new QEMU can reuse the mapping after cpr-exec instead of calling mmap()
again. It also adds a cpr-transfer blocker for devices with mmapped
regions.
> diff --git a/hw/vfio/device.c b/hw/vfio/device.c
> index 5a14a7d5c0a2e..88b9e8752cd93 100644
> --- a/hw/vfio/device.c
> +++ b/hw/vfio/device.c
> @@ -514,6 +514,7 @@ void vfio_device_unprepare(VFIODevice *vbasedev)
>
> g_clear_pointer(&vbasedev->reginfo, g_free);
> g_clear_pointer(&vbasedev->region_fds, g_free);
> + migrate_del_blocker(&vbasedev->cpr.mmap_blocker);
>
> QLIST_REMOVE(vbasedev, container_next);
> QLIST_REMOVE(vbasedev, global_next);
[ ... ]
> diff --git a/hw/vfio/region.c b/hw/vfio/region.c
> index 1deaef12fd0b8..fce09a0d17b1a 100644
> --- a/hw/vfio/region.c
> +++ b/hw/vfio/region.c
[ ... ]
> +static char *vfio_region_cpr_name(VFIORegion *region, int index)
> +{
> + g_autofree char *path = qdev_get_dev_path(region->vbasedev->dev);
> + return g_strdup_printf("%s%s%s mmaps[%d]", path ?: "", path ? "/" : "",
> + memory_region_name(region->mem), index);
> +}
[Severity: Medium]
The name built here depends on qdev_get_dev_path(), which returns NULL once
device_unparent() has cleared dev->parent_bus. Can the save-time and
delete-time names diverge for the display region?
The display region is mmapped lazily:
vfio_display_region_update()
vfio_region_setup()
vfio_region_mmap()
vfio_region_cpr_save() /* saved as "<path>/display mmaps[0]" */
On hot-unplug, vfio_exitfn() runs vfio_pci_bars_exit(), which covers only
the BAR regions. The display region is torn down later, from
vfio_pci_finalize():
vfio_pci_finalize()
vfio_pci_put_device()
vfio_display_finalize()
vfio_display_region_exit()
vfio_region_exit()
By then device_unparent() has run, so the name becomes "display mmaps[0]"
without the path prefix. Then cpr_delete_ramblock() in vfio_region_exit()
finds nothing, and the saved entry stays while vfio_region_finalize()
munmap()s the region.
Could this leave a stale entry that makes a later hot-replug of a device
with the same path hit exit(1) in vfio_region_cpr_find(), or makes a later
cpr-exec try MADV_DOEXEC on an unmapped range?
> +/* Same checks as for RAM blocks in ram_block_add() */
> +static void *vfio_region_cpr_find(VFIORegion *region, int index)
> +{
> + g_autofree char *name = vfio_region_cpr_name(region, index);
> + cpr_ramblock_t ram = cpr_find_ramblock(name);
> +
> + if (cpr_exec_is_incoming()) {
> + if (!ram.addr || !ram.length) {
> + error_report("CPR mode=exec is requested, but mapping %s can't "
> + "be reused: addr=%p length=%zu. Aborting",
> + name, (void *)ram.addr, ram.length);
> + exit(1);
> + }
[Severity: Medium]
Can this exit(1) be reached for a region that old QEMU legitimately could
not mmap?
In old QEMU, vfio_bar_register() treats a vfio_region_mmap() failure as
non-fatal:
if (vfio_region_mmap(&bar->region)) {
error_report("Failed to mmap %s BAR %d. Performance may be slow",
vdev->vbasedev.name, nr);
}
The no_mmap path in vfio_region_mmap() calls vfio_subregion_unmap(), which
calls cpr_delete_ramblock(). A failed region therefore has no CPR entry.
The only blocker added by this patch is mmap_blocker, which applies to
cpr-transfer only. Nothing blocks cpr-exec in this state, so the new QEMU
runs vfio_region_mmap() again. Then vfio_region_cpr_find() sees
cpr_exec_is_incoming() true and no entry, and exits.
The comment says this mirrors ram_block_add(), but RAM blocks are always
allocated while BAR mmaps are optional. Should this fall back to mmap() or
the slow path when no entry exists, or should cpr-exec be blocked when a
region could not be mapped?
> + assert(ram.length == region->mmaps[index].size);
> + } else if (ram.addr || ram.length) {
> + error_report("CPR mode=exec wasn't requested, but mapping %s was "
> + "saved: addr=%p length=%zu. Aborting",
> + name, (void *)ram.addr, ram.length);
> + exit(1);
> + }
[Severity: High]
Can this second branch kill QEMU after a cpr-exec that had otherwise
succeeded, for the lazily mapped display region?
The sequence would be:
1. Old QEMU maps the display buffer from vfio_display_region_update(), and
vfio_region_cpr_save() adds "<path>/display mmaps[0]" to the ramblock
list. cpr_exec_cb() preserves it.
2. New QEMU loads the entry in cpr_state_load(), which also sets
incoming_mode to MIG_MODE_CPR_EXEC. Nothing maps the display region at
realize time, since vfio_display_region_init() only registers the
gfx_update callback.
3. migration_incoming_state_destroy() calls
cpr_set_incoming_mode(MIG_MODE_NONE).
4. The first gfx_update with an active plane after that calls
vfio_region_setup() and vfio_region_mmap(), and then
vfio_region_cpr_find().
The saved entry is still present, because only vfio_subregion_unmap() and
vfio_region_exit() delete it. cpr_exec_is_incoming() is now false, so the
"wasn't requested, but mapping %s was saved" branch calls exit(1).
The check was copied from ram_block_add(), which runs at realize time
while the incoming mode is still set. Is it valid for a mapping that can be
created after the mode is reset?
Even without the exit, the preserved mapping would be neither reused nor
released.
[ ... ]
> @@ -307,6 +352,19 @@ int vfio_region_mmap(VFIORegion *region)
> return 0;
> }
>
> + /*
> + * Only cpr-exec keeps the address space, a cpr-transfer target would
> + * have to map the regions anew, which some drivers (NVIDIA vGPU) refuse
> + * while the old mappings exist. Not supported.
> + */
> + if (!region->vbasedev->cpr.mmap_blocker) {
> + error_setg(®ion->vbasedev->cpr.mmap_blocker,
> + "cpr-transfer is not supported with mmapped VFIO regions "
> + "(%s); use the cpr-exec mode", region->vbasedev->name);
> + migrate_add_blocker_modes(®ion->vbasedev->cpr.mmap_blocker,
> + &error_abort, MIG_MODE_CPR_TRANSFER, -1);
> + }
[Severity: Medium]
Can passing &error_abort here abort QEMU at runtime?
migrate_add_blocker_modes() calls is_busy(), which fails with -EBUSY and
fills errp when the following holds:
if (runstate_check(RUN_STATE_SAVE_VM) || migration_is_running()) {
error_propagate_prepend(errp, *reasonp, ...
vfio_region_mmap() is not only a realize-time call. The gfx_update
callback reaches it lazily:
vfio_display_region_update()
vfio_region_mmap(&dpy->region.buffer)
For a device whose BARs have no mmap, or whose BAR mmaps failed, the first
blocker registration happens at runtime. If a migration or snapshot is
running at that moment, error_abort turns the -EBUSY into abort().
Since is_busy() does not depend on the mode, would this also hit ordinary
non-CPR migrations? Could the -EBUSY be handled instead of aborting?
[Severity: Low]
Is the blocker installed too early here? It is added before the mmap loop
and is not removed on the no_mmap error path, nor by vfio_subregion_unmap()
or vfio_region_exit().
The only removal is migrate_del_blocker() in vfio_device_unprepare(). The
commit message says the block applies "for devices with mmapped regions",
but a device whose first mmap() fails, or whose mappings were all undone,
still blocks cpr-transfer until vfio_device_unprepare().
[Severity: Low]
Is the blocker intended to apply to every device with mmapped regions?
It is registered unconditionally for any region with nr_mmaps > 0, so
nearly every vfio-pci device blocks cpr-transfer. Before this patch, VFIO
devices reached cpr-transfer through no such check, since
vfio_devices_present() was only consulted through
migration_is_blocked()->migrate_cpr_is_blocked() when migrate_mode() ==
MIG_MODE_CPR_EXEC.
The commit message gives the NVIDIA vGPU case as the reason, but the code
does not narrow the block to drivers that refuse to remap. Could the
blocker be limited to the affected drivers or devices?
[ ... ]
> @@ -314,6 +372,11 @@ int vfio_region_mmap(VFIORegion *region)
> size_t align = MIN(1ULL << ctz64(region->mmaps[i].size), 1 * GiB);
> void *map_base, *map_align;
>
> + region->mmaps[i].mmap = vfio_region_cpr_find(region, i);
> + if (region->mmaps[i].mmap) {
> + goto mapped;
> + }
> +
[ ... ]
> @@ -410,6 +475,13 @@ void vfio_region_exit(VFIORegion *region)
>
> for (i = 0; i < region->nr_mmaps; i++) {
> if (region->mmaps[i].mmap) {
> + /*
> + * The mapping is unmapped by vfio_region_finalize(), but by
> + * then the device is unparented and its path, part of the CPR
> + * name, is gone. So drop the CPR entry here.
> + */
> + g_autofree char *cpr_name = vfio_region_cpr_name(region, i);
> + cpr_delete_ramblock(cpr_name);
> memory_region_del_subregion(region->mem, ®ion->mmaps[i].mem);
> }
> }
This comment is correct for the BAR path through vfio_exitfn(), but is the
same ordering guaranteed when vfio_region_exit() is reached from
vfio_display_finalize()? As described above, the device is already
unparented there.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013-vfio_v1-v2-0-433455c0a294@virtuozzo.com?part=23
^ permalink raw reply [relevance 5%]
* Re: [PATCH QEMU HCI-8.1 v2 21/24] VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved
2026-10-13 15:28 6% ` [PATCH QEMU HCI-8.1 v2 21/24] VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved Andrey Drobyshev
@ 2026-10-13 15:55 6% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 15:55 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] migration: preserve_ram() leaks original Error after migrate_set_error()
- [Low] migration: cpr-exec fail path leaves RAM read-only after execvp() failure
--
commit 02a023bc5ba37bcad09a7328d397c6b940edac14
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved
This patch makes cpr_exec_cb() treat a failed MADV_DOEXEC on any saved
ramblock as a failed cpr-exec. It jumps to a shared fail: label, which is
also reached when execvp() fails. That label restores close-on-exec on the
fds and clears MADV_DOEXEC on the ramblocks, so the VM keeps running in the
old QEMU.
> diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
> index 7b7609e19cd9..cb962ce72529 100644
> --- a/migration/cpr-exec.c
> +++ b/migration/cpr-exec.c
[ ... ]
> @@ -202,7 +215,10 @@ static void cpr_exec_cb(void *opaque)
> */
> cpr_exec_preserve_fds();
>
> - cpr_walk_ramblock(preserve_ram);
> + if (!cpr_walk_ramblock(preserve_ram)) {
> + /* preserve_ram() has already reported the error and failed migration */
> + goto fail;
> + }
[Severity: Low]
Does this new goto fail path leak the original Error object?
preserve_ram() builds the error like this:
error_setg_errno(&err, errno, "MADV_DOEXEC failed on memory region %s:"
" addr=%p length=%zu", name, (void *) ram.addr,
ram.length);
error_report_err(error_copy(err));
migrate_set_state(&s->state, s->state, MIGRATION_STATUS_FAILED);
migrate_set_error(s, err);
return false;
error_report_err(error_copy(err)) frees only the copy, and
migrate_set_error() stores its own error_copy() without taking ownership.
No error_free(err) is reached before return false, so the original Error
(the struct and its msg string) is never freed.
The execvp() failure path in cpr_exec_cb() does call error_free(err) after
migrate_set_error(). The leak in preserve_ram() predates this patch, but
this patch now relies on that path to handle the failure. Would an
error_free(err) before return false in preserve_ram() be appropriate?
> if (cpr_ram_rdonly) {
> cpr_walk_ramblock(mark_ram_rdonly);
> }
[ ... ]
> @@ -221,15 +237,17 @@ static void cpr_exec_cb(void *opaque)
> * or the system is very short on resources.
> */
> error_setg_errno(&err, errno, "execvp %s failed", argv[0]);
> - g_clear_pointer(&argv, g_strfreev);
> - cpr_exec_unpreserve_fds();
> -
> error_report_err(error_copy(err));
> migrate_set_state(&s->state, s->state, MIGRATION_STATUS_FAILED);
> migrate_set_error(s, err);
> error_free(err);
> err = NULL;
>
> +fail:
> + g_clear_pointer(&argv, g_strfreev);
> + cpr_exec_unpreserve_fds();
> + cpr_exec_unpreserve_ram();
> +
[Severity: Low]
Can the guest be resumed on read-only RAM when execvp() fails?
If cpr_ram_rdonly is set, cpr_exec_cb() calls:
if (cpr_ram_rdonly) {
cpr_walk_ramblock(mark_ram_rdonly);
}
before execvp(). If execvp() then fails, the code reaches fail:, which
clears MADV_DOEXEC and restores close-on-exec, but nothing restores the
RAM protection. cpr_ram_restore_rw() exists:
void cpr_ram_restore_rw(void)
{
cpr_walk_ramblock(mark_ram_rw);
}
but its only caller is system/vl.c, which runs in the new QEMU after a
successful exec.
The code after the label then runs:
if (runstate_is_live(s->vm_old_state)) {
vm_start();
}
With RAM still PROT_READ, can the first guest write, or a device emulation
write into guest memory, raise SIGSEGV and kill the VM? That seems contrary
to the commit message's statement that "the VM keeps running in the old
QEMU".
The preserve_ram() failure path jumps to fail: before mark_ram_rdonly() and
vhost_net_cpr_reset_owners() run, so it is not affected. The execvp()
failure path is affected. cpr_ram_rdonly is documented as a debug-only
option, which limits the exposure. Should the fail: path call
cpr_ram_restore_rw() when cpr_ram_rdonly is set?
Later patches in the series (the one that takes the vhost-net devices back
if exec fails) add a vhost_net_cpr_set_owners() call before the fail:
label. None of the later patches in the series restore RAM to read-write
on this path.
> /* Note, we can go from state COMPLETED to FAILED */
> migration_call_notifiers(MIG_EVENT_PRECOPY_FAILED, NULL);
>
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013-vfio_v1-v2-0-433455c0a294@virtuozzo.com?part=21
^ permalink raw reply [relevance 6%]
* Re: [PATCH QEMU HCI-8.1 v2 22/24] VZ: migration/cpr-exec: take the vhost-net devices back if exec fails
2026-10-13 15:28 4% ` [PATCH QEMU HCI-8.1 v2 22/24] VZ: migration/cpr-exec: take the vhost-net devices back if exec fails Andrey Drobyshev
@ 2026-10-13 15:55 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 15:55 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] vhost-net: cpr_set_owners() retries SET_OWNER on devices whose reset failed
--
commit 5dfb07a07fff9bf62ccc97d74b7cefc2e5892991
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
VZ: migration/cpr-exec: take the vhost-net devices back if exec fails
This patch adds vhost_net_cpr_set_owners(), which issues VHOST_SET_OWNER on
every kernel-backend vhost-net device. cpr_exec_cb() calls it after a failed
execvp(), so the old QEMU takes the devices back that it released with
VHOST_RESET_OWNER before the exec.
> diff --git a/hw/net/vhost_net.c b/hw/net/vhost_net.c
> --- a/hw/net/vhost_net.c
> +++ b/hw/net/vhost_net.c
[ ... ]
> +void vhost_net_cpr_set_owners(void)
> +{
> + NetClientState *nc;
> +
> + QTAILQ_FOREACH(nc, &net_clients, next) {
> + VHostNetState *net = get_vhost_net(nc);
> +
> + if (net && net->dev.vhost_ops &&
> + net->dev.vhost_ops->backend_type == VHOST_BACKEND_TYPE_KERNEL) {
> + int r = vhost_dev_set_owner(&net->dev);
> +
> + if (r < 0 && r != -ENOSYS) {
> + warn_report("cpr: failed to set vhost-net owner for %s: %s",
> + nc->name, strerror(-r));
> + }
> + }
> + }
> +}
[Severity: Low]
Can this issue VHOST_SET_OWNER on a device that was never released?
vhost_net_cpr_reset_owners() only warns when vhost_dev_reset_owner() fails
and does not record which devices were actually reset:
int r = vhost_dev_reset_owner(&net->dev);
if (r < 0 && r != -ENOSYS) {
warn_report("cpr: failed to reset vhost-net owner for %s: %s",
nc->name, strerror(-r));
}
A device whose reset failed is still owned by this mm. After a failed
execvp(), vhost_net_cpr_set_owners() then calls vhost_dev_set_owner() on it
as well. From my recollection of the kernel code, which I have not
verified, VHOST_SET_OWNER returns -EBUSY for a device that already has an
owner. If so, the rollback would log a second, misleading "cpr: failed to
set vhost-net owner" warning for that device. The device would still be
owned by the old QEMU, so I don't expect functional loss.
The vhost-vsock sibling avoids this. In vhost_vsock_pre_save(), owner_reset
is set only after a successful vhost_dev_reset_owner(), and
vhost_vsock_cpr_notifier() restores ownership only when it is set:
if (e->type == MIG_EVENT_PRECOPY_FAILED && vsock->owner_reset) {
ret = vhost_dev_set_owner(&vvc->vhost_dev);
Would it make sense to track per device whether the reset succeeded in
vhost_net_cpr_reset_owners(), and have vhost_net_cpr_set_owners() restore
only those devices?
> diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
> --- a/migration/cpr-exec.c
> +++ b/migration/cpr-exec.c
[ ... ]
> @@ -243,6 +243,9 @@ static void cpr_exec_cb(void *opaque)
> error_free(err);
> err = NULL;
>
> + /* Still the same mm, take the vhost-net devices back */
> + vhost_net_cpr_set_owners();
> +
This is the only rollback call site. It runs after a failed execvp() and
has no per-device information about which devices were released.
> fail:
> g_clear_pointer(&argv, g_strfreev);
> cpr_exec_unpreserve_fds();
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013-vfio_v1-v2-0-433455c0a294@virtuozzo.com?part=22
^ permalink raw reply [relevance 5%]
* Re: [PATCH QEMU HCI-8.1 v2 18/24] vfio/migration: don't migrate device state during CPR
@ 2026-10-13 15:53 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 15:53 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] vfio: migration save/load gated on mode parameter, not CPR in progress
- [Medium] vfio/migration: load_state not gated for CPR, unpatched source still hits live device
- [Medium] vfio/migration: switchover_ack_needed not gated for CPR, hangs migration
- [Medium] vfio/migration: stale multifd_transfer lets save thread stop device during CPR
--
commit 27c407560d6e12fc61d8fd9e47b3508c1eaa3fcb
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
vfio/migration: don't migrate device state during CPR
This patch skips VFIO device state migration in the cpr-transfer and
cpr-exec modes. It adds a vfio_is_active() callback and an early return in
vfio_save_state(), both gated on cpr_incoming_needed(). It also makes the
VM state change handlers return early when cpr_is_in_progress().
> diff --git a/hw/vfio/migration.c b/hw/vfio/migration.c
> --- a/hw/vfio/migration.c
> +++ b/hw/vfio/migration.c
[ ... ]
> @@ -674,6 +675,16 @@ static void vfio_save_state(QEMUFile *f, void *opaque)
> Error *local_err = NULL;
> int ret;
>
> + /*
> + * .is_active doesn't cover the non-iterable stage, so the section is
> + * still sent: leave it empty, or new QEMU would load the config into
> + * the preserved device and re-program its MSI-X.
> + */
> + if (cpr_incoming_needed(NULL)) {
> + qemu_put_be64(f, VFIO_MIG_FLAG_END_OF_STATE);
> + return;
> + }
> +
[Severity: Medium]
cpr_incoming_needed() only tests migrate_mode(), that is, the configured
mode parameter, and not whether a CPR operation is running.
If the mode parameter is left at cpr-transfer or cpr-exec (for example
after an earlier or failed CPR attempt) and a non-CPR save such as
snapshot-save runs, what happens?
save_snapshot()
vm_stop(RUN_STATE_SAVE_VM)
qemu_savevm_state()
vfio_save_state()
cpr_incoming_needed(NULL) is true -> only END_OF_STATE is written
In that case vfio_vmstate_change() is gated on cpr_is_in_progress(),
which requires migration_is_running() on the source, and that is false
for savevm. The device is therefore still moved to STOP, but no state is
written, so a later load restores nothing.
The commit message says the stop/cont hook uses cpr_is_in_progress()
because "the mode alone would also skip a plain stop/cont of a VM which
merely has it set". Does the same reasoning apply to vfio_save_state()
and vfio_is_active()? Should they use the same predicate as the
vmstate handlers?
[ ... ]
> @@ -824,7 +835,19 @@ static int vfio_switchover_start(void *opaque)
> return 0;
> }
>
> +static bool vfio_is_active(void *opaque)
> +{
> + return !cpr_incoming_needed(NULL);
> +}
> +
> static const SaveVMHandlers savevm_vfio_handlers = {
> + .is_active = vfio_is_active,
> .save_prepare = vfio_save_prepare,
> .save_setup = vfio_save_setup,
> .save_cleanup = vfio_save_cleanup,
[Severity: Medium]
This isn't limited to the case above: vfio_is_active() also returns false
on the mode parameter alone, so all VFIO save handlers are disabled
whenever the mode is cpr-transfer or cpr-exec, even with no CPR handover
running.
Separately, is the load side covered? .is_active only suppresses setup,
save and pending-estimate handling, while .load_state is still called on
the destination for any section the source sends.
If the source is a QEMU without this patch, vfio_load_state() would still
see these sections:
vfio_load_state()
case VFIO_MIG_FLAG_DEV_CONFIG_STATE:
return vfio_load_device_config_state(f, opaque);
That would apply the config to the preserved device and re-program MSI-X,
which is the failure the commit message describes. A DATA_STATE section
would reach vfio_load_buffer()->qemu_file_get_to_fd() with data_fd still
-1, because vfio_load_setup() is skipped, and the incoming migration
would fail.
Is a version requirement on the source intended here, and should
vfio_load_state() have its own CPR check?
[Severity: Medium]
Can this hang a CPR migration when switchover-ack is enabled?
vfio_switchover_ack_needed() still returns vfio_precopy_supported() and
is not gated by is_active. On the destination,
qemu_loadvm_state_switchover_ack_needed() does:
if (se->ops->switchover_ack_needed(se->opaque)) {
mis->switchover_ack_pending_num++;
}
without consulting is_active, so the VFIO device is counted as awaiting
an ACK. The fallback ACK on MIG_CMD_OPEN_RETURN_PATH then does not fire.
On the source, vfio_is_active() now returns false, so vfio_save_iterate()
is never called. That is the only place this is emitted:
qemu_put_be64(f, VFIO_MIG_FLAG_DEV_INIT_DATA_SENT);
The destination therefore never calls qemu_loadvm_approve_switchover().
migration_can_switchover() then keeps returning s->switchover_acked
(false) while the VM is running:
if (!runstate_is_running()) {
return true;
}
return s->switchover_acked;
migrate_caps_check() only requires return-path for switchover-ack and
does not reject it in CPR modes. Should vfio_switchover_ack_needed() also
return false in CPR mode, or should the capability be rejected there?
[ ... ]
> @@ -857,6 +880,11 @@ static void vfio_vmstate_change_prepare(void *opaque, bool running,
> Error *local_err = NULL;
> int ret;
>
> + /* The device is preserved, not a plain stop/cont: keep its state */
> + if (cpr_is_in_progress()) {
> + return;
> + }
> +
[ ... ]
> @@ -882,6 +910,10 @@ static void vfio_vmstate_change(void *opaque, bool running, RunState state)
> Error *local_err = NULL;
> int ret;
>
> + if (cpr_is_in_progress()) {
> + return;
> + }
> +
[Severity: Medium]
Can the multifd save thread still move the preserved device to STOP_COPY
during CPR?
qemu_savevm_state_complete_precopy_iterable() spawns
save_complete_precopy_thread for every handler that has one, without
checking .is_active. The thread's only guard is
vfio_multifd_transfer_enabled():
hw/vfio/migration-multifd.c:vfio_multifd_save_complete_precopy_thread() {
if (!vfio_multifd_transfer_enabled(vbasedev)) {
/* Nothing to do, vfio_save_complete_precopy() does the transfer. */
return true;
}
...
if (vfio_migration_set_state(vbasedev, VFIO_DEVICE_STATE_STOP_COPY,
VFIO_DEVICE_STATE_STOP, errp)) {
migration->multifd_transfer is written only in vfio_multifd_setup(), and
vfio_multifd_cleanup() does not reset it. With this patch
vfio_save_setup() is skipped in CPR modes, so the flag is no longer
refreshed.
If an earlier non-CPR migration attempt ran vfio_save_setup() with
multifd transfer enabled and then failed or was cancelled, and a later
cpr-transfer or cpr-exec is run in the same process, would the flag still
be true and the thread stop the preserved device and stream its state?
The same flag also gates vfio_switchover_start() without an is_active
check, though that is a destination-side hook in a fresh process and is
less exposed.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013-vfio_v1-v2-0-433455c0a294@virtuozzo.com?part=18
^ permalink raw reply [relevance 5%]
* Re: [PATCH QEMU HCI-8.1 v2 19/24] vfio/listener: skip dirty page tracking during CPR
@ 2026-10-13 15:50 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 15:50 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] vfio: log_global_start skipped in CPR mode is never retried when mode changes
--
commit 29c28bad6094fa55255321af5424128d0b541ec1
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
vfio/listener: skip dirty page tracking during CPR
This commit makes vfio_listener_log_global_start() return early without
starting VFIO dirty page tracking when the migration mode is cpr-transfer
or cpr-exec. Stopping dirty tracking is left unconditional.
> diff --git a/hw/vfio/listener.c b/hw/vfio/listener.c
> --- a/hw/vfio/listener.c
> +++ b/hw/vfio/listener.c
[ ... ]
> @@ -1012,6 +1013,11 @@ static bool vfio_listener_log_global_start(MemoryListener *listener,
> listener);
> bool ret;
>
> + /* Preserved devices keep DMA-ing into preserved RAM, nothing to track */
> + if (cpr_incoming_needed(NULL)) {
> + return true;
> + }
> +
[Severity: Medium]
Can this early return leave VFIO dirty tracking permanently off for a later
live migration?
The skip is decided from the current migrate_mode(), which is mutable
state, rather than from the reason global dirty logging was requested.
The listener callback runs only on the first transition of the global
dirty flags:
system/memory.c:memory_global_dirty_log_start() {
...
flags &= ~global_dirty_tracking;
if (!flags) {
return true;
}
old_flags = global_dirty_tracking;
global_dirty_tracking |= flags;
...
if (!old_flags) {
if (!memory_global_dirty_log_do_start(errp)) {
...
}
Consider this sequence:
1. While migrate_mode() is cpr-transfer or cpr-exec, some user takes a
global dirty flag. calc-dirty-rate in dirty-bitmap mode does this via
global_dirty_log_change() calling memory_global_dirty_log_start(). A
CPR migration that is still running, or whose stop is postponed, also
qualifies. vfio_listener_log_global_start() returns true and starts
nothing.
2. The mode parameter is changed through migrate-set-parameters, or
incoming_mode is reset by cpr_set_incoming_mode(MIG_MODE_NONE) in
migration cleanup.
3. A normal migration calls
memory_global_dirty_log_start(GLOBAL_DIRTY_MIGRATION). Since the other
flag is still held, old_flags is non-zero, so
memory_global_dirty_log_do_start() is not run again and
vfio_listener_log_global_start() is never called for this migration.
Would VFIO device DMA writes then go unsynced during that live migration,
which could corrupt guest memory on the destination?
The path needs an unusual mode switch while a global dirty flag is held,
but it is reachable through QMP. The series-final state does not change
hw/vfio/listener.c or system/memory.c, so this is not addressed by later
patches.
As a secondary effect, calc-dirty-rate while in a CPR mode silently
excludes VFIO device writes. Would it be better to base the decision on
why logging was requested, or to make the start retriable when the mode
changes?
> if (vfio_container_devices_dirty_tracking_is_supported(bcontainer)) {
> ret = vfio_devices_dma_logging_start(bcontainer, errp);
> } else {
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013-vfio_v1-v2-0-433455c0a294@virtuozzo.com?part=19
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 v2 14/24] migration: Fix crash on second migration when cancel early
2026-10-13 15:28 23% ` [PATCH QEMU HCI-8.1 v2 14/24] migration: Fix crash on second migration when cancel early Andrey Drobyshev
@ 2026-10-13 15:45 6% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 15:45 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] migration: cancel in CPR HUP wait fires FAILED without SETUP, tripping notify assert
- [Medium] migration: migration_cancel() asserts on live migrate_mode() while hup_source is set
--
commit e9dea0ff5e4d16a3a4c776bef4c26e24060a56d5
Author: Peter Xu <peterx@redhat.com>
migration: Fix crash on second migration when cancel early
This patch changes migration_cancel() so that, when a cpr-transfer
migration is waiting for the HUP event on the CPR channel, it calls
migration_cleanup() instead of directly setting the CANCELLED state. It
also detaches hup_source at the start of qmp_migrate_finish_cb() so the
wait window can be detected, and adds several assertions.
Link: https://lore.kernel.org/r/20260417184742.293061-1-marcandre.lureau@redhat.com
Link: https://lore.kernel.org/r/20260421175820.302795-1-peterx@redhat.com
> diff --git a/migration/migration.c b/migration/migration.c
> index 14ff16fa6a4e7..2e4be2db65ddd 100644
> --- a/migration/migration.c
> +++ b/migration/migration.c
[ ... ]
> @@ -1653,14 +1653,19 @@ void migration_cancel(void)
> }
>
> /*
> - * If qmp_migrate_finish has not been called, then there is no path that
> - * will complete the cancellation. Do it now.
> + * This is cpr-transfer specific processing.
> + *
> + * If this is true, it means cpr-transfer migration is waiting for the
> + * destination to send HUP event on CPR channel to continue the next
> + * phase. If so, do the cleanup proactively to avoid get stuck in
> + * CANCELLING state.
> */
> - if (setup && !s->to_dst_file) {
> - migrate_set_state(&s->state, MIGRATION_STATUS_CANCELLING,
> - MIGRATION_STATUS_CANCELLED);
> - cpr_state_close();
> - cpr_transfer_source_destroy(s);
> + if (cpr_transfer_source_active(s)) {
> + assert(migrate_mode() == MIG_MODE_CPR_TRANSFER);
[Severity: Medium]
Can this assert abort QEMU? migrate_mode() returns the live value of
migrate_get_current()->parameters.mode, since cpr_get_incoming_mode() is
NONE on the source.
Consider this sequence:
qmp_migrate() with mode=cpr-transfer
cpr_transfer_add_hup_watch() /* sets s->hup_source, state is SETUP */
migrate-set-parameters mode=normal
qmp_migrate_set_parameters()->migrate_params_apply()
s->parameters.mode = params->mode;
migrate-cancel
migration_cancel()
cpr_transfer_source_active(s) is true
assert(migrate_mode() == MIG_MODE_CPR_TRANSFER) fails
As far as I can tell, none of migrate_params_test_apply(),
migrate_params_check() or migrate_params_apply() reject a mode change
while a migration is running.
The same assert can be reached at exit, because migration_shutdown()
calls migration_cancel().
Since hup_source being set already implies that cpr-transfer was chosen
when the migration started, is this assertion on the mutable global mode
needed? Before this patch, migration_cancel() had no such assertion.
The second assertion, assert(setup && !s->to_dst_file), did not appear
to fire in the cases I checked.
> + assert(setup && !s->to_dst_file);
> + migration_cleanup(s);
[Severity: High]
Can this path abort QEMU when a VFIO PCI device is present?
migration_cleanup() ends with:
migration_call_notifiers(MIG_EVENT_PRECOPY_FAILED, NULL);
but in the HUP-wait window migration_connect() has not run yet, so
MIG_EVENT_PRECOPY_SETUP was never delivered:
qmp_migrate()
cpr_transfer_add_hup_watch() /* no SETUP notification yet */
migrate_cancel
migration_cancel()
migration_cleanup()
migration_call_notifiers(MIG_EVENT_PRECOPY_FAILED, NULL)
vfio_cpr_pci_notifier()
vfio_cpr_set_msi_virq(vdev, errp, true)
The FAILED handler in hw/vfio/cpr.c re-adds irqfds that only the SETUP
handler removes:
if (e->type == MIG_EVENT_PRECOPY_SETUP) {
return vfio_cpr_set_msi_virq(vdev, errp, false);
} else if (e->type == MIG_EVENT_PRECOPY_FAILED) {
return vfio_cpr_set_msi_virq(vdev, errp, true);
}
With SETUP skipped, the irqfds are still assigned, so a second
KVM_IRQFD assign may fail. I did not find this in the repository. It
is from my recollection that KVM rejects a duplicate eventfd with
-EBUSY.
If so, the notifier returns nonzero and migration_call_notifiers() hits:
ret = notifier->notify(notifier, &e, errp);
if (ret) {
assert(type == MIG_EVENT_PRECOPY_SETUP);
return ret;
}
The type is FAILED, so the assert fails.
Can the cancel path avoid sending FAILED to notifiers when SETUP was
never sent? Other notifiers (virtio-net failover, scsi-block,
vfio-migration) seem to tolerate an unpaired FAILED and do not return
errors.
The same unpaired FAILED already exists in the migration_connect()
error_in path. This patch adds a new route to it that a user can
trigger easily.
The later patches in the series do not appear to address this. The
series later removes the VFIO CPR block in migrate_cpr_is_blocked(),
which makes the VFIO plus cpr-transfer configuration reachable.
> + /* Now all things should have been released */
> + assert(!cpr_transfer_source_active(s));
> }
> }
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013-vfio_v1-v2-0-433455c0a294@virtuozzo.com?part=14
^ permalink raw reply [relevance 6%]
* Re: [PATCH QEMU HCI-8.1 v2 15/24] kvm: tolerate ioctls after kvm_close()
2026-10-13 15:28 23% ` [PATCH QEMU HCI-8.1 v2 15/24] kvm: tolerate ioctls after kvm_close() Andrey Drobyshev
@ 2026-10-13 15:44 7% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 15:44 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] kvm: kvm_set_irq() and listeners still deref NULL kvm_state after kvm_close()
- [Medium] kvm: -ENODEV from closed-state guards still aborts in fail-fast callers; stale errno in ioeventfd
- [Low] kvm: closed-state sentinel reuses -ENODEV, which kvm_send_msi() cannot tell from a kernel -ENODEV
--
commit 32c3566efef848fa0bd62b2fabb9b95d82114443
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
kvm: tolerate ioctls after kvm_close()
This patch makes kvm_ioctl(), kvm_vm_ioctl() and kvm_vcpu_ioctl() return
-ENODEV once kvm_close() has run. kvm_send_msi() no longer warns on
-ENODEV. The aim is to stop a retired cpr-transfer source from crashing
on late interrupts.
> diff --git a/accel/kvm/kvm-all.c b/accel/kvm/kvm-all.c
> --- a/accel/kvm/kvm-all.c
> +++ b/accel/kvm/kvm-all.c
[ ... ]
> @@ -3389,6 +3394,11 @@ int kvm_vm_ioctl(KVMState *s, unsigned long type, ...)
> arg = va_arg(ap, void *);
> va_end(ap);
>
> + /* kvm_close() has been called: there is no VM to talk to any more */
> + if (!s || s->vmfd == -1) {
> + return -ENODEV;
> + }
> +
[Severity: Medium]
Can callers that treat any negative return as fatal still bring down the
retired source? After kvm_close() the memory listeners are still
registered and kvm_enabled() is still true, so these callers now receive
-ENODEV:
kvm_mem_ioeventfd_del()
if (r < 0) {
fprintf(stderr, "%s: error deleting ioeventfd: %s (%d)\n",
__func__, strerror(-r), -r);
abort();
}
kvm_set_irq()
ret = kvm_vm_ioctl(s, s->irq_set_ioctl, &event);
if (ret < 0) {
perror("kvm_set_irq");
abort();
}
kvm_irqchip_commit_routes()
ret = kvm_vm_ioctl(s, KVM_SET_GSI_ROUTING, s->irq_routes);
assert(ret == 0);
kvm_update_clock() in hw/i386/kvm/clock.c
ret = kvm_vm_ioctl(kvm_state, KVM_GET_CLOCK, &data);
if (ret < 0) {
fprintf(stderr, "KVM_GET_CLOCK failed: %s\n", strerror(-ret));
abort();
The same applies to kvm_mem_ioeventfd_add(), the PIO ioeventfd variants and
kvm_log_clear().
Would a late legacy IRQ, a memory region update, a routing commit or a
clock query on the retired source still end in abort() or assert()? The
commit message says these cases now "fail gracefully".
Also, the new early return skips ioctl(), so errno is not updated. In
kvm_set_ioeventfd_mmio():
ret = kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &iofd);
if (ret < 0) {
return -errno;
}
Doesn't this return a stale errno, which may be 0 or an unrelated value,
when kvm_vm_ioctl() returns -ENODEV from the new guard?
[Severity: Medium]
This isn't fully fixed for legacy interrupt callers either. The guard
helps only if the call reaches kvm_vm_ioctl(), but kvm_set_irq()
evaluates s->irq_set_ioctl as an argument before the call:
kvm_set_irq(s, irq, level)
ret = kvm_vm_ioctl(s, s->irq_set_ioctl, &event);
With s == NULL this is a NULL dereference before the !s check can run. It
is reached with kvm_state == NULL from:
hw/i386/kvm/ioapic.c:kvm_ioapic_set_irq()
delivered = kvm_set_irq(kvm_state, s->kvm_gsi_base + irq, level);
hw/i386/kvm/i8259.c:kvm_pic_set_irq()
delivered = kvm_set_irq(kvm_state, irq, level);
The final return in kvm_set_irq() also reads s->irq_set_ioctl. Memory
listener callbacks such as kvm_coalesce_pio_del() also use kvm_state
directly:
KVMState *s = kvm_state;
if (s->coalesced_pio) {
kvm_close() sets kvm_state = NULL but does not unregister the memory
listeners or the irqchip users.
Can an emulated INTx device raising a legacy interrupt, or a memory
region removed during qemu_cleanup(), still hit the SIGSEGV this patch
is meant to remove?
The later patches in the series change only coalesced-MMIO ring handling
in kvm_close() and the CPR-exec and VFIO code. They do not address these
callers.
[Severity: Low]
Is -ENODEV distinct enough to mean "closed"? The check in kvm_vm_ioctl()
tests s->vmfd == -1, and kvm_accel_instance_init() also sets:
s->fd = -1;
s->vmfd = -1;
so callers that run before the VM is opened now get -ENODEV instead of
-EBADF. I did not find any caller in accel/kvm, target/i386 or hw/ that
depends on -EBADF.
[ ... ]
> diff --git a/hw/i386/kvm/apic.c b/hw/i386/kvm/apic.c
> --- a/hw/i386/kvm/apic.c
> +++ b/hw/i386/kvm/apic.c
> @@ -196,7 +196,11 @@ static void kvm_send_msi(MSIMessage *msg)
> msg->address = kvm_swizzle_msi_ext_dest_id(msg->address);
>
> ret = kvm_irqchip_send_msi(kvm_state, *msg);
> - if (ret < 0) {
> + /*
> + * -ENODEV means kvm_close() has already run (the VM was handed over
> + * via CPR); the interrupt is meaningless here and not worth a warning.
> + */
> + if (ret < 0 && ret != -ENODEV) {
[Severity: Low]
This isn't a bug I could confirm, but the check suppresses the "MSI lost"
warning for any -ENODEV, not only the post-kvm_close() case. Since the
same value is also an errno the kernel could return from KVM_SIGNAL_MSI,
the two cases cannot be told apart here. I could not confirm that the
kernel returns -ENODEV for that ioctl.
Would testing kvm_state == NULL in kvm_send_msi() before the call be a
more precise way to detect the closed state?
> fprintf(stderr, "KVM: injection failed, MSI lost (%s)\n",
> strerror(-ret));
> }
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013-vfio_v1-v2-0-433455c0a294@virtuozzo.com?part=15
^ permalink raw reply [relevance 7%]
* Re: [PATCH QEMU HCI-8.1 v2 11/24] migration: yank: Move register instance earlier
2026-10-13 15:28 21% ` [PATCH QEMU HCI-8.1 v2 11/24] migration: yank: Move register instance earlier Andrey Drobyshev
@ 2026-10-13 15:41 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 15:41 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] migration: yank: migrate_prepare() aborts QEMU on duplicate yank instance
--
commit d6ce8a94f0289aba78580f9f74c0003c88eebd49
Author: Fabiano Rosas <farosas@suse.de>
migration: yank: Move register instance earlier
This patch moves yank_register_instance() from qmp_migrate_finish() into
migrate_prepare(), so that it pairs with the unregister in
migration_cleanup(). The resume case is now detected from the migration
state instead of a resume_requested argument.
Link: https://lore.kernel.org/qemu-devel/20260123141656.6765-11-farosas@suse.de
> diff --git a/migration/migration.c b/migration/migration.c
> index 2f8ef20c0369..fb83e8bc695d 100644
> --- a/migration/migration.c
> +++ b/migration/migration.c
> @@ -2235,11 +2235,12 @@ static bool migrate_prepare(MigrationState *s, bool resume, Error **errp)
> return false;
> }
>
> + yank_register_instance(MIGRATION_YANK_INSTANCE, &error_abort);
> +
> return true;
> }
[Severity: Medium]
Can this abort QEMU when a yank instance is already registered?
Before this patch, a duplicate registration was reported through errp
from qmp_migrate_finish() as "duplicate yank instance". Now
migrate_prepare() passes &error_abort, so the duplicate check in
yank_register_instance() ends in abort():
util/yank.c:yank_register_instance() {
if (yank_find_entry(instance)) {
error_setg(errp, "duplicate yank instance");
return false;
}
}
The incoming side registers the same global MIGRATION_YANK_INSTANCE in
qmp_migrate_incoming(), and it keeps it until
migration_incoming_state_destroy():
migration/migration.c:migration_incoming_state_destroy() {
...
cpr_set_incoming_mode(MIG_MODE_NONE);
yank_unregister_instance(MIGRATION_YANK_INSTANCE);
}
For postcopy, that destroy happens at the end of postcopy from the
listen thread. If the incoming side is in POSTCOPY_PAUSED, it can wait
indefinitely. By then the guest is running, so the runstate is no
longer RUN_STATE_INMIGRATE.
The guards earlier in migrate_prepare() are migration_is_running(), which
only looks at the outgoing current_migration, RUN_STATE_INMIGRATE, and
RUN_STATE_POSTMIGRATE. None of them looks at incoming state.
A QMP migrate issued in that window would pass all of them, run
migrate_init(), and then hit yank_register_instance() with &error_abort.
This needs a nested or chained migration setup and a privileged QMP
command, so it should be rare. The upstream commit 7e714392437c has the
same &error_abort. I did not find a guard against the overlap, and
nothing later in this series adds one. The final series state still has
&error_abort in migrate_prepare(), while qmp_migrate_incoming() and
load_snapshot() still pass errp.
Could the registration here report the failure through errp instead, with
migrate_prepare() returning false? Alternatively, could the incoming
state be checked before registering?
The commit message also does not mention that a duplicate registration
changes from a recoverable error into an abort.
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013-vfio_v1-v2-0-433455c0a294@virtuozzo.com?part=11
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 v2 10/24] migration: vmstate_save_state_v: fix double error_setg
2026-10-13 15:28 11% ` [PATCH QEMU HCI-8.1 v2 10/24] migration: vmstate_save_state_v: fix double error_setg Andrey Drobyshev
@ 2026-10-13 15:38 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 15:38 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] migration: vmstate_save_state_v() sets Error only for ret<0 but prepends for any ret
--
commit 8108694e711b23e4f49933352e133e7c3dec993c
Author: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
migration: vmstate_save_state_v: fix double error_setg
This commit avoids calling error_setg() twice on the same errp when a nested
vmstate_save_state() or vmstate_save_state_v() call fails. It sets the Error
only for leaf put() failures, and it changes the field-level error_setg()
into error_prepend().
Link: https://lore.kernel.org/qemu-devel/20260304212303.667141-2-vsementsov@yandex-team.ru
> diff --git a/migration/vmstate.c b/migration/vmstate.c
> --- a/migration/vmstate.c
> +++ b/migration/vmstate.c
[ ... ]
> @@ -523,6 +523,9 @@ int vmstate_save_state_v(QEMUFile *f, const VMStateDescription *vmsd,
> } else {
> ret = inner_field->info->put(f, curr_elem, size,
> inner_field, vmdesc_loop);
> + if (ret < 0) {
> + error_setg(errp, "put failed");
> + }
> }
[Severity: Medium]
Can a positive return from put() leave *errp unset here?
The new error_setg() runs only for ret < 0, but the failure check further
down in vmstate_save_state_v() is a plain "if (ret)", which also runs for
positive values.
In-tree, put_power() in target/arm/machine.c (vmstate_powered_off) returns 1
when cpu->power_state is neither PSCI_ON nor PSCI_OFF:
} else {
return 1;
}
With that return value, no Error is created at this point.
> @@ -535,8 +538,8 @@ int vmstate_save_state_v(QEMUFile *f, const VMStateDescription *vmsd,
> }
>
> if (ret) {
> - error_setg(errp, "Save of field %s/%s failed",
> - vmsd->name, field->name);
> + error_prepend(errp, "Save of field %s/%s failed: ",
> + vmsd->name, field->name);
Is it safe to call error_prepend() on this path when *errp is NULL?
error_vprepend() in util/error.c checks only for a NULL errp, then reads the
message:
if (!errp) {
return;
}
...
g_string_append(newmsg, (*errp)->msg);
If a caller passed a non-NULL errp, such as a local Error pointer or
&error_fatal, and put_power() returned 1, would this dereference a NULL
*errp and crash QEMU during migration save?
Before this patch, error_setg() ran for every non-zero ret, so this case was
handled. Checking the series up to its final commit, I found no change to
migration/vmstate.c or migration/vmstate-types.c that addresses it.
Would it work to make the leaf-put condition "if (ret)" instead of
"if (ret < 0)", or otherwise ensure an Error is set before error_prepend()
runs?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013-vfio_v1-v2-0-433455c0a294@virtuozzo.com?part=10
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 v2 09/24] migration: Fix regression of passing error_fatal into vmstate_load_state()
2026-10-13 15:28 12% ` [PATCH QEMU HCI-8.1 v2 09/24] migration: Fix regression of passing error_fatal into vmstate_load_state() Andrey Drobyshev
@ 2026-10-13 15:36 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 15:36 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] migration: vmstate save/load callers only report Error when ret < 0
--
Subject: Re: [PATCH] migration: Fix regression of passing error_fatal into vmstate_load_state()
commit 7359b43733a10dec8030b1d9e924f7eb62045b03
Author: Arun Menon <armenon@redhat.com>
migration: Fix regression of passing error_fatal into vmstate_load_state()
This patch replaces &error_fatal with a local Error object in the callers
of vmstate_save_state() and vmstate_load_state(). It reports the error with
error_report_err() when the call fails, and returns the result where the
caller can use it.
Link: https://lore.kernel.org/r/20251028-solve_error_fatal_regression-v2-1-dab24c808a28@redhat.com
> diff --git a/hw/pci/pci.c b/hw/pci/pci.c
> --- a/hw/pci/pci.c
> +++ b/hw/pci/pci.c
[ ... ]
> void pci_device_save(PCIDevice *s, QEMUFile *f)
> {
> + Error *local_err = NULL;
> + int ret;
> +
> /* Clear interrupt status bit: it is implicit
> * in irq_state which we are saving.
> * This makes us compatible with old devices
> * which never set or clear this bit. */
> s->config[PCI_STATUS] &= ~PCI_STATUS_INTERRUPT;
> - vmstate_save_state(f, &vmstate_pci_device, s, NULL, &error_fatal);
> + ret = vmstate_save_state(f, &vmstate_pci_device, s, NULL, &local_err);
> + if (ret < 0) {
> + error_report_err(local_err);
> + }
[Severity: Low]
This is a speculative concern, but the same pattern appears in all the new
callers, so I am raising it once here.
vmstate_save_state_v() treats any nonzero return as a failure, for example
in the pre_save, post_save and put paths:
migration/vmstate.c:vmstate_save_state_v() {
...
if (ret) {
error_prepend(errp, "Save of field %s/%s failed: ", ...
...
return ret;
}
...
}
The new callers only report the Error when ret < 0. These callers are
pci_device_save(), virtio_save() (final subsection save), virtio_gpu_save(),
virtio_ccw_save_config(), vscsi_save_request(),
virtio_mmio_save_extra_state() and virtio_pci_save_extra_state().
If a callback ever returned a positive nonzero value, would local_err be
allocated but neither reported nor freed? The void wrappers would also
silently ignore the failure, and virtio_save() and virtio_gpu_save()
would return the positive value unchanged. Before this patch, error_fatal
would have exited on any nonzero return.
The load-side callers have the same exposure through pre_load() (returned
as-is) and post_load().
[ ... ]
> diff --git a/hw/virtio/virtio.c b/hw/virtio/virtio.c
> --- a/hw/virtio/virtio.c
> +++ b/hw/virtio/virtio.c
[ ... ]
> if (vdc->vmsd) {
> - int ret = vmstate_save_state(f, vdc->vmsd, vdev, NULL, &local_err);
> + ret = vmstate_save_state(f, vdc->vmsd, vdev, NULL, &local_err);
> if (ret) {
> error_report_err(local_err);
> return ret;
> @@ -3069,7 +3069,11 @@ int virtio_save(VirtIODevice *vdev, QEMUFile *f)
> }
>
> /* Subsections */
> - return vmstate_save_state(f, &vmstate_virtio, vdev, NULL, &error_fatal);
> + ret = vmstate_save_state(f, &vmstate_virtio, vdev, NULL, &local_err);
> + if (ret < 0) {
> + error_report_err(local_err);
> + }
> + return ret;
> }
[Severity: Low]
Within virtio_save(), the vdc->vmsd save a few lines earlier checks
"if (ret)", while this final subsection save checks "if (ret < 0)". Should
the two checks be consistent?
With "ret < 0", a positive return from vmstate_save_state() leaves
local_err unreported and unfreed, and virtio_save() returns the positive
value.
The later series commit "migration: vmstate_save_state_v: fix double
error_setg" does not appear to address this. It sets an Error in the put
branch only when ret < 0, but the following "if (ret)" block still calls
error_prepend(errp, ...). Could a positive return from a put callback then
reach error_prepend() with *errp still NULL when a caller passes
&local_err, as these callers now do? The callers are unchanged at the end of
that series.
In practice callbacks conventionally return negative errnos, so this is
likely a cold path.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013-vfio_v1-v2-0-433455c0a294@virtuozzo.com?part=9
^ permalink raw reply [relevance 5%]
* Re: [PATCH QEMU HCI-8.1 v2 07/24] q35: Fix migration of SMRAM state
2026-10-13 15:28 4% ` [PATCH QEMU HCI-8.1 v2 07/24] q35: Fix migration of SMRAM state Andrey Drobyshev
@ 2026-10-13 15:35 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 15:35 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] q35: mch_update_smbase_smram() skips region disable on IN_RAM post_load
- [Low] q35: mch_update_smbase_smram() does not restore wmask for loaded 0x00 SMBASE
--
commit fa09770dda8724554b3e3a42f4619ca6364af58c
Author: Igor Mammedov <imammedo@redhat.com>
q35: Fix migration of SMRAM state
This change drops the check in mch_update_smbase_smram() that cleared the
SMBASE register while wmask was 0xff. It resyncs wmask from the register
value instead, so a locked SMRAM state is preserved across migration. It
also removes the test hunk that wrote junk to the register before
negotiation.
Link: https://patchew.org/QEMU/20251203180851.6390-1-arbn@yandex-team.com/
> diff --git a/hw/pci-host/q35.c b/hw/pci-host/q35.c
> --- a/hw/pci-host/q35.c
> +++ b/hw/pci-host/q35.c
[ ... ]
> @@ -432,30 +432,27 @@ static void mch_update_smbase_smram(MCHPCIState *mch)
> }
>
> if (*reg == MCH_HOST_BRIDGE_F_SMBASE_QUERY) {
> - pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] =
> - MCH_HOST_BRIDGE_F_SMBASE_LCK;
> + pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = MCH_HOST_BRIDGE_F_SMBASE_LCK;
> *reg = MCH_HOST_BRIDGE_F_SMBASE_IN_RAM;
> return;
> }
>
> /*
> - * default/reset state, discard written value
> - * which will disable SMRAM balackhole at SMBASE
> + * reg value can come from register write/reset/migration source,
> + * update wmask to be in sync with it regardless of source
> */
> - if (pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] == 0xff) {
> - *reg = 0x00;
> + if (*reg == MCH_HOST_BRIDGE_F_SMBASE_IN_RAM) {
> + pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = MCH_HOST_BRIDGE_F_SMBASE_LCK;
> + return;
> }
[Severity: Medium]
Can this early return leave the SMBASE regions enabled? Before this patch,
a register value of 0x01 fell through to lck = false, which disabled both
smbase_blackhole and smbase_window. Now only wmask is updated, and
memory_region_set_enabled(false) is never called for 0x01.
Consider mch_post_load() -> mch_update() -> mch_update_smbase_smram() on a
live MCHPCIState that is currently locked (regions enabled) and has not been
through mch_reset(). If the incoming SMBASE value is 0x01, the regions stay
enabled while config[SMBASE] reads 0x01 (unlocked). The guest then sees a
black-holed SMBASE although the register says the range is unlocked. This
contradicts the comment "update wmask to be in sync with it regardless of
source".
The snapshot-load path calls qemu_system_reset() and a fresh migration
destination has the regions disabled, so neither is affected. The path that
looks reachable is COLO, where colo_process_incoming_thread() calls
qemu_loadvm_state_main() repeatedly on a running secondary VM without
qemu_system_reset(). If the primary locks SMRAM, reboots, and negotiates
0x01 within one checkpoint interval, the secondary loads 0x01 while its
regions are still enabled from the earlier lock. This needs COLO and a
machine with has_smram_at_smbase.
The final series state does not change hw/pci-host/q35.c, so this is not
addressed later in the series. Should the IN_RAM branch also disable
smbase_blackhole and smbase_window before returning?
> if (*reg & MCH_HOST_BRIDGE_F_SMBASE_LCK) {
> - /* disable all writes */
> - pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] &=
> - ~MCH_HOST_BRIDGE_F_SMBASE_LCK;
> + /* lock register at 0x2 and disable all writes */
> + pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = 0;
> *reg = MCH_HOST_BRIDGE_F_SMBASE_LCK;
> - lck = true;
> - } else {
> - lck = false;
> }
> +
> + lck = *reg & MCH_HOST_BRIDGE_F_SMBASE_LCK;
> + memory_region_transaction_begin();
> memory_region_set_enabled(&mch->smbase_blackhole, lck);
> memory_region_set_enabled(&mch->smbase_window, lck);
> memory_region_transaction_commit();
[Severity: Low]
Does this code restore wmask when a loaded SMBASE value is 0x00 or a junk
value without bit 1 set? wmask is resynced only for QUERY, IN_RAM and
values with the LCK bit. For other values wmask is left unchanged.
mch_reset() is the only place that sets wmask back to 0xff:
d->config[MCH_HOST_BRIDGE_F_SMBASE] = 0;
d->wmask[MCH_HOST_BRIDGE_F_SMBASE] = 0xff;
On the COLO secondary, colo_incoming_process_checkpoint() calls
qemu_load_device_state() -> qemu_loadvm_state_main() -> mch_post_load() ->
mch_update() -> mch_update_smbase_smram() without a device reset. If the
secondary's MCH is locked (wmask 0) and a later checkpoint carries
SMBASE=0x00, wmask stays 0. The register is then read-only and the guest can
no longer negotiate or lock SMRAM at SMBASE. The primary could carry 0x00
after a guest reset, before firmware negotiation.
This only shows up if the secondary takes over after such a checkpoint
without a guest reset, and the final series state does not change this file.
Is the comment "update wmask to be in sync with it regardless of source"
fully accurate here, and should wmask be set to 0xff for values that do not
match the QUERY, IN_RAM or LCK cases?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013-vfio_v1-v2-0-433455c0a294@virtuozzo.com?part=7
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 v2 01/24] migration: Plug memory leaks after migrate_set_error()
2026-10-13 15:28 13% ` [PATCH QEMU HCI-8.1 v2 01/24] migration: Plug memory leaks after migrate_set_error() Andrey Drobyshev
@ 2026-10-13 15:30 0% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 15:30 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev
> From: Markus Armbruster <armbru@redhat.com>
>
> migrate_set_error(s, err) stores a copy of @err in @s. The original
> @err is not freed. Most callers free it immediately. Some callers
> free it later, or pass it on. And some leak it. Fix those.
>
> Perhaps migrate_set_error(s, err) should take ownership of @err. The
> callers that free it immediately would become simpler, and avoid a
> copy and a deallocation. The others would have to pass
> error_copy(err).
>
> andrey.drobyshev:
> This commit is cherry-picked as-is, with no modifications, from the
> upstream commit 93817ec39656.
>
> Signed-off-by: Markus Armbruster <armbru@redhat.com>
> Link: https://lore.kernel.org/r/20251115083500.2753895-2-armbru@redhat.com
> Signed-off-by: Peter Xu <peterx@redhat.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013-vfio_v1-v2-0-433455c0a294@virtuozzo.com?part=1
^ permalink raw reply [relevance 0%]
* [PATCH QEMU HCI-8.1 v2 23/24] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860
` (13 preceding siblings ...)
2026-10-13 15:28 4% ` [PATCH QEMU HCI-8.1 v2 22/24] VZ: migration/cpr-exec: take the vhost-net devices back if exec fails Andrey Drobyshev
@ 2026-10-13 15:28 8% ` Andrey Drobyshev
2026-10-13 16:00 5% ` no-reply
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-13 15:28 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
After cpr-exec, new QEMU mmaps the device BARs again on the preserved
device fd. Some drivers don't allow that: the NVIDIA vGPU VFIO driver
refuses to map a VF BAR that is still recorded as mapped by old QEMU,
and that record is dropped only when the device fd is closed. So BAR1
and BAR3 of the vGPU can't be mapped, and the guest loses the device.
Keep the mappings instead. Save each BAR mapping in the CPR ramblock
list, so cpr_exec_cb() marks it with MADV_DOEXEC together with guest RAM,
and new QEMU finds it there and reuses it instead of calling mmap().
Same find-or-create pattern as for other preserved objects, e.g. vector
eventfds in vfio_notifier_init().
The ID is what cpr_name() would give to the mmap subregion:
"<device path>/<region name> mmaps[<index>]".
The saved addresses only make sense for cpr-exec, which keeps the
address space. A cpr-transfer target is another process and would have
to map the regions anew, which the drivers above refuse while the old
mappings exist. So block cpr-transfer for devices with mmapped regions.
New QEMU checks the saved mappings like ram_block_add() checks RAM
blocks: on cpr-exec an entry must exist, otherwise an entry is a bug.
This needs a kernel which supports MADV_DOEXEC on shared PFN mappings of
device memory. Without it cpr-exec of a VM with VFIO devices fails.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/vfio/device.c | 1 +
hw/vfio/region.c | 72 ++++++++++++++++++++++++++++++++++++++++++++++
include/hw/vfio/vfio-cpr.h | 1 +
3 files changed, 74 insertions(+)
diff --git a/hw/vfio/device.c b/hw/vfio/device.c
index 5a14a7d5c0a..88b9e8752cd 100644
--- a/hw/vfio/device.c
+++ b/hw/vfio/device.c
@@ -514,6 +514,7 @@ void vfio_device_unprepare(VFIODevice *vbasedev)
g_clear_pointer(&vbasedev->reginfo, g_free);
g_clear_pointer(&vbasedev->region_fds, g_free);
+ migrate_del_blocker(&vbasedev->cpr.mmap_blocker);
QLIST_REMOVE(vbasedev, container_next);
QLIST_REMOVE(vbasedev, global_next);
diff --git a/hw/vfio/region.c b/hw/vfio/region.c
index 1deaef12fd0..fce09a0d17b 100644
--- a/hw/vfio/region.c
+++ b/hw/vfio/region.c
@@ -30,6 +30,8 @@
#include "qemu/units.h"
#include "monitor/monitor.h"
#include "system/ramblock.h"
+#include "migration/blocker.h"
+#include "migration/cpr.h"
#include "vfio-helpers.h"
/*
@@ -227,8 +229,51 @@ int vfio_region_setup(Object *obj, VFIODevice *vbasedev, VFIORegion *region,
return 0;
}
+static char *vfio_region_cpr_name(VFIORegion *region, int index)
+{
+ g_autofree char *path = qdev_get_dev_path(region->vbasedev->dev);
+ return g_strdup_printf("%s%s%s mmaps[%d]", path ?: "", path ? "/" : "",
+ memory_region_name(region->mem), index);
+}
+
+/* Same checks as for RAM blocks in ram_block_add() */
+static void *vfio_region_cpr_find(VFIORegion *region, int index)
+{
+ g_autofree char *name = vfio_region_cpr_name(region, index);
+ cpr_ramblock_t ram = cpr_find_ramblock(name);
+
+ if (cpr_exec_is_incoming()) {
+ if (!ram.addr || !ram.length) {
+ error_report("CPR mode=exec is requested, but mapping %s can't "
+ "be reused: addr=%p length=%zu. Aborting",
+ name, (void *)ram.addr, ram.length);
+ exit(1);
+ }
+ assert(ram.length == region->mmaps[index].size);
+ } else if (ram.addr || ram.length) {
+ error_report("CPR mode=exec wasn't requested, but mapping %s was "
+ "saved: addr=%p length=%zu. Aborting",
+ name, (void *)ram.addr, ram.length);
+ exit(1);
+ }
+
+ return (void *)(uintptr_t)ram.addr;
+}
+
+static void vfio_region_cpr_save(VFIORegion *region, int index)
+{
+ g_autofree char *name = vfio_region_cpr_name(region, index);
+ void *addr = region->mmaps[index].mmap;
+ size_t size = region->mmaps[index].size;
+
+ cpr_save_ramblock(name, (cpr_ramblock_t) { (uintptr_t)addr, size });
+}
+
static void vfio_subregion_unmap(VFIORegion *region, int index)
{
+ g_autofree char *cpr_name = vfio_region_cpr_name(region, index);
+
+ cpr_delete_ramblock(cpr_name);
trace_vfio_region_unmap(memory_region_name(®ion->mmaps[index].mem),
region->mmaps[index].offset,
region->mmaps[index].offset +
@@ -307,6 +352,19 @@ int vfio_region_mmap(VFIORegion *region)
return 0;
}
+ /*
+ * Only cpr-exec keeps the address space, a cpr-transfer target would
+ * have to map the regions anew, which some drivers (NVIDIA vGPU) refuse
+ * while the old mappings exist. Not supported.
+ */
+ if (!region->vbasedev->cpr.mmap_blocker) {
+ error_setg(®ion->vbasedev->cpr.mmap_blocker,
+ "cpr-transfer is not supported with mmapped VFIO regions "
+ "(%s); use the cpr-exec mode", region->vbasedev->name);
+ migrate_add_blocker_modes(®ion->vbasedev->cpr.mmap_blocker,
+ &error_abort, MIG_MODE_CPR_TRANSFER, -1);
+ }
+
prot |= region->flags & VFIO_REGION_INFO_FLAG_READ ? PROT_READ : 0;
prot |= region->flags & VFIO_REGION_INFO_FLAG_WRITE ? PROT_WRITE : 0;
@@ -314,6 +372,11 @@ int vfio_region_mmap(VFIORegion *region)
size_t align = MIN(1ULL << ctz64(region->mmaps[i].size), 1 * GiB);
void *map_base, *map_align;
+ region->mmaps[i].mmap = vfio_region_cpr_find(region, i);
+ if (region->mmaps[i].mmap) {
+ goto mapped;
+ }
+
/*
* Align the mmap for more efficient mapping in the kernel. Ideally
* we'd know the PMD and PUD mapping sizes to use as discrete alignment
@@ -347,7 +410,9 @@ int vfio_region_mmap(VFIORegion *region)
ret = -errno;
goto no_mmap;
}
+ vfio_region_cpr_save(region, i);
+mapped:
name = g_strdup_printf("%s mmaps[%d]",
memory_region_name(region->mem), i);
memory_region_init_ram_device_ptr(®ion->mmaps[i].mem,
@@ -410,6 +475,13 @@ void vfio_region_exit(VFIORegion *region)
for (i = 0; i < region->nr_mmaps; i++) {
if (region->mmaps[i].mmap) {
+ /*
+ * The mapping is unmapped by vfio_region_finalize(), but by
+ * then the device is unparented and its path, part of the CPR
+ * name, is gone. So drop the CPR entry here.
+ */
+ g_autofree char *cpr_name = vfio_region_cpr_name(region, i);
+ cpr_delete_ramblock(cpr_name);
memory_region_del_subregion(region->mem, ®ion->mmaps[i].mem);
}
}
diff --git a/include/hw/vfio/vfio-cpr.h b/include/hw/vfio/vfio-cpr.h
index 4606da500a7..fa1699857ca 100644
--- a/include/hw/vfio/vfio-cpr.h
+++ b/include/hw/vfio/vfio-cpr.h
@@ -34,6 +34,7 @@ typedef struct VFIOContainerCPR {
typedef struct VFIODeviceCPR {
Error *mdev_blocker;
Error *id_blocker;
+ Error *mmap_blocker;
uint32_t hwpt_id;
uint32_t ioas_id;
} VFIODeviceCPR;
--
2.47.1
^ permalink raw reply [relevance 8%]
* [PATCH QEMU HCI-8.1 v2 21/24] VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved
` (11 preceding siblings ...)
@ 2026-10-13 15:28 6% ` Andrey Drobyshev
2026-10-13 15:55 6% ` no-reply
2026-10-13 15:28 4% ` [PATCH QEMU HCI-8.1 v2 22/24] VZ: migration/cpr-exec: take the vhost-net devices back if exec fails Andrey Drobyshev
2026-10-13 15:28 8% ` [PATCH QEMU HCI-8.1 v2 23/24] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860 Andrey Drobyshev
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-13 15:28 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
cpr_exec_cb() marks all saved ramblocks with MADV_DOEXEC before exec, but
ignores the result. If madvise() fails for some block, the walk stops
there and the remaining blocks are not marked, yet execvp() still runs.
The new QEMU then reuses addresses of guest RAM that wasn't preserved,
and the guest is lost.
Treat that as a failed cpr-exec: go to the same failure path as when
execvp() fails, so the VM keeps running in the old QEMU.
On that path also clear MADV_DOEXEC from all saved ramblocks again, just
like the preserved fds get close-on-exec back.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
migration/cpr-exec.c | 26 ++++++++++++++++++++++----
1 file changed, 22 insertions(+), 4 deletions(-)
diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
index 7b7609e19cd..cb962ce7252 100644
--- a/migration/cpr-exec.c
+++ b/migration/cpr-exec.c
@@ -154,6 +154,19 @@ static bool preserve_ram(cpr_ramblock_t ram, char *name)
return true;
}
+static bool unpreserve_ram(cpr_ramblock_t ram, char *name)
+{
+#ifdef MADV_DONTEXEC
+ qemu_madvise((void *) ram.addr, ram.length, MADV_DONTEXEC);
+#endif
+ return true;
+}
+
+static void cpr_exec_unpreserve_ram(void)
+{
+ cpr_walk_ramblock(unpreserve_ram);
+}
+
static bool mark_ram_rdonly(cpr_ramblock_t ram, char *name)
{
/*
@@ -202,7 +215,10 @@ static void cpr_exec_cb(void *opaque)
*/
cpr_exec_preserve_fds();
- cpr_walk_ramblock(preserve_ram);
+ if (!cpr_walk_ramblock(preserve_ram)) {
+ /* preserve_ram() has already reported the error and failed migration */
+ goto fail;
+ }
if (cpr_ram_rdonly) {
cpr_walk_ramblock(mark_ram_rdonly);
}
@@ -221,15 +237,17 @@ static void cpr_exec_cb(void *opaque)
* or the system is very short on resources.
*/
error_setg_errno(&err, errno, "execvp %s failed", argv[0]);
- g_clear_pointer(&argv, g_strfreev);
- cpr_exec_unpreserve_fds();
-
error_report_err(error_copy(err));
migrate_set_state(&s->state, s->state, MIGRATION_STATUS_FAILED);
migrate_set_error(s, err);
error_free(err);
err = NULL;
+fail:
+ g_clear_pointer(&argv, g_strfreev);
+ cpr_exec_unpreserve_fds();
+ cpr_exec_unpreserve_ram();
+
/* Note, we can go from state COMPLETED to FAILED */
migration_call_notifiers(MIG_EVENT_PRECOPY_FAILED, NULL);
--
2.47.1
^ permalink raw reply [relevance 6%]
* [PATCH QEMU HCI-8.1 v2 22/24] VZ: migration/cpr-exec: take the vhost-net devices back if exec fails
` (12 preceding siblings ...)
2026-10-13 15:28 6% ` [PATCH QEMU HCI-8.1 v2 21/24] VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved Andrey Drobyshev
@ 2026-10-13 15:28 4% ` Andrey Drobyshev
2026-10-13 15:55 5% ` no-reply
2026-10-13 15:28 8% ` [PATCH QEMU HCI-8.1 v2 23/24] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860 Andrey Drobyshev
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-13 15:28 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
Right before execve(), cpr_exec_cb() releases the kernel vhost-net
devices with VHOST_RESET_OWNER, so that the new QEMU can take them. If
execve() fails, the VM goes on in the old QEMU, but nobody takes the
devices back: the kernel refuses the ioctls of the next
vhost_net_start(), and the guest is left without network.
Take them back with VHOST_SET_OWNER right after the exec failure. The
devices are stopped at that point, and vhost_net_start() sets up the
rest again when the VM resumes. vhost-vsock does the same in its CPR
notifier.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/net/vhost_net.c | 25 +++++++++++++++++++++++++
include/net/vhost_net.h | 1 +
migration/cpr-exec.c | 3 +++
3 files changed, 29 insertions(+)
diff --git a/hw/net/vhost_net.c b/hw/net/vhost_net.c
index 56c697f4bcd..58233788ebb 100644
--- a/hw/net/vhost_net.c
+++ b/hw/net/vhost_net.c
@@ -599,6 +599,31 @@ void vhost_net_cpr_reset_owners(void)
}
}
+/*
+ * Take the ownership back when the exec fails and the VM goes on in the
+ * old QEMU. VHOST_RESET_OWNER has reset the kernel device, but the devices
+ * are stopped, and vhost_net_start() sets everything up again when the VM
+ * resumes.
+ */
+void vhost_net_cpr_set_owners(void)
+{
+ NetClientState *nc;
+
+ QTAILQ_FOREACH(nc, &net_clients, next) {
+ VHostNetState *net = get_vhost_net(nc);
+
+ if (net && net->dev.vhost_ops &&
+ net->dev.vhost_ops->backend_type == VHOST_BACKEND_TYPE_KERNEL) {
+ int r = vhost_dev_set_owner(&net->dev);
+
+ if (r < 0 && r != -ENOSYS) {
+ warn_report("cpr: failed to set vhost-net owner for %s: %s",
+ nc->name, strerror(-r));
+ }
+ }
+ }
+}
+
VHostNetState *get_vhost_net(NetClientState *nc)
{
if (!nc) {
diff --git a/include/net/vhost_net.h b/include/net/vhost_net.h
index 39ce7ad0e3d..cdfefa81500 100644
--- a/include/net/vhost_net.h
+++ b/include/net/vhost_net.h
@@ -90,4 +90,5 @@ int vhost_net_virtqueue_restart(VirtIODevice *vdev, NetClientState *nc,
void vhost_net_save_acked_features(NetClientState *nc);
void vhost_net_cpr_reset_owners(void);
+void vhost_net_cpr_set_owners(void);
#endif
diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
index cb962ce7252..9a428b43093 100644
--- a/migration/cpr-exec.c
+++ b/migration/cpr-exec.c
@@ -243,6 +243,9 @@ static void cpr_exec_cb(void *opaque)
error_free(err);
err = NULL;
+ /* Still the same mm, take the vhost-net devices back */
+ vhost_net_cpr_set_owners();
+
fail:
g_clear_pointer(&argv, g_strfreev);
cpr_exec_unpreserve_fds();
--
2.47.1
^ permalink raw reply [relevance 4%]
* [PATCH QEMU HCI-8.1 v2 13/24] migration: Move CPR HUP watch to cpr-transfer.c
` (6 preceding siblings ...)
2026-10-13 15:28 19% ` [PATCH QEMU HCI-8.1 v2 12/24] migration: Free cpr-transfer MigrationAddress along with gsource Andrey Drobyshev
@ 2026-10-13 15:28 28% ` Andrey Drobyshev
2026-10-13 15:28 23% ` [PATCH QEMU HCI-8.1 v2 14/24] migration: Fix crash on second migration when cancel early Andrey Drobyshev
` (6 subsequent siblings)
14 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-13 15:28 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Fabiano Rosas <farosas@suse.de>
Move this CPR-specific code into a cpr file. While here, give the
functions more significant names.
This makes the new idea (after cpr-transfer) of having two parts to
qmp_migrate slightly more obvious: either wait for the hangup or
continue directly.
andrey.drobyshev:
This commit was cherry-picked from the upstream commit f523e5aa5f56,
adapted for the hci-dev base, which predates the upstream qmp_migrate()
rework around MigrationChannel: the watch is still armed with the
plain MigrationAddress from qmp_migrate() rather than main_ch->addr,
and qmp_migrate_finish_cb() keeps closing the cpr state and destroying
the HUP source itself.
Reviewed-by: Peter Xu <peterx@redhat.com>
Reviewed-by: Prasad Pandit <pjp@fedoraproject.org>
Link: https://lore.kernel.org/qemu-devel/20260123141656.6765-24-farosas@suse.de
Signed-off-by: Fabiano Rosas <farosas@suse.de>
---
include/migration/cpr.h | 5 +++++
migration/cpr-transfer.c | 23 +++++++++++++++++++++++
migration/migration.c | 29 ++++-------------------------
3 files changed, 32 insertions(+), 25 deletions(-)
diff --git a/include/migration/cpr.h b/include/migration/cpr.h
index 8fb79eb0a28..ae441322ff5 100644
--- a/include/migration/cpr.h
+++ b/include/migration/cpr.h
@@ -9,6 +9,7 @@
#define MIGRATION_CPR_H
#include "qapi/qapi-types-migration.h"
+#include "io/channel.h"
#include "qemu/queue.h"
#define MIG_MODE_NONE -1
@@ -76,6 +77,10 @@ int cpr_get_fd_param(const char *name, const char *fdname, int index, bool cpr,
QEMUFile *cpr_transfer_output(MigrationChannel *channel, Error **errp);
QEMUFile *cpr_transfer_input(MigrationChannel *channel, Error **errp);
+void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
+ void *opaque);
+void cpr_transfer_source_destroy(MigrationState *s);
+
void cpr_exec_init(void);
QEMUFile *cpr_exec_output(Error **errp);
QEMUFile *cpr_exec_input(Error **errp);
diff --git a/migration/cpr-transfer.c b/migration/cpr-transfer.c
index 00371d17c3a..61d5c9dce2b 100644
--- a/migration/cpr-transfer.c
+++ b/migration/cpr-transfer.c
@@ -6,7 +6,10 @@
*/
#include "qemu/osdep.h"
+#include "qapi/clone-visitor.h"
#include "qapi/error.h"
+#include "qapi/qapi-visit-migration.h"
+#include "io/channel.h"
#include "io/channel-file.h"
#include "io/channel-socket.h"
#include "io/net-listener.h"
@@ -72,3 +75,23 @@ QEMUFile *cpr_transfer_input(MigrationChannel *channel, Error **errp)
return NULL;
}
}
+
+void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
+ void *opaque)
+{
+ s->hup_source = qio_channel_create_watch(cpr_state_ioc(), G_IO_HUP);
+ g_source_set_callback(s->hup_source,
+ (GSourceFunc)func,
+ QAPI_CLONE(MigrationAddress, opaque),
+ (GDestroyNotify)qapi_free_MigrationAddress);
+ g_source_attach(s->hup_source, NULL);
+}
+
+void cpr_transfer_source_destroy(MigrationState *s)
+{
+ if (s->hup_source) {
+ g_source_destroy(s->hup_source);
+ g_source_unref(s->hup_source);
+ s->hup_source = NULL;
+ }
+}
diff --git a/migration/migration.c b/migration/migration.c
index 61d458b2562..14ff16fa6a4 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -110,7 +110,6 @@ static bool migration_object_check(MigrationState *ms, Error **errp);
static bool migration_switchover_start(MigrationState *s, Error **errp);
static bool close_return_path_on_source(MigrationState *s);
static void migration_completion_end(MigrationState *s);
-static void migrate_hup_delete(MigrationState *s);
static void migration_downtime_start(MigrationState *s)
{
@@ -1500,7 +1499,7 @@ static void migration_cleanup(MigrationState *s)
qemu_savevm_state_cleanup();
cpr_state_close();
- migrate_hup_delete(s);
+ cpr_transfer_source_destroy(s);
close_return_path_on_source(s);
@@ -1661,7 +1660,7 @@ void migration_cancel(void)
migrate_set_state(&s->state, MIGRATION_STATUS_CANCELLING,
MIGRATION_STATUS_CANCELLED);
cpr_state_close();
- migrate_hup_delete(s);
+ cpr_transfer_source_destroy(s);
}
}
@@ -2242,25 +2241,6 @@ static bool migrate_prepare(MigrationState *s, bool resume, Error **errp)
static void qmp_migrate_finish(MigrationAddress *addr, Error **errp);
-static void migrate_hup_add(MigrationState *s, QIOChannel *ioc, GSourceFunc cb,
- void *opaque)
-{
- s->hup_source = qio_channel_create_watch(ioc, G_IO_HUP);
- g_source_set_callback(s->hup_source, cb,
- QAPI_CLONE(MigrationAddress, opaque),
- (GDestroyNotify)qapi_free_MigrationAddress);
- g_source_attach(s->hup_source, NULL);
-}
-
-static void migrate_hup_delete(MigrationState *s)
-{
- if (s->hup_source) {
- g_source_destroy(s->hup_source);
- g_source_unref(s->hup_source);
- s->hup_source = NULL;
- }
-}
-
static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
GIOCondition cond,
void *opaque)
@@ -2270,7 +2250,7 @@ static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
qmp_migrate_finish(addr, NULL);
cpr_state_close();
- migrate_hup_delete(migrate_get_current());
+ cpr_transfer_source_destroy(migrate_get_current());
return G_SOURCE_REMOVE;
}
@@ -2350,8 +2330,7 @@ void qmp_migrate(const char *uri, bool has_channels,
* connection, so qmp_migrate_finish will fail to connect, and then recover.
*/
if (migrate_mode() == MIG_MODE_CPR_TRANSFER) {
- migrate_hup_add(s, cpr_state_ioc(), (GSourceFunc)qmp_migrate_finish_cb,
- addr);
+ cpr_transfer_add_hup_watch(s, qmp_migrate_finish_cb, addr);
} else {
qmp_migrate_finish(addr, errp);
--
2.47.1
^ permalink raw reply [relevance 28%]
* [PATCH QEMU HCI-8.1 v2 14/24] migration: Fix crash on second migration when cancel early
` (7 preceding siblings ...)
2026-10-13 15:28 28% ` [PATCH QEMU HCI-8.1 v2 13/24] migration: Move CPR HUP watch to cpr-transfer.c Andrey Drobyshev
@ 2026-10-13 15:28 23% ` Andrey Drobyshev
2026-10-13 15:45 6% ` no-reply
2026-10-13 15:28 23% ` [PATCH QEMU HCI-8.1 v2 15/24] kvm: tolerate ioctls after kvm_close() Andrey Drobyshev
` (5 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-13 15:28 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Peter Xu <peterx@redhat.com>
Marc-André reported an issue on QEMU crash when retrying a cancelled
migration during early setup phase, see "Link:" for more information, and
also easy way to reproduce.
This patch is a replacement of the prior fix proposed by not only switching
to migration_cleanup(), but also fixing it from CPR side, so that we track
hup_source properly to know if src QEMU is waiting or the HUP signal.
To put it simple: this chunk of special casing in migration_cancel() should
not affect normal migration, but only cpr-transfer migration to cover the
small window when the src QEMU is waiting for a HUP signal on cpr
channel (so that src QEMU can continue the migration on the main channel).
To achieve that, we'll also need to remember to detach the hup_source
whenenver invoked: after that point, we should always be able to cleanup
the migration.
It's not a generic operation to explicitly detach a gsource from its
context while in its dispatch() function. But it should be safe, because
gsource disptch() will only happen with a boosted refcount for the
dispatcher so that the gsource will not be freed until the callback
completes. It's also safe to return G_SOURCE_REMOVE after the gsource is
detached, as glib will simply ignore the G_SOURCE_REMOVE.
One can refer to latest 2.86.5 glib code in g_main_dispatch() for that:
https://github.com/GNOME/glib/blob/2.86.5/glib/gmain.c#L3592
When at this, add a bunch of assertions to make sure nothing surprises us.
After this patch applied, the 2nd migration will not crash QEMU, instead
it'll be in CANCELLING until the socket connection times out (it will take
~2min on my Fedora default kernel). During this process no 2nd migration
will be allowed, and after it timed out migration can be restarted.
It's because so far we don't have control over socket_connect_outgoing(),
or anything yet managed by a task executed in qio_task_run_in_thread().
Speeding up the cancellation to be left for future.
I also tested cpr-transfer by only providing cpr channel not the main
channel (with -incoming defer), kickoff migration on source, then cancel it
on source directly without providing the main channel. It keeps working.
I wanted to add an unit test for that but it'll need to refactor current
cpr-transfer tests first; let's leave it for later.
andrey.drobyshev:
This commit was cherry-picked from the upstream commit 4f08b870f8f0,
adapted for the hci-dev base, which predates the upstream qmp_migrate()
rework: the HUP callback is still qmp_migrate_finish_cb() rather than
migration_connect_outgoing_cb(), so the early
cpr_transfer_source_destroy() call lands there, ahead of
qmp_migrate_finish() and the cpr_state_close() the callback keeps
doing itself.
Link: https://lore.kernel.org/r/20260417184742.293061-1-marcandre.lureau@redhat.com
Reported-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Tested-by: Fabiano Rosas <farosas@suse.de>
Reviewed-by: Fabiano Rosas <farosas@suse.de>
Link: https://lore.kernel.org/r/20260421175820.302795-1-peterx@redhat.com
Signed-off-by: Peter Xu <peterx@redhat.com>
---
include/migration/cpr.h | 1 +
migration/cpr-transfer.c | 10 ++++++++++
migration/migration.c | 32 ++++++++++++++++++++++++--------
migration/migration.h | 5 +++++
4 files changed, 40 insertions(+), 8 deletions(-)
diff --git a/include/migration/cpr.h b/include/migration/cpr.h
index ae441322ff5..4dd397a7c13 100644
--- a/include/migration/cpr.h
+++ b/include/migration/cpr.h
@@ -80,6 +80,7 @@ QEMUFile *cpr_transfer_input(MigrationChannel *channel, Error **errp);
void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
void *opaque);
void cpr_transfer_source_destroy(MigrationState *s);
+bool cpr_transfer_source_active(MigrationState *s);
void cpr_exec_init(void);
QEMUFile *cpr_exec_output(Error **errp);
diff --git a/migration/cpr-transfer.c b/migration/cpr-transfer.c
index 61d5c9dce2b..9defe7bad78 100644
--- a/migration/cpr-transfer.c
+++ b/migration/cpr-transfer.c
@@ -6,6 +6,7 @@
*/
#include "qemu/osdep.h"
+#include "qemu/main-loop.h"
#include "qapi/clone-visitor.h"
#include "qapi/error.h"
#include "qapi/qapi-visit-migration.h"
@@ -79,6 +80,7 @@ QEMUFile *cpr_transfer_input(MigrationChannel *channel, Error **errp)
void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
void *opaque)
{
+ assert(bql_locked());
s->hup_source = qio_channel_create_watch(cpr_state_ioc(), G_IO_HUP);
g_source_set_callback(s->hup_source,
(GSourceFunc)func,
@@ -89,9 +91,17 @@ void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
void cpr_transfer_source_destroy(MigrationState *s)
{
+ assert(bql_locked());
if (s->hup_source) {
g_source_destroy(s->hup_source);
g_source_unref(s->hup_source);
s->hup_source = NULL;
}
}
+
+bool cpr_transfer_source_active(MigrationState *s)
+{
+ /* Whenever the HUP gsource is available, it's active. */
+ assert(bql_locked());
+ return s->hup_source;
+}
diff --git a/migration/migration.c b/migration/migration.c
index 14ff16fa6a4..2e4be2db65d 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -1653,14 +1653,19 @@ void migration_cancel(void)
}
/*
- * If qmp_migrate_finish has not been called, then there is no path that
- * will complete the cancellation. Do it now.
+ * This is cpr-transfer specific processing.
+ *
+ * If this is true, it means cpr-transfer migration is waiting for the
+ * destination to send HUP event on CPR channel to continue the next
+ * phase. If so, do the cleanup proactively to avoid get stuck in
+ * CANCELLING state.
*/
- if (setup && !s->to_dst_file) {
- migrate_set_state(&s->state, MIGRATION_STATUS_CANCELLING,
- MIGRATION_STATUS_CANCELLED);
- cpr_state_close();
- cpr_transfer_source_destroy(s);
+ if (cpr_transfer_source_active(s)) {
+ assert(migrate_mode() == MIG_MODE_CPR_TRANSFER);
+ assert(setup && !s->to_dst_file);
+ migration_cleanup(s);
+ /* Now all things should have been released */
+ assert(!cpr_transfer_source_active(s));
}
}
@@ -2246,11 +2251,22 @@ static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
void *opaque)
{
MigrationAddress *addr = opaque;
+ MigrationState *s = migrate_get_current();
+
+ /*
+ * Detach and release the GSource right after use. We rely on this to
+ * detect this small cpr-transfer window of "waiting for HUP event".
+ */
+ cpr_transfer_source_destroy(s);
qmp_migrate_finish(addr, NULL);
cpr_state_close();
- cpr_transfer_source_destroy(migrate_get_current());
+
+ /*
+ * This is redundant as we do cpr_transfer_source_destroy() at the
+ * entry, but it's benign; glib will just skip the detach.
+ */
return G_SOURCE_REMOVE;
}
diff --git a/migration/migration.h b/migration/migration.h
index a7ea43e925d..ee15ca88461 100644
--- a/migration/migration.h
+++ b/migration/migration.h
@@ -509,6 +509,11 @@ struct MigrationState {
/* Is this a rdma migration */
bool rdma_migration;
+ /*
+ * When set, it means cpr-transfer is waiting for the HUP signal from
+ * destination to continue the 2nd step of migration via the main
+ * channel.
+ */
GSource *hup_source;
};
--
2.47.1
^ permalink raw reply [relevance 23%]
* [PATCH QEMU HCI-8.1 v2 15/24] kvm: tolerate ioctls after kvm_close()
` (8 preceding siblings ...)
2026-10-13 15:28 23% ` [PATCH QEMU HCI-8.1 v2 14/24] migration: Fix crash on second migration when cancel early Andrey Drobyshev
@ 2026-10-13 15:28 23% ` Andrey Drobyshev
2026-10-13 15:44 7% ` no-reply
` (4 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-13 15:28 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
Since commit 7ed0919119b0 ("migration: close kvm after cpr") a source
QEMU that has handed its VFIO devices over via cpr-transfer (or
cpr-exec) calls kvm_close() once the migration completes, so that the
destination can register its irq bypass consumers. kvm_close() closes
the VM and vCPU descriptors and sets kvm_state to NULL, but the source
process keeps living with all its device models intact until the
management layer terminates it.
Nothing prevents those device models from raising interrupts in the
meantime, and the regular exit path even guarantees one: net_cleanup()
deletes the netdev behind an emulated virtio-net, the NIC sees the link
go down and calls virtio_notify_config(), MSI-X delivers through the
in-kernel APIC, and kvm_irqchip_send_msi() dereferences the NULL
kvm_state:
#0 kvm_vm_ioctl (s=0x0, type=KVM_SIGNAL_MSI) accel/kvm/kvm-all.c
#1 kvm_irqchip_send_msi (s=0x0)
#2 kvm_send_msi hw/i386/kvm/apic.c
#3 kvm_apic_mem_write
...
#9 pci_msi_trigger
#11 msix_notify
#12 virtio_pci_notify
#14 virtio_notify_config
#15 virtio_net_set_link_status
#16 qemu_del_net_client
#17 net_cleanup
#18 qemu_cleanup
Every retired cpr-transfer source with a VFIO device thus dies with
SIGSEGV on SIGTERM/quit and leaves a core dump behind. Without a VFIO
device kvm_close() never runs and the same exit is harmless.
Make the ioctl wrappers honour the closed state instead of assuming a
live KVMState: kvm_ioctl(), kvm_vm_ioctl() and kvm_vcpu_ioctl() return
-ENODEV when kvm_close() has already been called, so any late interrupt
or query on the retired source fails gracefully. Teach kvm_send_msi()
that -ENODEV in this situation is expected and not worth the "MSI lost"
warning on every such interrupt.
Fixes: 7ed0919119b0 ("migration: close kvm after cpr")
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
accel/kvm/kvm-all.c | 15 +++++++++++++++
hw/i386/kvm/apic.c | 6 +++++-
2 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/accel/kvm/kvm-all.c b/accel/kvm/kvm-all.c
index a39c1ba260a..ea027c37829 100644
--- a/accel/kvm/kvm-all.c
+++ b/accel/kvm/kvm-all.c
@@ -3371,6 +3371,11 @@ int kvm_ioctl(KVMState *s, unsigned long type, ...)
arg = va_arg(ap, void *);
va_end(ap);
+ /* kvm_close() has been called: there is no KVM to talk to any more */
+ if (!s || s->fd == -1) {
+ return -ENODEV;
+ }
+
trace_kvm_ioctl(type, arg);
ret = ioctl(s->fd, type, arg);
if (ret == -1) {
@@ -3389,6 +3394,11 @@ int kvm_vm_ioctl(KVMState *s, unsigned long type, ...)
arg = va_arg(ap, void *);
va_end(ap);
+ /* kvm_close() has been called: there is no VM to talk to any more */
+ if (!s || s->vmfd == -1) {
+ return -ENODEV;
+ }
+
trace_kvm_vm_ioctl(type, arg);
accel_ioctl_begin();
ret = ioctl(s->vmfd, type, arg);
@@ -3409,6 +3419,11 @@ int kvm_vcpu_ioctl(CPUState *cpu, unsigned long type, ...)
arg = va_arg(ap, void *);
va_end(ap);
+ /* kvm_close() has been called: the vCPU fds are gone */
+ if (cpu->kvm_fd == -1) {
+ return -ENODEV;
+ }
+
trace_kvm_vcpu_ioctl(cpu->cpu_index, type, arg);
accel_cpu_ioctl_begin(cpu);
ret = ioctl(cpu->kvm_fd, type, arg);
diff --git a/hw/i386/kvm/apic.c b/hw/i386/kvm/apic.c
index 1be9bfe36e9..0c9f7759b6e 100644
--- a/hw/i386/kvm/apic.c
+++ b/hw/i386/kvm/apic.c
@@ -196,7 +196,11 @@ static void kvm_send_msi(MSIMessage *msg)
msg->address = kvm_swizzle_msi_ext_dest_id(msg->address);
ret = kvm_irqchip_send_msi(kvm_state, *msg);
- if (ret < 0) {
+ /*
+ * -ENODEV means kvm_close() has already run (the VM was handed over
+ * via CPR); the interrupt is meaningless here and not worth a warning.
+ */
+ if (ret < 0 && ret != -ENODEV) {
fprintf(stderr, "KVM: injection failed, MSI lost (%s)\n",
strerror(-ret));
}
--
2.47.1
^ permalink raw reply [relevance 23%]
* [PATCH QEMU HCI-8.1 v2 12/24] migration: Free cpr-transfer MigrationAddress along with gsource
` (5 preceding siblings ...)
2026-10-13 15:28 21% ` [PATCH QEMU HCI-8.1 v2 11/24] migration: yank: Move register instance earlier Andrey Drobyshev
@ 2026-10-13 15:28 19% ` Andrey Drobyshev
2026-10-13 15:28 28% ` [PATCH QEMU HCI-8.1 v2 13/24] migration: Move CPR HUP watch to cpr-transfer.c Andrey Drobyshev
` (7 subsequent siblings)
14 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-13 15:28 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Fabiano Rosas <farosas@suse.de>
When setting a callback on a Glib source and giving it a data pointer,
it's natural to also provide the destructor for the data in question.
Since migrate_hup_add() already needs to clone the MigrationAddress
when setting the qmp_migrate_finish_cb callback, also pass the
qapi_free_MigrationAddress as the GDestroyNotify callback.
With this the address doesn't need to be freed at the callback body,
making the management of that memory slightly simpler.
andrey.drobyshev:
This commit was cherry-picked from the upstream commit 6a887119ce18,
adapted for the hci-dev base: qmp_migrate_finish_cb() here still
closes the cpr state and deletes the HUP source itself (upstream had
already moved that into migration_cleanup()), so only the explicit
qapi_free_MigrationAddress() call goes away from it.
Cc: Mark Kanda <mark.kanda@oracle.com>
Cc: Ben Chaney <bchaney@akamai.com>
Reviewed-by: Peter Xu <peterx@redhat.com>
Reviewed-by: Prasad Pandit <pjp@fedoraproject.org>
Link: https://lore.kernel.org/qemu-devel/20260123141656.6765-23-farosas@suse.de
Signed-off-by: Fabiano Rosas <farosas@suse.de>
---
migration/migration.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/migration/migration.c b/migration/migration.c
index fb83e8bc695..61d458b2562 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -2245,9 +2245,11 @@ static void qmp_migrate_finish(MigrationAddress *addr, Error **errp);
static void migrate_hup_add(MigrationState *s, QIOChannel *ioc, GSourceFunc cb,
void *opaque)
{
- s->hup_source = qio_channel_create_watch(ioc, G_IO_HUP);
- g_source_set_callback(s->hup_source, cb, opaque, NULL);
- g_source_attach(s->hup_source, NULL);
+ s->hup_source = qio_channel_create_watch(ioc, G_IO_HUP);
+ g_source_set_callback(s->hup_source, cb,
+ QAPI_CLONE(MigrationAddress, opaque),
+ (GDestroyNotify)qapi_free_MigrationAddress);
+ g_source_attach(s->hup_source, NULL);
}
static void migrate_hup_delete(MigrationState *s)
@@ -2269,7 +2271,6 @@ static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
cpr_state_close();
migrate_hup_delete(migrate_get_current());
- qapi_free_MigrationAddress(addr);
return G_SOURCE_REMOVE;
}
@@ -2350,7 +2351,7 @@ void qmp_migrate(const char *uri, bool has_channels,
*/
if (migrate_mode() == MIG_MODE_CPR_TRANSFER) {
migrate_hup_add(s, cpr_state_ioc(), (GSourceFunc)qmp_migrate_finish_cb,
- QAPI_CLONE(MigrationAddress, addr));
+ addr);
} else {
qmp_migrate_finish(addr, errp);
--
2.47.1
^ permalink raw reply [relevance 19%]
* [PATCH QEMU HCI-8.1 v2 11/24] migration: yank: Move register instance earlier
` (4 preceding siblings ...)
2026-10-13 15:28 11% ` [PATCH QEMU HCI-8.1 v2 10/24] migration: vmstate_save_state_v: fix double error_setg Andrey Drobyshev
@ 2026-10-13 15:28 21% ` Andrey Drobyshev
2026-10-13 15:41 3% ` no-reply
2026-10-13 15:28 19% ` [PATCH QEMU HCI-8.1 v2 12/24] migration: Free cpr-transfer MigrationAddress along with gsource Andrey Drobyshev
` (8 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-13 15:28 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Fabiano Rosas <farosas@suse.de>
Move the register_instance call to migrate_prepare() so it can be
paired with the unregister_instance at migration_cleanup(). Otherwise,
the cleanup cannot be run when cpr_state_save() fails because the
instance is registered only after it.
When resuming from a paused postcopy migration, migrate_prepare()
returns early, but migration_cleanup() doesn't run, so the yank will
remain paired.
andrey.drobyshev:
This commit was cherry-picked from the upstream commit 7e714392437c,
adapted for the hci-dev base, which predates the upstream
migration_connect_set_error() -> migration_connect_error_propagate()
rename (77c82edadbe0): the error propagation calls in qmp_migrate()
and qmp_migrate_finish() keep the older spelling.
Also fixed here: upstream left the yank instance registered when
qmp_migrate_finish() fails before connecting (nothing calls
migration_cleanup() then), so the next migrate command aborted on
yank_register_instance(&error_abort); and the unregister at the out:
label ran on a resume, which never registered the instance. Upstream
fixed both in 6b587af5ec9d ("migration: Fold migration_cleanup() into
migration_connect_error_propagate()"), which needs the above rework;
this is the equivalent for our base.
Reviewed-by: Peter Xu <peterx@redhat.com>
Reviewed-by: Prasad Pandit <pjp@fedoraproject.org>
Link: https://lore.kernel.org/qemu-devel/20260123141656.6765-11-farosas@suse.de
Signed-off-by: Fabiano Rosas <farosas@suse.de>
---
migration/migration.c | 31 +++++++++++++++----------------
1 file changed, 15 insertions(+), 16 deletions(-)
diff --git a/migration/migration.c b/migration/migration.c
index 2f8ef20c036..fb83e8bc695 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -2235,11 +2235,12 @@ static bool migrate_prepare(MigrationState *s, bool resume, Error **errp)
return false;
}
+ yank_register_instance(MIGRATION_YANK_INSTANCE, &error_abort);
+
return true;
}
-static void qmp_migrate_finish(MigrationAddress *addr, bool resume_requested,
- Error **errp);
+static void qmp_migrate_finish(MigrationAddress *addr, Error **errp);
static void migrate_hup_add(MigrationState *s, QIOChannel *ioc, GSourceFunc cb,
void *opaque)
@@ -2264,7 +2265,7 @@ static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
{
MigrationAddress *addr = opaque;
- qmp_migrate_finish(addr, false, NULL);
+ qmp_migrate_finish(addr, NULL);
cpr_state_close();
migrate_hup_delete(migrate_get_current());
@@ -2276,7 +2277,6 @@ void qmp_migrate(const char *uri, bool has_channels,
MigrationChannelList *channels, bool has_detach, bool detach,
bool has_resume, bool resume, Error **errp)
{
- bool resume_requested;
Error *local_err = NULL;
MigrationState *s = migrate_get_current();
g_autoptr(MigrationChannel) channel = NULL;
@@ -2329,8 +2329,7 @@ void qmp_migrate(const char *uri, bool has_channels,
return;
}
- resume_requested = has_resume && resume;
- if (!migrate_prepare(s, resume_requested, errp)) {
+ if (!migrate_prepare(s, has_resume && resume, errp)) {
/* Error detected, put into errp */
return;
}
@@ -2354,27 +2353,26 @@ void qmp_migrate(const char *uri, bool has_channels,
QAPI_CLONE(MigrationAddress, addr));
} else {
- qmp_migrate_finish(addr, resume_requested, errp);
+ qmp_migrate_finish(addr, errp);
}
out:
if (local_err) {
+ /* A resume didn't register it, see migrate_prepare() */
+ if (s->state != MIGRATION_STATUS_POSTCOPY_RECOVER_SETUP) {
+ yank_unregister_instance(MIGRATION_YANK_INSTANCE);
+ }
migration_connect_set_error(s, local_err);
error_propagate(errp, local_err);
}
}
-static void qmp_migrate_finish(MigrationAddress *addr, bool resume_requested,
- Error **errp)
+static void qmp_migrate_finish(MigrationAddress *addr, Error **errp)
{
MigrationState *s = migrate_get_current();
Error *local_err = NULL;
-
- if (!resume_requested) {
- if (!yank_register_instance(MIGRATION_YANK_INSTANCE, errp)) {
- return;
- }
- }
+ /* Before the start functions below change the state */
+ bool resume = s->state == MIGRATION_STATUS_POSTCOPY_RECOVER_SETUP;
if (addr->transport == MIGRATION_ADDRESS_TYPE_SOCKET) {
SocketAddress *saddr = &addr->u.socket;
@@ -2401,7 +2399,8 @@ static void qmp_migrate_finish(MigrationAddress *addr, bool resume_requested,
}
if (local_err) {
- if (!resume_requested) {
+ /* Nothing connected: migration_cleanup() won't unregister it */
+ if (!resume) {
yank_unregister_instance(MIGRATION_YANK_INSTANCE);
}
migration_connect_set_error(s, local_err);
--
2.47.1
^ permalink raw reply [relevance 21%]
* [PATCH QEMU HCI-8.1 v2 09/24] migration: Fix regression of passing error_fatal into vmstate_load_state()
` (2 preceding siblings ...)
2026-10-13 15:28 4% ` [PATCH QEMU HCI-8.1 v2 07/24] q35: Fix migration of SMRAM state Andrey Drobyshev
@ 2026-10-13 15:28 12% ` Andrey Drobyshev
2026-10-13 15:36 5% ` no-reply
2026-10-13 15:28 11% ` [PATCH QEMU HCI-8.1 v2 10/24] migration: vmstate_save_state_v: fix double error_setg Andrey Drobyshev
` (10 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-13 15:28 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Arun Menon <armenon@redhat.com>
error_fatal is passed to vmstate_load_state() and vmstate_save_state()
functions. This was introduced in commit c632ffbd74. This would exit(1)
on error, and therefore does not allow to propagate the error back to
the caller.
To maintain consistency with prior error handling i.e. either propagating
the error to the caller or reporting it, we must set the error within a
local Error object instead of using error_fatal.
Reviewed-by: Cornelia Huck <cohuck@redhat.com>
Signed-off-by: Arun Menon <armenon@redhat.com>
Reviewed-by: Akihiko Odaki <odaki@rsg.ci.i.u-tokyo.ac.jp>
Link: https://lore.kernel.org/r/20251028-solve_error_fatal_regression-v2-1-dab24c808a28@redhat.com
[peterx: always uninit var ret, per Akihiko]
[peterx: touchups on line ordering, spacings etc.]
andrey.drobyshev:
This commit was cherry-picked from the upstream commit 986c3292c6b8,
adapted for the hci-dev base, which predates the upstream commit
bcb536cabe10 ("error: Kill @error_warn"): virtio_gpu_load() has no
'err' local there yet, so it is declared here.
Signed-off-by: Peter Xu <peterx@redhat.com>
---
hw/display/virtio-gpu.c | 22 +++++++++++++++-------
hw/pci/pci.c | 15 +++++++++++++--
hw/s390x/virtio-ccw.c | 17 +++++++++++++++--
hw/scsi/spapr_vscsi.c | 10 ++++++++--
hw/virtio/virtio-mmio.c | 15 +++++++++++++--
hw/virtio/virtio-pci.c | 15 +++++++++++++--
hw/virtio/virtio.c | 10 +++++++---
7 files changed, 84 insertions(+), 20 deletions(-)
diff --git a/hw/display/virtio-gpu.c b/hw/display/virtio-gpu.c
index 477ec700a13..4bc8c778508 100644
--- a/hw/display/virtio-gpu.c
+++ b/hw/display/virtio-gpu.c
@@ -1223,7 +1223,8 @@ static int virtio_gpu_save(QEMUFile *f, void *opaque, size_t size,
{
VirtIOGPU *g = opaque;
struct virtio_gpu_simple_resource *res;
- int i;
+ Error *err = NULL;
+ int i, ret;
/* in 2d mode we should never find unprocessed commands here */
assert(QTAILQ_EMPTY(&g->cmdq));
@@ -1246,8 +1247,12 @@ static int virtio_gpu_save(QEMUFile *f, void *opaque, size_t size,
}
qemu_put_be32(f, 0); /* end of list */
- return vmstate_save_state(f, &vmstate_virtio_gpu_scanouts, g, NULL,
- &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_virtio_gpu_scanouts, g, NULL,
+ &err);
+ if (ret < 0) {
+ error_report_err(err);
+ }
+ return ret;
}
static bool virtio_gpu_load_restore_mapping(VirtIOGPU *g,
@@ -1283,9 +1288,10 @@ static int virtio_gpu_load(QEMUFile *f, void *opaque, size_t size,
const VMStateField *field)
{
VirtIOGPU *g = opaque;
+ Error *err = NULL;
struct virtio_gpu_simple_resource *res;
uint32_t resource_id, pformat;
- int i;
+ int i, ret;
g->hostmem = 0;
@@ -1344,9 +1350,11 @@ static int virtio_gpu_load(QEMUFile *f, void *opaque, size_t size,
}
/* load & apply scanout state */
- vmstate_load_state(f, &vmstate_virtio_gpu_scanouts, g, 1, &error_fatal);
-
- return 0;
+ ret = vmstate_load_state(f, &vmstate_virtio_gpu_scanouts, g, 1, &err);
+ if (ret < 0) {
+ error_report_err(err);
+ }
+ return ret;
}
static int virtio_gpu_blob_save(QEMUFile *f, void *opaque, size_t size,
diff --git a/hw/pci/pci.c b/hw/pci/pci.c
index 5d7d17f6529..50f062045a9 100644
--- a/hw/pci/pci.c
+++ b/hw/pci/pci.c
@@ -926,21 +926,32 @@ const VMStateDescription vmstate_pci_device = {
void pci_device_save(PCIDevice *s, QEMUFile *f)
{
+ Error *local_err = NULL;
+ int ret;
+
/* Clear interrupt status bit: it is implicit
* in irq_state which we are saving.
* This makes us compatible with old devices
* which never set or clear this bit. */
s->config[PCI_STATUS] &= ~PCI_STATUS_INTERRUPT;
- vmstate_save_state(f, &vmstate_pci_device, s, NULL, &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_pci_device, s, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
/* Restore the interrupt status bit. */
pci_update_irq_status(s);
}
int pci_device_load(PCIDevice *s, QEMUFile *f)
{
+ Error *local_err = NULL;
int ret;
+
ret = vmstate_load_state(f, &vmstate_pci_device, s, s->version_id,
- &error_fatal);
+ &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
/* Restore the interrupt status bit. */
pci_update_irq_status(s);
return ret;
diff --git a/hw/s390x/virtio-ccw.c b/hw/s390x/virtio-ccw.c
index 4cb1ced001a..4a3ffb84f8f 100644
--- a/hw/s390x/virtio-ccw.c
+++ b/hw/s390x/virtio-ccw.c
@@ -1130,13 +1130,26 @@ static int virtio_ccw_load_queue(DeviceState *d, int n, QEMUFile *f)
static void virtio_ccw_save_config(DeviceState *d, QEMUFile *f)
{
VirtioCcwDevice *dev = VIRTIO_CCW_DEVICE(d);
- vmstate_save_state(f, &vmstate_virtio_ccw_dev, dev, NULL, &error_fatal);
+ Error *local_err = NULL;
+ int ret;
+
+ ret = vmstate_save_state(f, &vmstate_virtio_ccw_dev, dev, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
}
static int virtio_ccw_load_config(DeviceState *d, QEMUFile *f)
{
VirtioCcwDevice *dev = VIRTIO_CCW_DEVICE(d);
- return vmstate_load_state(f, &vmstate_virtio_ccw_dev, dev, 1, &error_fatal);
+ Error *local_err = NULL;
+ int ret;
+
+ ret = vmstate_load_state(f, &vmstate_virtio_ccw_dev, dev, 1, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
+ return ret;
}
static void virtio_ccw_pre_plugged(DeviceState *d, Error **errp)
diff --git a/hw/scsi/spapr_vscsi.c b/hw/scsi/spapr_vscsi.c
index f0a7dd2b882..a6591319db7 100644
--- a/hw/scsi/spapr_vscsi.c
+++ b/hw/scsi/spapr_vscsi.c
@@ -628,10 +628,16 @@ static const VMStateDescription vmstate_spapr_vscsi_req = {
static void vscsi_save_request(QEMUFile *f, SCSIRequest *sreq)
{
vscsi_req *req = sreq->hba_private;
- assert(req->active);
+ Error *local_err = NULL;
+ int rc;
- vmstate_save_state(f, &vmstate_spapr_vscsi_req, req, NULL, &error_fatal);
+ assert(req->active);
+ rc = vmstate_save_state(f, &vmstate_spapr_vscsi_req, req, NULL, &local_err);
+ if (rc < 0) {
+ error_report_err(local_err);
+ return;
+ }
trace_spapr_vscsi_save_request(req->qtag, req->cur_desc_num,
req->cur_desc_offset);
}
diff --git a/hw/virtio/virtio-mmio.c b/hw/virtio/virtio-mmio.c
index fb58c364527..c05c00bcd4a 100644
--- a/hw/virtio/virtio-mmio.c
+++ b/hw/virtio/virtio-mmio.c
@@ -612,15 +612,26 @@ static const VMStateDescription vmstate_virtio_mmio = {
static void virtio_mmio_save_extra_state(DeviceState *opaque, QEMUFile *f)
{
VirtIOMMIOProxy *proxy = VIRTIO_MMIO(opaque);
+ Error *local_err = NULL;
+ int ret;
- vmstate_save_state(f, &vmstate_virtio_mmio, proxy, NULL, &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_virtio_mmio, proxy, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
}
static int virtio_mmio_load_extra_state(DeviceState *opaque, QEMUFile *f)
{
VirtIOMMIOProxy *proxy = VIRTIO_MMIO(opaque);
+ Error *local_err = NULL;
+ int ret;
- return vmstate_load_state(f, &vmstate_virtio_mmio, proxy, 1, &error_fatal);
+ ret = vmstate_load_state(f, &vmstate_virtio_mmio, proxy, 1, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
+ return ret;
}
static bool virtio_mmio_has_extra_state(DeviceState *opaque)
diff --git a/hw/virtio/virtio-pci.c b/hw/virtio/virtio-pci.c
index 7273da4c018..c276fbb017c 100644
--- a/hw/virtio/virtio-pci.c
+++ b/hw/virtio/virtio-pci.c
@@ -189,15 +189,26 @@ static bool virtio_pci_has_extra_state(DeviceState *d)
static void virtio_pci_save_extra_state(DeviceState *d, QEMUFile *f)
{
VirtIOPCIProxy *proxy = to_virtio_pci_proxy(d);
+ Error *local_err = NULL;
+ int ret;
- vmstate_save_state(f, &vmstate_virtio_pci, proxy, NULL, &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_virtio_pci, proxy, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
}
static int virtio_pci_load_extra_state(DeviceState *d, QEMUFile *f)
{
VirtIOPCIProxy *proxy = to_virtio_pci_proxy(d);
+ Error *local_err = NULL;
+ int ret;
- return vmstate_load_state(f, &vmstate_virtio_pci, proxy, 1, &error_fatal);
+ ret = vmstate_load_state(f, &vmstate_virtio_pci, proxy, 1, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
+ return ret;
}
static void virtio_pci_save_queue(DeviceState *d, int n, QEMUFile *f)
diff --git a/hw/virtio/virtio.c b/hw/virtio/virtio.c
index a7063d5555e..d4b2e67c274 100644
--- a/hw/virtio/virtio.c
+++ b/hw/virtio/virtio.c
@@ -3016,7 +3016,7 @@ int virtio_save(VirtIODevice *vdev, QEMUFile *f)
VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
VirtioDeviceClass *vdc = VIRTIO_DEVICE_GET_CLASS(vdev);
uint32_t guest_features_lo = (vdev->guest_features & 0xffffffff);
- int i;
+ int i, ret;
Error *local_err = NULL;
if (k->save_config) {
@@ -3061,7 +3061,7 @@ int virtio_save(VirtIODevice *vdev, QEMUFile *f)
}
if (vdc->vmsd) {
- int ret = vmstate_save_state(f, vdc->vmsd, vdev, NULL, &local_err);
+ ret = vmstate_save_state(f, vdc->vmsd, vdev, NULL, &local_err);
if (ret) {
error_report_err(local_err);
return ret;
@@ -3069,7 +3069,11 @@ int virtio_save(VirtIODevice *vdev, QEMUFile *f)
}
/* Subsections */
- return vmstate_save_state(f, &vmstate_virtio, vdev, NULL, &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_virtio, vdev, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
+ return ret;
}
/* A wrapper for use as a VMState .put function */
--
2.47.1
^ permalink raw reply [relevance 12%]
* [PATCH QEMU HCI-8.1 v2 10/24] migration: vmstate_save_state_v: fix double error_setg
` (3 preceding siblings ...)
2026-10-13 15:28 12% ` [PATCH QEMU HCI-8.1 v2 09/24] migration: Fix regression of passing error_fatal into vmstate_load_state() Andrey Drobyshev
@ 2026-10-13 15:28 11% ` Andrey Drobyshev
2026-10-13 15:38 3% ` no-reply
2026-10-13 15:28 21% ` [PATCH QEMU HCI-8.1 v2 11/24] migration: yank: Move register instance earlier Andrey Drobyshev
` (9 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-13 15:28 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
We may call error_setg twice on same errp if inner
vmstate_save_state_v() or vmstate_save_state() call fails. Next we will
crash on assertion in error_setv().
andrey.drobyshev:
This commit is cherry-picked as-is, with no modifications, from the
upstream commit d41ce10d0f5a.
Fixes: 848a0503422d043 "migration: Update error description outside migration.c"
Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Fabiano Rosas <farosas@suse.de>
Reviewed-by: Peter Xu <peterx@redhat.com>
Link: https://lore.kernel.org/qemu-devel/20260304212303.667141-2-vsementsov@yandex-team.ru
Signed-off-by: Fabiano Rosas <farosas@suse.de>
---
migration/vmstate.c | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/migration/vmstate.c b/migration/vmstate.c
index aea41c09946..04903131257 100644
--- a/migration/vmstate.c
+++ b/migration/vmstate.c
@@ -523,6 +523,9 @@ int vmstate_save_state_v(QEMUFile *f, const VMStateDescription *vmsd,
} else {
ret = inner_field->info->put(f, curr_elem, size,
inner_field, vmdesc_loop);
+ if (ret < 0) {
+ error_setg(errp, "put failed");
+ }
}
written_bytes = qemu_file_transferred(f) - old_offset;
@@ -535,8 +538,8 @@ int vmstate_save_state_v(QEMUFile *f, const VMStateDescription *vmsd,
}
if (ret) {
- error_setg(errp, "Save of field %s/%s failed",
- vmsd->name, field->name);
+ error_prepend(errp, "Save of field %s/%s failed: ",
+ vmsd->name, field->name);
if (vmsd->post_save) {
vmsd->post_save(opaque);
}
--
2.47.1
^ permalink raw reply [relevance 11%]
* [PATCH QEMU HCI-8.1 v2 07/24] q35: Fix migration of SMRAM state
2026-10-13 15:28 13% ` [PATCH QEMU HCI-8.1 v2 01/24] migration: Plug memory leaks after migrate_set_error() Andrey Drobyshev
2026-10-13 15:28 5% ` [PATCH QEMU HCI-8.1 v2 05/24] vfio/pci: Initialize rom_read_failed in vfio_pci_load_rom() Andrey Drobyshev
@ 2026-10-13 15:28 4% ` Andrey Drobyshev
2026-10-13 15:35 3% ` no-reply
2026-10-13 15:28 12% ` [PATCH QEMU HCI-8.1 v2 09/24] migration: Fix regression of passing error_fatal into vmstate_load_state() Andrey Drobyshev
` (11 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-13 15:28 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Igor Mammedov <imammedo@redhat.com>
When migrating, dst QEMU by default has SMRAM unlocked,
and since wmask is not migrated, the migrated value of
MCH_HOST_BRIDGE_F_SMBASE in config space fall to prey of
mch_update_smbase_smram()
...
if (pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] == 0xff) {
*reg = 0x00;
and is getting cleared and leads to unlocked smram
on dst even if on source it's been locked.
As Andrey has pointed out [1], we should derive wmask
from config and not other way around.
Drop offending chunk and resync wmask based on MCH_HOST_BRIDGE_F_SMBASE
register value. That would preserve the register during
migration and set smram regions into corresponding state.
What that changes is:
that it would let guest write junk values in register
(with no apparent effect) until it's stumbles upon
reserved 0x1 [|] 0x2 values, at which point it
would be only possible to lock register and trigger
switch to SMRAM blackhole in CPU AS.
While at it, fix up test by removing junk discard before negotiation hunk.
PS2:
Instead of adding a dedicated post_load handler for it,
reuse mch_update->mch_update_smbase_smram call chain
that is called on write/reset/post_load to be consistent
with how we handle mch registers.
PS3:
for prosterity here is erro message Andrey got due to this bug:
qemu: vfio_container_dma_map(0x..., 0x0, 0xa0000, 0x....) = -22 (Invalid argument)
qemu: hardware error: vfio: DMA mapping failed, unable to continue
1) https://patchew.org/QEMU/20251203180851.6390-1-arbn@yandex-team.com/
andrey.drobyshev:
This commit is cherry-picked as-is, with no modifications, from the
upstream commit 66cf169e29b0.
Fixes: f404220e279c ("q35: implement 128K SMRAM at default SMBASE address")
Reported-by: Andrey Ryabinin <arbn@yandex-team.com>
Signed-off-by: Igor Mammedov <imammedo@redhat.com>
Reviewed-by: Andrey Ryabinin <arbn@yandex-team.com>
Reviewed-by: Michael S. Tsirkin <mst@redhat.com>
Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
Message-Id: <20251211165454.288476-1-imammedo@redhat.com>
---
hw/pci-host/q35.c | 25 +++++++++++--------------
tests/qtest/q35-test.c | 6 ------
2 files changed, 11 insertions(+), 20 deletions(-)
diff --git a/hw/pci-host/q35.c b/hw/pci-host/q35.c
index a708758d361..946342ba585 100644
--- a/hw/pci-host/q35.c
+++ b/hw/pci-host/q35.c
@@ -432,30 +432,27 @@ static void mch_update_smbase_smram(MCHPCIState *mch)
}
if (*reg == MCH_HOST_BRIDGE_F_SMBASE_QUERY) {
- pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] =
- MCH_HOST_BRIDGE_F_SMBASE_LCK;
+ pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = MCH_HOST_BRIDGE_F_SMBASE_LCK;
*reg = MCH_HOST_BRIDGE_F_SMBASE_IN_RAM;
return;
}
/*
- * default/reset state, discard written value
- * which will disable SMRAM balackhole at SMBASE
+ * reg value can come from register write/reset/migration source,
+ * update wmask to be in sync with it regardless of source
*/
- if (pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] == 0xff) {
- *reg = 0x00;
+ if (*reg == MCH_HOST_BRIDGE_F_SMBASE_IN_RAM) {
+ pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = MCH_HOST_BRIDGE_F_SMBASE_LCK;
+ return;
}
-
- memory_region_transaction_begin();
if (*reg & MCH_HOST_BRIDGE_F_SMBASE_LCK) {
- /* disable all writes */
- pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] &=
- ~MCH_HOST_BRIDGE_F_SMBASE_LCK;
+ /* lock register at 0x2 and disable all writes */
+ pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = 0;
*reg = MCH_HOST_BRIDGE_F_SMBASE_LCK;
- lck = true;
- } else {
- lck = false;
}
+
+ lck = *reg & MCH_HOST_BRIDGE_F_SMBASE_LCK;
+ memory_region_transaction_begin();
memory_region_set_enabled(&mch->smbase_blackhole, lck);
memory_region_set_enabled(&mch->smbase_window, lck);
memory_region_transaction_commit();
diff --git a/tests/qtest/q35-test.c b/tests/qtest/q35-test.c
index 62fff49fc89..4e3a4457f62 100644
--- a/tests/qtest/q35-test.c
+++ b/tests/qtest/q35-test.c
@@ -206,12 +206,6 @@ static void test_smram_smbase_lock(void)
qtest_writeb(qts, SMBASE, SMRAM_TEST_PATTERN);
g_assert_cmpint(qtest_readb(qts, SMBASE), ==, SMRAM_TEST_PATTERN);
- /* check that writing junk to 0x9c before before negotiating is ignored */
- for (i = 0; i < 0xff; i++) {
- qpci_config_writeb(pcidev, MCH_HOST_BRIDGE_F_SMBASE, i);
- g_assert(qpci_config_readb(pcidev, MCH_HOST_BRIDGE_F_SMBASE) == 0);
- }
-
/* enable SMRAM at SMBASE */
qpci_config_writeb(pcidev, MCH_HOST_BRIDGE_F_SMBASE, 0xff);
g_assert(qpci_config_readb(pcidev, MCH_HOST_BRIDGE_F_SMBASE) == 0x01);
--
2.47.1
^ permalink raw reply [relevance 4%]
* [PATCH QEMU HCI-8.1 v2 05/24] vfio/pci: Initialize rom_read_failed in vfio_pci_load_rom()
2026-10-13 15:28 13% ` [PATCH QEMU HCI-8.1 v2 01/24] migration: Plug memory leaks after migrate_set_error() Andrey Drobyshev
@ 2026-10-13 15:28 5% ` Andrey Drobyshev
2026-10-13 15:28 4% ` [PATCH QEMU HCI-8.1 v2 07/24] q35: Fix migration of SMRAM state Andrey Drobyshev
` (12 subsequent siblings)
14 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-13 15:28 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Mario Casquero <mcasquer@redhat.com>
When vfio_device_get_region_info() fails in vfio_pci_load_rom(),
the function returns without setting vdev->rom_read_failed to true,
and without allocating vdev->rom. This leaves vdev->rom as NULL.
andrey.drobyshev:
This commit is cherry-picked as-is, with no modifications, from the
upstream commit c822ecd80661.
Signed-off-by: Mario Casquero <mcasquer@redhat.com>
Reviewed-by: Cédric Le Goater <clg@redhat.com>
Link: https://lore.kernel.org/qemu-devel/20260602070857.356526-1-mcasquer@redhat.com
Signed-off-by: Cédric Le Goater <clg@redhat.com>
---
hw/vfio/pci.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/hw/vfio/pci.c b/hw/vfio/pci.c
index fa2ca0d779d..bea478d521f 100644
--- a/hw/vfio/pci.c
+++ b/hw/vfio/pci.c
@@ -1039,7 +1039,7 @@ static void vfio_update_msi(VFIOPCIDevice *vdev)
}
}
-static void vfio_pci_load_rom(VFIOPCIDevice *vdev)
+static bool vfio_pci_load_rom(VFIOPCIDevice *vdev)
{
VFIODevice *vbasedev = &vdev->vbasedev;
struct vfio_region_info *reg_info = NULL;
@@ -1053,7 +1053,7 @@ static void vfio_pci_load_rom(VFIOPCIDevice *vdev)
if (ret != 0) {
error_report("vfio: Error getting ROM info: %s", strerror(-ret));
- return;
+ return false;
}
trace_vfio_pci_load_rom(vbasedev->name, (unsigned long)reg_info->size,
@@ -1064,12 +1064,11 @@ static void vfio_pci_load_rom(VFIOPCIDevice *vdev)
vdev->rom_offset = reg_info->offset;
if (!vdev->rom_size) {
- vdev->rom_read_failed = true;
error_report("vfio-pci: Cannot read device rom at %s", vbasedev->name);
error_printf("Device option ROM contents are probably invalid "
"(check dmesg).\nSkip option ROM probe with rombar=0, "
"or load from file with romfile=\n");
- return;
+ return false;
}
vdev->rom = g_malloc(size);
@@ -1125,6 +1124,8 @@ static void vfio_pci_load_rom(VFIOPCIDevice *vdev)
data[6] = -csum;
}
}
+
+ return true;
}
/* "Raw" read of underlying config space. */
@@ -1158,7 +1159,7 @@ static uint64_t vfio_rom_read(void *opaque, hwaddr addr, unsigned size)
/* Load the ROM lazily when the guest tries to read it */
if (unlikely(!vdev->rom && !vdev->rom_read_failed)) {
- vfio_pci_load_rom(vdev);
+ vdev->rom_read_failed = !vfio_pci_load_rom(vdev);
}
memcpy(&val, vdev->rom + addr,
--
2.47.1
^ permalink raw reply [relevance 5%]
* [PATCH QEMU HCI-8.1 v2 01/24] migration: Plug memory leaks after migrate_set_error()
@ 2026-10-13 15:28 13% ` Andrey Drobyshev
2026-10-13 15:30 0% ` no-reply
2026-10-13 15:28 5% ` [PATCH QEMU HCI-8.1 v2 05/24] vfio/pci: Initialize rom_read_failed in vfio_pci_load_rom() Andrey Drobyshev
` (13 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-13 15:28 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev
From: Markus Armbruster <armbru@redhat.com>
migrate_set_error(s, err) stores a copy of @err in @s. The original
@err is not freed. Most callers free it immediately. Some callers
free it later, or pass it on. And some leak it. Fix those.
Perhaps migrate_set_error(s, err) should take ownership of @err. The
callers that free it immediately would become simpler, and avoid a
copy and a deallocation. The others would have to pass
error_copy(err).
andrey.drobyshev:
This commit is cherry-picked as-is, with no modifications, from the
upstream commit 93817ec39656.
Signed-off-by: Markus Armbruster <armbru@redhat.com>
Link: https://lore.kernel.org/r/20251115083500.2753895-2-armbru@redhat.com
Signed-off-by: Peter Xu <peterx@redhat.com>
---
migration/cpr-exec.c | 3 ++-
migration/multifd.c | 2 ++
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
index edace3f0bd8..7b7609e19cd 100644
--- a/migration/cpr-exec.c
+++ b/migration/cpr-exec.c
@@ -227,11 +227,12 @@ static void cpr_exec_cb(void *opaque)
error_report_err(error_copy(err));
migrate_set_state(&s->state, s->state, MIGRATION_STATUS_FAILED);
migrate_set_error(s, err);
+ error_free(err);
+ err = NULL;
/* Note, we can go from state COMPLETED to FAILED */
migration_call_notifiers(MIG_EVENT_PRECOPY_FAILED, NULL);
- err = NULL;
if (!migration_block_activate(&err)) {
/* error was already reported */
error_free(err);
diff --git a/migration/multifd.c b/migration/multifd.c
index 5a723ae25d2..e369421b3ff 100644
--- a/migration/multifd.c
+++ b/migration/multifd.c
@@ -966,6 +966,7 @@ bool multifd_send_setup(void)
if (!multifd_new_send_channel_create(p, &local_err)) {
migrate_set_error(s, local_err);
+ error_free(local_err);
ret = -1;
}
}
@@ -990,6 +991,7 @@ bool multifd_send_setup(void)
ret = multifd_send_state->ops->send_setup(p, &local_err);
if (ret) {
migrate_set_error(s, local_err);
+ error_free(local_err);
goto err;
}
assert(p->iov);
--
2.47.1
^ permalink raw reply [relevance 13%]
* Re: [Devel] [PATCH hci-8.1 v6 12/12] dm-ploop: truncate unused tail of top delta on suspend
2026-10-13 11:38 3% ` no-reply
@ 2026-10-13 12:53 0% ` Alexander Ivanov
0 siblings, 0 replies; 200+ results
From: Alexander Ivanov @ 2026-10-13 12:53 UTC (permalink / raw)
To: no-reply, den; +Cc: devel
On 10/13/26 13:38, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Low] dm-ploop: ploop_postsuspend() holds ctl_rwsem across unbounded waits and fs I/O
> --
>
> commit 1aa3a8ea27bee4e747c238df1ff3b02cb36cb3fd
> Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
>
> dm-ploop: truncate unused tail of top delta on suspend
>
> This patch adds ploop_calc_used_end() and ploop_truncate_unused_tail().
> ploop_postsuspend() now truncates the unused tail of the top delta. It
> does this under ctl_rwsem, so that ploop messages don't change the image
> in parallel.
>
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
>
>> diff --git a/drivers/md/dm-ploop-target.c b/drivers/md/dm-ploop-target.c
>> --- a/drivers/md/dm-ploop-target.c
>> +++ b/drivers/md/dm-ploop-target.c
> [ ... ]
>
>> static void ploop_postsuspend(struct dm_target *ti)
>> {
>> struct ploop *ploop = ti->private;
>> + unsigned int noio_flags;
>>
>> + down_write(&ploop->ctl_rwsem);
>> /*
>> * Let the next resumed ploop (maybe of reloaded table)
>> * see final size of the top delta. Don't wait in standby
> [ ... ]
>
>> wait_event(ploop->inflight_md_wq, !atomic_read(&ploop->inflight_md));
>> - ploop_set_suspended(ti, true);
>> + /* The device is suspended, see ploop_preresume() */
>> + noio_flags = memalloc_noio_save();
>> + ploop_truncate_unused_tail(ploop);
>> + memalloc_noio_restore(noio_flags);
>> + ploop->suspended = true;
>> + up_write(&ploop->ctl_rwsem);
>> }
> [Severity: Low]
> Is it necessary to take ctl_rwsem for write before the waits in
> ploop_postsuspend()?
>
> The write lock is now taken before the killable ploop_wait_prealloc() and
> the uninterruptible wait_event(ploop->inflight_md_wq, ...), and it stays
> held through ploop_truncate_unused_tail(), which calls vfs_iter_read()
> (via ploop_calc_used_end()), vfs_fsync() and do_ftruncate().
>
> Before this patch the lock was only taken briefly in ploop_set_suspended(),
> after both waits. Those waits could already block for a long time. In
> standby, or with a dead or stuck backing file, inflight_md may never reach
> zero. Now other ctl_rwsem users block behind them as well:
>
> ploop_status()
> down_read(&ploop->ctl_rwsem); /* STATUSTYPE_TABLE */
>
> ploop_message()
> if (read)
> down_read(&ploop->ctl_rwsem);
> else
> down_write(&ploop->ctl_rwsem);
>
> Every message handler except get_event would hang while postsuspend is
> stalled. The same stall can come from a slow vfs_fsync() or do_ftruncate()
> on a stuck filesystem.
>
> I don't see a deadlock. Nothing on the inflight_md completion path or in
> the allocator takes ctl_rwsem. dm_suspend() also already hangs in that
> state with md->suspend_lock held, so the practical impact looks small.
>
> The two waits don't appear to need the lock. Only
> ploop_truncate_unused_tail() and the store to ploop->suspended do. Would
> it make sense to take the lock after the waits? That would avoid
> extending the blocking to status and message handlers while the wait
> phase is stuck.
The extra blocking only happens when postsuspend is already stuck:
dm_suspend() hangs with md->suspend_lock held and the device can't be
recovered either way. Holding the lock across the waits keeps messages
(e.g. resize) from slipping in between the waits and the truncation and
leaving new preallocation behind, which would make us skip the
truncation. So I'd keep it as is.
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH hci-8.1 v6 11/12] dm-ploop: reload metadata on resume
2026-10-13 11:35 3% ` no-reply
@ 2026-10-13 12:46 0% ` Alexander Ivanov
0 siblings, 0 replies; 200+ results
From: Alexander Ivanov @ 2026-10-13 12:46 UTC (permalink / raw)
To: no-reply, den; +Cc: devel
On 10/13/26 13:35, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> - [Medium] dm-ploop: postsuspend waits unkillably on inflight_md even in standby
> - [Medium] dm-ploop: killed wait in postsuspend() leaves prealloc pending after device is marked suspended
> --
>
> commit 881f9bc269db1bbaa88bf766f12f3d91d98c7d92
> Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
>
> dm-ploop: reload metadata on resume
>
> This patch reloads the top delta header, the BAT of every delta and the
> delta file sizes in ploop_preresume(), and rebuilds the BAT tree and
> holes_bitmap. It also moves preallocation to after the reload and makes
> postsuspend wait for pending preallocation and for inflight_md. Standby
> state is now tracked per ploop instead of through the queue bit.
>
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
>
>> diff --git a/drivers/md/dm-ploop-map.c b/drivers/md/dm-ploop-map.c
>> --- a/drivers/md/dm-ploop-map.c
>> +++ b/drivers/md/dm-ploop-map.c
> [ ... ]
>
>> @@ -1259,6 +1259,12 @@ static int ploop_no_pending_prealloc(struct ploop *ploop)
>> return ret;
>> }
>>
>> +void ploop_wait_prealloc(struct ploop *ploop)
>> +{
>> + wait_event_killable(ploop->dispatcher_wq_prealloc,
>> + ploop_no_pending_prealloc(ploop));
>> +}
>> +
> [Severity: Medium]
> The return value of wait_event_killable() is discarded here, so a killed
> wait looks the same as a completed one to the caller.
>
> Can this leave a prealloc pending after the device is marked suspended?
>
> Suppose the dmsetup task gets a fatal signal while ploop_postsuspend() is
> in ploop_wait_prealloc(), and prealloc_size or prealloc_in_progress is
> still non-zero. Then:
>
> ploop_postsuspend()
> ploop_wait_prealloc() /* returns -ERESTARTSYS, ignored */
> wait_event(ploop->inflight_md_wq, ...);
> ploop_set_suspended(ti, true);
>
> The postsuspend callback returns void, so dm_suspend() cannot abort. The
> old allocator thread keeps running and can still extend or truncate the top
> delta.
>
> The new ploop's ploop_preresume() checks only its own prealloc state, which
> is zero:
>
> ploop_preresume()
> } else if (!ploop_is_standby(ploop) &&
> ploop_no_pending_prealloc(ploop)) {
> reload = true;
> ...
> ret = ploop_reload_metadata(ploop);
>
> It therefore reloads and caches a file size that the old allocator is still
> changing. Can the old allocator's later truncate then cut off clusters that
> the new ploop has already written?
>
> The new comment in ploop_postsuspend() says "a ploop of a reloaded table may
> see a stale size, and a late truncate of our allocator may shrink the file
> under it", but nothing in the code prevents that outcome. Is there a way to
> detect the killed wait, for example by checking the return value and
> refusing to proceed?
Known and documented: it needs a reload plus a fatal signal exactly
during this wait. Postsuspend can't fail, so there's no way to refuse,
and having no wait at all was strictly worse.
>> diff --git a/drivers/md/dm-ploop-target.c b/drivers/md/dm-ploop-target.c
>> --- a/drivers/md/dm-ploop-target.c
>> +++ b/drivers/md/dm-ploop-target.c
> [ ... ]
>
>> static void ploop_postsuspend(struct dm_target *ti)
>> {
>> + struct ploop *ploop = ti->private;
>> +
>> + /*
>> + * Let the next resumed ploop (maybe of reloaded table)
>> + * see final size of the top delta. Don't wait in standby
>> + * mode: the allocator may be stuck on the dead file, while
>> + * dm_replace() of libploop must be able to suspend the device
>> + * to recover it. The wait is killable for the same reason.
>> + * If the wait is skipped or killed, a ploop of a reloaded
>> + * table may see a stale size, and a late truncate of our
>> + * allocator may shrink the file under it.
>> + */
>> + if (!ploop_is_standby(ploop))
>> + ploop_wait_prealloc(ploop);
>> + /*
>> + * Data bios are completed before BAT writeback is finished
>> + * (see ploop_put_piwb()), so wait for it explicitly to make
>> + * md pages clean for ploop_reload_metadata().
>> + */
>> + wait_event(ploop->inflight_md_wq, !atomic_read(&ploop->inflight_md));
> [Severity: Medium]
> The prealloc wait above is skipped in standby and is killable, because the
> comment says the allocator may be stuck on a dead file. The inflight_md
> wait here is neither.
>
> Can this wait_event() hang suspend in uninterruptible sleep on a failing
> device?
>
> inflight_md only drops when the BAT write pio completes:
>
> ploop_index_wb_submit()
> ploop_runners_add_work(ploop, pio); /* file IO on the top delta */
> ...
> ploop_put_piwb()
> ploop_dec_inflight_md()
> if (atomic_dec_and_test(&ploop->inflight_md))
> wake_up_locked(&ploop->inflight_md_wq);
>
> If the device is in standby because of a hung backing file and a BAT write
> is in flight on that same file, inflight_md never reaches zero.
> ploop_presuspend() only waits for kt_worker->inflight_pios, so it does not
> drain this.
>
> Before this patch postsuspend never waited on inflight_md, so dm_replace()
> could suspend such a device. Should this wait also be skipped in standby
> and made killable, like ploop_wait_prealloc()?
>
> A follow-up patch in the series, 'dm-ploop: truncate unused tail of top
> delta on suspend', keeps this wait_event() unchanged and moves the call
> under ctl_rwsem, so a hang there would hold ctl_rwsem for write while
> waiting.
Skipping the wait or making it killable just moves the hang to resume or
dtr, which must wait for in-flight BAT writes anyway. Unlike the
allocator, a BAT write is a plain pio, so it completes, with an error if
need be. If the file is dead enough for it to hang, data IO hangs too
and suspend never gets this far.
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH hci-8.1 v6 12/12] dm-ploop: truncate unused tail of top delta on suspend
@ 2026-10-13 11:38 3% ` no-reply
2026-10-13 12:53 0% ` Alexander Ivanov
0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-13 11:38 UTC (permalink / raw)
To: den, Alexander Ivanov; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] dm-ploop: ploop_postsuspend() holds ctl_rwsem across unbounded waits and fs I/O
--
commit 1aa3a8ea27bee4e747c238df1ff3b02cb36cb3fd
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: truncate unused tail of top delta on suspend
This patch adds ploop_calc_used_end() and ploop_truncate_unused_tail().
ploop_postsuspend() now truncates the unused tail of the top delta. It
does this under ctl_rwsem, so that ploop messages don't change the image
in parallel.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop-target.c b/drivers/md/dm-ploop-target.c
> --- a/drivers/md/dm-ploop-target.c
> +++ b/drivers/md/dm-ploop-target.c
[ ... ]
> static void ploop_postsuspend(struct dm_target *ti)
> {
> struct ploop *ploop = ti->private;
> + unsigned int noio_flags;
>
> + down_write(&ploop->ctl_rwsem);
> /*
> * Let the next resumed ploop (maybe of reloaded table)
> * see final size of the top delta. Don't wait in standby
[ ... ]
> wait_event(ploop->inflight_md_wq, !atomic_read(&ploop->inflight_md));
> - ploop_set_suspended(ti, true);
> + /* The device is suspended, see ploop_preresume() */
> + noio_flags = memalloc_noio_save();
> + ploop_truncate_unused_tail(ploop);
> + memalloc_noio_restore(noio_flags);
> + ploop->suspended = true;
> + up_write(&ploop->ctl_rwsem);
> }
[Severity: Low]
Is it necessary to take ctl_rwsem for write before the waits in
ploop_postsuspend()?
The write lock is now taken before the killable ploop_wait_prealloc() and
the uninterruptible wait_event(ploop->inflight_md_wq, ...), and it stays
held through ploop_truncate_unused_tail(), which calls vfs_iter_read()
(via ploop_calc_used_end()), vfs_fsync() and do_ftruncate().
Before this patch the lock was only taken briefly in ploop_set_suspended(),
after both waits. Those waits could already block for a long time. In
standby, or with a dead or stuck backing file, inflight_md may never reach
zero. Now other ctl_rwsem users block behind them as well:
ploop_status()
down_read(&ploop->ctl_rwsem); /* STATUSTYPE_TABLE */
ploop_message()
if (read)
down_read(&ploop->ctl_rwsem);
else
down_write(&ploop->ctl_rwsem);
Every message handler except get_event would hang while postsuspend is
stalled. The same stall can come from a slow vfs_fsync() or do_ftruncate()
on a stuck filesystem.
I don't see a deadlock. Nothing on the inflight_md completion path or in
the allocator takes ctl_rwsem. dm_suspend() also already hangs in that
state with md->suspend_lock held, so the practical impact looks small.
The two waits don't appear to need the lock. Only
ploop_truncate_unused_tail() and the store to ploop->suspended do. Would
it make sense to take the lock after the waits? That would avoid
extending the blocking to status and message handlers while the wait
phase is stuck.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013111604.142989-1-alexander.ivanov@virtuozzo.com?part=12
_______________________________________________
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 11/12] dm-ploop: reload metadata on resume
@ 2026-10-13 11:35 3% ` no-reply
2026-10-13 12:46 0% ` Alexander Ivanov
0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-13 11:35 UTC (permalink / raw)
To: den, Alexander Ivanov; +Cc: devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] dm-ploop: postsuspend waits unkillably on inflight_md even in standby
- [Medium] dm-ploop: killed wait in postsuspend() leaves prealloc pending after device is marked suspended
--
commit 881f9bc269db1bbaa88bf766f12f3d91d98c7d92
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: reload metadata on resume
This patch reloads the top delta header, the BAT of every delta and the
delta file sizes in ploop_preresume(), and rebuilds the BAT tree and
holes_bitmap. It also moves preallocation to after the reload and makes
postsuspend wait for pending preallocation and for inflight_md. Standby
state is now tracked per ploop instead of through the queue bit.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop-map.c b/drivers/md/dm-ploop-map.c
> --- a/drivers/md/dm-ploop-map.c
> +++ b/drivers/md/dm-ploop-map.c
[ ... ]
> @@ -1259,6 +1259,12 @@ static int ploop_no_pending_prealloc(struct ploop *ploop)
> return ret;
> }
>
> +void ploop_wait_prealloc(struct ploop *ploop)
> +{
> + wait_event_killable(ploop->dispatcher_wq_prealloc,
> + ploop_no_pending_prealloc(ploop));
> +}
> +
[Severity: Medium]
The return value of wait_event_killable() is discarded here, so a killed
wait looks the same as a completed one to the caller.
Can this leave a prealloc pending after the device is marked suspended?
Suppose the dmsetup task gets a fatal signal while ploop_postsuspend() is
in ploop_wait_prealloc(), and prealloc_size or prealloc_in_progress is
still non-zero. Then:
ploop_postsuspend()
ploop_wait_prealloc() /* returns -ERESTARTSYS, ignored */
wait_event(ploop->inflight_md_wq, ...);
ploop_set_suspended(ti, true);
The postsuspend callback returns void, so dm_suspend() cannot abort. The
old allocator thread keeps running and can still extend or truncate the top
delta.
The new ploop's ploop_preresume() checks only its own prealloc state, which
is zero:
ploop_preresume()
} else if (!ploop_is_standby(ploop) &&
ploop_no_pending_prealloc(ploop)) {
reload = true;
...
ret = ploop_reload_metadata(ploop);
It therefore reloads and caches a file size that the old allocator is still
changing. Can the old allocator's later truncate then cut off clusters that
the new ploop has already written?
The new comment in ploop_postsuspend() says "a ploop of a reloaded table may
see a stale size, and a late truncate of our allocator may shrink the file
under it", but nothing in the code prevents that outcome. Is there a way to
detect the killed wait, for example by checking the return value and
refusing to proceed?
> diff --git a/drivers/md/dm-ploop-target.c b/drivers/md/dm-ploop-target.c
> --- a/drivers/md/dm-ploop-target.c
> +++ b/drivers/md/dm-ploop-target.c
[ ... ]
> static void ploop_postsuspend(struct dm_target *ti)
> {
> + struct ploop *ploop = ti->private;
> +
> + /*
> + * Let the next resumed ploop (maybe of reloaded table)
> + * see final size of the top delta. Don't wait in standby
> + * mode: the allocator may be stuck on the dead file, while
> + * dm_replace() of libploop must be able to suspend the device
> + * to recover it. The wait is killable for the same reason.
> + * If the wait is skipped or killed, a ploop of a reloaded
> + * table may see a stale size, and a late truncate of our
> + * allocator may shrink the file under it.
> + */
> + if (!ploop_is_standby(ploop))
> + ploop_wait_prealloc(ploop);
> + /*
> + * Data bios are completed before BAT writeback is finished
> + * (see ploop_put_piwb()), so wait for it explicitly to make
> + * md pages clean for ploop_reload_metadata().
> + */
> + wait_event(ploop->inflight_md_wq, !atomic_read(&ploop->inflight_md));
[Severity: Medium]
The prealloc wait above is skipped in standby and is killable, because the
comment says the allocator may be stuck on a dead file. The inflight_md
wait here is neither.
Can this wait_event() hang suspend in uninterruptible sleep on a failing
device?
inflight_md only drops when the BAT write pio completes:
ploop_index_wb_submit()
ploop_runners_add_work(ploop, pio); /* file IO on the top delta */
...
ploop_put_piwb()
ploop_dec_inflight_md()
if (atomic_dec_and_test(&ploop->inflight_md))
wake_up_locked(&ploop->inflight_md_wq);
If the device is in standby because of a hung backing file and a BAT write
is in flight on that same file, inflight_md never reaches zero.
ploop_presuspend() only waits for kt_worker->inflight_pios, so it does not
drain this.
Before this patch postsuspend never waited on inflight_md, so dm_replace()
could suspend such a device. Should this wait also be skipped in standby
and made killable, like ploop_wait_prealloc()?
A follow-up patch in the series, 'dm-ploop: truncate unused tail of top
delta on suspend', keeps this wait_event() unchanged and moves the call
under ctl_rwsem, so a hang there would hold ctl_rwsem for write while
waiting.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013111604.142989-1-alexander.ivanov@virtuozzo.com?part=11
_______________________________________________
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 01/12] dm-ploop: fix u32 overflow of the delta BAT byte size
@ 2026-10-13 11:23 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 11:23 UTC (permalink / raw)
To: Alexander Ivanov, den; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] dm-ploop: ploop_bat_clu_to_page_nr() still wraps u32 on 32-bit builds
--
Subject: Re: [PATCH] dm-ploop: fix u32 overflow of the delta BAT byte size
commit 637ffe4e923149b9a169a498b933c3d5f2b39ef7
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: fix u32 overflow of the delta BAT byte size
This patch computes the byte size of the header and BAT in u64 and uses
ploop_nr_bat_clusters() everywhere for the BAT cluster count. It also
rejects a header or a resize whose hb_nr exceeds PLOOP_MAX_HB_NR.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop.h b/drivers/md/dm-ploop.h
> --- a/drivers/md/dm-ploop.h
> +++ b/drivers/md/dm-ploop.h
> @@ -12,6 +12,8 @@
> #include <linux/bio.h>
>
> #define PLOOP_MAP_OFFSET 16
> +/* Keeps DIV_ROUND_UP(hb_nr, 8) and clu + PLOOP_MAP_OFFSET within u32 */
> +#define PLOOP_MAX_HB_NR round_down(U32_MAX - PLOOP_MAP_OFFSET, 8)
> typedef u32 map_index_t;
> #define BAT_ENTRIES_PER_PAGE (PAGE_SIZE / sizeof(map_index_t))
[ ... ]
> static inline unsigned int ploop_nr_bat_clusters(struct ploop *ploop,
> u32 nr_bat_entries)
> {
> - unsigned long size, bat_clusters;
> + u64 size, bat_clusters;
>
> - size = (PLOOP_MAP_OFFSET + nr_bat_entries) * sizeof(map_index_t);
> + size = ((u64)nr_bat_entries + PLOOP_MAP_OFFSET) * sizeof(map_index_t);
> bat_clusters = DIV_ROUND_UP(size, CLU_SIZE(ploop));
>
> return bat_clusters;
[Severity: Medium]
This isn't addressed by the hunk above, but a related helper in the same
header appears to have been left with the same u32 wrap:
drivers/md/dm-ploop.h:ploop_bat_clu_to_page_nr() {
u64 byte;
byte = (clu + PLOOP_MAP_OFFSET) * sizeof(map_index_t);
return byte >> PAGE_SHIFT;
}
Here clu is u32 and sizeof(map_index_t) is size_t, which is 32 bits on
32-bit architectures. Doesn't the multiply wrap before the result is
widened to the u64 byte?
DM_PLOOP depends only on BLK_DEV_DM, so 32-bit builds are not excluded.
With PLOOP_MAX_HB_NR now allowing hb_nr up to 0xFFFFFFE8, a delta header
m_Size or a resize request with clu + 16 >= 2^30 would make this helper
return a too-small page number on a 32-bit kernel.
That value feeds ploop_prealloc_md_pages():
new_nr_pages = ploop_bat_clu_to_page_nr(new_nr_bat_entries - 1) + 1;
which is called from ploop_resize() and ploop_read_delta_metadata(), so
too few md pages could be allocated for a large BAT. init_be_iter() uses
the same helper to compute last_page, so the BAT page coverage could
become inconsistent with nr_bat_entries.
The commit message says the byte size is computed in u64 "in several
places" and that the u32 wrap is fixed everywhere. Should this helper
use ((u64)clu + PLOOP_MAP_OFFSET) * sizeof(map_index_t) as well?
On 64-bit builds the multiply does not wrap, so this only affects 32-bit
kernels.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013111604.142989-1-alexander.ivanov@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 v5 11/11] dm-ploop: truncate unused tail of top delta on suspend
@ 2026-10-13 8:20 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 8:20 UTC (permalink / raw)
To: den, Alexander Ivanov; +Cc: devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] dm-ploop: no fsync between BAT writeback and top delta truncation in postsuspend
- [Low] dm-ploop: calc_used_end() WARNs on a clean-check that postsuspend does not fully wait for
--
commit 847969f12202a7013753f4e3c79c265f1c673b03
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: truncate unused tail of top delta on suspend
This patch makes ploop_postsuspend() truncate the unused tail of the top
delta. The used end is computed by the new ploop_calc_used_end(), which
reads the on-disk BAT and the format extension offset. The truncation is
skipped for read-only, raw and standby devices, and when the file size
differs from top->file_size.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop-bat.c b/drivers/md/dm-ploop-bat.c
> --- a/drivers/md/dm-ploop-bat.c
> +++ b/drivers/md/dm-ploop-bat.c
[ ... ]
> +loff_t ploop_calc_used_end(struct ploop *ploop, struct file *file)
> +{
> + u32 i, end, nr_be, dst_clu, used_clus, *bat_entries;
[ ... ]
> + /* BAT on disk may miss allocated clusters until writeback is done */
> + if (WARN_ON_ONCE(!ploop_md_pages_are_clean(ploop)))
> + return -EBUSY;
[Severity: Low]
The commit message says "Skip it if md pages are not clean", but this
code raises a kernel WARN and then the caller logs an error.
ploop_postsuspend() only waits for inflight_md:
wait_event(ploop->inflight_md_wq, !atomic_read(&ploop->inflight_md));
ploop_md_pages_are_clean() also tests MD_DIRTY, MD_UPDATING,
wb_batch_llist, md->piwb and md->wait_llist.
In the error paths of ploop_process_one_deferred_bio(), the sequence is:
ploop_break_bat_update(ploop, md, piwb); /* drops inflight_md */
ploop_pio_endio(pio);
spin_lock_irqsave(&ploop->bat_lock, flags);
if (add_to_wblist)
clear_bit(MD_DIRTY, &md->status);
clear_bit(MD_UPDATING, &md->status);
Can suspend complete and reach ploop_postsuspend() with inflight_md == 0
while MD_DIRTY or MD_UPDATING are still set? If so, this would hit the
WARN_ON_ONCE and the truncation would be silently skipped.
Would it make sense to treat unclean md pages as an expected skip, without
the WARN and without the error message from ploop_truncate_unused_tail()?
[ ... ]
> diff --git a/drivers/md/dm-ploop-target.c b/drivers/md/dm-ploop-target.c
> --- a/drivers/md/dm-ploop-target.c
> +++ b/drivers/md/dm-ploop-target.c
[ ... ]
> + end = ploop_calc_used_end(ploop, top->file);
> + if (end < 0) {
> + PL_ERR("Can't find used end of top delta: %lld", end);
> + return;
> + }
> + if (end >= top->file_size)
> + return;
> +
> + ret = do_ftruncate(top->file, end, 0);
[Severity: Medium]
Is a flush of the top delta needed before this truncate?
ploop_calc_used_end() reads the BAT back with vfs_iter_read(), which can be
served from the page cache. The BAT page writes are issued by
ploop_index_wb_submit() as plain writes, with no flush or FUA. The only
vfs_fsync() of the top delta in the suspend path is in ploop_presuspend(),
which runs before ploop_postsuspend() waits for inflight_md:
ploop_presuspend()
vfs_fsync(ploop_top_delta(ploop)->file, 0);
ploop_postsuspend()
wait_event(ploop->inflight_md_wq, !atomic_read(&ploop->inflight_md));
ploop_truncate_unused_tail()
do_ftruncate(top->file, end, 0);
A BAT writeback that completes in that wait, for example one zeroing a
discarded cluster, is not covered by the earlier fsync.
If the host crashes after the size change is committed but before those BAT
pages are durable, the on-disk BAT can still reference clusters beyond the
new EOF. The next ploop_ctr() or ploop_reload_metadata() would then fail in
ploop_convert_bat_entries() with -EPROTO, which is the failure the commit
message says it is avoiding.
Should a vfs_fsync() of top->file be done before do_ftruncate()?
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013075513.90618-1-alexander.ivanov@virtuozzo.com?part=11
_______________________________________________
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 10/11] dm-ploop: reload metadata on resume
@ 2026-10-13 8:16 2% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 8:16 UTC (permalink / raw)
To: Alexander Ivanov, den; +Cc: devel
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] dm-ploop: ploop_postsuspend() waits uninterruptibly for BAT writeback, even in standby
- [Medium] dm-ploop: new ploop keeps suspended=false after failed preresume on a suspended DM device
- [Medium] dm-ploop: postsuspend ignores killed prealloc wait, old allocator can resize top delta after reload
- [Low] dm-ploop: per-instance standby flag is never cleared, so reload is skipped
--
commit fe4b80dd50a64ca3e5385df8c28c40c9de854171
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: reload metadata on resume
This patch reloads the top delta header, the BAT of every delta and the
delta file sizes in ploop_preresume(), and rebuilds the BAT tree and
holes_bitmap. It also moves preallocation to after the reload, tracks
standby per ploop instance, and makes ploop_postsuspend() wait for
pending preallocation and for inflight BAT writeback.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop-map.c b/drivers/md/dm-ploop-map.c
> --- a/drivers/md/dm-ploop-map.c
> +++ b/drivers/md/dm-ploop-map.c
[ ... ]
> +void ploop_wait_prealloc(struct ploop *ploop)
> +{
> + wait_event_killable(ploop->dispatcher_wq_prealloc,
> + ploop_no_pending_prealloc(ploop));
> +}
[Severity: Medium]
ploop_wait_prealloc() discards the return value of wait_event_killable().
If the suspending task is killed while the old allocator is still inside
ploop_preallocate_cluster(), ploop_postsuspend() carries on and marks
the device suspended:
ploop_postsuspend()
ploop_wait_prealloc() /* -ERESTARTSYS dropped */
wait_event(inflight_md_wq, ...)
ploop_set_suspended(ti, true);
A reloaded table can then resume. Its ploop_preresume() only checks
ploop_no_pending_prealloc() on the new ploop, so it cannot see the old
instance's in-flight allocation. When the old allocator finishes, it
runs:
ploop_preallocate_cluster()
new_len = top->file_size + ploop->prealloc_in_progress;
...
ploop_truncate_prealloc_safe(ploop, top->file_size, new_len, ...)
new_len was computed from the old, stale file_size. Can this set the
file size below what the new ploop has already reloaded and extended,
cutting off clusters the new ploop allocated?
The comment in ploop_postsuspend() acknowledges this ("a late truncate of
our allocator may shrink the file under it"), but nothing seems to handle
it. Should the wait result be propagated, or the old allocator otherwise
fenced?
> diff --git a/drivers/md/dm-ploop-target.c b/drivers/md/dm-ploop-target.c
> --- a/drivers/md/dm-ploop-target.c
> +++ b/drivers/md/dm-ploop-target.c
[ ... ]
> static void ploop_postsuspend(struct dm_target *ti)
> {
> + struct ploop *ploop = ti->private;
> +
> + /*
> + * Let the next resumed ploop (maybe of reloaded table)
> [ ... ]
> + */
> + if (!ploop_is_standby(ploop))
> + ploop_wait_prealloc(ploop);
> + /*
> + * Data bios are completed before BAT writeback is finished
> + * (see ploop_put_piwb()), so wait for it explicitly to make
> + * md pages clean for ploop_reload_metadata().
> + */
> + wait_event(ploop->inflight_md_wq, !atomic_read(&ploop->inflight_md));
> ploop_set_suspended(ti, true);
> }
[Severity: Medium]
The comment above says the prealloc wait is skipped in standby and made
killable so that dm_replace() of libploop can suspend a device whose
backing file is dead. Is the new wait_event() on inflight_md_wq missing
the same escape?
inflight_md is raised in ploop_prepare_bat_update():
piwb->type = type;
atomic_inc(&ploop->inflight_md);
and is only dropped through ploop_dec_inflight_md(), from
ploop_put_piwb() or ploop_break_bat_update(). The BAT write is queued
from ploop_index_wb_submit() via ploop_runners_add_work() and completes
only when the backing file completes the I/O.
ploop_check_standby_mode() only reacts to -EBUSY, -ENOTCONN or -EIO, so
entering standby does not guarantee that a pending BAT write completes.
If the backing file stalls without returning an error, can
ploop_postsuspend() sleep forever in D state here?
ploop_presuspend() waits only on kt_worker->inflight_pios, so it does not
drain inflight_md. A BAT write with no attached data pio would not be
covered by that wait.
Should this wait be skipped in standby and made killable, like the
prealloc wait just above it?
Also, the later patch "dm-ploop: truncate unused tail of top delta on
suspend" keeps this wait_event() and moves it inside
down_write(&ploop->ctl_rwsem). In that state a stuck wait also blocks
ploop_message() and ploop_preresume() behind ctl_rwsem.
[ ... ]
> static int ploop_preresume(struct dm_target *ti)
> {
[ ... ]
> + down_write(&ploop->ctl_rwsem);
> + if (ploop->noresume) {
> ret = -EAGAIN;
> + } else if (!ploop_is_standby(ploop) &&
> + ploop_no_pending_prealloc(ploop)) {
> + reload = true;
[ ... ]
> + ret = ploop_reload_metadata(ploop);
> + memalloc_noio_restore(noio_flags);
> + }
> + up_write(&ploop->ctl_rwsem);
>
> if (ret == 0) {
[ ... ]
> ploop_set_suspended(ti, false);
> ploop_set_wants_suspend(ti, false);
> }
> return ret;
> }
[Severity: Medium]
On a reloaded table, the new ploop never runs ploop_postsuspend(), and
ploop_ctr() never initializes ->suspended, so it stays false from
kzalloc. If ploop_reload_metadata() fails (-ENOMEM, -EIO, -EBADSLT) or
noresume is set, ploop_preresume() returns an error and skips
ploop_set_suspended(ti, false), while DM keeps the device suspended.
do_resume() then still runs dm_table_destroy(old_map) even though
dm_resume() failed. That destroys the old ploop together with its
top_flipped state and in-memory IN_USE signature.
The live ploop now reports suspended == false while the DM device is
suspended. The flip_upper_deltas message handler in dm-ploop-cmd.c
checks:
if (!ploop->suspended || !ploop->noresume || ploop->maintaince)
return -EBUSY;
so it would return -EBUSY until a resume succeeds and the device is
suspended again. Is that intended? I have not checked how libploop
uses flip_upper_deltas after a failed resume, so this may be harmless in
practice. Should the new ploop's suspended flag be synchronized with DM
state on this path?
> diff --git a/drivers/md/dm-ploop-map.c b/drivers/md/dm-ploop-map.c
> --- a/drivers/md/dm-ploop-map.c
> +++ b/drivers/md/dm-ploop-map.c
> @@ -1446,6 +1452,7 @@ static void ploop_check_standby_mode(struct ploop *ploop, long res)
> spin_lock_irqsave(&q->queue_lock, flags);
> prev = test_and_set_bit(QUEUE_FLAG_STANDBY, &q->queue_flags);
> spin_unlock_irqrestore(&q->queue_lock, flags);
> + WRITE_ONCE(ploop->standby, true);
[Severity: Low]
This is the only place ploop->standby is written, and nothing in
drivers/md/dm-ploop-*.c clears it. The queue bit, by contrast, is
cleared in ploop_ctr():
if (blk_queue_standby(ploop_blk_queue(ploop))) {
blk_queue_flag_clear(QUEUE_FLAG_STANDBY, ...);
so the two can diverge. Once set, ploop_preresume() permanently skips
ploop_reload_metadata() and the following ploop_should_prealloc() for
this instance:
} else if (!ploop_is_standby(ploop) &&
ploop_no_pending_prealloc(ploop)) {
and ploop_postsuspend() permanently skips ploop_wait_prealloc().
Consider this sequence: ploop_check_standby_mode() sets ->standby, a new
table is loaded and ploop_ctr() clears the queue bit, the new table is
abandoned, and userspace resumes the old instance. The queue is no
longer in standby, but the old ploop skips the reload and runs on
possibly stale BAT and file sizes.
The commit message says userspace must not resume the previous instance
on the same images, but the driver does not enforce that. Should the
flag be cleared when the instance leaves standby, for example on a
successful resume?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013075513.90618-1-alexander.ivanov@virtuozzo.com?part=10
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 2%]
* Re: [Devel] [PATCH hci-8.1 v5 01/11] dm-ploop: fix u32 overflow of the delta BAT byte size
@ 2026-10-13 8:02 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-13 8:02 UTC (permalink / raw)
To: den, Alexander Ivanov; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] dm-ploop: ploop_bat_clu_to_page_nr() still wraps in size_t on 32-bit
--
commit 101e85108bc98e48077f0a87fa099eedec6fc95d
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: fix u32 overflow of the delta BAT byte size
This commit computes the byte size of the header and BAT in u64 and uses
ploop_nr_bat_clusters() for the number of BAT clusters everywhere. It also
rejects a header or a resize whose hb_nr exceeds PLOOP_MAX_HB_NR.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop.h b/drivers/md/dm-ploop.h
> --- a/drivers/md/dm-ploop.h
> +++ b/drivers/md/dm-ploop.h
> @@ -12,6 +12,8 @@
> #define PLOOP_MAP_OFFSET 16
> +/* Keeps DIV_ROUND_UP(hb_nr, 8) and clu + PLOOP_MAP_OFFSET within u32 */
> +#define PLOOP_MAX_HB_NR round_down(U32_MAX - PLOOP_MAP_OFFSET, 8)
> typedef u32 map_index_t;
[ ... ]
> @@ -410,9 +412,9 @@ static inline unsigned int ploop_nr_pages_in_cluster(struct ploop *ploop)
> static inline unsigned int ploop_nr_bat_clusters(struct ploop *ploop,
> u32 nr_bat_entries)
> {
> - unsigned long size, bat_clusters;
> + u64 size, bat_clusters;
>
> - size = (PLOOP_MAP_OFFSET + nr_bat_entries) * sizeof(map_index_t);
> + size = ((u64)nr_bat_entries + PLOOP_MAP_OFFSET) * sizeof(map_index_t);
> bat_clusters = DIV_ROUND_UP(size, CLU_SIZE(ploop));
>
> return bat_clusters;
[Severity: Low]
The commit message says the u32 overflow is fixed "everywhere", but the
sibling helper ploop_bat_clu_to_page_nr() in this same header still has
the same pattern:
static inline u32 ploop_bat_clu_to_page_nr(u32 clu)
{
u64 byte;
byte = (clu + PLOOP_MAP_OFFSET) * sizeof(map_index_t);
return byte >> PAGE_SHIFT;
}
Here the multiplication is evaluated at size_t width, and only the
result is widened to u64. On a 32-bit build this wraps for
clu >= 2^30 - PLOOP_MAP_OFFSET, which is the same range the commit
message describes.
DM_PLOOP in drivers/md/Kconfig depends only on BLK_DEV_DM, with no 64BIT
restriction, so a 32-bit build appears possible. PLOOP_MAX_HB_NR is
about 2^32 - 24, so ploop_setup_metadata() and ploop_resize() would still
accept a BAT in that range.
Can ploop_prealloc_md_pages() then under-allocate md pages on 32-bit?
ploop_resize()
ploop_prealloc_md_pages()
new_nr_pages = ploop_bat_clu_to_page_nr(new_nr_bat_entries - 1) + 1;
The same wrapped value would also feed init_be_iter(),
ploop_bat_entries() and the other callers of this helper.
This is probably not reachable in practice, since a BAT of 2^30 entries
needs more than 4 GiB of md pages. Would it make sense to cast clu to
u64 before the multiplication, as is done in ploop_nr_bat_clusters(), so
the helpers stay consistent with the commit message?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261013075513.90618-1-alexander.ivanov@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 v4 11/11] dm-ploop: truncate unused tail of top delta on suspend
@ 2026-10-12 13:47 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-12 13:47 UTC (permalink / raw)
To: den, Alexander Ivanov; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] dm-ploop: ctl_rwsem held across unbounded waits in ploop_postsuspend()
--
commit d2ae283df5af32ea17061c2b914cc8df355ea0dd
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: truncate unused tail of top delta on suspend
This patch adds ploop_calc_used_end() and uses it from ploop_postsuspend()
to truncate the unused tail of the top delta file. The truncation runs
under ctl_rwsem, which is now taken at the start of ploop_postsuspend().
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop-target.c b/drivers/md/dm-ploop-target.c
> --- a/drivers/md/dm-ploop-target.c
> +++ b/drivers/md/dm-ploop-target.c
[ ... ]
> static void ploop_postsuspend(struct dm_target *ti)
> {
> struct ploop *ploop = ti->private;
> + unsigned int noio_flags;
>
> + down_write(&ploop->ctl_rwsem);
> /*
> * Let the next resumed ploop (maybe of reloaded table)
> * see final size of the top delta. Don't wait in standby
[ ... ]
> + if (!ploop_is_standby(ploop))
> + ploop_wait_prealloc(ploop);
> wait_event(ploop->inflight_md_wq, !atomic_read(&ploop->inflight_md));
> - ploop_set_suspended(ti, true);
> + /* The device is suspended, see ploop_preresume() */
> + noio_flags = memalloc_noio_save();
> + ploop_truncate_unused_tail(ploop);
> + memalloc_noio_restore(noio_flags);
> + ploop->suspended = true;
> + up_write(&ploop->ctl_rwsem);
> }
[Severity: Medium]
Can ctl_rwsem being held across these waits cause a hang?
ploop_postsuspend() now takes down_write(&ploop->ctl_rwsem) before
ploop_wait_prealloc() and before the wait_event() on inflight_md_wq.
Previously those waits ran without the lock, and ploop_set_suspended()
took it only briefly.
ploop_wait_prealloc() is only killable, and the wait_event() on
inflight_md_wq is not interruptible at all. ploop_calc_used_end() does
a vfs_iter_read() of the BAT, and do_ftruncate() is also IO-bound. If
the top delta stalls before ploop->standby has been set, none of these
waits may finish while the write lock is held.
In that state, ploop_status() for STATUSTYPE_TABLE blocks in
down_read(&ploop->ctl_rwsem). ploop_message() blocks in
down_read()/down_write() as well. Only get_event does not take the lock.
Recovery messages such as set_noresume and flip_upper_deltas are also
blocked.
The commit message says the standby skip exists so that dm_replace() of
libploop can still replace or remove a device stuck on a dead file. Does
holding the write lock across the earlier waits undermine that goal?
From the code, ctl_rwsem only needs to cover ploop_truncate_unused_tail()
and the update of ploop->suspended. Could the lock be taken after the two
waits instead?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261012132646.41494-1-alexander.ivanov@virtuozzo.com?part=11
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH hci-8.1 v4 10/11] dm-ploop: reload metadata on resume
@ 2026-10-12 13:44 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-12 13:44 UTC (permalink / raw)
To: den, Alexander Ivanov; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] dm-ploop: ploop_postsuspend() waits uninterruptibly for BAT writeback, even in standby
--
Subject: Re: [PATCH] dm-ploop: reload metadata on resume
commit bd5e28d3cdaaa9ef0b4e2b1af46f0379281a0d21
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: reload metadata on resume
This patch reloads the top delta header, the BAT of every delta and the
delta file sizes in ploop_preresume(), and rebuilds the BAT tree and
holes_bitmap. It also moves preallocation from ploop_ctr() to
ploop_preresume(), tracks standby per ploop, and adds waits in
ploop_postsuspend() for pending preallocation and for inflight BAT
writeback.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop-target.c b/drivers/md/dm-ploop-target.c
> --- a/drivers/md/dm-ploop-target.c
> +++ b/drivers/md/dm-ploop-target.c
[ ... ]
> static void ploop_postsuspend(struct dm_target *ti)
> {
> + struct ploop *ploop = ti->private;
> +
> + /*
> + * Let the next resumed ploop (maybe of reloaded table)
> + * see final size of the top delta. Don't wait in standby
> + * mode: the allocator may be stuck on the dead file, while
> + * dm_replace() of libploop must be able to suspend the device
> + * to recover it. The wait is killable for the same reason.
> + * [ ... ]
> + */
> + if (!ploop_is_standby(ploop))
> + ploop_wait_prealloc(ploop);
> + /*
> + * Data bios are completed before BAT writeback is finished
> + * (see ploop_put_piwb()), so wait for it explicitly to make
> + * md pages clean for ploop_reload_metadata().
> + */
> + wait_event(ploop->inflight_md_wq, !atomic_read(&ploop->inflight_md));
> ploop_set_suspended(ti, true);
> }
[Severity: Medium]
Can this wait_event() hang suspend when the backing file is dead?
The preallocation wait just above is skipped in standby and is killable,
because the allocator may be stuck on the dead file and dm_replace() of
libploop has to be able to suspend the device to recover it.
The inflight_md wait has no such escape. It is uninterruptible, and it is
also reached when ploop_is_standby() is true.
inflight_md is decremented only when the BAT write kiocb completes:
ploop_bat_write_complete()
ploop_put_piwb()
ploop_dec_inflight_md()
atomic_dec_and_test(&ploop->inflight_md)
wake_up_locked(&ploop->inflight_md_wq)
Standby is set only in ploop_check_standby_mode(), after some IO has
already completed with -EBUSY, -ENOTCONN or -EIO:
ploop_check_standby_mode()
...
WRITE_ONCE(ploop->standby, true);
That does not cancel or complete other BAT writes already in flight. If one
of them is stuck on the same dead file, inflight_md stays above zero and the
suspending task sits in D state.
ploop_postsuspend() returns void, so the wait cannot be turned into an
error. Whether a BAT write to a dead file hangs or fails depends on the
backing filesystem, so I could not confirm this from the code alone.
The follow-up patch "dm-ploop: truncate unused tail of top delta on
suspend" keeps this as a plain wait_event(), and moves it to run under
ctl_rwsem held for write. Other paths that take ctl_rwsem would then also
block behind it.
Would it make sense to make this wait killable, or to skip or bound it in
standby, as is done for the preallocation wait?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261012132646.41494-1-alexander.ivanov@virtuozzo.com?part=10
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH hci-8.1 v4 09/11] dm-ploop: fix use-after-free on the last inflight_md decrement
@ 2026-10-12 13:40 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-12 13:40 UTC (permalink / raw)
To: Alexander Ivanov, den; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] dm-ploop: ploop_destroy() self-deadlocks using wait_event_lock_irq() on wq.lock
--
commit e7aad5678d9b3ec2b2234a7e4beccd4508f457af
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: fix use-after-free on the last inflight_md decrement
This patch moves the inflight_md decrement and wakeup to the end of the
piwb completion path, so that it is the last access to the ploop. The
decrement and wakeup are done under inflight_md_wq.lock, the waiters check
the counter under the same lock, and ploop_destroy() now waits for
inflight_md to reach zero before freeing anything.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop-target.c b/drivers/md/dm-ploop-target.c
> --- a/drivers/md/dm-ploop-target.c
> +++ b/drivers/md/dm-ploop-target.c
[ ... ]
> @@ -192,6 +192,13 @@ static void ploop_destroy(struct ploop *ploop)
> if (ploop->kt_allocator)
> kthread_stop(ploop->kt_allocator->task);
>
> + /* BAT writes may still be completing from aio context */
> + spin_lock_irq(&ploop->inflight_md_wq.lock);
> + wait_event_lock_irq(ploop->inflight_md_wq,
> + !atomic_read(&ploop->inflight_md),
> + ploop->inflight_md_wq.lock);
> + spin_unlock_irq(&ploop->inflight_md_wq.lock);
> +
[Severity: High]
Can this deadlock on the waitqueue lock when inflight_md is non-zero?
ploop_destroy() takes inflight_md_wq.lock and then passes that same lock
as the lock argument to wait_event_lock_irq(). If the condition is
already true, the macro exits through its fast path and nothing goes
wrong. If inflight_md is still non-zero, the call chain is:
ploop_destroy()
spin_lock_irq(&ploop->inflight_md_wq.lock)
wait_event_lock_irq()
__wait_event_lock_irq()
___wait_event()
prepare_to_wait_event()
spin_lock_irqsave(&wq_head->lock, flags)
prepare_to_wait_event() acquires wq_head->lock, which is the lock the
caller already holds. The spinlock is not recursive, so the task would
spin on itself with interrupts disabled. The spin_unlock_irq() inside the
macro, before schedule(), is never reached.
This is the case the patch is meant to handle, namely BAT writes still
completing from aio context when ploop_destroy() runs.
wait_event_lock_irq() looks only usable when the lock argument is a
different lock from the waitqueue's own lock. ploop_suspend_submitting_pios()
uses wait_event_interruptible_locked_irq() for this situation.
Would it work to drop the outer spin_lock_irq()/spin_unlock_irq() and use a
plain wait_event() here, since the waker already takes the waitqueue lock
in wake_up_locked()? Alternatively, a *_locked_irq style wait could be
used, as in ploop_suspend_submitting_pios().
> WARN_ON(!llist_empty(&ploop->pios[PLOOP_LIST_PREPARE]));
> WARN_ON(!llist_empty(&ploop->llresubmit_pios));
> WARN_ON(!llist_empty(&ploop->enospc_pios));
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261012132646.41494-1-alexander.ivanov@virtuozzo.com?part=9
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH hci-8.1 v4 01/11] dm-ploop: fix u32 overflow of the delta BAT byte size
@ 2026-10-12 13:33 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-12 13:33 UTC (permalink / raw)
To: Alexander Ivanov, den; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] dm-ploop: ploop_bat_clu_to_page_nr() still wraps size_t multiply before widening
--
commit 490ad8e2663849858d62f84d5062973178fffee9
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: fix u32 overflow of the delta BAT byte size
This patch computes the byte size of the header and BAT in u64 and uses
ploop_nr_bat_clusters() everywhere for the BAT cluster count. It also
rejects a header or a resize whose hb_nr exceeds PLOOP_MAX_HB_NR.
https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop.h b/drivers/md/dm-ploop.h
> --- a/drivers/md/dm-ploop.h
> +++ b/drivers/md/dm-ploop.h
[ ... ]
> @@ -410,9 +412,9 @@ static inline unsigned int ploop_nr_pages_in_cluster(struct ploop *ploop)
> static inline unsigned int ploop_nr_bat_clusters(struct ploop *ploop,
> u32 nr_bat_entries)
> {
> - unsigned long size, bat_clusters;
> + u64 size, bat_clusters;
>
> - size = (PLOOP_MAP_OFFSET + nr_bat_entries) * sizeof(map_index_t);
> + size = ((u64)nr_bat_entries + PLOOP_MAP_OFFSET) * sizeof(map_index_t);
> bat_clusters = DIV_ROUND_UP(size, CLU_SIZE(ploop));
>
> return bat_clusters;
[Severity: Medium]
Does the sibling helper ploop_bat_clu_to_page_nr() in dm-ploop.h still
wrap the same multiplication?
static inline u32 ploop_bat_clu_to_page_nr(u32 clu)
{
u64 byte;
byte = (clu + PLOOP_MAP_OFFSET) * sizeof(map_index_t);
return byte >> PAGE_SHIFT;
}
The multiplication is evaluated in size_t before the result is widened to
u64. Where size_t is 32 bits, this wraps for clu >= 2^30 - PLOOP_MAP_OFFSET.
That is the same range the commit message says is being fixed ("It wraps
for m_Size >= 2^30 - PLOOP_MAP_OFFSET"), and the message also says the size
is now computed in u64 "everywhere".
The new PLOOP_MAX_HB_NR limit only caps nr_bat_entries plus bat_clusters
near 2^32. A header or a resize with nr_bat_entries >= 2^30 - 16 therefore
still passes the new checks in ploop_setup_metadata() and ploop_resize().
Can a wrapped page number then reach these callers?
ploop_resize()->ploop_prealloc_md_pages()
new_nr_pages = ploop_bat_clu_to_page_nr(new_nr_bat_entries - 1) + 1;
This could allocate too few md pages.
init_be_iter()
u32 rem, last_page = ploop_bat_clu_to_page_nr(nr_be - 1);
This could give a wrong iteration end for the BAT entries.
ploop_map_bat_entry()
id = ploop_bat_clu_to_page_nr(clu);
md = ploop_md_page_find(ploop, id);
BUG_ON(!md);
A page id that wraps would then either miss the lookup and hit BUG_ON(!md),
or index past the pages that were allocated.
On 64-bit, size_t is 64 bits and clu + 16 cannot wrap in u32 because of the
PLOOP_MAX_HB_NR cap, so this looks specific to 32-bit builds. The DM_PLOOP
entry in drivers/md/Kconfig depends only on BLK_DEV_DM, so nothing prevents
such a build.
I have not confirmed whether other 32-bit limits make these sizes unusable
first. For example, 2^30 entries would need about 4 GiB of md pages, which
would probably fail to allocate.
Would it make sense to widen the multiplication in ploop_bat_clu_to_page_nr()
the same way, with ((u64)clu + PLOOP_MAP_OFFSET) * sizeof(map_index_t)?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261012132646.41494-1-alexander.ivanov@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 vz10] fs: let the protection flags of a mount be locked from userspace
@ 2026-10-12 10:28 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-12 10:28 UTC (permalink / raw)
To: ptikhomirov, khorenko, Mirian Shilakadze; +Cc: devel
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] fs: do_remount() returns -EPERM after reconfigure_super() already changed the sb
- [Low] fs: MOUNT_ATTR_VE_LOCK lock state is write-only, not reported by mnt_to_attr_flags()
- [Low] perf beauty: MOUNT_ATTR_VE_LOCK grows fsmount_attr_flags to 65 entries, UB on 32-bit
--
commit 8c0f169fd03f2f47c6755d18498f04737a8f7f21
Author: Mirian Shilakadze <mirian.shilakadze@virtuozzo.com>
fs: let the protection flags of a mount be locked from userspace
This patch adds MOUNT_ATTR_VE_LOCK to mount_setattr(). With it, a host task
with CAP_SYS_ADMIN can lock or unlock the ro, nosuid, nodev and noexec
protections of a mount. It also re-tests can_change_locked_flags() under the
mount hash in do_reconfigure_mnt() and do_remount().
Link: https://virtuozzo.atlassian.net/browse/VSTOR-141092
> diff --git a/fs/namespace.c b/fs/namespace.c
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
[ ... ]
> @@ -3483,7 +3520,15 @@ static int do_remount(struct path *path, int ms_flags, int sb_flags,
> err = reconfigure_super(fc);
> if (!err) {
> lock_mount_hash();
> - set_mount_attributes(mnt, mnt_flags);
> + /*
> + * A lock may have landed since the first test.
> + * The superblock change above stands, the mount
> + * flags do not change.
> + */
> + if (can_change_locked_flags(mnt, mnt_flags))
> + set_mount_attributes(mnt, mnt_flags);
> + else
> + err = -EPERM;
> unlock_mount_hash();
> }
> }
[Severity: Medium]
Can do_remount() return -EPERM after the superblock has already been
changed?
The first can_change_locked_flags() test is made with no lock held. Then
reconfigure_super(fc) runs under s_umount. It rewrites sb->s_flags (for
example it clears SB_RDONLY) and calls fc->ops->reconfigure(). Neither can
be rolled back.
mount_setattr() takes only the mount hash, not s_umount. So nothing
serializes a host MOUNT_ATTR_VE_LOCK against this window. If the lock lands
between the first test and the re-test, the re-test fails and err is set to
-EPERM.
The caller then sees a failure, but the shared superblock may have gone rw
or taken new options. That affects every other mount of the same sb. The
comment in the patch says "The superblock change above stands".
Would it be possible to run the re-test before reconfigure_super(), or to
exclude the lock path from remount, for example by taking s_umount shared in
the lock path?
[ ... ]
> @@ -5230,8 +5278,33 @@ static int build_mount_kattr(const struct mount_attr *attr, size_t usize,
> if ((attr->attr_set | attr->attr_clr) & ~MOUNT_SETATTR_VALID_FLAGS)
> return -EINVAL;
>
> + kattr->ve_lock = !!(attr->attr_set & MOUNT_ATTR_VE_LOCK);
[Severity: Low]
This isn't a bug, but the lock state set here looks write-only. Can
userspace read it back?
This is the only place the new flag is consumed. mnt_to_attr_flags(), which
builds the attribute word that statmount() returns as sm.mnt_attr, ignores
MNT_LOCK_READONLY, MNT_LOCK_NOSUID, MNT_LOCK_NODEV and MNT_LOCK_NOEXEC. It
never returns MOUNT_ATTR_VE_LOCK.
The commit message says libvzctl locks bindmounts at container start, on a
live add and again after restore. How is it expected to learn whether a
mount is already locked? The only probe appears to be a remount that is
expected to fail with EPERM.
[ ... ]
> diff --git a/tools/perf/trace/beauty/include/uapi/linux/mount.h b/tools/perf/trace/beauty/include/uapi/linux/mount.h
> --- a/tools/perf/trace/beauty/include/uapi/linux/mount.h
> +++ b/tools/perf/trace/beauty/include/uapi/linux/mount.h
[ ... ]
> +#define MOUNT_ATTR_VE_LOCK 0x8000000000000000ULL
[Severity: Low]
Can this define break the perf beauty flag table on 32-bit builds?
tools/perf/trace/beauty/fsmount.sh matches the define with its regex. The
ULL suffix is absorbed by the trailing ".*". It then emits this entry:
[ilog2(0x8000000000000000) + 1] = "VE_LOCK",
That is index 64, so fsmount_attr_flags grows to 65 entries.
strarray__scnprintf_flags() then loops over those entries:
tools/perf/trace/beauty/pkey_alloc.c:strarray__scnprintf_flags() {
for (i = 1; i < sa->nr_entries; ++i) {
unsigned long bit = 1UL << (i - 1);
...
}
On a 32-bit host, unsigned long is 32 bits wide. Every iteration with
i >= 34 shifts by 33 or more, which is undefined behaviour. The shift
can alias low flag bits, so perf trace may print wrong flag names for
fsmount() attributes.
Before this patch the highest index was around 22. Also, fsmount() takes
only 32-bit attributes, so the VE_LOCK entry would never be used there.
Could this define be kept out of the copy that the beauty scripts parse, or
could fsmount.sh skip entries that do not fit in 32 bits?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261012102058.46896-1-mirian.shilakadze@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 v3 6/6] ve: add neigh_tbl failcounter
2026-10-11 22:52 9% ` [Devel] [PATCH hci-8.1 v3 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
@ 2026-10-12 7:59 0% ` Vasileios Almpanis
0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-10-12 7:59 UTC (permalink / raw)
To: Vladimir Riabchun; +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>
>
> 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));
Here on hosts with more neighbors than the threshold the host will get a
message which points to a failcount file it doesn't exist. Maybe only
increase failcount in case ve is not super?
--
Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH hci-8.1 v3 4/6] ve/printk: Fix time virtualization
2026-10-11 22:52 4% ` [Devel] [PATCH hci-8.1 v3 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
@ 2026-10-11 23:03 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-11 23:03 UTC (permalink / raw)
To: vasileios.almpanis, ptikhomirov, Konstantin Khorenko, Vladimir Riabchun
Cc: devel
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] printk: vprintk_store_log() can drop last time_ns ref and run free_time_ns()
- [Medium] ve/printk: store-time boottime shift leaves pre-start/post-stop VE records on host time
- [Low] printk: refcounted time_ns get/put on every message in vprintk_store_log()
--
commit d24cdca46b24c622f9dc8616a9a118ce2d8625f8
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
ve/printk: Fix time virtualization
This patch moves the container boottime offset adjustment from the
read side (print_time() and info_print_ext_header()) to the store side
in vprintk_store_log(). The offset is taken from the time namespace of
the VE that owns the destination log_state, via a new owner_ve field.
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
[ ... ]
> -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;
> }
[Severity: Low]
Is a counted reference needed here? ve_get_time_ns() does a
refcount_inc() on the time_ns under rcu_read_lock(), and the function
only reads the offsets before calling put_time_ns().
ve0 is not special-cased. init_ve has ve_nsproxy pointing at
init_nsproxy, so every host printk now reaches vprintk_store_log() ->
ve_timens_add_boottime_ns() -> ve_get_time_ns() -> get_time_ns() and
increments the init_time_ns refcount, with IRQs disabled inside
printk_enter_irqsave(). Previously this cost was paid only on the
reader side, in process context.
Could the offsets be read under rcu_read_lock() without taking a
reference?
[Severity: Medium]
Can the put_time_ns() here be the last put and run free_time_ns() from
the printk store path?
The sequence in ve_drop_context() is:
rcu_assign_pointer(ve->ve_nsproxy, NULL);
synchronize_rcu();
put_nsproxy(ve_nsproxy);
A printer that took its time_ns reference through ve_get_time_ns()
before the pointer was cleared keeps its own reference. If it runs
concurrently with container stop, and the container tasks are already
gone, its put_time_ns() in ve_timens_add_boottime_ns() can drop the
last reference.
free_time_ns() then runs inside vprintk_store_log():
dec_time_namespaces(ns->ucounts);
put_user_ns(ns->user_ns);
ns_free_inum(&ns->ns);
__free_page(ns->vvar_page);
kfree(ns);
This takes the proc_inum_ida lock and allocator and slab locks, and may
release a user_ns. Those locks could already be held by the context
that printk interrupted, so can this recurse or deadlock, or trigger a
lockdep wait-context warning?
Before the patch, the get/put ran only in task context on the reader
side. After it, any log with owner_ve set does the get/put on the store
path. The failcount patch later in this series adds
__ve_pr_warn_ratelimited() calls from ve_failcount_inc() with an
explicit ve, and those can come from arbitrary and atomic contexts.
Could the offsets be read without a counted reference, or the put be
deferred?
[ ... ]
> @@ -1431,9 +1432,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);
[Severity: Medium]
Is it intended that records stored while the VE has no ve_nsproxy now
keep the raw host timestamp permanently?
With the read-side shift removed here, print_time() and
info_print_ext_header() print whatever is in the ring buffer. In
vprintk_store_log(), ve_timens_add_boottime_ns() returns the unshifted
local_clock() value when ve_get_time_ns() returns NULL ("container not
yet started").
ve_nsproxy is NULL before ve_grab_context() in ve_start_container(),
while ve_log_init() sets up the log_state at cgroup creation. It is NULL
again after ve_drop_context(), which runs in the start error path and
when the VE is stopped.
The VE log can be written in those windows, since __ve_vprintk_emit()
and ve_log_printk() only check ve_is_super() and not whether the VE is
running. For example, ve_log_printk(net->owner_ve, ...) from
net/netfilter/nf_log.c, or ve_printk(VE_LOG, ...) from kernel/exit.c,
mm/memory.c, net/core/dev.c and net/ipv4/netfilter/ip_tables.c.
Records stored before start or after stop would then carry host uptime,
while records stored in between carry host uptime plus the (usually
negative) container boottime offset. Can the same container dmesg or
/dev/kmsg then show large timestamps followed by small ones?
Since nothing re-bases old records at read time any more, would it make
sense to mention this trade-off in the commit message?
[ ... ]
> @@ -2329,6 +2327,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);
This is the call site for the two comments above on
ve_timens_add_boottime_ns().
>
> caller_id = printk_caller_id();
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261012-ve-printk-irq-v3-0-efc95fd5c78a@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 v3 3/6] ve/printk: Specify ve in printks
2026-10-11 22:52 15% ` [Devel] [PATCH hci-8.1 v3 3/6] ve/printk: Specify ve in printks Vladimir Riabchun
@ 2026-10-11 23:02 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-11 23:02 UTC (permalink / raw)
To: ptikhomirov, Vladimir Riabchun, vasileios.almpanis, Konstantin Khorenko
Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] ve/ratelimit: global net_ratelimit_state mixes suppressed counts across VEs
--
commit e1b1b77160f4d75abebd7d4609682186ec201a82
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
ve/printk: Specify ve in printks
This commit adds ve_printk helpers that take an explicit VE pointer, so
that printing and ratelimiting no longer depend on get_exec_env() where it
is unreliable. The VE requirement is forwarded through ve_net_ratelimit()
and the net_velog_ratelimited()/net_veboth_ratelimited() macros, and the
netfilter callers now pass net->owner_ve.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
> diff --git a/lib/ratelimit.c b/lib/ratelimit.c
> --- a/lib/ratelimit.c
> +++ b/lib/ratelimit.c
[ ... ]
> @@ -55,7 +56,7 @@ int __ve_ratelimit(int dst, struct ve_struct *ve, 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);
> }
> }
[Severity: Low]
This isn't a regression in the new interface, but the miss count printed
here comes from the shared ratelimit_state, so it includes suppressions
caused by every VE. Is it intended that the aggregated count is written
only into the log of whichever VE happens to reset the interval?
The shared state is defined once in net/core/utils.c:
net/core/utils.c:
DEFINE_RATELIMIT_STATE(net_ratelimit_state, 5 * HZ, 10);
...
int ve_net_ratelimit(int dst, struct ve_struct *ve)
{
return ve_ratelimit(dst, ve, &net_ratelimit_state);
}
For example, if a container fills its own conntrack table and triggers
net_veboth_ratelimited() in __nf_conntrack_alloc(), the "N callbacks
suppressed" notice in another VE's log can include misses from the first
VE. A busy VE can also consume the shared burst of 10 per 5 seconds and
suppress the netfilter warnings of other VEs.
This patch now makes the target VE explicit, but the ratelimit_state
remains global. Would a per-VE ratelimit_state be needed for the ve
argument to give per-VE behaviour?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261012-ve-printk-irq-v3-0-efc95fd5c78a@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 v3 2/6] ve/printk: Report potentially leaking usage of ve_printk functions
@ 2026-10-11 22:59 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-11 22:59 UTC (permalink / raw)
To: Konstantin Khorenko, Vladimir Riabchun, vasileios.almpanis, ptikhomirov
Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] ve/printk: host-log branch still keyed on get_exec_env() from IRQ/softirq
--
commit 8d09395a5f1fa2043bb21991f8eb2152c9a5ad97
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_LOG branches of
ve_vprintk(), ve_vprintk_deferred() and ve_dev_printk_emit(), and to
ve_log_state(). The goal is to avoid relying on get_exec_env() in
non-task context, so that nothing leaks to a random container.
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 the host-log branch still keyed on get_exec_env() in softirq or IRQ
context?
Only the VE_LOG branch is guarded by in_task(). The preceding host
branch is unchanged:
if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
r = dev_vprintk_emit(level, dev, fmt, args);
With dst == VE_LOG in softirq, the result depends on which task was
interrupted:
- host task interrupted: the message goes to the host log, and then
the VE branch only produces a WARN, even though that branch would
have been skipped anyway.
- container task interrupted: neither branch runs, so the message is
silently dropped.
In both cases r stays 0, so the caller cannot tell what happened.
Nothing leaks to a container, but the outcome is not deterministic.
Would it make sense to check in_task() once before either branch and
apply a single rule for non-task context?
ve_dev_printk_emit() takes no ve argument, so it always relies on
get_exec_env(). The in-tree caller net/core/dev.c:__ve_netdev_printk()
passes no ve and can run from softirq/NAPI context, so it reaches this
path.
The later patches in the series ("Specify ve in printks" and "Use
ve_printk with specified ve") only help callers that pass an explicit ve.
This netdev path and ve_dev_printk_emit() are not covered by them.
> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c
[ ... ]
> @@ -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);
[Severity: Low]
Does ve_vprintk() have the same problem?
The host-log decision still uses ve_is_super(get_exec_env()) without an
in_task() check. In the series-final state it reads:
if (!ve)
ve = get_exec_env();
va_copy(args2, args);
if (ve_is_super(ve) || (dst & VE0_LOG))
r = vprintk(fmt, args);
if ((dst & VE_LOG)) {
if (!ve_provided && WARN_ON_ONCE(!in_task()))
goto done;
So a VE_LOG-only message from softirq with ve == NULL is either printed
to the host log or dropped, depending on the interrupted task. The WARN
can also fire when a host task was interrupted, although the VE branch
would have been skipped anyway.
Could the host-branch decision be made deterministic for non-task
context with no explicit ve?
> @@ -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);
[Severity: Low]
The same pattern appears in ve_vprintk_deferred(). The host branch
ve_is_super(get_exec_env()) || (dst & VE0_LOG) is unguarded, and the
final-state code only guards the VE_LOG branch with
"if (!ve_provided && WARN_ON_ONCE(!in_task())) goto done;".
Can the same non-deterministic host-log versus drop outcome occur here
when this is called from IRQ or softirq with ve == NULL?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261012-ve-printk-irq-v3-0-efc95fd5c78a@virtuozzo.com?part=2
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* [Devel] [PATCH hci-8.1 v3 6/6] ve: add neigh_tbl failcounter
` (3 preceding siblings ...)
2026-10-11 22:52 8% ` [Devel] [PATCH hci-8.1 v3 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
@ 2026-10-11 22:52 9% ` Vladimir Riabchun
2026-10-12 7:59 0% ` Vasileios Almpanis
4 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-11 22:52 UTC (permalink / raw)
To: vasileios.almpanis, ptikhomirov, 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 v3 5/6] ve/failcount: Use ve_printk with specified ve
` (2 preceding siblings ...)
2026-10-11 22:52 4% ` [Devel] [PATCH hci-8.1 v3 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
@ 2026-10-11 22:52 8% ` Vladimir Riabchun
2026-10-11 22:52 9% ` [Devel] [PATCH hci-8.1 v3 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
4 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-11 22:52 UTC (permalink / raw)
To: vasileios.almpanis, ptikhomirov, 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 8428ad64ffa0..e15087a25918 100644
--- a/include/linux/printk.h
+++ b/include/linux/printk.h
@@ -754,24 +754,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, ...) \
@@ -789,6 +796,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, ...) \
@@ -799,6 +807,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 v3 4/6] ve/printk: Fix time virtualization
2026-10-11 22:52 15% ` [Devel] [PATCH hci-8.1 v3 3/6] ve/printk: Specify ve in printks Vladimir Riabchun
@ 2026-10-11 22:52 4% ` Vladimir Riabchun
2026-10-11 23:03 3% ` no-reply
2026-10-11 22:52 8% ` [Devel] [PATCH hci-8.1 v3 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
2026-10-11 22:52 9% ` [Devel] [PATCH hci-8.1 v3 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
4 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-11 22:52 UTC (permalink / raw)
To: vasileios.almpanis, ptikhomirov, 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 | 2 ++
kernel/printk/printk.c | 14 +++++++-------
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/kernel/printk/internal.h b/kernel/printk/internal.h
index d00ac40d8401..10c2c15f9fb8 100644
--- a/kernel/printk/internal.h
+++ b/kernel/printk/internal.h
@@ -354,11 +354,13 @@ struct log_state {
*/
struct latched_seq clear_seq;
struct printk_ringbuffer *prb;
+ struct ve_struct *owner_ve;
};
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 f0897de062a8..efe2d3069d7a 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -534,6 +534,9 @@ struct log_state init_log_state = {
.val[1] = 0,
},
.prb = &printk_rb_static,
+#ifdef CONFIG_VE
+ .owner_ve = &ve0,
+#endif
};
/* kdump relies on some log_* symbols, let's make it happy */
@@ -698,12 +701,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 +734,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 +1432,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 +2327,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();
@@ -5292,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%]
* [Devel] [PATCH hci-8.1 v3 3/6] ve/printk: Specify ve in printks
@ 2026-10-11 22:52 15% ` Vladimir Riabchun
2026-10-11 23:02 3% ` no-reply
2026-10-11 22:52 4% ` [Devel] [PATCH hci-8.1 v3 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
` (2 subsequent siblings)
4 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-11 22:52 UTC (permalink / raw)
To: vasileios.almpanis, ptikhomirov, 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 | 29 +++++++++++---
include/linux/ratelimit_types.h | 6 ++-
kernel/printk/printk.c | 80 ++++++++++++++++++++++++++++++-------
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(+), 40 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..8428ad64ffa0 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,10 +177,12 @@ 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);
void ve_log_destroy(struct ve_struct *ve);
asmlinkage __printf(2, 3) __cold
@@ -248,8 +256,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 +759,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..f0897de062a8 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,44 @@ 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 +2588,61 @@ 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%]
* Re: [Devel] [PATCH hci-8.1 v2 6/6] ve: add neigh_tbl failcounter
2026-10-06 22:06 4% ` no-reply
@ 2026-10-11 22:45 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-11 22:45 UTC (permalink / raw)
To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel
On 10/7/26 00:06, no-reply@sashiko.virtuozzo.com wrote:
> 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.
Kept old message to print table id. You asked in v1.
>
> [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?
As said in v1 reply, limit is global and doesn't depend on the type.
The code there is already ugly, no point in making it more complex.
>
> [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;
>> }
Yes, it can. We don't have per-container ratelimits, they won't work.
>
--
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 v2 4/6] ve/printk: Fix time virtualization
2026-10-06 22:04 3% ` no-reply
@ 2026-10-11 22:38 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-11 22:38 UTC (permalink / raw)
To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel
On 10/7/26 00:04, no-reply@sashiko.virtuozzo.com wrote:
> 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?
True, missed it. Will remove in v3.
>
>> 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?
Eh, will fix in v3.
>
> [ ... ]
>
>> -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?
According to RCU doc: "anything that disables bottom halves, preemption,
or interrupts also enters an RCU read-side critical section."
ve_timens_add_boottime_ns is called from vprintk_store_log right after
printk_enter_irqsave, so synchronize_rcu will wait anyway.
Also: ve_exit_ns means that VE is stopped and no tasks are there.
But reaching printk with given VE means that either an interrupted task
is still in VE or we got it from somewhere like net structure.
In any case, this is not a deferred usage, so we can't use stale values
there.
About performance - I don't think it's a big problem. Dmesg logs are
expected to be rare enough, frequent printing is bounded by serial
console and may cause stalls.
>
> [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.
Shifting time when printing was not reliable as well, it could happen
anytime.
The idea of this whole patchset is to avoid printing messages to
random containers. We normally can't get a situation when VE is stopped
and there are processes inside it(except selftests), so it is not
possible to print into stopped VE.
>
> [ ... ]
>
--
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 v2 3/6] ve/printk: Specify ve in printks
2026-10-06 22:03 3% ` no-reply
@ 2026-10-11 21:35 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-11 21:35 UTC (permalink / raw)
To: no-reply, vasileios.almpanis, Konstantin Khorenko; +Cc: devel
On 10/7/26 00:03, no-reply@sashiko.virtuozzo.com wrote:
> 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().
True, will remove in v3.
>
>> 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.
Yes, this is intentional.
>
> Can this path now run from softirq or BH-disabled context?
It could do it before the patch series as well.
>
> __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?
I don't want to debug working printk. It works, it used to work.
The only thing I change in the end is rejecting prints to
random containers.
>
> 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().
Interrupt != scheduler.
>
> Also, ve_printk_ratelimited() in printk.h still falls back to ve0 when
> !in_task(). So the two ratelimited paths now follow different rules.
net_veboth_ratelimited uses explicit ve, so no rules are needed.
With generic ve_ratelimit we will redirect message to host and drop
a warning. Consistent with the behavior "print to host or drop".
>
> [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.
True, will drop the comment in v3 since
"!ve_provided && WARN_ON_ONCE(!in_task())" is self-descriptive.
>
> [ ... ]
>
>> +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, ".
I was looking for this whitespace for days...
>
> [ ... ]
>
--
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 v2 2/6] ve/printk: Report potentially leaking usage of ve_printk functions
2026-10-06 21:59 3% ` no-reply
@ 2026-10-11 21:18 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-11 21:18 UTC (permalink / raw)
To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel
On 10/6/26 23:59, no-reply@sashiko.virtuozzo.com wrote:
> 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?
This is correct - interrupted task is a random value, we report
incorrect logic, not specific situation.
>
> 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().
It is fine if message goes to host.
We will drop a warning so we'll be able to get printk location
based on the call trace.
>
>> 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.
Replied in v1.
>
> [ ... ]
>
>> @@ -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.
All of the above is incorrect due to the nature of WARN.
>
--
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 v2 1/6] ve/printk: Add sanity checks in ve_printk functions
2026-10-06 21:58 4% ` no-reply
@ 2026-10-11 20:58 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-11 20:58 UTC (permalink / raw)
To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel
On 10/6/26 23:58, no-reply@sashiko.virtuozzo.com wrote:
> 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?
No. Replied to v1, same issue:
1. dst == 0 is actually a bug
2. WARN_ON is not printing anything himself.
No fixes needed.
>
>> 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);
>
--
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 3/3] dm-qcow2: truncate unused tail of image on suspend
2026-10-08 17:08 3% ` no-reply
@ 2026-10-10 12:42 0% ` Alexander Ivanov
0 siblings, 0 replies; 200+ results
From: Alexander Ivanov @ 2026-10-10 12:42 UTC (permalink / raw)
To: no-reply, den; +Cc: devel
On 10/8/26 19:08, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Medium] dm-qcow2: truncate_preallocations() trusts unvalidated on-disk refcounts as file length
> --
>
> commit e3b5d03194a42ef95bbb361e199ba3c183aed97e
> Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
>
> dm-qcow2: truncate unused tail of image on suspend
>
> This patch adds qcow2_calc_used_end(), which scans the refcount blocks backward and returns the end of the last cluster with a non-zero refcount. qcow2_truncate_preallocations() now truncates the image file to that value on postsuspend. If the scan fails, or md writeback failed, it falls back to the old behaviour.
>
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
>
>> diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
>> --- a/drivers/md/dm-qcow2-map.c
>> +++ b/drivers/md/dm-qcow2-map.c
> [ ... ]
>
>> +loff_t qcow2_calc_used_end(struct qcow2 *qcow2)
>> +{
> [ ... ]
>
>> + for (i = r2.index_in_page; i >= 0; i--, pos -= clu_size) {
>> + /* Last cluster is partial if size isn't cluster-aligned */
>> + if (get_r2_entry(qcow2, r2.md, i))
>> + return min_t(loff_t, pos + clu_size,
>> + qcow2->file_preallocated_area_start);
>> + }
>> + }
>> +
>> + return -EIO;
>> +}
> [Severity: Medium]
> The used end here comes only from the on-disk refcounts. Can it be wrong when those refcounts are stale or corrupt?
>
> An image written by QEMU with lazy refcounts, or one left dirty after a crash, can legitimately have zero refcounts for clusters that are still in use. A corrupt or crafted image can too.
>
> In that case qcow2_calc_used_end() returns an end below clusters that are still referenced. Nothing cross-checks the result against the L1/L2 tables, the snapshot extents, or the header and refcount table/refblock offsets.
>
> Before this patch the truncate went only down to file_preallocated_area_start. That value is set to file_size at load and is only raised by this driver's own preallocation, so the old code never cut off referenced data.
>
>> diff --git a/drivers/md/dm-qcow2-target.c b/drivers/md/dm-qcow2-target.c
>> --- a/drivers/md/dm-qcow2-target.c
>> +++ b/drivers/md/dm-qcow2-target.c
> [ ... ]
>
>> + /* Refcounts on disk are stale after failed md writeback */
>> + if (!tgt->md_writeback_error) {
>> + used_end = qcow2_calc_used_end(qcow2);
>> + if (used_end < 0)
>> + QC_ERR(ti, "Can't find used end: %pe", ERR_PTR(used_end));
>> + else
>> + end = used_end;
>> + }
>> +
>> if (end == qcow2->file_size)
>> return;
>>
>> ret = qcow2_truncate_safe(qcow2->file, end);
> [Severity: Medium]
> In qcow2_truncate_preallocations(), the value from qcow2_calc_used_end() is passed straight to qcow2_truncate_safe() as the new file length. The fallback only covers scan errors (-EIO and similar). It does not cover refcounts that are consistent to parse but logically wrong.
>
> The md_writeback_error check only covers this driver's own writeback failures. It does not cover refcounts that were already stale when the image was loaded.
>
> Is there a way to validate the result before truncating? For example, the shrink could be skipped when the dirty or lazy-refcount flag is set, or the used end could be checked against the L1/L2 and metadata extents.
>
> This ties into qcow2_check_convert_hdr(), where the dirty-bit check is commented out:
>
> // if (!is_ro && kernel_sets_dirty_bit !=
> // !(hdr->incompatible_features & INCOMPATIBLE_FEATURES_DIRTY_BIT))
> // return kernel_sets_dirty_bit ? -EUCLEAN : -ENOLCK;
>
> With that check disabled, images with stale refcounts are accepted at load time. I also don't see any other refcount consistency validation at load.
Before we return the image to the kernel we do "qemu-img check -r" and
the refcounts must be consistent.
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [PATCH QEMU HCI-8.1 20/21] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860
2026-10-09 14:56 9% ` [PATCH QEMU HCI-8.1 20/21] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860 Andrey Drobyshev
@ 2026-10-09 15:36 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:36 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] vfio: region BAR reuse via CPR ramblock is not gated on cpr_exec_is_incoming()
- [Medium] vfio: stale CPR ramblock entry left after region finalize, reused by cpr_find
- [Low] vfio: BAR mappings in the CPR ramblock list get RAM-only mprotect handling
--
commit d9465df038b744893fa658afc7f09871b9b52f12
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860
This patch saves each VFIO BAR mmap in the CPR ramblock list, so
cpr_exec_cb() marks it with MADV_DOEXEC. The new QEMU then finds the entry
by name and reuses the mapping instead of calling mmap() again. This avoids
the NVIDIA vGPU driver refusing to map a BAR that old QEMU still has
recorded as mapped.
> diff --git a/hw/vfio/region.c b/hw/vfio/region.c
> index 1deaef12fd0b..07027ee79f3c 100644
> --- a/hw/vfio/region.c
> +++ b/hw/vfio/region.c
[ ... ]
> +static char *vfio_region_cpr_name(VFIORegion *region, int index)
> +{
> + g_autofree char *path = qdev_get_dev_path(region->vbasedev->dev);
> + return g_strdup_printf("%s%s%s mmaps[%d]", path ?: "", path ? "/" : "",
> + memory_region_name(region->mem), index);
> +}
> +
> +static void *vfio_region_cpr_find(VFIORegion *region, int index)
> +{
> + g_autofree char *name = vfio_region_cpr_name(region, index);
> + cpr_ramblock_t ram = cpr_find_ramblock(name);
> +
> + if (ram.addr) {
> + assert(ram.length == region->mmaps[index].size);
> + }
> +
> + return (void *)(uintptr_t)ram.addr;
> +}
[Severity: High]
Should vfio_region_cpr_find() be gated on cpr_exec_is_incoming()?
The lookup is by name only. cpr_state_save() serializes the ramblock list
for both cpr-transfer and cpr-exec, and cpr_state_load() loads it in both
modes:
migration/cpr.c:cpr_state_load() {
...
} else if (channel) {
mode = MIG_MODE_CPR_TRANSFER;
cpr_set_incoming_mode(mode);
f = cpr_transfer_input(channel, errp);
...
}
In transfer mode cpr_exec_is_incoming() is false, but the entry is still
found. The address from the old process would then be passed to
memory_region_init_ram_device_ptr() in vfio_region_mmap(). Guest BAR
accesses could then fault or hit unrelated memory.
In the opposite case, an exec-incoming QEMU with a missing entry silently
falls back to mmap(). That brings back the NVIDIA "BAR already mapped"
failure with no clear error.
ram_block_add() in system/physmem.c checks cpr_exec_is_incoming() and
reports an explicit error in both mismatch cases:
if (cpr_exec_is_incoming()) { ... } else {
if (ram.addr || ram.length) {
error_report("CPR mode=exec wasn't requested, but ram block %s was saved...");
exit(1);
Could the VFIO path have an equivalent check?
[Severity: Low]
Also, vfio_region_cpr_save() puts device MMIO BAR mappings into the generic
RAM preservation list:
> +static void vfio_region_cpr_save(VFIORegion *region, int index)
> +{
> + g_autofree char *name = vfio_region_cpr_name(region, index);
> + void *addr = region->mmaps[index].mmap;
> + size_t size = region->mmaps[index].size;
> +
> + cpr_save_ramblock(name, (cpr_ramblock_t) { (uintptr_t)addr, size });
> +}
Every cpr_walk_ramblock() user now treats these BARs as guest RAM. With the
debug option -cpr-ram-rdonly, cpr_exec_cb() runs:
if (cpr_ram_rdonly) {
cpr_walk_ramblock(mark_ram_rdonly);
}
This mprotects the BAR mappings read-only. Neither the fail: label nor the
execvp failure path restores RW. cpr_ram_restore_rw() only runs on the
incoming side. After a failed cpr-exec, the next guest MMIO store through a
ram_device pointer could SIGSEGV the old QEMU.
On the incoming side, mark_ram_rw() does this:
if (qemu_mprotect_rw((void *) ram.addr, ram.length)) {
...
abort();
}
Could that abort QEMU for a BAR mapped without PROT_WRITE? I did not
confirm that the kernel's VFIO mmap path rejects the upgrade, so this part
is speculative. The behavior is the same as guest RAM already has in this
debug mode, but the commit message does not mention the interaction.
> static void vfio_subregion_unmap(VFIORegion *region, int index)
> {
> + g_autofree char *cpr_name = vfio_region_cpr_name(region, index);
> +
> + cpr_delete_ramblock(cpr_name);
[Severity: Medium]
Can a stale CPR ramblock entry outlive its BAR mapping?
This is the only place the patch deletes the entry. It is not reached from
vfio_region_exit()->vfio_region_finalize(), which munmap()s the mappings
directly:
hw/vfio/region.c:vfio_region_finalize() {
...
for (i = 0; i < region->nr_mmaps; i++) {
if (region->mmaps[i].mmap) {
munmap(region->mmaps[i].mmap, region->mmaps[i].size);
}
}
...
}
Two paths seem affected.
First, vfio_display_region_update() calls:
vfio_region_exit(&dpy->region.buffer);
vfio_region_finalize(&dpy->region.buffer);
It then calls vfio_region_setup() and vfio_region_mmap() again under the
same name "display". vfio_region_cpr_find() returns the stale address, so
mmap() is skipped and a dead pointer goes to
memory_region_init_ram_device_ptr(). If the new size differs,
assert(ram.length == region->mmaps[index].size) aborts QEMU.
Second, PCI device unplug and re-hotplug goes through
vfio_pci_bars_exit()/vfio_bars_finalize()->vfio_region_finalize(). The
stale entry stays in cpr_state.ramblocks, and the next cpr-exec applies
MADV_DOEXEC to an unmapped or reused range.
The fallback cleanup in qemu_ram_free() does not seem to cover this. In the
display case, vfio_region_finalize() never unparents the mmaps[i].mem
child, so that MemoryRegion is not finalized and qemu_ram_free() never
runs. On unplug, qemu_ram_free() runs from object_property_del_all() during
object finalize. By then the qdev path is already unset (see the VSTOR-100254
comment in qemu_ram_unset_idstr()). cpr_name() then yields
"<region> mmaps[i]" without the device path, which does not match
vfio_region_cpr_name().
Should vfio_region_finalize() also call cpr_delete_ramblock() for each
mapping?
> int vfio_region_mmap(VFIORegion *region)
> {
[ ... ]
> + region->mmaps[i].mmap = vfio_region_cpr_find(region, i);
> + if (region->mmaps[i].mmap) {
> + goto mapped;
> + }
The reuse decision here depends only on whether an entry is present. It
does not check the incoming CPR mode and does not check that the old
mapping still exists. See the comments above on vfio_region_cpr_find() and
vfio_subregion_unmap().
[ ... ]
> + vfio_region_cpr_save(region, i);
>
> +mapped:
> name = g_strdup_printf("%s mmaps[%d]",
> memory_region_name(region->mem), i);
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-2ae91d5b8048@virtuozzo.com?part=20
^ permalink raw reply [relevance 5%]
* Re: [PATCH QEMU HCI-8.1 21/21] VZ: migration/cpr-exec: allow cpr-exec with VFIO devices #VSTOR-103860
@ 2026-10-09 15:34 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:34 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] migration: cpr-exec VFIO blocker removed with no check that RAM is shared/memfd-backed
- [Medium] vfio: removing vfio_devices_present() leaves vfio-user devices unblocked for cpr-exec
- [Low] migration: cpr-exec with VFIO has no up-front check for MADV_DOEXEC support
- [Low] migration: stale hw/vfio/vfio-device.h include left in migration.c
--
commit 3953515702dee287eb17eba476062c856bd30dd8
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
VZ: migration/cpr-exec: allow cpr-exec with VFIO devices #VSTOR-103860
This patch removes the check in migrate_cpr_is_blocked() that rejected
cpr-exec migration whenever any VFIO device was present. It also removes
the now unused vfio_devices_present() helper and its declaration.
> diff --git a/hw/vfio/device.c b/hw/vfio/device.c
> index 5a14a7d5c0a2e..9689f40daa172 100644
> --- a/hw/vfio/device.c
> +++ b/hw/vfio/device.c
> @@ -36,11 +36,6 @@
> VFIODeviceList vfio_device_list =
> QLIST_HEAD_INITIALIZER(vfio_device_list);
>
> -bool vfio_devices_present(void)
> -{
> - return !QLIST_EMPTY(&vfio_device_list);
> -}
> -
> /*
> * We want to differentiate hot reset of multiple in-use devices vs
> * hot reset of a single in-use device. VFIO_DEVICE_RESET will already
[ ... ]
> diff --git a/migration/migration.c b/migration/migration.c
> index ab1f1641c2170..c513427df4367 100644
> --- a/migration/migration.c
> +++ b/migration/migration.c
> @@ -2103,9 +2103,6 @@ static bool migrate_cpr_is_blocked(Error **errp)
> } else if (cbw_filter_present()) {
> error_setg(errp, "CPR is prohibited: active block snapshots (CBW "
> "filter) present");
> - } else if (vfio_devices_present()) {
> - error_setg(errp, "CPR is prohibited: passthrough (VFIO) "
> - "devices present");
> } else {
> res = false;
> }
[Severity: Medium]
Can this corrupt guest memory when the guest RAM is anonymous or private?
With the VFIO branch gone, migrate_cpr_is_blocked() accepts cpr-exec for
any RAM backend. cpr_exec_cb() then calls cpr_walk_ramblock(preserve_ram),
and preserve_ram() only does:
migration/cpr-exec.c:preserve_ram() {
if (qemu_madvise((void *) ram.addr, ram.length, QEMU_MADV_DOEXEC)) {
...
}
It does not check that the backing is shared or memfd. ram_block_add()
in system/physmem.c registers anonymous RAM through cpr_save_ramblock()
without any restriction, and cpr_exec_cb() then calls execvp() with the
VFIO mappings still pinned.
The sibling commit "VZ: physmem: align fd-backed RAM like anonymous RAM"
says anonymous RAM preserved via MADV_DOEXEC and pinned by
VFIO_IOMMU_MAP_DMA is replaced by copies on first guest write after
exec(), while the device keeps doing DMA to the old pinned pages. Would
that leave device DMA and guest memory incoherent?
The remaining VFIO blockers do not seem to cover this. In
hw/vfio/cpr-legacy.c, vfio_cpr_supported() only checks VFIO_UPDATE_VADDR
and VFIO_UNMAP_ALL. In hw/vfio/cpr-iommufd.c, it only checks
IOMMU_IOAS_CHANGE_PROCESS. The qemu_ram_is_shared() check in
hw/vfio/container.c only selects IOMMU_IOAS_MAP_FILE for iommufd, and the
legacy container has no equivalent.
The sibling commit relies on using memory-backend-memfd for such VMs, but
QEMU does not enforce that. Should the blanket VFIO blocker be replaced
with one that rejects cpr-exec when a VFIO device is present and any
RAMBlock is not shared and fd-backed?
[Severity: Low]
Is there an up-front check for MADV_DOEXEC support on the host kernel?
With the VFIO check removed, migrate_cpr_is_blocked() is the only gate
and it no longer verifies this. On a kernel without MADV_DOEXEC support
for shared PFN mappings of device memory, the failure shows up late:
migration/cpr-exec.c:cpr_exec_cb()
cpr_walk_ramblock(preserve_ram)
preserve_ram()
qemu_madvise(..., QEMU_MADV_DOEXEC) fails
migrate_set_state(..., MIGRATION_STATUS_FAILED)
By then the guest has been stopped and its state has been sent. The
rollback at the fail: label in cpr_exec_cb() looks correct, but could a
capability probe be added so that the migrate command is rejected up
front? The dependency appears to be documented only in the commit
message of "VZ: vfio/region: preserve BAR mappings across cpr-exec".
[Severity: Medium]
Does removing vfio_devices_present() leave vfio-user devices unblocked
for cpr-exec?
vfio_user_pci_realize() reaches vfio_device_prepare(), which inserts the
device into vfio_device_list. That list was what vfio_devices_present()
tested, so it used to reject vfio-user devices.
The other blockers do not appear to apply. vfio-user attaches through
vfio_device_attach_by_iommu_type(TYPE_VFIO_IOMMU_USER) rather than the
legacy container, so it never reaches the mdev blocker in
hw/vfio/container-legacy.c:
if (vbasedev->mdev) {
error_setg(&vbasedev->cpr.mdev_blocker,
"CPR does not support vfio mdev %s", vbasedev->name);
if (migrate_add_blocker_modes(&vbasedev->cpr.mdev_blocker, errp,
MIG_MODE_CPR_TRANSFER, MIG_MODE_CPR_EXEC,
-1) < 0) {
A grep for blocker, cpr and CPR in hw/vfio-user/ finds no matches. Can a
cpr-exec migration now proceed with a vfio-user-pci device attached,
even though its connection to the external server has no CPR support?
Should a blocker be added for vfio-user devices?
[Severity: Low]
This isn't a bug, but is the include of hw/vfio/vfio-device.h in
migration/migration.c still needed?
It appears to have been used only for vfio_devices_present(). The
remaining VFIO helpers used in that file, migration_populate_vfio_info()
and migration_reset_vfio_bytes_transferred(), are declared in
migration/migration.h. This has not been confirmed with a build, so
could the include be dropped if it compiles cleanly?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-2ae91d5b8048@virtuozzo.com?part=21
^ permalink raw reply [relevance 4%]
* Re: [PATCH QEMU HCI-8.1 19/21] VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved
2026-10-09 14:56 6% ` [PATCH QEMU HCI-8.1 19/21] VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved Andrey Drobyshev
@ 2026-10-09 15:31 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:31 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] migration: cpr-exec failure rollback does not restore vhost-net ownership
- [Low] migration: unpreserve_ram() ignores MADV_DONTEXEC failure in cpr-exec rollback
--
commit e886be97f560362e9b11744697b4398676717c11
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved
This commit makes cpr_exec_cb() jump to the failure path when
cpr_walk_ramblock(preserve_ram) fails, instead of calling execvp() anyway.
It adds unpreserve_ram() and cpr_exec_unpreserve_ram() to clear
MADV_DOEXEC on the saved ramblocks, and calls them from a shared fail:
label that execvp() failure also uses.
> diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
> --- a/migration/cpr-exec.c
> +++ b/migration/cpr-exec.c
[ ... ]
> +static bool unpreserve_ram(cpr_ramblock_t ram, char *name)
> +{
> +#ifdef MADV_DONTEXEC
> + qemu_madvise((void *) ram.addr, ram.length, MADV_DONTEXEC);
> +#endif
> + return true;
> +}
[Severity: Low]
Can a failed MADV_DONTEXEC go unnoticed here?
The return value of qemu_madvise() in unpreserve_ram() is discarded. If
the call fails for a block that preserve_ram() marked with MADV_DOEXEC, the
block keeps the flag in the surviving old QEMU, and nothing is logged.
By contrast, preserve_ram() reports a MADV_DOEXEC failure with
error_report_err(). The caller, cpr_exec_cb(), also drops the return value
of cpr_walk_ramblock(unpreserve_ram).
Would it be worth at least emitting an error_report() when the rollback
madvise fails?
[ ... ]
> @@ -221,15 +237,17 @@ static void cpr_exec_cb(void *opaque)
> trace_cpr_exec();
> execvp(argv[0], argv);
[ ... ]
> +fail:
> + g_clear_pointer(&argv, g_strfreev);
> + cpr_exec_unpreserve_fds();
> + cpr_exec_unpreserve_ram();
> +
> /* Note, we can go from state COMPLETED to FAILED */
> migration_call_notifiers(MIG_EVENT_PRECOPY_FAILED, NULL);
[Severity: Medium]
Does this failure path restore vhost-net ownership after an execvp()
failure?
Earlier in cpr_exec_cb(), just before execvp(), this is called:
vhost_net_cpr_reset_owners();
trace_cpr_exec();
execvp(argv[0], argv);
vhost_net_cpr_reset_owners() calls vhost_dev_reset_owner() for each kernel
vhost-net device, which issues VHOST_RESET_OWNER.
If execvp() fails, control reaches the fail: label. The cleanup there runs
cpr_exec_unpreserve_fds() and cpr_exec_unpreserve_ram(), but nothing
re-issues VHOST_SET_OWNER. The only caller of vhost_dev_set_owner() in the
net path is vhost_dev_init(), which runs once at device creation.
After vm_start(), virtio-net calls vhost_net_start() on devices that have
no owner. My recollection is that the kernel rejects the vring and memory
ioctls in that state, because vhost_dev_check_owner() requires
dev->mm == current->mm. I have not verified that against the kernel
source.
If that is right, virtio-net would log "unable to start vhost net ... falling
back on userspace virtio", or fail outright for vhost-force setups.
The new early goto fail after preserve_ram() failure runs before
vhost_net_cpr_reset_owners(), so it is not affected. Only the execvp()
failure path is.
Since the commit message says the VM keeps running in the old QEMU on this
path, should the rollback also re-take ownership of the vhost devices?
A diff of migration/cpr-exec.c and hw/net/vhost_net.c from this commit to
the end of the series is empty, so no later patch appears to address this.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-2ae91d5b8048@virtuozzo.com?part=19
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 18/21] VZ: physmem: align fd-backed RAM like anonymous RAM #VSTOR-103860
@ 2026-10-09 15:31 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:31 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] physmem: file_ram_alloc() raises shared mr->align to 2M for all fd RAM, with no compat
- [Medium] physmem: 2M alignment for fd-backed RAM shrinks NVDIMM PMEM size
- [Medium] physmem: 2M align forced on all fd-backed RAM, incl. non-shmem files and offsets
--
commit 06425618e51a0375322bf7c00e7205bad272313c
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
VZ: physmem: align fd-backed RAM like anonymous RAM #VSTOR-103860
This commit raises the alignment of fd-backed RAM blocks in
file_ram_alloc() to QEMU_VMALLOC_ALIGN on all hosts, instead of only on
s390x KVM. The goal is to make memfd and tmpfs backed guest RAM eligible
for 2M transparent huge pages.
> diff --git a/system/physmem.c b/system/physmem.c
> index 6f89232d257f..ae5aa534c60a 100644
> --- a/system/physmem.c
> +++ b/system/physmem.c
> @@ -1476,11 +1476,14 @@ static void *file_ram_alloc(RAMBlock *block,
> return NULL;
> }
> block->mr->align = MAX(block->page_size, block->mr->align);
> -#if defined(__s390x__)
> - if (kvm_enabled()) {
> - block->mr->align = MAX(block->mr->align, QEMU_VMALLOC_ALIGN);
> - }
> -#endif
> +
> + /*
> + * Align like anonymous RAM (qemu_anon_ram_alloc), so that shmem-backed
> + * RAM (memfd, tmpfs files) is eligible for THP.
> + *
> + * On s390x KVM this alignment is required.
> + */
> + block->mr->align = MAX(block->mr->align, QEMU_VMALLOC_ALIGN);
[Severity: Medium]
Can this unconditional bump of block->mr->align reject configurations
that were accepted before? mr->align is the same field returned by
memory_region_get_alignment(), so it is not only an mmap alignment.
memory_device_pre_plug() checks it against the backend size:
hw/mem/memory-device.c:memory_device_pre_plug() {
...
if (!QEMU_IS_ALIGNED(memory_region_size(mr),
memory_region_get_alignment(mr))) {
error_setg(errp, "backend memory size must be multiple of 0x%"
...
}
A memory-backend-file or memfd backend (not hugetlbfs) used with pc-dimm,
virtio-mem, nvdimm or hv-balloon, whose size is a 4K multiple but not a
2M multiple, was accepted before. Would it now fail at plug time with
"backend memory size must be multiple of 0x200000"?
The same function also does:
align = MAX(align, memory_region_get_alignment(mr));
and passes it to memory_device_get_free_addr(). Can this change the
auto-assigned guest-physical address of file-backed DIMMs once earlier
devices have odd sizes, or trigger the "memory fragmented due to
alignments" error?
virtio_mem_device_realize() (vmem->mr->align =
memory_region_get_alignment(&vmem->memdev->mr)) and hv-balloon inherit
the larger alignment as well.
No compat property or machine-type gating is added, and the commit
message does not mention these side effects. Is that intended?
[Severity: Medium]
Can this change shrink the guest-visible NVDIMM size, or make realize
fail, for file-backed NVDIMM backends?
hw/mem/nvdimm.c:nvdimm_prepare_memory_region() {
...
align = memory_region_get_alignment(mr);
...
pmem_size = QEMU_ALIGN_DOWN(pmem_size, align);
if (size <= nvdimm->label_size || !pmem_size) {
...
nvdimm->nvdimm_mr->align = align;
}
With align now 2M instead of 4K, a 1G backend with a 128K label would
expose 1G-2M rather than 1G-128K. A backend smaller than label plus 2M
would hit the "too small to contain nvdimm label and aligned PMEM" error.
The 2M alignment is also carried into nvdimm_mr->align and so into later
memory device placement. Could this break migration or compatibility
with guests expecting the old size? The commit message only discusses
memfd and THP and does not address NVDIMM sizing.
[Severity: Medium]
The commit message and comment justify this change for shmem-backed RAM
(memfd, tmpfs), but the bump applies to every fd, including regular
files, DAX/pmem files, and memory-backend-file with an offset. Should it
be limited to the shmem case?
Non-shmem fds gain nothing from the larger alignment, yet
qemu_ram_mmap() now reserves size + align of address space for them:
util/mmap-alloc.c:qemu_ram_mmap() {
total = size + align;
...
offset = QEMU_ALIGN_UP((uintptr_t)guardptr, align) - (uintptr_t)guardptr;
ptr = mmap_activate(guardptr + offset, size, fd, qemu_map_flags,
map_offset);
...
}
Also, only the virtual address is aligned here. The file offset is
passed unchanged as map_offset, and file_ram_alloc() only requires the
offset to be a multiple of page_size:
area = qemu_ram_mmap(fd, memory, block->mr->align, qemu_map_flags, offset);
A PMD mapping needs the virtual address and file offset to be congruent
modulo 2M. With a memory-backend-file offset that is a 4K multiple but
not a 2M multiple, would this alignment still give no THP benefit?
Downstream, the memory_device_pre_plug() size check and the
nvdimm_prepare_memory_region() QEMU_ALIGN_DOWN(pmem_size, align) now
apply 2M constraints to these file-backed regions as well.
>
> if (memory < block->page_size) {
> error_setg(errp, "memory size 0x" RAM_ADDR_FMT " must be equal to "
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-2ae91d5b8048@virtuozzo.com?part=18
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 16/21] vfio/migration: don't migrate device state during CPR
@ 2026-10-09 15:29 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:29 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] vfio/migration: CPR gating via cpr_incoming_needed() reads persistent parameters.mode
--
commit 1d6b3550509ad85a9b9898cb0af982dc9b67382f
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
vfio/migration: don't migrate device state during CPR
This patch skips VFIO device state migration when the migration mode is
cpr-transfer or cpr-exec. It gates the save_state handler, adds an
is_active callback, and returns early from the two vmstate-change
handlers, all using cpr_incoming_needed().
> diff --git a/hw/vfio/migration.c b/hw/vfio/migration.c
> --- a/hw/vfio/migration.c
> +++ b/hw/vfio/migration.c
[ ... ]
> @@ -857,6 +880,10 @@ static void vfio_vmstate_change_prepare(void *opaque, bool running,
> Error *local_err = NULL;
> int ret;
>
> + if (cpr_incoming_needed(NULL)) {
> + return;
> + }
> +
> new_state = migration->device_state == VFIO_DEVICE_STATE_PRE_COPY ?
> VFIO_DEVICE_STATE_PRE_COPY_P2P :
> VFIO_DEVICE_STATE_RUNNING_P2P;
[Severity: Medium]
Can this early return skip the P2P quiesce step outside of an in-flight
CPR?
cpr_incoming_needed() only checks the configured mode:
migration/cpr.c:cpr_incoming_needed() {
MigMode mode = migrate_mode();
return mode == MIG_MODE_CPR_TRANSFER || mode == MIG_MODE_CPR_EXEC;
}
migrate_mode() uses the incoming mode when set, and otherwise falls back
to the persistent parameters.mode:
migration/options.c:migrate_mode() {
MigMode mode = cpr_get_incoming_mode();
if (mode == MIG_MODE_NONE) {
mode = migrate_get_current()->parameters.mode;
}
...
}
On the destination, vm_start() runs in process_incoming_migration_bh()
before migration_incoming_state_destroy() calls
cpr_set_incoming_mode(MIG_MODE_NONE). After that, only parameters.mode is
seen, and it stays cpr-transfer or cpr-exec. A later plain stop or cont on
the destination would then return early here and in vfio_vmstate_change().
In that case the device is never moved to STOP and could keep running,
and possibly DMAing, while the guest is paused.
The same applies if the mode is changed between a stop and the matching
start. It also applies if a normal migration is started while the VM is
already stopped, where the device would never be moved to STOP_COPY.
The gate works in the intended CPR flow. On the source the mode is set
before the migrate command, and on the destination incoming_mode is still
set during vm_start(). The concern is that these handlers run on every
stop and cont, not only during a CPR.
> @@ -882,6 +909,10 @@ static void vfio_vmstate_change(void *opaque, bool running, RunState state)
> Error *local_err = NULL;
> int ret;
>
> + if (cpr_incoming_needed(NULL)) {
> + return;
> + }
> +
> if (running) {
> new_state = VFIO_DEVICE_STATE_RUNNING;
> } else {
[Severity: Medium]
Does this early return leave the tracked migration->device_state out of
sync with the VM runstate?
The same persistent parameters.mode dependence applies here as in
vfio_vmstate_change_prepare(). A stop or cont while parameters.mode is
still cpr-transfer or cpr-exec would skip the RUNNING/STOP transition.
Is there a per-migration indicator that could be used instead of
cpr_incoming_needed()?
The commit message says "Use cpr_incoming_needed(), which tells exactly
when the device is preserved by CPR, both on the old and the new QEMU
side." Is this accurate?
cpr_incoming_needed() is used elsewhere for vmstate .needed callbacks,
which are evaluated only during a CPR save or load. Here it is used in
handlers that run on every stop and cont.
The series-final state (patches 17 to 21) leaves these gating sites
unchanged: hw/vfio/migration.c in vfio_save_state(), vfio_is_active(),
vfio_vmstate_change_prepare() and vfio_vmstate_change().
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-2ae91d5b8048@virtuozzo.com?part=16
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 17/21] vfio/listener: skip dirty page tracking during CPR
@ 2026-10-09 15:27 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:27 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] vfio: log_global_stop() gated on live migrate_mode(), can leak dirty tracking
- [Low] vfio: log_global_start/stop skip tracking by configured mode, not by CPR migration
--
commit 6ce52b002b526df0523d3f2c394521c467727df4
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
vfio/listener: skip dirty page tracking during CPR
This patch makes vfio_listener_log_global_start() and
vfio_listener_log_global_stop() return early when cpr_incoming_needed()
is true. The goal is to avoid starting or stopping VFIO dirty tracking
in the cpr-transfer and cpr-exec modes, where guest RAM is preserved in
place.
> diff --git a/hw/vfio/listener.c b/hw/vfio/listener.c
> --- a/hw/vfio/listener.c
> +++ b/hw/vfio/listener.c
[ ... ]
> @@ -1012,6 +1013,11 @@ static bool vfio_listener_log_global_start(MemoryListener *listener,
> listener);
> bool ret;
>
> + /* Preserved devices keep DMA-ing into preserved RAM, nothing to track */
> + if (cpr_incoming_needed(NULL)) {
> + return true;
> + }
> +
[Severity: Low]
Is the cpr_incoming_needed(NULL) check the right condition here?
cpr_incoming_needed() returns true whenever migrate_mode() is
MIG_MODE_CPR_TRANSFER or MIG_MODE_CPR_EXEC. migrate_mode() falls back to
the persistent parameter when no incoming mode is set:
migration/options.c:migrate_mode() {
MigMode mode = cpr_get_incoming_mode();
if (mode == MIG_MODE_NONE) {
mode = migrate_get_current()->parameters.mode;
}
...
}
So the check is true whenever a CPR mode is merely configured through
migrate-set-parameters, whether or not a CPR migration is running.
The listener callbacks also run for the other global dirty-log clients.
One example is calc-dirty-rate, and dirty-limit reaches them through
global_dirty_log_change() in migration/dirtyrate.c:
ret = memory_global_dirty_log_start(flag, &local_err);
With a CPR mode configured, those clients no longer start VFIO container
or device dirty tracking. Because vfio_log_sync_needed() requires
vfio_container_dirty_tracking_is_started(), log_sync is skipped too. Can
the dirty-rate and dirty-limit results then undercount writes made by VFIO
devices?
Would a check tied to a running CPR migration, or to the
GLOBAL_DIRTY_MIGRATION flag, be more precise? The commit message says
"during CPR" but does not mention this wider scope.
Also, cpr_incoming_needed() is the incoming-side VMState predicate, and
here it is reused for a source-side decision. That is not obvious to a
reader.
> @@ -1031,6 +1037,10 @@ static void vfio_listener_log_global_stop(MemoryListener *listener)
> Error *local_err = NULL;
> int ret = 0;
>
> + if (cpr_incoming_needed(NULL)) {
> + return;
> + }
> +
[Severity: Medium]
Can this early return leave VFIO dirty tracking running?
The stop is skipped based on the current migration mode, not on whether
tracking was actually started. Consider this sequence:
memory_global_dirty_log_start()
vfio_listener_log_global_start() /* mode is normal, tracking starts */
qmp migrate-set-parameters mode=cpr-transfer (or cpr-exec)
migrate_params_apply()
s->parameters.mode = params->mode; /* no check for active tracking */
memory_global_dirty_log_do_stop()
vfio_listener_log_global_stop()
cpr_incoming_needed(NULL) /* now true */
return;
Neither vfio_devices_dma_logging_stop() nor
vfio_container_set_dirty_page_tracking(bcontainer, false, ...) runs.
vbasedev->dirty_tracking and the container dirty_pages_started state stay
set, and DMA logging stays enabled on the device. That is the vGPU
situation the commit message wants to avoid.
The window can be wide. memory_global_dirty_log_stop() postpones the stop
when the VM is not running:
if (!runstate_is_running()) {
/* Postpone the dirty log stop, e.g., to when VM starts again */
so the mode can also change during that gap.
The reverse case (start skipped under CPR, stop run under a normal mode)
looks harmless, since vfio_devices_dma_logging_stop() checks
vbasedev->dirty_tracking and vfio_container_set_dirty_page_tracking()
returns early when the state already matches.
Could the cpr_incoming_needed() check be dropped from
vfio_listener_log_global_stop(), given that the stop helpers are
idempotent? Alternatively, could start record whether tracking was
actually started, with stop testing that state?
I did not find changes to hw/vfio/listener.c, migration/options.c,
system/memory.c or migration/cpr.c later in the series that address this.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-2ae91d5b8048@virtuozzo.com?part=17
^ permalink raw reply [relevance 4%]
* Re: [PATCH QEMU HCI-8.1 14/21] kvm: tolerate ioctls after kvm_close()
2026-10-09 14:56 22% ` [PATCH QEMU HCI-8.1 14/21] kvm: tolerate ioctls after kvm_close() Andrey Drobyshev
@ 2026-10-09 15:22 7% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:22 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] kvm: kvm_set_irq() still crashes or aborts after kvm_close() on legacy IRQ paths
- [Medium] kvm: irq-route and accessor helpers still deref NULL kvm_state after kvm_close()
- [Medium] kvm: early -ENODEV in kvm_vm_ioctl() leaves errno stale for callers that use errno
--
commit a16b8da8a608436a2f83533cd5dc4b33bbf1644c
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
kvm: tolerate ioctls after kvm_close()
This patch makes kvm_ioctl(), kvm_vm_ioctl() and kvm_vcpu_ioctl() return
-ENODEV once kvm_close() has run. kvm_send_msi() is also changed so that
-ENODEV does not print the "MSI lost" warning. The aim is to stop a retired
cpr-transfer source from crashing on late interrupts.
> diff --git a/accel/kvm/kvm-all.c b/accel/kvm/kvm-all.c
> --- a/accel/kvm/kvm-all.c
> +++ b/accel/kvm/kvm-all.c
[ ... ]
> @@ -3389,6 +3394,11 @@ int kvm_vm_ioctl(KVMState *s, unsigned long type, ...)
> arg = va_arg(ap, void *);
> va_end(ap);
>
> + /* kvm_close() has been called: there is no VM to talk to any more */
> + if (!s || s->vmfd == -1) {
> + return -ENODEV;
> + }
> +
> trace_kvm_vm_ioctl(type, arg);
[Severity: Medium]
The early return here, and the matching ones in kvm_ioctl() and
kvm_vcpu_ioctl(), never call ioctl(), so errno is not set. Some callers
look at errno after a negative return instead of using the returned value.
kvm_set_ioeventfd_mmio() is one of them:
ret = kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &iofd);
if (ret < 0) {
return -errno;
}
return 0;
After kvm_close(), can this return 0 (silent success) if errno happens to
be 0, or an unrelated stale errno otherwise? Its callers
kvm_mem_ioeventfd_add() and kvm_mem_ioeventfd_del() call abort() on any
negative result:
r = kvm_set_ioeventfd_mmio(fd, section->offset_within_address_space,
data, false, int128_get64(section->size),
match_data);
if (r < 0) {
fprintf(stderr, "%s: error deleting ioeventfd: %s (%d)\n",
__func__, strerror(-r), -r);
abort();
So a late ioeventfd removal on the retired source, for example from a
memory region commit during device teardown, could still end in abort()
instead of failing gracefully.
kvm_create_guest_memfd() has the same pattern:
fd = kvm_vm_ioctl(kvm_state, KVM_CREATE_GUEST_MEMFD, &guest_memfd);
if (fd < 0) {
error_setg_errno(errp, errno, "Error creating KVM guest_memfd");
This reports errno rather than -ENODEV. kvm_set_memory_attributes() uses
strerror(errno) in the same way.
The KVM_GET_STATS_FD callers test "== -1", which -ENODEV does not match, so
they would carry on with stats_fd set to -19.
Would setting errno = ENODEV before each early return, or changing these
callers to use the returned value, be more robust?
[Severity: Medium]
Separately, the commit message says "any late interrupt or query on the
retired source fails gracefully". Does that hold for the helpers that
dereference kvm_state before any ioctl wrapper runs? kvm_close() sets
kvm_state to NULL, but kvm_enabled() stays true:
accel/accel-irq.c:accel_irqchip_release_virq() {
if (kvm_enabled()) {
kvm_irqchip_release_virq(kvm_state, virq);
}
...
}
accel/kvm/kvm-all.c:kvm_irqchip_release_virq() {
...
for (i = 0; i < s->irq_routes->nr; i++) {
...
}
With s == NULL, this faults on s->irq_routes before the new "!s" check can
be reached. The only early return is kvm_gsi_direct_mapping().
The same applies to kvm_irqchip_commit_routes(), which writes
s->irq_routes->flags. It is reached via accel_irqchip_commit_routes() from
the virtio-pci unmask path and from the ioapic code.
kvm_irqchip_update_msi_route() ends in kvm_update_routing_entry(s, ...),
which reads s->irq_routes->nr.
The plain accessors kvm_kernel_irqchip_split(), kvm_kernel_irqchip_allowed(),
kvm_kernel_irqchip_required(), kvm_dirty_ring_size(), kvm_has_sync_mmu() and
kvm_has_vcpu_events() read kvm_state->field with no NULL check. Only
kvm_dirty_ring_enabled() has a "kvm_state &&" guard.
One reachable caller is the virtio-pci teardown path:
hw/virtio/virtio-pci.c:kvm_virtio_pci_vq_vector_release() {
...
accel_irqchip_release_virq(irqfd->virq);
...
}
This could be reached through set_guest_notifiers(false), device reset or
device_del on the retired source, since QMP is still served. I did not trace
one specific exit or QMP sequence that reaches an unguarded helper.
Nothing later in the series appears to guard these helpers. The follow-up
patch "kvm: fix coalesced MMIO flush racing with kvm_close()" only adds
"kvm_state->coalesced_mmio_ring = NULL; drain_call_rcu();" to kvm_close().
Should these helpers check for a closed kvm_state as well?
[Severity: Medium]
The commit message claim that any late interrupt fails gracefully also looks
too broad for the legacy IRQ path. kvm_set_irq() evaluates s->irq_set_ioctl
before kvm_vm_ioctl() is entered:
accel/kvm/kvm-all.c:kvm_set_irq() {
...
ret = kvm_vm_ioctl(s, s->irq_set_ioctl, &event);
if (ret < 0) {
perror("kvm_set_irq");
abort();
}
return (s->irq_set_ioctl == KVM_IRQ_LINE) ? 1 : event.status;
}
After kvm_close(), can a late level-IRQ change reach this through the
callers below, all of which pass kvm_state?
hw/i386/kvm/ioapic.c:kvm_ioapic_set_irq()
delivered = kvm_set_irq(kvm_state, s->kvm_gsi_base + irq, level);
hw/i386/kvm/i8259.c:kvm_pic_set_irq()
delivered = kvm_set_irq(kvm_state, irq, level);
hw/intc/ioapic.c:ioapic_update_kvm_routes()
kvm_set_irq(kvm_state, i, 1);
With s == NULL, the s->irq_set_ioctl read faults before the new guard runs.
If s were non-NULL, the new -ENODEV return would reach perror() followed by
abort(). Either way the retired source would still die with SIGSEGV or
SIGABRT and leave a core dump, as the commit message describes for the MSI
path.
I did not trace a concrete device that drives these legacy lines on a
retired source. Could kvm_set_irq() and its callers be made to tolerate the
closed state too?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-2ae91d5b8048@virtuozzo.com?part=14
^ permalink raw reply [relevance 7%]
* Re: [PATCH QEMU HCI-8.1 13/21] migration: Fix crash on second migration when cancel early
2026-10-09 14:56 23% ` [PATCH QEMU HCI-8.1 13/21] migration: Fix crash on second migration when cancel early Andrey Drobyshev
@ 2026-10-09 15:20 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:20 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] migration: stale comment in qmp_migrate_finish_cb() describes a removed call
--
Commit ea2a5dd391e35be2ede79c38ea1ba67a80a4cdb1
Author: Peter Xu <peterx@redhat.com>
migration: Fix crash on second migration when cancel early
This commit changes migration_cancel() so that it calls migration_cleanup()
only when the cpr-transfer HUP gsource is still pending. It also makes
qmp_migrate_finish_cb() destroy the HUP source on entry, and it adds
assertions around the hup_source handling.
Link: https://lore.kernel.org/r/20260417184742.293061-1-marcandre.lureau@redhat.com
Link: https://lore.kernel.org/r/20260421175820.302795-1-peterx@redhat.com
> diff --git a/migration/migration.c b/migration/migration.c
> index 81d5d4dd4c06..ab1f1641c217 100644
> --- a/migration/migration.c
> +++ b/migration/migration.c
[ ... ]
> @@ -2246,11 +2251,22 @@ static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
> void *opaque)
> {
> MigrationAddress *addr = opaque;
> + MigrationState *s = migrate_get_current();
> +
> + /*
> + * Detach and release the GSource right after use. We rely on this to
> + * detect this small cpr-transfer window of "waiting for HUP event".
> + */
> + cpr_transfer_source_destroy(s);
>
> qmp_migrate_finish(addr, NULL);
>
> cpr_state_close();
> - cpr_transfer_source_destroy(migrate_get_current());
> +
> + /*
> + * This is redundant as we do cpr_transfer_source_destroy() at the
> + * entry, but it's benign; glib will just skip the detach.
> + */
> return G_SOURCE_REMOVE;
> }
[Severity: Low]
This isn't a bug, but the comment placed just before return G_SOURCE_REMOVE
in qmp_migrate_finish_cb() describes code that isn't there. It says that a
redundant cpr_transfer_source_destroy() is done at entry and that glib will
skip the detach.
In this backport, the trailing cpr_transfer_source_destroy(migrate_get_current())
was removed. The only destroy call is now the one at function entry, and no
second call follows the comment.
Could the comment be dropped or reworded? It could say that the source was
already destroyed at entry, so returning G_SOURCE_REMOVE is harmless. As it
stands it may mislead readers. Runtime behaviour is correct.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-2ae91d5b8048@virtuozzo.com?part=13
^ permalink raw reply [relevance 4%]
* Re: [PATCH QEMU HCI-8.1 12/21] migration: Move CPR HUP watch to cpr-transfer.c
2026-10-09 14:56 28% ` [PATCH QEMU HCI-8.1 12/21] migration: Move CPR HUP watch to cpr-transfer.c Andrey Drobyshev
@ 2026-10-09 15:18 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:18 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] migration: cpr_transfer_add_hup_watch() takes untyped opaque but needs MigrationAddress
--
commit c7d2819ddcd0f6868aa501a95d87ca2adba9d5d9
Author: Fabiano Rosas <farosas@suse.de>
migration: Move CPR HUP watch to cpr-transfer.c
This commit moves the CPR HUP watch helpers out of migration.c into
cpr-transfer.c and renames them cpr_transfer_add_hup_watch() and
cpr_transfer_source_destroy(). Callers in migration_cleanup(),
migration_cancel(), qmp_migrate_finish_cb() and qmp_migrate() are
updated to use the new names.
Link: https://lore.kernel.org/qemu-devel/20260123141656.6765-24-farosas@suse.de
> diff --git a/include/migration/cpr.h b/include/migration/cpr.h
> --- a/include/migration/cpr.h
> +++ b/include/migration/cpr.h
[ ... ]
> @@ -76,6 +77,10 @@ int cpr_get_fd_param(const char *name, const char *fdname, int index, bool cpr,
> QEMUFile *cpr_transfer_output(MigrationChannel *channel, Error **errp);
> QEMUFile *cpr_transfer_input(MigrationChannel *channel, Error **errp);
>
> +void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
> + void *opaque);
> +void cpr_transfer_source_destroy(MigrationState *s);
[Severity: Low]
This isn't a bug today, but the prototype of cpr_transfer_add_hup_watch()
takes a generic "void *opaque", while the implementation in
migration/cpr-transfer.c always does
QAPI_CLONE(MigrationAddress, opaque) and registers
qapi_free_MigrationAddress as the destroy notifier.
Could the parameter be declared as "MigrationAddress *addr" instead? A
future caller passing any other pointer type would compile cleanly, and
the function would then clone and free the wrong object.
The only caller is qmp_migrate(), which passes addr, a MigrationAddress *,
so behaviour is preserved.
> diff --git a/migration/cpr-transfer.c b/migration/cpr-transfer.c
> --- a/migration/cpr-transfer.c
> +++ b/migration/cpr-transfer.c
[ ... ]
> +void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
> + void *opaque)
> +{
> + s->hup_source = qio_channel_create_watch(cpr_state_ioc(), G_IO_HUP);
> + g_source_set_callback(s->hup_source,
> + (GSourceFunc)func,
> + QAPI_CLONE(MigrationAddress, opaque),
> + (GDestroyNotify)qapi_free_MigrationAddress);
> + g_source_attach(s->hup_source, NULL);
> +}
The same untyped "void *opaque" appears here, and the contract that it
must be a MigrationAddress is only visible inside the function body.
The old migrate_hup_add() also took "void *opaque", and the series
through 3953515702de does not change this, so a typed parameter would
have to be done here.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-2ae91d5b8048@virtuozzo.com?part=12
^ permalink raw reply [relevance 4%]
* Re: [PATCH QEMU HCI-8.1 09/21] migration: Fix regression of passing error_fatal into vmstate_load_state()
2026-10-09 14:56 12% ` [PATCH QEMU HCI-8.1 09/21] migration: Fix regression of passing error_fatal into vmstate_load_state() Andrey Drobyshev
@ 2026-10-09 15:17 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:17 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] migration: vmstate_save_state_v() calls error_setg() on an errp already set by the nested call
- [Low] migration: void vmstate save helpers log failure but never flag the QEMUFile
--
Commit e12869dadb039e65c940e64b56a6161151d7d971
Author: Arun Menon <armenon@redhat.com>
migration: Fix regression of passing error_fatal into vmstate_load_state()
This replaces &error_fatal in the vmstate_save_state() and
vmstate_load_state() callers with a local Error object. Errors are now
reported with error_report_err() and the return value is propagated where
the caller has one.
Link: https://lore.kernel.org/r/20251028-solve_error_fatal_regression-v2-1-dab24c808a28@redhat.com
> diff --git a/hw/pci/pci.c b/hw/pci/pci.c
> --- a/hw/pci/pci.c
> +++ b/hw/pci/pci.c
[ ... ]
> void pci_device_save(PCIDevice *s, QEMUFile *f)
> {
> + Error *local_err = NULL;
> + int ret;
> +
> /* Clear interrupt status bit: it is implicit
> * in irq_state which we are saving.
> * This makes us compatible with old devices
> * which never set or clear this bit. */
> s->config[PCI_STATUS] &= ~PCI_STATUS_INTERRUPT;
> - vmstate_save_state(f, &vmstate_pci_device, s, NULL, &error_fatal);
> + ret = vmstate_save_state(f, &vmstate_pci_device, s, NULL, &local_err);
> + if (ret < 0) {
> + error_report_err(local_err);
> + }
> /* Restore the interrupt status bit. */
> pci_update_irq_status(s);
> }
[Severity: Medium]
Can this abort QEMU through the assertion in error_setv()?
In vmstate_save_state_v(), a nested VMS_STRUCT, VMS_VSTRUCT or subsection
save is called with the same errp:
if (inner_field->flags & VMS_STRUCT) {
ret = vmstate_save_state(f, inner_field->vmsd, curr_elem,
vmdesc_loop, errp);
}
...
if (ret) {
error_setg(errp, "Save of field %s/%s failed",
vmsd->name, field->name);
If the nested call fails, it has already set *errp. The outer loop then
calls error_setg() on the same errp, which reaches:
assert(*errp == NULL);
With &error_fatal, QEMU exited before this point. With a real Error * as
passed here, the second error_setg() looks like it would hit the assertion
instead of propagating the error.
The same applies to the other converted save callers in this patch, such
as virtio_pci_save_extra_state() and virtio_save(). vmstate_save_state_v()
does not appear to use ERRP_GUARD or check for an already set error.
Could the outer error_setg() be skipped when *errp is already set, or could
error_prepend() be used there instead?
[Severity: Low]
Is logging the error here enough?
pci_device_save() returns void and does not call qemu_file_set_error().
vmstate_save_state_v() only does error_setg() and returns ret on failure,
unlike the load path, which calls qemu_file_set_error(). So the QEMUFile
error state stays clean after a failed save.
The source then continues writing the stream, and the failure would show
up later as a load error on the destination. Before this patch, QEMU
exited at the point of failure.
> diff --git a/hw/scsi/spapr_vscsi.c b/hw/scsi/spapr_vscsi.c
> --- a/hw/scsi/spapr_vscsi.c
> +++ b/hw/scsi/spapr_vscsi.c
[ ... ]
> + rc = vmstate_save_state(f, &vmstate_spapr_vscsi_req, req, NULL, &local_err);
> + if (rc < 0) {
> + error_report_err(local_err);
> + return;
> + }
[Severity: Low]
Does vscsi_save_request() leave its caller unaware of the failed save?
This void function reports the error and returns. Nothing calls
qemu_file_set_error(f, rc), so the caller continues as if the save
succeeded.
> diff --git a/hw/virtio/virtio-pci.c b/hw/virtio/virtio-pci.c
> --- a/hw/virtio/virtio-pci.c
> +++ b/hw/virtio/virtio-pci.c
[ ... ]
> static void virtio_pci_save_extra_state(DeviceState *d, QEMUFile *f)
> {
> VirtIOPCIProxy *proxy = to_virtio_pci_proxy(d);
> + Error *local_err = NULL;
> + int ret;
>
> - vmstate_save_state(f, &vmstate_virtio_pci, proxy, NULL, &error_fatal);
> + ret = vmstate_save_state(f, &vmstate_virtio_pci, proxy, NULL, &local_err);
> + if (ret < 0) {
> + error_report_err(local_err);
> + }
> }
[Severity: Low]
Does this void save callback have the same gap as pci_device_save()?
The failure is only logged and the QEMUFile is not flagged with
qemu_file_set_error(). The same pattern appears in
virtio_mmio_save_extra_state() and virtio_ccw_save_config().
Would it make sense for vmstate_save_state_v() or these helpers to set the
QEMUFile error on failure?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-2ae91d5b8048@virtuozzo.com?part=9
^ permalink raw reply [relevance 5%]
* Re: [PATCH QEMU HCI-8.1 10/21] migration: yank: Move register instance earlier
2026-10-09 14:56 20% ` [PATCH QEMU HCI-8.1 10/21] migration: yank: Move register instance earlier Andrey Drobyshev
@ 2026-10-09 15:16 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:16 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] migration: yank instance leaked when qmp_migrate_finish() fails synchronously, aborting next migrate
- [Medium] migration: unconditional yank_unregister_instance() in qmp_migrate() out: path on resume
--
commit 8b6d573d4b1ea369ae6968e2938e2e1b1c4366c3
Author: Fabiano Rosas <farosas@suse.de>
migration: yank: Move register instance earlier
This commit moves yank_register_instance() from qmp_migrate_finish() into
migrate_prepare(), so that it can be paired with the unregister in
migration_cleanup(). It also drops the resume_requested argument from
qmp_migrate_finish() and adds a yank_unregister_instance() call to the
local_err path at the end of qmp_migrate().
Link: https://lore.kernel.org/qemu-devel/20260123141656.6765-11-farosas@suse.de
> diff --git a/migration/migration.c b/migration/migration.c
> --- a/migration/migration.c
> +++ b/migration/migration.c
[ ... ]
> @@ -2235,11 +2235,12 @@ static bool migrate_prepare(MigrationState *s, bool resume, Error **errp)
> return false;
> }
>
> + yank_register_instance(MIGRATION_YANK_INSTANCE, &error_abort);
> +
> return true;
> }
[Severity: High]
Can this call abort QEMU on a retry after a failed migrate command?
If a previous attempt failed synchronously in qmp_migrate_finish(), the
instance registered here is never unregistered (see the comments below).
The state is then FAILED, so migration_is_running() is false and the next
migrate passes the checks in migrate_prepare().
yank_register_instance() then finds the stale entry via yank_find_entry()
and fails with "duplicate yank instance". Because &error_abort is passed,
QEMU aborts.
A privileged QMP client can reach this with an fd: URI naming a
nonexistent fd name, an exec: command that cannot be spawned, or a file:
target that cannot be opened, followed by any further migrate.
[ ... ]
> @@ -2354,28 +2353,22 @@ void qmp_migrate(const char *uri, bool has_channels,
> QAPI_CLONE(MigrationAddress, addr));
>
> } else {
> - qmp_migrate_finish(addr, resume_requested, errp);
> + qmp_migrate_finish(addr, errp);
> }
>
> out:
> if (local_err) {
> + yank_unregister_instance(MIGRATION_YANK_INSTANCE);
> migration_connect_set_error(s, local_err);
> error_propagate(errp, local_err);
> }
> }
[Severity: High]
Is this unregister reached when qmp_migrate_finish() fails?
qmp_migrate_finish() is passed errp rather than &local_err, so only a
cpr_state_save() failure sets local_err in qmp_migrate(). Failures from
qmp_migrate_finish() never reach this yank_unregister_instance() call.
[Severity: Medium]
Separately, this unregister is not conditioned on this call having
registered the instance. When resume is requested, migrate_prepare()
takes the resume branch and returns true before yank_register_instance().
cpr_state_save() looks only at migrate_mode() and not at the resume flag.
If mode is cpr-exec or cpr-transfer and cpr_state_save() fails (for
example cpr_exec_output() failing), the out: block calls
yank_unregister_instance() on the instance owned by the paused postcopy
migration.
Would the later migration_cleanup() then call yank_unregister_instance()
again and hit assert(entry)? The old code guarded both register and
unregister with !resume_requested. Should the unregister here be skipped
for resume?
> -static void qmp_migrate_finish(MigrationAddress *addr, bool resume_requested,
> - Error **errp)
> +static void qmp_migrate_finish(MigrationAddress *addr, Error **errp)
> {
> MigrationState *s = migrate_get_current();
> Error *local_err = NULL;
>
> - if (!resume_requested) {
> - if (!yank_register_instance(MIGRATION_YANK_INSTANCE, errp)) {
> - return;
> - }
> - }
> -
> if (addr->transport == MIGRATION_ADDRESS_TYPE_SOCKET) {
[ ... ]
> if (local_err) {
> - if (!resume_requested) {
> - yank_unregister_instance(MIGRATION_YANK_INSTANCE);
> - }
> migration_connect_set_error(s, local_err);
> error_propagate(errp, local_err);
> return;
[Severity: High]
Does this error path now leak the yank instance?
The removed yank_unregister_instance() was the only release on this path.
migration_connect_set_error() only calls migrate_set_state() to FAILED and
migrate_set_error(); it does not call migration_cleanup().
When the start function fails before any channel exists, for example
fd_start_outgoing_migration() returning after monitor_get_fd() == -1,
neither migration_channel_connect() nor migration_connect() runs, so
migration_cleanup() is never reached. Does the instance registered in
migrate_prepare() then stay registered?
The same applies to the cpr-transfer deferred path:
qmp_migrate_finish_cb()
qmp_migrate_finish(addr, NULL);
cpr_state_close();
migrate_hup_delete(migrate_get_current());
No yank unregister is done there after a start failure either.
The later patches in the series change the HUP watch handling and
migration_cancel(), but do not change the yank handling in
qmp_migrate_finish() or qmp_migrate_finish_cb(). The issue is therefore
not fixed within the series.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-2ae91d5b8048@virtuozzo.com?part=10
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 07/21] q35: Fix migration of SMRAM state
2026-10-09 14:56 4% ` [PATCH QEMU HCI-8.1 07/21] q35: Fix migration of SMRAM state Andrey Drobyshev
@ 2026-10-09 15:12 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:12 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] q35: mch_update_smbase_smram() locks SMBASE on a pre-handshake write with bit 1 set
--
commit ba3fa220214fc6ee0a78056885c0b2f72b87e024
Author: Igor Mammedov <imammedo@redhat.com>
q35: Fix migration of SMRAM state
This patch stops mch_update_smbase_smram() from clearing the migrated
MCH_HOST_BRIDGE_F_SMBASE value. It re-derives wmask from the register
value instead, so a locked SMRAM state survives migration. It also
removes the qtest check that junk written before the handshake reads
back as 0.
Link: https://patchew.org/QEMU/20251203180851.6390-1-arbn@yandex-team.com/
> diff --git a/hw/pci-host/q35.c b/hw/pci-host/q35.c
> --- a/hw/pci-host/q35.c
> +++ b/hw/pci-host/q35.c
[ ... ]
> /*
> - * default/reset state, discard written value
> - * which will disable SMRAM balackhole at SMBASE
> + * reg value can come from register write/reset/migration source,
> + * update wmask to be in sync with it regardless of source
> */
> - if (pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] == 0xff) {
> - *reg = 0x00;
> + if (*reg == MCH_HOST_BRIDGE_F_SMBASE_IN_RAM) {
> + pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = MCH_HOST_BRIDGE_F_SMBASE_LCK;
> + return;
> }
> -
> - memory_region_transaction_begin();
> if (*reg & MCH_HOST_BRIDGE_F_SMBASE_LCK) {
> - /* disable all writes */
> - pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] &=
> - ~MCH_HOST_BRIDGE_F_SMBASE_LCK;
> + /* lock register at 0x2 and disable all writes */
> + pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = 0;
> *reg = MCH_HOST_BRIDGE_F_SMBASE_LCK;
> - lck = true;
> - } else {
> - lck = false;
> }
> +
> + lck = *reg & MCH_HOST_BRIDGE_F_SMBASE_LCK;
> + memory_region_transaction_begin();
> memory_region_set_enabled(&mch->smbase_blackhole, lck);
> memory_region_set_enabled(&mch->smbase_window, lck);
> memory_region_transaction_commit();
[Severity: Low]
Can a guest now lock SMBASE without doing the 0xff handshake? After
mch_reset(), config[0x9c] is 0 and wmask[0x9c] is 0xff. A guest config
write of 0x02, 0x03 or 0xfe is stored by pci_default_write_config(),
because wmask is 0xff.
mch_write_config() then calls mch_update_smbase_smram(). The value is
neither 0xff nor 0x01, so it reaches this branch:
if (*reg & MCH_HOST_BRIDGE_F_SMBASE_LCK) {
pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = 0;
*reg = MCH_HOST_BRIDGE_F_SMBASE_LCK;
}
wmask becomes 0 and *reg becomes 0x02. smbase_blackhole and
smbase_window are then enabled, so the 128K SMRAM window at SMBASE is
black-holed until machine reset.
The old wmask == 0xff check discarded such a write and set *reg to 0.
Other junk values are now stored as written, with no effect.
Only the writing guest is affected, and a reset clears it. Firmware such
as SeaBIOS and OVMF uses the 0xff query handshake first, so it is
unaffected. The commit message describes this trade-off, and the change
matches upstream QEMU 66cf169e29b0. Is this guest-visible change
intended?
The migration fix itself looks correct. mch_post_load() calls
mch_update(), which calls mch_update_smbase_smram() and re-derives wmask
from the migrated register value. A value of 0x01 gives wmask 0x02 with
no black hole. A value of 0x02 gives wmask 0 with the black hole
enabled. A value of 0 keeps the reset wmask of 0xff.
> diff --git a/tests/qtest/q35-test.c b/tests/qtest/q35-test.c
> --- a/tests/qtest/q35-test.c
> +++ b/tests/qtest/q35-test.c
> @@ -206,12 +206,6 @@ static void test_smram_smbase_lock(void)
> qtest_writeb(qts, SMBASE, SMRAM_TEST_PATTERN);
> g_assert_cmpint(qtest_readb(qts, SMBASE), ==, SMRAM_TEST_PATTERN);
>
> - /* check that writing junk to 0x9c before before negotiating is ignored */
> - for (i = 0; i < 0xff; i++) {
> - qpci_config_writeb(pcidev, MCH_HOST_BRIDGE_F_SMBASE, i);
> - g_assert(qpci_config_readb(pcidev, MCH_HOST_BRIDGE_F_SMBASE) == 0);
> - }
> -
[Severity: Low]
This isn't a bug, but the removed check is not replaced by anything.
test_smram_smbase_lock() no longer covers a junk write before the
handshake, or the new lock-on-junk behaviour where a value with bit 1
set locks the register. No test covers migration of the SMRAM state
either.
Could a test be added for these cases?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-2ae91d5b8048@virtuozzo.com?part=7
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.1 01/21] migration: Plug memory leaks after migrate_set_error()
2026-10-09 14:56 13% ` [PATCH QEMU HCI-8.1 01/21] migration: Plug memory leaks after migrate_set_error() Andrey Drobyshev
@ 2026-10-09 15:09 0% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:09 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev
> From: Markus Armbruster <armbru@redhat.com>
>
> migrate_set_error(s, err) stores a copy of @err in @s. The original
> @err is not freed. Most callers free it immediately. Some callers
> free it later, or pass it on. And some leak it. Fix those.
>
> Perhaps migrate_set_error(s, err) should take ownership of @err. The
> callers that free it immediately would become simpler, and avoid a
> copy and a deallocation. The others would have to pass
> error_copy(err).
>
> andrey.drobyshev:
> This commit is cherry-picked as-is, with no modifications, from the
> upstream commit 93817ec39656.
>
> Signed-off-by: Markus Armbruster <armbru@redhat.com>
> Link: https://lore.kernel.org/r/20251115083500.2753895-2-armbru@redhat.com
> Signed-off-by: Peter Xu <peterx@redhat.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-2ae91d5b8048@virtuozzo.com?part=1
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH RFC PATCH hci-8.1 3/4] mm: move MADV_DOEXEC memory instead of copying it
@ 2026-10-09 15:08 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:08 UTC (permalink / raw)
To: Andrey Drobyshev, Konstantin Khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] mm: vma_dup() copies vm_file without get_file() for private /dev/zero VMAs
- [Low] mm: vma_dup() discards ksm_madvise() flags so VM_MERGEABLE stays on moved VMA
- [Low] mm: padata workers serialize on anon_vma write lock in move_ptes()
--
commit b89406aa304220e73194db3c04662412c6cbfe44
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
mm: move MADV_DOEXEC memory instead of copying it
This commit changes MADV_DOEXEC preservation across exec from a
fork-style copy to a move of the page tables into the new mm. vma_dup()
now clones the anon_vma chain, unmerges KSM, drops mlock state, and calls
move_page_range_exec(). The parallel copy code and the exec-specific
pinned-page exception in the rmap dup path are removed.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-103860
> diff --git a/mm/mmap.c b/mm/mmap.c
> --- a/mm/mmap.c
> +++ b/mm/mmap.c
[ ... ]
> int vma_dup(struct vm_area_struct *old_vma, struct mm_struct *mm)
> {
> struct vm_area_struct *vma;
> unsigned long npages;
> + vm_flags_t flags = old_vma->vm_flags;
> int ret = -ENOMEM;
[ ... ]
> + /* KSM tracks mappings by mm and address; unmerge before moving them. */
> + ret = ksm_madvise(old_vma, old_vma->vm_start, old_vma->vm_end,
> + MADV_UNMERGEABLE, &flags);
> + if (ret)
> + return ret;
[Severity: Low]
Is the result of this ksm_madvise() call ever applied?
ksm_madvise() clears VM_MERGEABLE only through its vm_flags output
argument:
mm/ksm.c:ksm_madvise() {
...
*vm_flags &= ~VM_MERGEABLE;
...
}
Here that argument is the local variable flags, which is never written
back to old_vma->vm_flags and is not used afterwards.
vm_area_dup() then copies the original vm_flags into the new VMA, and the
later vm_flags_clear() on the new VMA does not include VM_MERGEABLE:
> + vm_flags_clear(vma, VM_LOCKED_MASK | VM_UFFD_MISSING | VM_UFFD_WP |
> + VM_UFFD_MINOR | VM_EXEC_KEEP);
[ ... ]
> + if (!test_bit(MMF_VM_MERGEABLE, &mm->flags))
> + ksm_fork(mm, old_vma->vm_mm);
> + return move_page_range_exec(vma, old_vma);
For a VMA with both MADV_DOEXEC and MADV_MERGEABLE, ksm_fork() registers
the new mm with ksmd while the moved VMA still has VM_MERGEABLE set.
Could ksmd then scan and re-merge pages in the moved VMA?
The commit message says the patch will "unmerge KSM". Is the intent to
leave the moved VMA KSM-eligible, as fork() does, or to disable KSM on it?
If the latter, should VM_MERGEABLE be cleared on the new VMA, for example
by adding it to the vm_flags_clear() call?
> + /*
> + * Transfer the reservation to the linked destination VMA. If moving
> + * page tables fails partway through, exec cannot resume the old image:
> + * it must terminate and tear down both mms. Each mm then frees its
> + * remaining mappings, and the destination releases the reservation.
> */
> - __vm_flags_mod(old_vma, 0, VM_ACCOUNT);
> + vm_flags_clear(old_vma, VM_ACCOUNT);
[ ... ]
> vma = vm_area_dup(old_vma);
> if (!vma)
> goto fail_nomem;
[Severity: Critical]
Can this leave two VMAs holding a single reference on vm_file?
vm_area_dup() memcpy()s the VMA, including vm_file, and takes no file
reference. Nothing later in vma_dup() calls get_file() or clears vm_file,
and vma_link() then calls vma_link_file() on the new VMA.
The MADV_DOEXEC gate in madvise_vma_behavior() only requires
vma_is_anonymous() and rejects VM_EXEC, VM_SHARED and VM_STACK. It does
not reject vm_file != NULL. A private mapping of /dev/zero passes that
gate, because mmap_zero() calls vma_set_anonymous() and leaves vm_file
set:
drivers/char/mem.c:mmap_zero() {
...
if (vma->vm_flags & VM_SHARED)
return shmem_zero_setup(vma);
vma_set_anonymous(vma);
return 0;
}
The call chain would then be:
exec_mmap()
vma_dup_some()
vma_dup()
vm_area_dup() /* copies vm_file, no get_file() */
vma_link() /* new VMA now in the file's i_mmap */
Each mm's teardown then reaches remove_vma():
mm/vma.c:remove_vma() {
...
if (vma->vm_file)
fput(vma->vm_file);
}
That would call fput() twice for one reference. Should vma_dup() either
take get_file() when vma->vm_file is set, or reject VMAs with a vm_file in
the MADV_DOEXEC check?
The old vma_dup() had the same vm_area_dup() path. However, this patch
rewrites the function and builds the new move semantics on it, so it seemed
worth raising here.
> diff --git a/mm/mremap.c b/mm/mremap.c
[ ... ]
> static int move_page_range_chunk(unsigned long addr, unsigned long end, void *arg)
> {
> ...
> return move_page_tables(args->old_vma, addr, args->new_vma, addr,
> len, true, false) == len ? 0 : -ENOMEM;
> }
[Severity: Low]
Is the 16-thread padata job in move_page_range_exec() able to scale here?
Every worker calls move_page_tables() with need_rmap_locks set to true.
For each PMD extent, move_ptes() then does:
move_page_range_chunk()
move_page_tables(..., need_rmap_locks = true, ...)
move_ptes()
take_rmap_locks()
anon_vma_lock_write(vma->anon_vma)
The anon_vma write lock is held across the per-PMD PTE move loop and the
TLB flush, and is dropped only in drop_rmap_locks() at the out label. All
workers operate on the same old_vma, so they take the same anon_vma root
rwsem for write.
Does this mostly serialize the workers, so that padata_do_multithreaded()
with max_threads = 16 gives little speedup over a single thread? I don't
see a correctness problem. Has the performance relative to the old
copy_page_range_mt() path been measured?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-1901cd6c3c74@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 RFC PATCH hci-8.1 4/4] selftests/mm: test memory preservation across exec
2026-10-09 14:00 4% ` [Devel] [PATCH RFC PATCH hci-8.1 4/4] selftests/mm: test memory preservation across exec Andrey Drobyshev
@ 2026-10-09 15:07 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:07 UTC (permalink / raw)
To: Konstantin Khorenko, Andrey Drobyshev; +Cc: devel
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Medium] selftests/mm: vhost_owner_exec() closes the fd, so VHOST_RESET_OWNER is not tested
- [Low] selftests/mm: doexec binary missing from .gitignore
- [Low] selftests/mm: doexec.c ELF note asm uses @note, breaks on ARM
- [Low] selftests/mm: doexec.c ignores write()/pwrite() return values
- [Low] selftests/mm: doexec THP and mlock cases never verify THP or VM_LOCKED after exec
--
commit 7d4cb550ffa3b6f0d72d5c986f22c04da82182ae
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
selftests/mm: test memory preservation across exec
This adds a selftest, doexec, that checks MADV_DOEXEC mappings keep their
backing pages across repeated exec and a later fork. It covers long-term
GUP pins, CLONE_VM rejection and retry, proc readers, vhost owner reset,
and an optional allocation-failure sweep. The test is built through
TEST_GEN_FILES and added to run_vmtests.sh.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-103860
> diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
> --- a/tools/testing/selftests/mm/Makefile
> +++ b/tools/testing/selftests/mm/Makefile
> @@ -74,6 +74,7 @@ endif
>
> TEST_GEN_FILES = cow
> TEST_GEN_FILES += compaction_test
> +TEST_GEN_FILES += doexec
[Severity: Low]
This isn't a bug, but the new generated binary has no entry in
tools/testing/selftests/mm/.gitignore, where siblings such as
gup_longterm are listed.
Should doexec be added there so it does not show up as untracked in git
status after an in-tree build?
> TEST_GEN_FILES += gup_longterm
> TEST_GEN_FILES += gup_test
> TEST_GEN_FILES += hmm-tests
[ ... ]
> diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
> --- /dev/null
> +++ b/tools/testing/selftests/mm/doexec.c
[ ... ]
> +/* The downstream ELF loader requires this opt-in in a PT_NOTE segment. */
> +asm(".pushsection .note.doexec,\"a\",@note\n"
> + ".balign 4\n"
[Severity: Low]
Will this assemble on 32-bit ARM?
GNU as on ARM treats '@' as a comment character, so the section type
there has to be spelled %note. Since the Makefile adds doexec to
TEST_GEN_FILES with no architecture guard, an ARM build of the mm selftests
could either fail or produce a section that is not SHT_NOTE, so the loader
opt-in would not be recognised.
Would a portable spelling, or an architecture guard, be more suitable?
[ ... ]
> + if (own_thread) {
> + if (pthread_create(&thread, NULL, blocked_thread, &pipe_fd[0]))
> + return KSFT_FAIL;
> + check_exec(ram, size, 1, false);
> + write(pipe_fd[1], "x", 1);
> + pthread_join(thread, NULL);
> + return KSFT_FAIL;
> + }
[ ... ]
> + write(pipe_fd[1], "x", 1);
> + if (wait_child(pid))
> + ret = KSFT_FAIL;
[Severity: Low]
Several write() and pwrite() return values are ignored: here in
shared_mm_exec(), and also in clone_race_child(), clone_race_thread(), the
--check handler in main(), and allocation_failures(). These will trigger
glibc warn_unused_result warnings.
In shared_mm_exec(), the write() above is what wakes the CLONE_VM task
blocked in read() inside blocked_task(). If it failed, wouldn't the
following wait_child() block forever?
Could the return values be checked?
[ ... ]
> +/* The caller must release its backend owner before preserving exec. */
> +static int vhost_owner_exec(void)
> +{
> + size_t size = sysconf(_SC_PAGESIZE);
> + char *ram;
> + int fd = open("/dev/vhost-net", O_RDWR);
> +
> + if (fd < 0)
> + return KSFT_SKIP;
> + ram = preserved_ram(size);
> + if (!ram)
> + return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
> + if (ioctl(fd, VHOST_SET_OWNER))
> + return KSFT_SKIP;
> + if (ioctl(fd, VHOST_RESET_OWNER))
> + return KSFT_FAIL;
> + close(fd);
> + check_exec(ram, size, 1, false);
> + return KSFT_FAIL;
> +}
[Severity: Medium]
Does this test actually exercise VHOST_RESET_OWNER? The fd is closed before
the exec, and close() releases the owner's mm through the same cleanup
path:
vhost_net_release()
vhost_dev_cleanup(&n->dev)
vhost_detach_mm()
VHOST_RESET_OWNER reaches the same function:
vhost_net_reset_owner()
vhost_dev_reset_owner()
vhost_dev_cleanup()
So the exec sees the same state whether or not the reset ioctl worked, and
a broken reset-owner would go unnoticed, despite the subtest name "vhost
owner: reset before preserving exec".
Could the exec be done with the fd still open after the reset? It would
also be useful to check that the exec is refused while the owner is still
set, so the rejection side is covered too.
[ ... ]
> +static int after_exec(int fd, char *ram, size_t size, int kind, int stage)
> +{
> + unsigned char before = stage == 1 ? 0x31 : 0x42;
> + unsigned char after = stage == 1 ? 0x42 : 0x53;
> + pid_t pid;
> +
> + if (check_pin(fd, ram, size, before))
> + return KSFT_FAIL;
> + if (kind == PROTECTED && mprotect(ram, size, PROT_READ | PROT_WRITE))
> + return KSFT_FAIL;
> + memset(ram, after, size);
> + if (check_pin(fd, ram, size, after))
> + return KSFT_FAIL;
[ ... ]
> + if (kind == HUGE && madvise(ram, size, MADV_COLLAPSE))
> + return KSFT_SKIP;
> + if (kind == LOCKED && mlock(ram, size))
> + return KSFT_SKIP;
[Severity: Low]
Do the "PMD huge pages" and "mlocked pages" subtests verify the property
they are named for? The THP collapse and the mlock() are done only in
before_exec(). After exec, after_exec() looks at kind only for PROTECTED,
and otherwise compares bytes and pin contents.
A regression that splits the PMD THP or drops VM_LOCKED during the
preserving exec would still report PASS.
Could after_exec() check AnonHugePages in /proc/self/smaps and VmLck in
/proc/self/status for these two kinds?
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-1901cd6c3c74@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 RFC PATCH hci-8.1 1/4] mm: support moving anonymous page tables between address spaces
@ 2026-10-09 15:02 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 15:02 UTC (permalink / raw)
To: Konstantin Khorenko, Andrey Drobyshev; +Cc: devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] mm: cross-mm move forces rmap locks, serializing padata workers on one anon_vma
- [Low] mm: padata workers in move_page_range_exec() allocate page tables outside exec task memcg
--
commit 8261a13046a4e58b21e6c687a520d3921507b391
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
mm: support moving anonymous page tables between address spaces
This teaches move_page_tables() and move_huge_pmd() to use the destination
mm for page-table operations and to transfer RSS, swap-list and huge-page
table accounting. Cross-mm moves are restricted to same-address anonymous
DOEXEC VMAs. It also adds a parallel exec move wrapper,
move_page_range_exec(), built on padata.
Reference in the commit message:
https://virtuozzo.atlassian.net/browse/VSTOR-103860
> diff --git a/mm/mremap.c b/mm/mremap.c
> --- a/mm/mremap.c
> +++ b/mm/mremap.c
[ ... ]
> @@ -610,10 +646,19 @@ unsigned long move_page_tables(struct vm_area_struct *vma,
> struct mmu_notifier_range range;
> pmd_t *old_pmd, *new_pmd;
> pud_t *old_pud, *new_pud;
> + bool cross_mm = vma->vm_mm != new_vma->vm_mm;
>
> if (!len)
> return 0;
>
> + /* Cross-mm moves are only used for same-address anonymous DOEXEC VMAs. */
> + if (cross_mm) {
> + if (WARN_ON_ONCE(!vma_is_anonymous(vma) ||
> + !(vma->vm_flags & VM_EXEC_KEEP) ||
> + old_addr != new_addr || !need_rmap_locks))
> + return 0;
> + }
[Severity: Medium]
Does requiring need_rmap_locks for every cross-mm move serialize the padata
workers added below?
move_page_range_chunk() passes need_rmap_locks=true for every chunk:
return move_page_tables(args->old_vma, addr, args->new_vma, addr,
len, true, false) == len ? 0 : -ENOMEM;
For each PMD-sized extent, move_ptes() and move_pgt_entry() then call
take_rmap_locks(vma), which does:
if (vma->anon_vma)
anon_vma_lock_write(vma->anon_vma);
All workers operate on the same old VMA, so they all contend on the same
anon_vma write lock, and the PTE-moving loop runs inside that critical
section. Only page-table allocation and cond_resched() can overlap.
Since passing false is rejected by the WARN_ON_ONCE() above, the parallel
path cannot avoid the lock. Does that largely cancel the speedup that
move_page_range_exec() is meant to provide with up to 16 threads?
The commit message does not say why rmap locks are required for cross-mm
moves, or that the workers end up serialized on this lock. Could that be
documented, or could the requirement be relaxed for this case?
[ ... ]
> +static int move_page_range_chunk(unsigned long addr, unsigned long end, void *arg)
> +{
> + struct move_page_range_args *args = arg;
> + unsigned long len = end - addr;
> +
> + return move_page_tables(args->old_vma, addr, args->new_vma, addr,
> + len, true, false) == len ? 0 : -ENOMEM;
> +}
[ ... ]
> +int move_page_range_exec(struct vm_area_struct *new_vma,
> + struct vm_area_struct *old_vma)
> +{
[ ... ]
> +#ifdef CONFIG_PADATA
> + return padata_do_multithreaded(&job);
> +#else
> + return move_page_range_chunk(old_vma->vm_start, old_vma->vm_end, &args);
> +#endif
> +}
[Severity: Low]
Are the destination page tables allocated by the padata helpers charged to
the right memcg?
When the job spans more than one chunk, padata_do_multithreaded() queues the
helpers on system_unbound_wq. Each helper then runs:
padata_mt_helper()
move_page_range_chunk()
move_page_tables()
alloc_new_pud(new_vma->vm_mm, ...)
alloc_new_pmd(new_vma->vm_mm, ...)
pte_alloc(new_vma->vm_mm, new_pmd)
These use GFP_PGTABLE_USER, which includes __GFP_ACCOUNT. The charge is
resolved from active_memcg() or current, and here that is the kworker rather
than the exec'ing task.
Neither padata nor this patch calls set_active_memcg() or otherwise attaches
the workers to the submitter's cgroup. Can part of the exec'ing task's
page-table memory therefore escape its memcg limit?
copy_page_range_mt() in mm/memory.c appears to have the same behavior, but
this adds a new padata consumer that relies on it. Should the workers run
under the submitter's memcg, for example by wrapping the chunk function with
set_active_memcg()?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-1901cd6c3c74@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 RFC PATCH hci-8.1 2/4] exec: protect preserved memory against surviving shared-mm tasks
@ 2026-10-09 14:57 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 14:57 UTC (permalink / raw)
To: Andrey Drobyshev, Konstantin Khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] exec: reserve_preserved_mem() fails execve() with -EBUSY for vfork/CLONE_VM parents
- [Medium] fork: CLONE_VM clones, including sibling pthread_create(), fail with -EBUSY during reservation
- [Medium] exec: stale VM_EXEC_KEEP snapshot in reserve_preserved_mem() drops later-marked VMAs
- [Medium] exec: preserved-mm reservation misses non-task mm users like kthread_use_mm()
--
commit 0203d4ae0ee568e8d416b284444f0842edfb59ae
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
exec: protect preserved memory against surviving shared-mm tasks
This patch reserves the mm holding VM_EXEC_KEEP VMAs before exec becomes
irreversible. It rejects tasks outside the execing thread group that share
the mm, and tracks in-flight CLONE_VM clones through a counter in
mm_struct. New shared-mm clones are refused while the reservation is held.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-103860
> diff --git a/fs/exec.c b/fs/exec.c
> --- a/fs/exec.c
> +++ b/fs/exec.c
[ ... ]
> +static int reserve_preserved_mem(struct linux_binprm *bprm)
> +{
[ ... ]
> + mmap_read_unlock(mm);
> + if (!preserve)
> + return 0;
[Severity: Medium]
Can a VMA marked after this scan be silently dropped?
Sibling threads of the execing group are exempt from admission and keep
running until de_thread(). If one of them calls madvise(MADV_DOEXEC) after
this scan returned 0 with preserving_mem unset, exec_mmap() no longer
re-evaluates the VMAs, so vma_dup_some() is skipped and the newly marked
VMA is not preserved.
Before this patch, vma_dup_some() ran after de_thread() under
mmap_read_lock, so the set of marked VMAs was stable at transfer time.
Should the decision be re-checked after de_thread()?
> + /* A clone is either counted here or already visible in the task list. */
> + spin_lock(&mm->page_table_lock);
> + if (mm->exec_keep_active || mm->exec_keep_clones)
> + ret = -EBUSY;
[Severity: Medium]
Can an in-flight pthread_create() from a sibling thread make the whole
execve() fail here?
If another thread of the execing group is inside copy_mm() between
exec_keep_clone_begin() and exec_keep_clone_end(), exec_keep_clones is
non-zero and reserve_preserved_mem() returns -EBUSY with no retry. Such
siblings are about to be removed by de_thread() anyway.
> + else
> + mm->exec_keep_active = true;
> + spin_unlock(&mm->page_table_lock);
> + if (ret)
> + return ret;
> + bprm->preserving_mem = true;
> +
> + read_lock(&tasklist_lock);
> + for_each_process_thread(p, t) {
> + if (same_thread_group(current, t))
> + continue;
> + task_lock(t);
> + if (t->mm == mm)
> + ret = -EBUSY;
[Severity: Medium]
Does this turn a valid vfork() or posix_spawn() exec into a failure?
If a process has a VM_EXEC_KEEP VMA and calls vfork() or
clone(CLONE_VM|CLONE_VFORK), the child shares the parent's mm. When the
child calls execve() on a binary with accepts_preserved_mem set, the
suspended parent is in a different thread group and has t->mm == mm, so
reserve_preserved_mem() returns -EBUSY. Before this patch that exec
succeeded.
Is failing the entire execve() with an -EBUSY that userspace does not expect
the intended behavior here, rather than exec without preservation?
Separately, this check is a point-in-time scan of t->mm. Can it miss other
users of the mm that are not in a task's ->mm field, such as a kthread
holding the mm via kthread_use_mm() on a stored mm, or any holder of an
mmget() reference? exec_keep_active only gates CLONE_VM in copy_mm(), and
mm_users is not examined.
> + task_unlock(t);
> + if (ret)
> + goto out;
> + }
> +out:
> + read_unlock(&tasklist_lock);
> + if (ret)
> + release_preserved_mem(bprm, mm);
> + return ret;
> +}
[ ... ]
> @@ -1067,6 +1140,7 @@ static int exec_mmap(struct linux_binprm *bprm)
> BUG_ON(active_mm != old_mm);
> setmax_mm_hiwater_rss(&tsk->signal->maxrss, old_mm);
> mm_update_next_owner(old_mm);
> + release_preserved_mem(bprm, old_mm);
> mmput(old_mm);
[Severity: Medium]
Can an mm user that the reservation never saw keep old_mm alive after this
point?
By the time the reservation is released here, the VM_EXEC_KEEP VMAs have
already been moved to the new mm by vma_dup_some(). A holder such as one
using kthread_use_mm() on a stored mm would then see empty, newly faulted
pages in the old VMAs instead of the preserved data.
The commit message says other tasks using the old mm are quiesced, but this
appears to be enforced only for tasks on the task list. Is there a check
that mm_users is no longer greater than one after de_thread()?
[ ... ]
> diff --git a/kernel/fork.c b/kernel/fork.c
> --- a/kernel/fork.c
> +++ b/kernel/fork.c
[ ... ]
> +static int exec_keep_clone_begin(struct mm_struct *mm)
> +{
> + int ret = 0;
> +
> + spin_lock(&mm->page_table_lock);
> + if (mm->exec_keep_active)
> + ret = -EBUSY;
[Severity: Medium]
Is it intended that every CLONE_VM clone is rejected while exec_keep_active
is set, including CLONE_THREAD clones from sibling threads of the execing
group?
The reservation window runs from reserve_preserved_mem() until exec_mmap()
or free_bprm(), and de_thread() can wait for siblings to exit inside that
window. During that time a sibling's pthread_create() fails with -EBUSY
even though that sibling is about to be killed.
Also, exec_keep_active is checked only in copy_mm(), so other paths that
attach an existing mm to a task or kthread are not gated.
> + else
> + mm->exec_keep_clones++;
> + spin_unlock(&mm->page_table_lock);
> + return ret;
> +}
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009-vfio_v1-v1-0-1901cd6c3c74@virtuozzo.com?part=2
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* [PATCH QEMU HCI-8.1 20/21] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860
` (12 preceding siblings ...)
2026-10-09 14:56 6% ` [PATCH QEMU HCI-8.1 19/21] VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved Andrey Drobyshev
@ 2026-10-09 14:56 9% ` Andrey Drobyshev
2026-10-09 15:36 5% ` no-reply
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:56 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev
After cpr-exec, new QEMU mmaps the device BARs again on the preserved
device fd. Some drivers don't allow that: the NVIDIA vGPU VFIO driver
refuses to map a VF BAR that is still recorded as mapped by old QEMU,
and that record is dropped only when the device fd is closed. So BAR1
and BAR3 of the vGPU can't be mapped, and the guest loses the device.
Keep the mappings instead. Save each BAR mapping in the CPR ramblock
list, so cpr_exec_cb() marks it with MADV_DOEXEC together with guest RAM,
and new QEMU finds it there and reuses it instead of calling mmap().
Same find-or-create pattern as for other preserved objects, e.g. vector
eventfds in vfio_notifier_init().
The ID is what cpr_name() would give to the mmap subregion:
"<device path>/<region name> mmaps[<index>]".
This needs a kernel which supports MADV_DOEXEC on shared PFN mappings of
device memory. Without it cpr-exec of a VM with VFIO devices fails, and
the VM keeps running in old QEMU.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/vfio/region.c | 39 +++++++++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/hw/vfio/region.c b/hw/vfio/region.c
index 1deaef12fd0..07027ee79f3 100644
--- a/hw/vfio/region.c
+++ b/hw/vfio/region.c
@@ -30,6 +30,7 @@
#include "qemu/units.h"
#include "monitor/monitor.h"
#include "system/ramblock.h"
+#include "migration/cpr.h"
#include "vfio-helpers.h"
/*
@@ -227,8 +228,39 @@ int vfio_region_setup(Object *obj, VFIODevice *vbasedev, VFIORegion *region,
return 0;
}
+static char *vfio_region_cpr_name(VFIORegion *region, int index)
+{
+ g_autofree char *path = qdev_get_dev_path(region->vbasedev->dev);
+ return g_strdup_printf("%s%s%s mmaps[%d]", path ?: "", path ? "/" : "",
+ memory_region_name(region->mem), index);
+}
+
+static void *vfio_region_cpr_find(VFIORegion *region, int index)
+{
+ g_autofree char *name = vfio_region_cpr_name(region, index);
+ cpr_ramblock_t ram = cpr_find_ramblock(name);
+
+ if (ram.addr) {
+ assert(ram.length == region->mmaps[index].size);
+ }
+
+ return (void *)(uintptr_t)ram.addr;
+}
+
+static void vfio_region_cpr_save(VFIORegion *region, int index)
+{
+ g_autofree char *name = vfio_region_cpr_name(region, index);
+ void *addr = region->mmaps[index].mmap;
+ size_t size = region->mmaps[index].size;
+
+ cpr_save_ramblock(name, (cpr_ramblock_t) { (uintptr_t)addr, size });
+}
+
static void vfio_subregion_unmap(VFIORegion *region, int index)
{
+ g_autofree char *cpr_name = vfio_region_cpr_name(region, index);
+
+ cpr_delete_ramblock(cpr_name);
trace_vfio_region_unmap(memory_region_name(®ion->mmaps[index].mem),
region->mmaps[index].offset,
region->mmaps[index].offset +
@@ -314,6 +346,11 @@ int vfio_region_mmap(VFIORegion *region)
size_t align = MIN(1ULL << ctz64(region->mmaps[i].size), 1 * GiB);
void *map_base, *map_align;
+ region->mmaps[i].mmap = vfio_region_cpr_find(region, i);
+ if (region->mmaps[i].mmap) {
+ goto mapped;
+ }
+
/*
* Align the mmap for more efficient mapping in the kernel. Ideally
* we'd know the PMD and PUD mapping sizes to use as discrete alignment
@@ -347,7 +384,9 @@ int vfio_region_mmap(VFIORegion *region)
ret = -errno;
goto no_mmap;
}
+ vfio_region_cpr_save(region, i);
+mapped:
name = g_strdup_printf("%s mmaps[%d]",
memory_region_name(region->mem), i);
memory_region_init_ram_device_ptr(®ion->mmaps[i].mem,
--
2.47.1
^ permalink raw reply [relevance 9%]
* [PATCH QEMU HCI-8.1 19/21] VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved
` (11 preceding siblings ...)
@ 2026-10-09 14:56 6% ` Andrey Drobyshev
2026-10-09 15:31 3% ` no-reply
2026-10-09 14:56 9% ` [PATCH QEMU HCI-8.1 20/21] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860 Andrey Drobyshev
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:56 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev
cpr_exec_cb() marks all saved ramblocks with MADV_DOEXEC before exec, but
ignores the result. If madvise() fails for some block, the walk stops
there and the remaining blocks are not marked, yet execvp() still runs.
The new QEMU then reuses addresses of guest RAM that wasn't preserved,
and the guest is lost.
Treat that as a failed cpr-exec: go to the same failure path as when
execvp() fails, so the VM keeps running in the old QEMU.
On that path also clear MADV_DOEXEC from all saved ramblocks again, just
like the preserved fds get close-on-exec back.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
migration/cpr-exec.c | 26 ++++++++++++++++++++++----
1 file changed, 22 insertions(+), 4 deletions(-)
diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
index 7b7609e19cd..cb962ce7252 100644
--- a/migration/cpr-exec.c
+++ b/migration/cpr-exec.c
@@ -154,6 +154,19 @@ static bool preserve_ram(cpr_ramblock_t ram, char *name)
return true;
}
+static bool unpreserve_ram(cpr_ramblock_t ram, char *name)
+{
+#ifdef MADV_DONTEXEC
+ qemu_madvise((void *) ram.addr, ram.length, MADV_DONTEXEC);
+#endif
+ return true;
+}
+
+static void cpr_exec_unpreserve_ram(void)
+{
+ cpr_walk_ramblock(unpreserve_ram);
+}
+
static bool mark_ram_rdonly(cpr_ramblock_t ram, char *name)
{
/*
@@ -202,7 +215,10 @@ static void cpr_exec_cb(void *opaque)
*/
cpr_exec_preserve_fds();
- cpr_walk_ramblock(preserve_ram);
+ if (!cpr_walk_ramblock(preserve_ram)) {
+ /* preserve_ram() has already reported the error and failed migration */
+ goto fail;
+ }
if (cpr_ram_rdonly) {
cpr_walk_ramblock(mark_ram_rdonly);
}
@@ -221,15 +237,17 @@ static void cpr_exec_cb(void *opaque)
* or the system is very short on resources.
*/
error_setg_errno(&err, errno, "execvp %s failed", argv[0]);
- g_clear_pointer(&argv, g_strfreev);
- cpr_exec_unpreserve_fds();
-
error_report_err(error_copy(err));
migrate_set_state(&s->state, s->state, MIGRATION_STATUS_FAILED);
migrate_set_error(s, err);
error_free(err);
err = NULL;
+fail:
+ g_clear_pointer(&argv, g_strfreev);
+ cpr_exec_unpreserve_fds();
+ cpr_exec_unpreserve_ram();
+
/* Note, we can go from state COMPLETED to FAILED */
migration_call_notifiers(MIG_EVENT_PRECOPY_FAILED, NULL);
--
2.47.1
^ permalink raw reply [relevance 6%]
* [PATCH QEMU HCI-8.1 14/21] kvm: tolerate ioctls after kvm_close()
` (7 preceding siblings ...)
2026-10-09 14:56 23% ` [PATCH QEMU HCI-8.1 13/21] migration: Fix crash on second migration when cancel early Andrey Drobyshev
@ 2026-10-09 14:56 22% ` Andrey Drobyshev
2026-10-09 15:22 7% ` no-reply
` (5 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:56 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev
Since commit 7ed0919119b0 ("migration: close kvm after cpr") a source
QEMU that has handed its VFIO devices over via cpr-transfer (or
cpr-exec) calls kvm_close() once the migration completes, so that the
destination can register its irq bypass consumers. kvm_close() closes
the VM and vCPU descriptors and sets kvm_state to NULL, but the source
process keeps living with all its device models intact until the
management layer terminates it.
Nothing prevents those device models from raising interrupts in the
meantime, and the regular exit path even guarantees one: net_cleanup()
deletes the netdev behind an emulated virtio-net, the NIC sees the link
go down and calls virtio_notify_config(), MSI-X delivers through the
in-kernel APIC, and kvm_irqchip_send_msi() dereferences the NULL
kvm_state:
#0 kvm_vm_ioctl (s=0x0, type=KVM_SIGNAL_MSI) accel/kvm/kvm-all.c
#1 kvm_irqchip_send_msi (s=0x0)
#2 kvm_send_msi hw/i386/kvm/apic.c
#3 kvm_apic_mem_write
...
#9 pci_msi_trigger
#11 msix_notify
#12 virtio_pci_notify
#14 virtio_notify_config
#15 virtio_net_set_link_status
#16 qemu_del_net_client
#17 net_cleanup
#18 qemu_cleanup
Every retired cpr-transfer source with a VFIO device thus dies with
SIGSEGV on SIGTERM/quit and leaves a core dump behind. Without a VFIO
device kvm_close() never runs and the same exit is harmless.
Make the ioctl wrappers honour the closed state instead of assuming a
live KVMState: kvm_ioctl(), kvm_vm_ioctl() and kvm_vcpu_ioctl() return
-ENODEV when kvm_close() has already been called, so any late interrupt
or query on the retired source fails gracefully. Teach kvm_send_msi()
that -ENODEV in this situation is expected and not worth the "MSI lost"
warning on every such interrupt.
Fixes: 7ed0919119b0 ("migration: close kvm after cpr")
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
accel/kvm/kvm-all.c | 15 +++++++++++++++
hw/i386/kvm/apic.c | 6 +++++-
2 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/accel/kvm/kvm-all.c b/accel/kvm/kvm-all.c
index a39c1ba260a..ea027c37829 100644
--- a/accel/kvm/kvm-all.c
+++ b/accel/kvm/kvm-all.c
@@ -3371,6 +3371,11 @@ int kvm_ioctl(KVMState *s, unsigned long type, ...)
arg = va_arg(ap, void *);
va_end(ap);
+ /* kvm_close() has been called: there is no KVM to talk to any more */
+ if (!s || s->fd == -1) {
+ return -ENODEV;
+ }
+
trace_kvm_ioctl(type, arg);
ret = ioctl(s->fd, type, arg);
if (ret == -1) {
@@ -3389,6 +3394,11 @@ int kvm_vm_ioctl(KVMState *s, unsigned long type, ...)
arg = va_arg(ap, void *);
va_end(ap);
+ /* kvm_close() has been called: there is no VM to talk to any more */
+ if (!s || s->vmfd == -1) {
+ return -ENODEV;
+ }
+
trace_kvm_vm_ioctl(type, arg);
accel_ioctl_begin();
ret = ioctl(s->vmfd, type, arg);
@@ -3409,6 +3419,11 @@ int kvm_vcpu_ioctl(CPUState *cpu, unsigned long type, ...)
arg = va_arg(ap, void *);
va_end(ap);
+ /* kvm_close() has been called: the vCPU fds are gone */
+ if (cpu->kvm_fd == -1) {
+ return -ENODEV;
+ }
+
trace_kvm_vcpu_ioctl(cpu->cpu_index, type, arg);
accel_cpu_ioctl_begin(cpu);
ret = ioctl(cpu->kvm_fd, type, arg);
diff --git a/hw/i386/kvm/apic.c b/hw/i386/kvm/apic.c
index 1be9bfe36e9..0c9f7759b6e 100644
--- a/hw/i386/kvm/apic.c
+++ b/hw/i386/kvm/apic.c
@@ -196,7 +196,11 @@ static void kvm_send_msi(MSIMessage *msg)
msg->address = kvm_swizzle_msi_ext_dest_id(msg->address);
ret = kvm_irqchip_send_msi(kvm_state, *msg);
- if (ret < 0) {
+ /*
+ * -ENODEV means kvm_close() has already run (the VM was handed over
+ * via CPR); the interrupt is meaningless here and not worth a warning.
+ */
+ if (ret < 0 && ret != -ENODEV) {
fprintf(stderr, "KVM: injection failed, MSI lost (%s)\n",
strerror(-ret));
}
--
2.47.1
^ permalink raw reply [relevance 22%]
* [PATCH QEMU HCI-8.1 13/21] migration: Fix crash on second migration when cancel early
` (6 preceding siblings ...)
2026-10-09 14:56 28% ` [PATCH QEMU HCI-8.1 12/21] migration: Move CPR HUP watch to cpr-transfer.c Andrey Drobyshev
@ 2026-10-09 14:56 23% ` Andrey Drobyshev
2026-10-09 15:20 4% ` no-reply
2026-10-09 14:56 22% ` [PATCH QEMU HCI-8.1 14/21] kvm: tolerate ioctls after kvm_close() Andrey Drobyshev
` (6 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:56 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev
From: Peter Xu <peterx@redhat.com>
Marc-André reported an issue on QEMU crash when retrying a cancelled
migration during early setup phase, see "Link:" for more information, and
also easy way to reproduce.
This patch is a replacement of the prior fix proposed by not only switching
to migration_cleanup(), but also fixing it from CPR side, so that we track
hup_source properly to know if src QEMU is waiting or the HUP signal.
To put it simple: this chunk of special casing in migration_cancel() should
not affect normal migration, but only cpr-transfer migration to cover the
small window when the src QEMU is waiting for a HUP signal on cpr
channel (so that src QEMU can continue the migration on the main channel).
To achieve that, we'll also need to remember to detach the hup_source
whenenver invoked: after that point, we should always be able to cleanup
the migration.
It's not a generic operation to explicitly detach a gsource from its
context while in its dispatch() function. But it should be safe, because
gsource disptch() will only happen with a boosted refcount for the
dispatcher so that the gsource will not be freed until the callback
completes. It's also safe to return G_SOURCE_REMOVE after the gsource is
detached, as glib will simply ignore the G_SOURCE_REMOVE.
One can refer to latest 2.86.5 glib code in g_main_dispatch() for that:
https://github.com/GNOME/glib/blob/2.86.5/glib/gmain.c#L3592
When at this, add a bunch of assertions to make sure nothing surprises us.
After this patch applied, the 2nd migration will not crash QEMU, instead
it'll be in CANCELLING until the socket connection times out (it will take
~2min on my Fedora default kernel). During this process no 2nd migration
will be allowed, and after it timed out migration can be restarted.
It's because so far we don't have control over socket_connect_outgoing(),
or anything yet managed by a task executed in qio_task_run_in_thread().
Speeding up the cancellation to be left for future.
I also tested cpr-transfer by only providing cpr channel not the main
channel (with -incoming defer), kickoff migration on source, then cancel it
on source directly without providing the main channel. It keeps working.
I wanted to add an unit test for that but it'll need to refactor current
cpr-transfer tests first; let's leave it for later.
andrey.drobyshev:
This commit was cherry-picked from the upstream commit 4f08b870f8f0,
adapted for the hci-dev base, which predates the upstream qmp_migrate()
rework: the HUP callback is still qmp_migrate_finish_cb() rather than
migration_connect_outgoing_cb(), so the early
cpr_transfer_source_destroy() call lands there, ahead of
qmp_migrate_finish() and the cpr_state_close() the callback keeps
doing itself.
Link: https://lore.kernel.org/r/20260417184742.293061-1-marcandre.lureau@redhat.com
Reported-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Tested-by: Fabiano Rosas <farosas@suse.de>
Reviewed-by: Fabiano Rosas <farosas@suse.de>
Link: https://lore.kernel.org/r/20260421175820.302795-1-peterx@redhat.com
Signed-off-by: Peter Xu <peterx@redhat.com>
---
include/migration/cpr.h | 1 +
migration/cpr-transfer.c | 10 ++++++++++
migration/migration.c | 32 ++++++++++++++++++++++++--------
migration/migration.h | 5 +++++
4 files changed, 40 insertions(+), 8 deletions(-)
diff --git a/include/migration/cpr.h b/include/migration/cpr.h
index ae441322ff5..4dd397a7c13 100644
--- a/include/migration/cpr.h
+++ b/include/migration/cpr.h
@@ -80,6 +80,7 @@ QEMUFile *cpr_transfer_input(MigrationChannel *channel, Error **errp);
void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
void *opaque);
void cpr_transfer_source_destroy(MigrationState *s);
+bool cpr_transfer_source_active(MigrationState *s);
void cpr_exec_init(void);
QEMUFile *cpr_exec_output(Error **errp);
diff --git a/migration/cpr-transfer.c b/migration/cpr-transfer.c
index 61d5c9dce2b..9defe7bad78 100644
--- a/migration/cpr-transfer.c
+++ b/migration/cpr-transfer.c
@@ -6,6 +6,7 @@
*/
#include "qemu/osdep.h"
+#include "qemu/main-loop.h"
#include "qapi/clone-visitor.h"
#include "qapi/error.h"
#include "qapi/qapi-visit-migration.h"
@@ -79,6 +80,7 @@ QEMUFile *cpr_transfer_input(MigrationChannel *channel, Error **errp)
void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
void *opaque)
{
+ assert(bql_locked());
s->hup_source = qio_channel_create_watch(cpr_state_ioc(), G_IO_HUP);
g_source_set_callback(s->hup_source,
(GSourceFunc)func,
@@ -89,9 +91,17 @@ void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
void cpr_transfer_source_destroy(MigrationState *s)
{
+ assert(bql_locked());
if (s->hup_source) {
g_source_destroy(s->hup_source);
g_source_unref(s->hup_source);
s->hup_source = NULL;
}
}
+
+bool cpr_transfer_source_active(MigrationState *s)
+{
+ /* Whenever the HUP gsource is available, it's active. */
+ assert(bql_locked());
+ return s->hup_source;
+}
diff --git a/migration/migration.c b/migration/migration.c
index 81d5d4dd4c0..ab1f1641c21 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -1653,14 +1653,19 @@ void migration_cancel(void)
}
/*
- * If qmp_migrate_finish has not been called, then there is no path that
- * will complete the cancellation. Do it now.
+ * This is cpr-transfer specific processing.
+ *
+ * If this is true, it means cpr-transfer migration is waiting for the
+ * destination to send HUP event on CPR channel to continue the next
+ * phase. If so, do the cleanup proactively to avoid get stuck in
+ * CANCELLING state.
*/
- if (setup && !s->to_dst_file) {
- migrate_set_state(&s->state, MIGRATION_STATUS_CANCELLING,
- MIGRATION_STATUS_CANCELLED);
- cpr_state_close();
- cpr_transfer_source_destroy(s);
+ if (cpr_transfer_source_active(s)) {
+ assert(migrate_mode() == MIG_MODE_CPR_TRANSFER);
+ assert(setup && !s->to_dst_file);
+ migration_cleanup(s);
+ /* Now all things should have been released */
+ assert(!cpr_transfer_source_active(s));
}
}
@@ -2246,11 +2251,22 @@ static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
void *opaque)
{
MigrationAddress *addr = opaque;
+ MigrationState *s = migrate_get_current();
+
+ /*
+ * Detach and release the GSource right after use. We rely on this to
+ * detect this small cpr-transfer window of "waiting for HUP event".
+ */
+ cpr_transfer_source_destroy(s);
qmp_migrate_finish(addr, NULL);
cpr_state_close();
- cpr_transfer_source_destroy(migrate_get_current());
+
+ /*
+ * This is redundant as we do cpr_transfer_source_destroy() at the
+ * entry, but it's benign; glib will just skip the detach.
+ */
return G_SOURCE_REMOVE;
}
diff --git a/migration/migration.h b/migration/migration.h
index a7ea43e925d..ee15ca88461 100644
--- a/migration/migration.h
+++ b/migration/migration.h
@@ -509,6 +509,11 @@ struct MigrationState {
/* Is this a rdma migration */
bool rdma_migration;
+ /*
+ * When set, it means cpr-transfer is waiting for the HUP signal from
+ * destination to continue the 2nd step of migration via the main
+ * channel.
+ */
GSource *hup_source;
};
--
2.47.1
^ permalink raw reply [relevance 23%]
* [PATCH QEMU HCI-8.1 12/21] migration: Move CPR HUP watch to cpr-transfer.c
` (5 preceding siblings ...)
2026-10-09 14:56 19% ` [PATCH QEMU HCI-8.1 11/21] migration: Free cpr-transfer MigrationAddress along with gsource Andrey Drobyshev
@ 2026-10-09 14:56 28% ` Andrey Drobyshev
2026-10-09 15:18 4% ` no-reply
2026-10-09 14:56 23% ` [PATCH QEMU HCI-8.1 13/21] migration: Fix crash on second migration when cancel early Andrey Drobyshev
` (7 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:56 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev
From: Fabiano Rosas <farosas@suse.de>
Move this CPR-specific code into a cpr file. While here, give the
functions more significant names.
This makes the new idea (after cpr-transfer) of having two parts to
qmp_migrate slightly more obvious: either wait for the hangup or
continue directly.
andrey.drobyshev:
This commit was cherry-picked from the upstream commit f523e5aa5f56,
adapted for the hci-dev base, which predates the upstream qmp_migrate()
rework around MigrationChannel: the watch is still armed with the
plain MigrationAddress from qmp_migrate() rather than main_ch->addr,
and qmp_migrate_finish_cb() keeps closing the cpr state and destroying
the HUP source itself.
Reviewed-by: Peter Xu <peterx@redhat.com>
Reviewed-by: Prasad Pandit <pjp@fedoraproject.org>
Link: https://lore.kernel.org/qemu-devel/20260123141656.6765-24-farosas@suse.de
Signed-off-by: Fabiano Rosas <farosas@suse.de>
---
include/migration/cpr.h | 5 +++++
migration/cpr-transfer.c | 23 +++++++++++++++++++++++
migration/migration.c | 29 ++++-------------------------
3 files changed, 32 insertions(+), 25 deletions(-)
diff --git a/include/migration/cpr.h b/include/migration/cpr.h
index 8fb79eb0a28..ae441322ff5 100644
--- a/include/migration/cpr.h
+++ b/include/migration/cpr.h
@@ -9,6 +9,7 @@
#define MIGRATION_CPR_H
#include "qapi/qapi-types-migration.h"
+#include "io/channel.h"
#include "qemu/queue.h"
#define MIG_MODE_NONE -1
@@ -76,6 +77,10 @@ int cpr_get_fd_param(const char *name, const char *fdname, int index, bool cpr,
QEMUFile *cpr_transfer_output(MigrationChannel *channel, Error **errp);
QEMUFile *cpr_transfer_input(MigrationChannel *channel, Error **errp);
+void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
+ void *opaque);
+void cpr_transfer_source_destroy(MigrationState *s);
+
void cpr_exec_init(void);
QEMUFile *cpr_exec_output(Error **errp);
QEMUFile *cpr_exec_input(Error **errp);
diff --git a/migration/cpr-transfer.c b/migration/cpr-transfer.c
index 00371d17c3a..61d5c9dce2b 100644
--- a/migration/cpr-transfer.c
+++ b/migration/cpr-transfer.c
@@ -6,7 +6,10 @@
*/
#include "qemu/osdep.h"
+#include "qapi/clone-visitor.h"
#include "qapi/error.h"
+#include "qapi/qapi-visit-migration.h"
+#include "io/channel.h"
#include "io/channel-file.h"
#include "io/channel-socket.h"
#include "io/net-listener.h"
@@ -72,3 +75,23 @@ QEMUFile *cpr_transfer_input(MigrationChannel *channel, Error **errp)
return NULL;
}
}
+
+void cpr_transfer_add_hup_watch(MigrationState *s, QIOChannelFunc func,
+ void *opaque)
+{
+ s->hup_source = qio_channel_create_watch(cpr_state_ioc(), G_IO_HUP);
+ g_source_set_callback(s->hup_source,
+ (GSourceFunc)func,
+ QAPI_CLONE(MigrationAddress, opaque),
+ (GDestroyNotify)qapi_free_MigrationAddress);
+ g_source_attach(s->hup_source, NULL);
+}
+
+void cpr_transfer_source_destroy(MigrationState *s)
+{
+ if (s->hup_source) {
+ g_source_destroy(s->hup_source);
+ g_source_unref(s->hup_source);
+ s->hup_source = NULL;
+ }
+}
diff --git a/migration/migration.c b/migration/migration.c
index b1112556a22..81d5d4dd4c0 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -110,7 +110,6 @@ static bool migration_object_check(MigrationState *ms, Error **errp);
static bool migration_switchover_start(MigrationState *s, Error **errp);
static bool close_return_path_on_source(MigrationState *s);
static void migration_completion_end(MigrationState *s);
-static void migrate_hup_delete(MigrationState *s);
static void migration_downtime_start(MigrationState *s)
{
@@ -1500,7 +1499,7 @@ static void migration_cleanup(MigrationState *s)
qemu_savevm_state_cleanup();
cpr_state_close();
- migrate_hup_delete(s);
+ cpr_transfer_source_destroy(s);
close_return_path_on_source(s);
@@ -1661,7 +1660,7 @@ void migration_cancel(void)
migrate_set_state(&s->state, MIGRATION_STATUS_CANCELLING,
MIGRATION_STATUS_CANCELLED);
cpr_state_close();
- migrate_hup_delete(s);
+ cpr_transfer_source_destroy(s);
}
}
@@ -2242,25 +2241,6 @@ static bool migrate_prepare(MigrationState *s, bool resume, Error **errp)
static void qmp_migrate_finish(MigrationAddress *addr, Error **errp);
-static void migrate_hup_add(MigrationState *s, QIOChannel *ioc, GSourceFunc cb,
- void *opaque)
-{
- s->hup_source = qio_channel_create_watch(ioc, G_IO_HUP);
- g_source_set_callback(s->hup_source, cb,
- QAPI_CLONE(MigrationAddress, opaque),
- (GDestroyNotify)qapi_free_MigrationAddress);
- g_source_attach(s->hup_source, NULL);
-}
-
-static void migrate_hup_delete(MigrationState *s)
-{
- if (s->hup_source) {
- g_source_destroy(s->hup_source);
- g_source_unref(s->hup_source);
- s->hup_source = NULL;
- }
-}
-
static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
GIOCondition cond,
void *opaque)
@@ -2270,7 +2250,7 @@ static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
qmp_migrate_finish(addr, NULL);
cpr_state_close();
- migrate_hup_delete(migrate_get_current());
+ cpr_transfer_source_destroy(migrate_get_current());
return G_SOURCE_REMOVE;
}
@@ -2350,8 +2330,7 @@ void qmp_migrate(const char *uri, bool has_channels,
* connection, so qmp_migrate_finish will fail to connect, and then recover.
*/
if (migrate_mode() == MIG_MODE_CPR_TRANSFER) {
- migrate_hup_add(s, cpr_state_ioc(), (GSourceFunc)qmp_migrate_finish_cb,
- addr);
+ cpr_transfer_add_hup_watch(s, qmp_migrate_finish_cb, addr);
} else {
qmp_migrate_finish(addr, errp);
--
2.47.1
^ permalink raw reply [relevance 28%]
* [PATCH QEMU HCI-8.1 11/21] migration: Free cpr-transfer MigrationAddress along with gsource
` (4 preceding siblings ...)
2026-10-09 14:56 20% ` [PATCH QEMU HCI-8.1 10/21] migration: yank: Move register instance earlier Andrey Drobyshev
@ 2026-10-09 14:56 19% ` Andrey Drobyshev
2026-10-09 14:56 28% ` [PATCH QEMU HCI-8.1 12/21] migration: Move CPR HUP watch to cpr-transfer.c Andrey Drobyshev
` (8 subsequent siblings)
14 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:56 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev
From: Fabiano Rosas <farosas@suse.de>
When setting a callback on a Glib source and giving it a data pointer,
it's natural to also provide the destructor for the data in question.
Since migrate_hup_add() already needs to clone the MigrationAddress
when setting the qmp_migrate_finish_cb callback, also pass the
qapi_free_MigrationAddress as the GDestroyNotify callback.
With this the address doesn't need to be freed at the callback body,
making the management of that memory slightly simpler.
andrey.drobyshev:
This commit was cherry-picked from the upstream commit 6a887119ce18,
adapted for the hci-dev base: qmp_migrate_finish_cb() here still
closes the cpr state and deletes the HUP source itself (upstream had
already moved that into migration_cleanup()), so only the explicit
qapi_free_MigrationAddress() call goes away from it.
Cc: Mark Kanda <mark.kanda@oracle.com>
Cc: Ben Chaney <bchaney@akamai.com>
Reviewed-by: Peter Xu <peterx@redhat.com>
Reviewed-by: Prasad Pandit <pjp@fedoraproject.org>
Link: https://lore.kernel.org/qemu-devel/20260123141656.6765-23-farosas@suse.de
Signed-off-by: Fabiano Rosas <farosas@suse.de>
---
migration/migration.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/migration/migration.c b/migration/migration.c
index 638fa783ef2..b1112556a22 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -2245,9 +2245,11 @@ static void qmp_migrate_finish(MigrationAddress *addr, Error **errp);
static void migrate_hup_add(MigrationState *s, QIOChannel *ioc, GSourceFunc cb,
void *opaque)
{
- s->hup_source = qio_channel_create_watch(ioc, G_IO_HUP);
- g_source_set_callback(s->hup_source, cb, opaque, NULL);
- g_source_attach(s->hup_source, NULL);
+ s->hup_source = qio_channel_create_watch(ioc, G_IO_HUP);
+ g_source_set_callback(s->hup_source, cb,
+ QAPI_CLONE(MigrationAddress, opaque),
+ (GDestroyNotify)qapi_free_MigrationAddress);
+ g_source_attach(s->hup_source, NULL);
}
static void migrate_hup_delete(MigrationState *s)
@@ -2269,7 +2271,6 @@ static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
cpr_state_close();
migrate_hup_delete(migrate_get_current());
- qapi_free_MigrationAddress(addr);
return G_SOURCE_REMOVE;
}
@@ -2350,7 +2351,7 @@ void qmp_migrate(const char *uri, bool has_channels,
*/
if (migrate_mode() == MIG_MODE_CPR_TRANSFER) {
migrate_hup_add(s, cpr_state_ioc(), (GSourceFunc)qmp_migrate_finish_cb,
- QAPI_CLONE(MigrationAddress, addr));
+ addr);
} else {
qmp_migrate_finish(addr, errp);
--
2.47.1
^ permalink raw reply [relevance 19%]
* [PATCH QEMU HCI-8.1 10/21] migration: yank: Move register instance earlier
` (3 preceding siblings ...)
2026-10-09 14:56 12% ` [PATCH QEMU HCI-8.1 09/21] migration: Fix regression of passing error_fatal into vmstate_load_state() Andrey Drobyshev
@ 2026-10-09 14:56 20% ` Andrey Drobyshev
2026-10-09 15:16 3% ` no-reply
2026-10-09 14:56 19% ` [PATCH QEMU HCI-8.1 11/21] migration: Free cpr-transfer MigrationAddress along with gsource Andrey Drobyshev
` (9 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:56 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev
From: Fabiano Rosas <farosas@suse.de>
Move the register_instance call to migrate_prepare() so it can be
paired with the unregister_instance at migration_cleanup(). Otherwise,
the cleanup cannot be run when cpr_state_save() fails because the
instance is registered only after it.
When resuming from a paused postcopy migration, migrate_prepare()
returns early, but migration_cleanup() doesn't run, so the yank will
remain paired.
andrey.drobyshev:
This commit was cherry-picked from the upstream commit 7e714392437c,
adapted for the hci-dev base, which predates the upstream
migration_connect_set_error() -> migration_connect_error_propagate()
rename (77c82edadbe0): the error propagation calls in qmp_migrate()
and qmp_migrate_finish() keep the older spelling.
Reviewed-by: Peter Xu <peterx@redhat.com>
Reviewed-by: Prasad Pandit <pjp@fedoraproject.org>
Link: https://lore.kernel.org/qemu-devel/20260123141656.6765-11-farosas@suse.de
Signed-off-by: Fabiano Rosas <farosas@suse.de>
---
migration/migration.c | 26 ++++++++------------------
1 file changed, 8 insertions(+), 18 deletions(-)
diff --git a/migration/migration.c b/migration/migration.c
index 2f8ef20c036..638fa783ef2 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -2235,11 +2235,12 @@ static bool migrate_prepare(MigrationState *s, bool resume, Error **errp)
return false;
}
+ yank_register_instance(MIGRATION_YANK_INSTANCE, &error_abort);
+
return true;
}
-static void qmp_migrate_finish(MigrationAddress *addr, bool resume_requested,
- Error **errp);
+static void qmp_migrate_finish(MigrationAddress *addr, Error **errp);
static void migrate_hup_add(MigrationState *s, QIOChannel *ioc, GSourceFunc cb,
void *opaque)
@@ -2264,7 +2265,7 @@ static gboolean qmp_migrate_finish_cb(QIOChannel *channel,
{
MigrationAddress *addr = opaque;
- qmp_migrate_finish(addr, false, NULL);
+ qmp_migrate_finish(addr, NULL);
cpr_state_close();
migrate_hup_delete(migrate_get_current());
@@ -2276,7 +2277,6 @@ void qmp_migrate(const char *uri, bool has_channels,
MigrationChannelList *channels, bool has_detach, bool detach,
bool has_resume, bool resume, Error **errp)
{
- bool resume_requested;
Error *local_err = NULL;
MigrationState *s = migrate_get_current();
g_autoptr(MigrationChannel) channel = NULL;
@@ -2329,8 +2329,7 @@ void qmp_migrate(const char *uri, bool has_channels,
return;
}
- resume_requested = has_resume && resume;
- if (!migrate_prepare(s, resume_requested, errp)) {
+ if (!migrate_prepare(s, has_resume && resume, errp)) {
/* Error detected, put into errp */
return;
}
@@ -2354,28 +2353,22 @@ void qmp_migrate(const char *uri, bool has_channels,
QAPI_CLONE(MigrationAddress, addr));
} else {
- qmp_migrate_finish(addr, resume_requested, errp);
+ qmp_migrate_finish(addr, errp);
}
out:
if (local_err) {
+ yank_unregister_instance(MIGRATION_YANK_INSTANCE);
migration_connect_set_error(s, local_err);
error_propagate(errp, local_err);
}
}
-static void qmp_migrate_finish(MigrationAddress *addr, bool resume_requested,
- Error **errp)
+static void qmp_migrate_finish(MigrationAddress *addr, Error **errp)
{
MigrationState *s = migrate_get_current();
Error *local_err = NULL;
- if (!resume_requested) {
- if (!yank_register_instance(MIGRATION_YANK_INSTANCE, errp)) {
- return;
- }
- }
-
if (addr->transport == MIGRATION_ADDRESS_TYPE_SOCKET) {
SocketAddress *saddr = &addr->u.socket;
if (saddr->type == SOCKET_ADDRESS_TYPE_INET ||
@@ -2401,9 +2394,6 @@ static void qmp_migrate_finish(MigrationAddress *addr, bool resume_requested,
}
if (local_err) {
- if (!resume_requested) {
- yank_unregister_instance(MIGRATION_YANK_INSTANCE);
- }
migration_connect_set_error(s, local_err);
error_propagate(errp, local_err);
return;
--
2.47.1
^ permalink raw reply [relevance 20%]
* [PATCH QEMU HCI-8.1 09/21] migration: Fix regression of passing error_fatal into vmstate_load_state()
` (2 preceding siblings ...)
2026-10-09 14:56 4% ` [PATCH QEMU HCI-8.1 07/21] q35: Fix migration of SMRAM state Andrey Drobyshev
@ 2026-10-09 14:56 12% ` Andrey Drobyshev
2026-10-09 15:17 5% ` no-reply
2026-10-09 14:56 20% ` [PATCH QEMU HCI-8.1 10/21] migration: yank: Move register instance earlier Andrey Drobyshev
` (10 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:56 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev
From: Arun Menon <armenon@redhat.com>
error_fatal is passed to vmstate_load_state() and vmstate_save_state()
functions. This was introduced in commit c632ffbd74. This would exit(1)
on error, and therefore does not allow to propagate the error back to
the caller.
To maintain consistency with prior error handling i.e. either propagating
the error to the caller or reporting it, we must set the error within a
local Error object instead of using error_fatal.
Reviewed-by: Cornelia Huck <cohuck@redhat.com>
Signed-off-by: Arun Menon <armenon@redhat.com>
Reviewed-by: Akihiko Odaki <odaki@rsg.ci.i.u-tokyo.ac.jp>
Link: https://lore.kernel.org/r/20251028-solve_error_fatal_regression-v2-1-dab24c808a28@redhat.com
[peterx: always uninit var ret, per Akihiko]
[peterx: touchups on line ordering, spacings etc.]
andrey.drobyshev:
This commit was cherry-picked from the upstream commit 986c3292c6b8,
adapted for the hci-dev base, which predates the upstream commit
bcb536cabe10 ("error: Kill @error_warn"): virtio_gpu_load() has no
'err' local there yet, so it is declared here.
Signed-off-by: Peter Xu <peterx@redhat.com>
---
hw/display/virtio-gpu.c | 22 +++++++++++++++-------
hw/pci/pci.c | 15 +++++++++++++--
hw/s390x/virtio-ccw.c | 17 +++++++++++++++--
hw/scsi/spapr_vscsi.c | 10 ++++++++--
hw/virtio/virtio-mmio.c | 15 +++++++++++++--
hw/virtio/virtio-pci.c | 15 +++++++++++++--
hw/virtio/virtio.c | 10 +++++++---
7 files changed, 84 insertions(+), 20 deletions(-)
diff --git a/hw/display/virtio-gpu.c b/hw/display/virtio-gpu.c
index 477ec700a13..4bc8c778508 100644
--- a/hw/display/virtio-gpu.c
+++ b/hw/display/virtio-gpu.c
@@ -1223,7 +1223,8 @@ static int virtio_gpu_save(QEMUFile *f, void *opaque, size_t size,
{
VirtIOGPU *g = opaque;
struct virtio_gpu_simple_resource *res;
- int i;
+ Error *err = NULL;
+ int i, ret;
/* in 2d mode we should never find unprocessed commands here */
assert(QTAILQ_EMPTY(&g->cmdq));
@@ -1246,8 +1247,12 @@ static int virtio_gpu_save(QEMUFile *f, void *opaque, size_t size,
}
qemu_put_be32(f, 0); /* end of list */
- return vmstate_save_state(f, &vmstate_virtio_gpu_scanouts, g, NULL,
- &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_virtio_gpu_scanouts, g, NULL,
+ &err);
+ if (ret < 0) {
+ error_report_err(err);
+ }
+ return ret;
}
static bool virtio_gpu_load_restore_mapping(VirtIOGPU *g,
@@ -1283,9 +1288,10 @@ static int virtio_gpu_load(QEMUFile *f, void *opaque, size_t size,
const VMStateField *field)
{
VirtIOGPU *g = opaque;
+ Error *err = NULL;
struct virtio_gpu_simple_resource *res;
uint32_t resource_id, pformat;
- int i;
+ int i, ret;
g->hostmem = 0;
@@ -1344,9 +1350,11 @@ static int virtio_gpu_load(QEMUFile *f, void *opaque, size_t size,
}
/* load & apply scanout state */
- vmstate_load_state(f, &vmstate_virtio_gpu_scanouts, g, 1, &error_fatal);
-
- return 0;
+ ret = vmstate_load_state(f, &vmstate_virtio_gpu_scanouts, g, 1, &err);
+ if (ret < 0) {
+ error_report_err(err);
+ }
+ return ret;
}
static int virtio_gpu_blob_save(QEMUFile *f, void *opaque, size_t size,
diff --git a/hw/pci/pci.c b/hw/pci/pci.c
index 5d7d17f6529..50f062045a9 100644
--- a/hw/pci/pci.c
+++ b/hw/pci/pci.c
@@ -926,21 +926,32 @@ const VMStateDescription vmstate_pci_device = {
void pci_device_save(PCIDevice *s, QEMUFile *f)
{
+ Error *local_err = NULL;
+ int ret;
+
/* Clear interrupt status bit: it is implicit
* in irq_state which we are saving.
* This makes us compatible with old devices
* which never set or clear this bit. */
s->config[PCI_STATUS] &= ~PCI_STATUS_INTERRUPT;
- vmstate_save_state(f, &vmstate_pci_device, s, NULL, &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_pci_device, s, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
/* Restore the interrupt status bit. */
pci_update_irq_status(s);
}
int pci_device_load(PCIDevice *s, QEMUFile *f)
{
+ Error *local_err = NULL;
int ret;
+
ret = vmstate_load_state(f, &vmstate_pci_device, s, s->version_id,
- &error_fatal);
+ &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
/* Restore the interrupt status bit. */
pci_update_irq_status(s);
return ret;
diff --git a/hw/s390x/virtio-ccw.c b/hw/s390x/virtio-ccw.c
index 4cb1ced001a..4a3ffb84f8f 100644
--- a/hw/s390x/virtio-ccw.c
+++ b/hw/s390x/virtio-ccw.c
@@ -1130,13 +1130,26 @@ static int virtio_ccw_load_queue(DeviceState *d, int n, QEMUFile *f)
static void virtio_ccw_save_config(DeviceState *d, QEMUFile *f)
{
VirtioCcwDevice *dev = VIRTIO_CCW_DEVICE(d);
- vmstate_save_state(f, &vmstate_virtio_ccw_dev, dev, NULL, &error_fatal);
+ Error *local_err = NULL;
+ int ret;
+
+ ret = vmstate_save_state(f, &vmstate_virtio_ccw_dev, dev, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
}
static int virtio_ccw_load_config(DeviceState *d, QEMUFile *f)
{
VirtioCcwDevice *dev = VIRTIO_CCW_DEVICE(d);
- return vmstate_load_state(f, &vmstate_virtio_ccw_dev, dev, 1, &error_fatal);
+ Error *local_err = NULL;
+ int ret;
+
+ ret = vmstate_load_state(f, &vmstate_virtio_ccw_dev, dev, 1, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
+ return ret;
}
static void virtio_ccw_pre_plugged(DeviceState *d, Error **errp)
diff --git a/hw/scsi/spapr_vscsi.c b/hw/scsi/spapr_vscsi.c
index f0a7dd2b882..a6591319db7 100644
--- a/hw/scsi/spapr_vscsi.c
+++ b/hw/scsi/spapr_vscsi.c
@@ -628,10 +628,16 @@ static const VMStateDescription vmstate_spapr_vscsi_req = {
static void vscsi_save_request(QEMUFile *f, SCSIRequest *sreq)
{
vscsi_req *req = sreq->hba_private;
- assert(req->active);
+ Error *local_err = NULL;
+ int rc;
- vmstate_save_state(f, &vmstate_spapr_vscsi_req, req, NULL, &error_fatal);
+ assert(req->active);
+ rc = vmstate_save_state(f, &vmstate_spapr_vscsi_req, req, NULL, &local_err);
+ if (rc < 0) {
+ error_report_err(local_err);
+ return;
+ }
trace_spapr_vscsi_save_request(req->qtag, req->cur_desc_num,
req->cur_desc_offset);
}
diff --git a/hw/virtio/virtio-mmio.c b/hw/virtio/virtio-mmio.c
index fb58c364527..c05c00bcd4a 100644
--- a/hw/virtio/virtio-mmio.c
+++ b/hw/virtio/virtio-mmio.c
@@ -612,15 +612,26 @@ static const VMStateDescription vmstate_virtio_mmio = {
static void virtio_mmio_save_extra_state(DeviceState *opaque, QEMUFile *f)
{
VirtIOMMIOProxy *proxy = VIRTIO_MMIO(opaque);
+ Error *local_err = NULL;
+ int ret;
- vmstate_save_state(f, &vmstate_virtio_mmio, proxy, NULL, &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_virtio_mmio, proxy, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
}
static int virtio_mmio_load_extra_state(DeviceState *opaque, QEMUFile *f)
{
VirtIOMMIOProxy *proxy = VIRTIO_MMIO(opaque);
+ Error *local_err = NULL;
+ int ret;
- return vmstate_load_state(f, &vmstate_virtio_mmio, proxy, 1, &error_fatal);
+ ret = vmstate_load_state(f, &vmstate_virtio_mmio, proxy, 1, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
+ return ret;
}
static bool virtio_mmio_has_extra_state(DeviceState *opaque)
diff --git a/hw/virtio/virtio-pci.c b/hw/virtio/virtio-pci.c
index 7273da4c018..c276fbb017c 100644
--- a/hw/virtio/virtio-pci.c
+++ b/hw/virtio/virtio-pci.c
@@ -189,15 +189,26 @@ static bool virtio_pci_has_extra_state(DeviceState *d)
static void virtio_pci_save_extra_state(DeviceState *d, QEMUFile *f)
{
VirtIOPCIProxy *proxy = to_virtio_pci_proxy(d);
+ Error *local_err = NULL;
+ int ret;
- vmstate_save_state(f, &vmstate_virtio_pci, proxy, NULL, &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_virtio_pci, proxy, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
}
static int virtio_pci_load_extra_state(DeviceState *d, QEMUFile *f)
{
VirtIOPCIProxy *proxy = to_virtio_pci_proxy(d);
+ Error *local_err = NULL;
+ int ret;
- return vmstate_load_state(f, &vmstate_virtio_pci, proxy, 1, &error_fatal);
+ ret = vmstate_load_state(f, &vmstate_virtio_pci, proxy, 1, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
+ return ret;
}
static void virtio_pci_save_queue(DeviceState *d, int n, QEMUFile *f)
diff --git a/hw/virtio/virtio.c b/hw/virtio/virtio.c
index a7063d5555e..d4b2e67c274 100644
--- a/hw/virtio/virtio.c
+++ b/hw/virtio/virtio.c
@@ -3016,7 +3016,7 @@ int virtio_save(VirtIODevice *vdev, QEMUFile *f)
VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
VirtioDeviceClass *vdc = VIRTIO_DEVICE_GET_CLASS(vdev);
uint32_t guest_features_lo = (vdev->guest_features & 0xffffffff);
- int i;
+ int i, ret;
Error *local_err = NULL;
if (k->save_config) {
@@ -3061,7 +3061,7 @@ int virtio_save(VirtIODevice *vdev, QEMUFile *f)
}
if (vdc->vmsd) {
- int ret = vmstate_save_state(f, vdc->vmsd, vdev, NULL, &local_err);
+ ret = vmstate_save_state(f, vdc->vmsd, vdev, NULL, &local_err);
if (ret) {
error_report_err(local_err);
return ret;
@@ -3069,7 +3069,11 @@ int virtio_save(VirtIODevice *vdev, QEMUFile *f)
}
/* Subsections */
- return vmstate_save_state(f, &vmstate_virtio, vdev, NULL, &error_fatal);
+ ret = vmstate_save_state(f, &vmstate_virtio, vdev, NULL, &local_err);
+ if (ret < 0) {
+ error_report_err(local_err);
+ }
+ return ret;
}
/* A wrapper for use as a VMState .put function */
--
2.47.1
^ permalink raw reply [relevance 12%]
* [PATCH QEMU HCI-8.1 07/21] q35: Fix migration of SMRAM state
2026-10-09 14:56 13% ` [PATCH QEMU HCI-8.1 01/21] migration: Plug memory leaks after migrate_set_error() Andrey Drobyshev
2026-10-09 14:56 5% ` [PATCH QEMU HCI-8.1 05/21] vfio/pci: Initialize rom_read_failed in vfio_pci_load_rom() Andrey Drobyshev
@ 2026-10-09 14:56 4% ` Andrey Drobyshev
2026-10-09 15:12 3% ` no-reply
2026-10-09 14:56 12% ` [PATCH QEMU HCI-8.1 09/21] migration: Fix regression of passing error_fatal into vmstate_load_state() Andrey Drobyshev
` (11 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:56 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev
From: Igor Mammedov <imammedo@redhat.com>
When migrating, dst QEMU by default has SMRAM unlocked,
and since wmask is not migrated, the migrated value of
MCH_HOST_BRIDGE_F_SMBASE in config space fall to prey of
mch_update_smbase_smram()
...
if (pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] == 0xff) {
*reg = 0x00;
and is getting cleared and leads to unlocked smram
on dst even if on source it's been locked.
As Andrey has pointed out [1], we should derive wmask
from config and not other way around.
Drop offending chunk and resync wmask based on MCH_HOST_BRIDGE_F_SMBASE
register value. That would preserve the register during
migration and set smram regions into corresponding state.
What that changes is:
that it would let guest write junk values in register
(with no apparent effect) until it's stumbles upon
reserved 0x1 [|] 0x2 values, at which point it
would be only possible to lock register and trigger
switch to SMRAM blackhole in CPU AS.
While at it, fix up test by removing junk discard before negotiation hunk.
PS2:
Instead of adding a dedicated post_load handler for it,
reuse mch_update->mch_update_smbase_smram call chain
that is called on write/reset/post_load to be consistent
with how we handle mch registers.
PS3:
for prosterity here is erro message Andrey got due to this bug:
qemu: vfio_container_dma_map(0x..., 0x0, 0xa0000, 0x....) = -22 (Invalid argument)
qemu: hardware error: vfio: DMA mapping failed, unable to continue
1) https://patchew.org/QEMU/20251203180851.6390-1-arbn@yandex-team.com/
andrey.drobyshev:
This commit is cherry-picked as-is, with no modifications, from the
upstream commit 66cf169e29b0.
Fixes: f404220e279c ("q35: implement 128K SMRAM at default SMBASE address")
Reported-by: Andrey Ryabinin <arbn@yandex-team.com>
Signed-off-by: Igor Mammedov <imammedo@redhat.com>
Reviewed-by: Andrey Ryabinin <arbn@yandex-team.com>
Reviewed-by: Michael S. Tsirkin <mst@redhat.com>
Signed-off-by: Michael S. Tsirkin <mst@redhat.com>
Message-Id: <20251211165454.288476-1-imammedo@redhat.com>
---
hw/pci-host/q35.c | 25 +++++++++++--------------
tests/qtest/q35-test.c | 6 ------
2 files changed, 11 insertions(+), 20 deletions(-)
diff --git a/hw/pci-host/q35.c b/hw/pci-host/q35.c
index a708758d361..946342ba585 100644
--- a/hw/pci-host/q35.c
+++ b/hw/pci-host/q35.c
@@ -432,30 +432,27 @@ static void mch_update_smbase_smram(MCHPCIState *mch)
}
if (*reg == MCH_HOST_BRIDGE_F_SMBASE_QUERY) {
- pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] =
- MCH_HOST_BRIDGE_F_SMBASE_LCK;
+ pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = MCH_HOST_BRIDGE_F_SMBASE_LCK;
*reg = MCH_HOST_BRIDGE_F_SMBASE_IN_RAM;
return;
}
/*
- * default/reset state, discard written value
- * which will disable SMRAM balackhole at SMBASE
+ * reg value can come from register write/reset/migration source,
+ * update wmask to be in sync with it regardless of source
*/
- if (pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] == 0xff) {
- *reg = 0x00;
+ if (*reg == MCH_HOST_BRIDGE_F_SMBASE_IN_RAM) {
+ pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = MCH_HOST_BRIDGE_F_SMBASE_LCK;
+ return;
}
-
- memory_region_transaction_begin();
if (*reg & MCH_HOST_BRIDGE_F_SMBASE_LCK) {
- /* disable all writes */
- pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] &=
- ~MCH_HOST_BRIDGE_F_SMBASE_LCK;
+ /* lock register at 0x2 and disable all writes */
+ pd->wmask[MCH_HOST_BRIDGE_F_SMBASE] = 0;
*reg = MCH_HOST_BRIDGE_F_SMBASE_LCK;
- lck = true;
- } else {
- lck = false;
}
+
+ lck = *reg & MCH_HOST_BRIDGE_F_SMBASE_LCK;
+ memory_region_transaction_begin();
memory_region_set_enabled(&mch->smbase_blackhole, lck);
memory_region_set_enabled(&mch->smbase_window, lck);
memory_region_transaction_commit();
diff --git a/tests/qtest/q35-test.c b/tests/qtest/q35-test.c
index 62fff49fc89..4e3a4457f62 100644
--- a/tests/qtest/q35-test.c
+++ b/tests/qtest/q35-test.c
@@ -206,12 +206,6 @@ static void test_smram_smbase_lock(void)
qtest_writeb(qts, SMBASE, SMRAM_TEST_PATTERN);
g_assert_cmpint(qtest_readb(qts, SMBASE), ==, SMRAM_TEST_PATTERN);
- /* check that writing junk to 0x9c before before negotiating is ignored */
- for (i = 0; i < 0xff; i++) {
- qpci_config_writeb(pcidev, MCH_HOST_BRIDGE_F_SMBASE, i);
- g_assert(qpci_config_readb(pcidev, MCH_HOST_BRIDGE_F_SMBASE) == 0);
- }
-
/* enable SMRAM at SMBASE */
qpci_config_writeb(pcidev, MCH_HOST_BRIDGE_F_SMBASE, 0xff);
g_assert(qpci_config_readb(pcidev, MCH_HOST_BRIDGE_F_SMBASE) == 0x01);
--
2.47.1
^ permalink raw reply [relevance 4%]
* [PATCH QEMU HCI-8.1 05/21] vfio/pci: Initialize rom_read_failed in vfio_pci_load_rom()
2026-10-09 14:56 13% ` [PATCH QEMU HCI-8.1 01/21] migration: Plug memory leaks after migrate_set_error() Andrey Drobyshev
@ 2026-10-09 14:56 5% ` Andrey Drobyshev
2026-10-09 14:56 4% ` [PATCH QEMU HCI-8.1 07/21] q35: Fix migration of SMRAM state Andrey Drobyshev
` (12 subsequent siblings)
14 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:56 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev
From: Mario Casquero <mcasquer@redhat.com>
When vfio_device_get_region_info() fails in vfio_pci_load_rom(),
the function returns without setting vdev->rom_read_failed to true,
and without allocating vdev->rom. This leaves vdev->rom as NULL.
andrey.drobyshev:
This commit is cherry-picked as-is, with no modifications, from the
upstream commit c822ecd80661.
Signed-off-by: Mario Casquero <mcasquer@redhat.com>
Reviewed-by: Cédric Le Goater <clg@redhat.com>
Link: https://lore.kernel.org/qemu-devel/20260602070857.356526-1-mcasquer@redhat.com
Signed-off-by: Cédric Le Goater <clg@redhat.com>
---
hw/vfio/pci.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/hw/vfio/pci.c b/hw/vfio/pci.c
index fa2ca0d779d..bea478d521f 100644
--- a/hw/vfio/pci.c
+++ b/hw/vfio/pci.c
@@ -1039,7 +1039,7 @@ static void vfio_update_msi(VFIOPCIDevice *vdev)
}
}
-static void vfio_pci_load_rom(VFIOPCIDevice *vdev)
+static bool vfio_pci_load_rom(VFIOPCIDevice *vdev)
{
VFIODevice *vbasedev = &vdev->vbasedev;
struct vfio_region_info *reg_info = NULL;
@@ -1053,7 +1053,7 @@ static void vfio_pci_load_rom(VFIOPCIDevice *vdev)
if (ret != 0) {
error_report("vfio: Error getting ROM info: %s", strerror(-ret));
- return;
+ return false;
}
trace_vfio_pci_load_rom(vbasedev->name, (unsigned long)reg_info->size,
@@ -1064,12 +1064,11 @@ static void vfio_pci_load_rom(VFIOPCIDevice *vdev)
vdev->rom_offset = reg_info->offset;
if (!vdev->rom_size) {
- vdev->rom_read_failed = true;
error_report("vfio-pci: Cannot read device rom at %s", vbasedev->name);
error_printf("Device option ROM contents are probably invalid "
"(check dmesg).\nSkip option ROM probe with rombar=0, "
"or load from file with romfile=\n");
- return;
+ return false;
}
vdev->rom = g_malloc(size);
@@ -1125,6 +1124,8 @@ static void vfio_pci_load_rom(VFIOPCIDevice *vdev)
data[6] = -csum;
}
}
+
+ return true;
}
/* "Raw" read of underlying config space. */
@@ -1158,7 +1159,7 @@ static uint64_t vfio_rom_read(void *opaque, hwaddr addr, unsigned size)
/* Load the ROM lazily when the guest tries to read it */
if (unlikely(!vdev->rom && !vdev->rom_read_failed)) {
- vfio_pci_load_rom(vdev);
+ vdev->rom_read_failed = !vfio_pci_load_rom(vdev);
}
memcpy(&val, vdev->rom + addr,
--
2.47.1
^ permalink raw reply [relevance 5%]
* [PATCH QEMU HCI-8.1 01/21] migration: Plug memory leaks after migrate_set_error()
@ 2026-10-09 14:56 13% ` Andrey Drobyshev
2026-10-09 15:09 0% ` no-reply
2026-10-09 14:56 5% ` [PATCH QEMU HCI-8.1 05/21] vfio/pci: Initialize rom_read_failed in vfio_pci_load_rom() Andrey Drobyshev
` (13 subsequent siblings)
14 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:56 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev
From: Markus Armbruster <armbru@redhat.com>
migrate_set_error(s, err) stores a copy of @err in @s. The original
@err is not freed. Most callers free it immediately. Some callers
free it later, or pass it on. And some leak it. Fix those.
Perhaps migrate_set_error(s, err) should take ownership of @err. The
callers that free it immediately would become simpler, and avoid a
copy and a deallocation. The others would have to pass
error_copy(err).
andrey.drobyshev:
This commit is cherry-picked as-is, with no modifications, from the
upstream commit 93817ec39656.
Signed-off-by: Markus Armbruster <armbru@redhat.com>
Link: https://lore.kernel.org/r/20251115083500.2753895-2-armbru@redhat.com
Signed-off-by: Peter Xu <peterx@redhat.com>
---
migration/cpr-exec.c | 3 ++-
migration/multifd.c | 2 ++
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/migration/cpr-exec.c b/migration/cpr-exec.c
index edace3f0bd8..7b7609e19cd 100644
--- a/migration/cpr-exec.c
+++ b/migration/cpr-exec.c
@@ -227,11 +227,12 @@ static void cpr_exec_cb(void *opaque)
error_report_err(error_copy(err));
migrate_set_state(&s->state, s->state, MIGRATION_STATUS_FAILED);
migrate_set_error(s, err);
+ error_free(err);
+ err = NULL;
/* Note, we can go from state COMPLETED to FAILED */
migration_call_notifiers(MIG_EVENT_PRECOPY_FAILED, NULL);
- err = NULL;
if (!migration_block_activate(&err)) {
/* error was already reported */
error_free(err);
diff --git a/migration/multifd.c b/migration/multifd.c
index 5a723ae25d2..e369421b3ff 100644
--- a/migration/multifd.c
+++ b/migration/multifd.c
@@ -966,6 +966,7 @@ bool multifd_send_setup(void)
if (!multifd_new_send_channel_create(p, &local_err)) {
migrate_set_error(s, local_err);
+ error_free(local_err);
ret = -1;
}
}
@@ -990,6 +991,7 @@ bool multifd_send_setup(void)
ret = multifd_send_state->ops->send_setup(p, &local_err);
if (ret) {
migrate_set_error(s, local_err);
+ error_free(local_err);
goto err;
}
assert(p->iov);
--
2.47.1
^ permalink raw reply [relevance 13%]
* [Devel] [PATCH RFC PATCH hci-8.1 4/4] selftests/mm: test memory preservation across exec
` (2 preceding siblings ...)
@ 2026-10-09 14:00 4% ` Andrey Drobyshev
2026-10-09 15:07 3% ` no-reply
3 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:00 UTC (permalink / raw)
To: Konstantin Khorenko; +Cc: devel
Add an ELF-opted-in MADV_DOEXEC test and wire it into the mm test runner.
Compare CPU writes against retained long-term GUP pins across repeated
exec and a later fork, including base pages, THP, mlock, protected mappings
and fast GUP. Check existing COW mappings separately.
Exercise recoverable rejection and retry with an external CLONE_VM task,
own and non-leader threads, concurrent shared-mm clone creation, proc
inspection during repeated exec, and vhost owner reset before exec. Add
an optional task-local allocation-failure sweep.
https://virtuozzo.atlassian.net/browse/VSTOR-103860
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
tools/testing/selftests/mm/Makefile | 1 +
tools/testing/selftests/mm/doexec.c | 774 ++++++++++++++++++++++++++++++
tools/testing/selftests/mm/run_vmtests.sh | 1 +
3 files changed, 776 insertions(+)
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index c837b0549c89..7f9efb0e0a38 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -74,6 +74,7 @@ endif
TEST_GEN_FILES = cow
TEST_GEN_FILES += compaction_test
+TEST_GEN_FILES += doexec
TEST_GEN_FILES += gup_longterm
TEST_GEN_FILES += gup_test
TEST_GEN_FILES += hmm-tests
diff --git a/tools/testing/selftests/mm/doexec.c b/tools/testing/selftests/mm/doexec.c
new file mode 100644
index 000000000000..6ec09e868679
--- /dev/null
+++ b/tools/testing/selftests/mm/doexec.c
@@ -0,0 +1,774 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Verify that exec preserves the memory backing long-term DMA pins. */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/vhost.h>
+#include <poll.h>
+#include <pthread.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/resource.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../../../../mm/gup_test.h"
+#include "../kselftest.h"
+
+#ifndef MADV_DOEXEC
+#define MADV_DOEXEC 201
+#endif
+#ifndef MADV_COLLAPSE
+#define MADV_COLLAPSE 25
+#endif
+
+/* The downstream ELF loader requires this opt-in in a PT_NOTE segment. */
+asm(".pushsection .note.doexec,\"a\",@note\n"
+ ".balign 4\n"
+ ".long 17, 0, 0x07c1feed\n"
+ ".asciz \"preserved-mem-ok\"\n"
+ ".balign 4\n"
+ ".popsection\n");
+
+enum test_kind {
+ BASE,
+ HUGE,
+ LOCKED,
+ PROTECTED,
+ READ_PIN,
+ FAST_PIN,
+ NR_TESTS,
+};
+
+static const char * const names[] = {
+ "base pages",
+ "PMD huge pages",
+ "mlocked pages",
+ "read-only mapping",
+ "read-only pins",
+ "fast GUP pins",
+};
+
+static int wait_child(pid_t pid)
+{
+ int status;
+ pid_t ret;
+
+ do {
+ ret = waitpid(pid, &status, 0);
+ } while (ret < 0 && errno == EINTR);
+ if (ret < 0 || !WIFEXITED(status))
+ return KSFT_FAIL;
+ return WEXITSTATUS(status);
+}
+
+static int check_pin(int fd, char *ram, size_t size, unsigned char value)
+{
+ char *copy = malloc(size);
+ uint64_t addr = (uintptr_t)copy;
+ size_t i;
+ int ret = KSFT_FAIL;
+
+ if (!copy)
+ return ret;
+ if (ioctl(fd, PIN_LONGTERM_TEST_READ, &addr)) {
+ ksft_print_msg("read pin: %s\n", strerror(errno));
+ goto out;
+ }
+ for (i = 0; i < size; i++) {
+ if ((unsigned char)ram[i] != value ||
+ (unsigned char)copy[i] != value) {
+ ksft_print_msg("CPU/pin mismatch at byte %zu\n", i);
+ goto out;
+ }
+ }
+ ret = KSFT_PASS;
+out:
+ free(copy);
+ return ret;
+}
+
+static int check_bytes(char *ram, size_t size, unsigned char value)
+{
+ size_t i;
+
+ for (i = 0; i < size; i++)
+ if ((unsigned char)ram[i] != value)
+ return KSFT_FAIL;
+ return KSFT_PASS;
+}
+
+static int after_cow_exec(char *ram, size_t size, pid_t sibling, int pipe_fd)
+{
+ char ready = 1;
+ int ret = check_bytes(ram, size, 0x31);
+
+ memset(ram, 0x42, size);
+ if (write(pipe_fd, &ready, 1) != 1)
+ ret = KSFT_FAIL;
+ close(pipe_fd);
+ if (wait_child(sibling))
+ ret = KSFT_FAIL;
+ return ret;
+}
+
+/* Pages shared with a separate mm must retain their pre-existing COW state. */
+static int before_cow_exec(void)
+{
+ size_t size = 16 * sysconf(_SC_PAGESIZE);
+ char addr_arg[32], size_arg[32], pid_arg[32], fd_arg[32], ready;
+ int pipe_fd[2];
+ char *ram;
+ pid_t pid;
+
+ ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (ram == MAP_FAILED)
+ return KSFT_FAIL;
+ memset(ram, 0x31, size);
+ if (madvise(ram, size, MADV_DOEXEC))
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ if (pipe(pipe_fd))
+ return KSFT_FAIL;
+ pid = fork();
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ close(pipe_fd[1]);
+ if (read(pipe_fd[0], &ready, 1) != 1)
+ _exit(KSFT_FAIL);
+ _exit(check_bytes(ram, size, 0x31));
+ }
+ close(pipe_fd[0]);
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)ram);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ snprintf(pid_arg, sizeof(pid_arg), "%d", pid);
+ snprintf(fd_arg, sizeof(fd_arg), "%d", pipe_fd[1]);
+ execl("/proc/self/exe", "doexec", "--cow", addr_arg, size_arg,
+ pid_arg, fd_arg, NULL);
+ close(pipe_fd[1]);
+ wait_child(pid);
+ return KSFT_FAIL;
+}
+
+static void exec_stage(int fd, char *ram, size_t size, int kind, int stage)
+{
+ char fd_arg[32], addr_arg[32], size_arg[32], kind_arg[32], stage_arg[32];
+
+ snprintf(fd_arg, sizeof(fd_arg), "%d", fd);
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)ram);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ snprintf(kind_arg, sizeof(kind_arg), "%d", kind);
+ snprintf(stage_arg, sizeof(stage_arg), "%d", stage);
+ execl("/proc/self/exe", "doexec", "--exec", fd_arg, addr_arg, size_arg,
+ kind_arg, stage_arg, NULL);
+ ksft_print_msg("exec: %s\n", strerror(errno));
+ _exit(KSFT_FAIL);
+}
+
+static int after_exec(int fd, char *ram, size_t size, int kind, int stage)
+{
+ unsigned char before = stage == 1 ? 0x31 : 0x42;
+ unsigned char after = stage == 1 ? 0x42 : 0x53;
+ pid_t pid;
+
+ if (check_pin(fd, ram, size, before))
+ return KSFT_FAIL;
+ if (kind == PROTECTED && mprotect(ram, size, PROT_READ | PROT_WRITE))
+ return KSFT_FAIL;
+ memset(ram, after, size);
+ if (check_pin(fd, ram, size, after))
+ return KSFT_FAIL;
+ if (stage == 1) {
+ /* VM_EXEC_KEEP is consumed, so mark again for the second exec. */
+ if (madvise(ram, size, MADV_DOEXEC))
+ return KSFT_FAIL;
+ if (kind == PROTECTED && mprotect(ram, size, PROT_READ))
+ return KSFT_FAIL;
+ exec_stage(fd, ram, size, kind, 2);
+ }
+
+ /* Fork must copy into the child and retain the parent's pinned pages. */
+ pid = fork();
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ memset(ram, 0x64, size);
+ _exit(KSFT_PASS);
+ }
+ if (wait_child(pid) || check_pin(fd, ram, size, after))
+ return KSFT_FAIL;
+ memset(ram, 0x75, size);
+ if (check_pin(fd, ram, size, 0x75))
+ return KSFT_FAIL;
+ return ioctl(fd, PIN_LONGTERM_TEST_STOP) ? KSFT_FAIL : KSFT_PASS;
+}
+
+static size_t huge_page_size(void)
+{
+ FILE *file = fopen("/sys/kernel/mm/transparent_hugepage/hpage_pmd_size", "r");
+ size_t size = 0;
+
+ if (file) {
+ if (fscanf(file, "%zu", &size) != 1)
+ size = 0;
+ fclose(file);
+ }
+ return size;
+}
+
+static int before_exec(int kind)
+{
+ size_t size = 16 * sysconf(_SC_PAGESIZE);
+ struct pin_longterm_test pin;
+ char *ram, *mapping;
+ uintptr_t aligned;
+ int fd;
+
+ if (kind == HUGE) {
+ size = huge_page_size();
+ if (!size)
+ return KSFT_SKIP;
+ }
+ fd = open("/sys/kernel/debug/gup_test", O_RDWR);
+ if (fd < 0)
+ return KSFT_SKIP;
+ mapping = mmap(NULL, 2 * size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (mapping == MAP_FAILED)
+ return KSFT_FAIL;
+ aligned = ((uintptr_t)mapping + size - 1) & ~(size - 1);
+ ram = (char *)aligned;
+ if (ram != mapping)
+ munmap(mapping, ram - mapping);
+ if (ram + size != mapping + 2 * size)
+ munmap(ram + size, mapping + 2 * size - (ram + size));
+ if (madvise(ram, size, kind == HUGE ? MADV_HUGEPAGE : MADV_NOHUGEPAGE))
+ return KSFT_FAIL;
+ memset(ram, 0x31, size);
+ if (kind == HUGE && madvise(ram, size, MADV_COLLAPSE))
+ return KSFT_SKIP;
+ if (kind == LOCKED && mlock(ram, size))
+ return KSFT_SKIP;
+ if (madvise(ram, size, MADV_DOEXEC)) {
+ ksft_print_msg("MADV_DOEXEC: %s\n", strerror(errno));
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ }
+ pin = (struct pin_longterm_test) {
+ .addr = (uintptr_t)ram,
+ .size = size,
+ .flags = kind == READ_PIN ? 0 : PIN_LONGTERM_TEST_FLAG_USE_WRITE,
+ };
+ if (kind == FAST_PIN)
+ pin.flags |= PIN_LONGTERM_TEST_FLAG_USE_FAST;
+ if (ioctl(fd, PIN_LONGTERM_TEST_START, &pin)) {
+ ksft_print_msg("pin: %s\n", strerror(errno));
+ return errno == ENOTTY ? KSFT_SKIP : KSFT_FAIL;
+ }
+ if (kind == PROTECTED && mprotect(ram, size, PROT_READ))
+ return KSFT_FAIL;
+ exec_stage(fd, ram, size, kind, 1);
+ return KSFT_FAIL;
+}
+
+static int check_exec(char *ram, size_t size, size_t stride, bool reject)
+{
+ char addr_arg[32], size_arg[32], stride_arg[32];
+
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)ram);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ snprintf(stride_arg, sizeof(stride_arg), "%zu", stride);
+ execl("/proc/self/exe", "doexec", reject ? "--reject" : "--check", addr_arg, size_arg,
+ stride_arg, NULL);
+ return errno;
+}
+
+static char *preserved_ram(size_t size)
+{
+ char *ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+
+ if (ram == MAP_FAILED)
+ return NULL;
+ memset(ram, 0x31, size);
+ if (madvise(ram, size, MADV_DOEXEC)) {
+ munmap(ram, size);
+ return NULL;
+ }
+ return ram;
+}
+
+static int blocked_task(void *arg)
+{
+ int fd = *(int *)arg;
+ char ready;
+
+ return read(fd, &ready, 1) == 1 ? KSFT_PASS : KSFT_FAIL;
+}
+
+static void *blocked_thread(void *arg)
+{
+ blocked_task(arg);
+ return NULL;
+}
+
+/* Own threads may be killed by exec; a separate CLONE_VM task may not. */
+static int shared_mm_exec(bool own_thread)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size), *stack;
+ int pipe_fd[2], ret;
+ pthread_t thread;
+ pid_t pid;
+
+ if (!ram)
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ if (pipe(pipe_fd))
+ return KSFT_FAIL;
+ if (own_thread) {
+ if (pthread_create(&thread, NULL, blocked_thread, &pipe_fd[0]))
+ return KSFT_FAIL;
+ check_exec(ram, size, 1, false);
+ write(pipe_fd[1], "x", 1);
+ pthread_join(thread, NULL);
+ return KSFT_FAIL;
+ }
+ stack = mmap(NULL, 65536, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
+ if (stack == MAP_FAILED)
+ return KSFT_FAIL;
+ pid = clone(blocked_task, stack + 65536, CLONE_VM | SIGCHLD, &pipe_fd[0]);
+ if (pid < 0)
+ return KSFT_FAIL;
+ ret = check_exec(ram, size, 1, true);
+ /* Exec must return EBUSY with the old mapping intact. */
+ if (ret != EBUSY || check_bytes(ram, size, 0x31))
+ ret = KSFT_FAIL;
+ else
+ ret = KSFT_PASS;
+ write(pipe_fd[1], "x", 1);
+ if (wait_child(pid))
+ ret = KSFT_FAIL;
+ if (ret)
+ return ret;
+ /* Releasing the outside user must allow a retry. */
+ check_exec(ram, size, 1, false);
+ return KSFT_FAIL;
+}
+
+/* The caller must release its backend owner before preserving exec. */
+static int vhost_owner_exec(void)
+{
+ size_t size = sysconf(_SC_PAGESIZE);
+ char *ram;
+ int fd = open("/dev/vhost-net", O_RDWR);
+
+ if (fd < 0)
+ return KSFT_SKIP;
+ ram = preserved_ram(size);
+ if (!ram)
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ if (ioctl(fd, VHOST_SET_OWNER))
+ return KSFT_SKIP;
+ if (ioctl(fd, VHOST_RESET_OWNER))
+ return KSFT_FAIL;
+ close(fd);
+ check_exec(ram, size, 1, false);
+ return KSFT_FAIL;
+}
+
+static void *exec_thread(void *arg)
+{
+ check_exec(arg, sysconf(_SC_PAGESIZE), 1, false);
+ return NULL;
+}
+
+static int nonleader_exec(void)
+{
+ char *ram = preserved_ram(sysconf(_SC_PAGESIZE));
+ pthread_t thread;
+
+ if (!ram)
+ return errno == EINVAL ? KSFT_SKIP : KSFT_FAIL;
+ if (pthread_create(&thread, NULL, exec_thread, ram))
+ return KSFT_FAIL;
+ pthread_join(thread, NULL);
+ return KSFT_FAIL;
+}
+
+static int proc_exec(char *ram, size_t size, int stage)
+{
+ char addr_arg[32], size_arg[32], stage_arg[32];
+
+ if (check_bytes(ram, size, 0x31))
+ return KSFT_FAIL;
+ if (stage == 32)
+ return KSFT_PASS;
+ if (madvise(ram, size, MADV_DOEXEC))
+ return KSFT_FAIL;
+ snprintf(addr_arg, sizeof(addr_arg), "%llu",
+ (unsigned long long)(uintptr_t)ram);
+ snprintf(size_arg, sizeof(size_arg), "%zu", size);
+ snprintf(stage_arg, sizeof(stage_arg), "%d", stage + 1);
+ execl("/proc/self/exe", "doexec", "--proc", addr_arg, size_arg,
+ stage_arg, NULL);
+ return KSFT_FAIL;
+}
+
+/* Read both pre-opened and freshly opened proc files during repeated exec. */
+static int proc_readers_exec(void)
+{
+ static const char * const files[] = { "maps", "smaps_rollup", "pagemap" };
+ int ready[2], go[2], fds[3] = { -1, -1, -1 }, status, ret = KSFT_FAIL;
+ char paths[3][80], buffer[4096];
+ unsigned int round = 0;
+ pid_t pid, waited;
+ int i;
+
+ if (pipe(ready) || pipe(go))
+ return KSFT_FAIL;
+ pid = fork();
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ size_t size = 16 * sysconf(_SC_PAGESIZE);
+ char *ram = preserved_ram(size);
+
+ close(ready[0]);
+ close(go[1]);
+ if (!ram)
+ _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ if (write(ready[1], "x", 1) != 1 || read(go[0], buffer, 1) != 1)
+ _exit(KSFT_FAIL);
+ close(ready[1]);
+ close(go[0]);
+ _exit(proc_exec(ram, size, 0));
+ }
+ close(ready[1]);
+ close(go[0]);
+ if (read(ready[0], buffer, 1) != 1) {
+ close(ready[0]);
+ close(go[1]);
+ return wait_child(pid);
+ }
+ close(ready[0]);
+ for (i = 0; i < 3; i++) {
+ snprintf(paths[i], sizeof(paths[i]), "/proc/%d/%s", pid, files[i]);
+ fds[i] = open(paths[i], O_RDONLY);
+ if (fds[i] < 0) {
+ ret = KSFT_SKIP;
+ goto stop;
+ }
+ }
+ if (write(go[1], "x", 1) != 1)
+ goto stop;
+ close(go[1]);
+ go[1] = -1;
+ for (;;) {
+ waited = waitpid(pid, &status, WNOHANG);
+ if (waited == pid) {
+ ret = WIFEXITED(status) ? WEXITSTATUS(status) : KSFT_FAIL;
+ break;
+ }
+ if (waited < 0 && errno != EINTR)
+ break;
+ for (i = 0; i < 3; i++) {
+ lseek(fds[i], 0, SEEK_SET);
+ if (read(fds[i], buffer, sizeof(buffer)) < 0 &&
+ errno != EINTR && errno != ESRCH)
+ goto stop;
+ }
+ /* Keep two old handles while refreshing the third. */
+ i = round++ % 3;
+ close(fds[i]);
+ fds[i] = open(paths[i], O_RDONLY);
+ if (fds[i] < 0) {
+ ret = wait_child(pid);
+ break;
+ }
+ }
+ for (i = 0; i < 3; i++)
+ if (fds[i] >= 0)
+ close(fds[i]);
+ return ret;
+stop:
+ if (go[1] >= 0)
+ close(go[1]);
+ kill(pid, SIGKILL);
+ wait_child(pid);
+ for (i = 0; i < 3; i++)
+ if (fds[i] >= 0)
+ close(fds[i]);
+ return ret;
+}
+
+struct clone_race {
+ char *ram;
+ int report;
+ atomic_bool stop;
+ atomic_uint rounds;
+};
+
+static int clone_race_child(void *arg)
+{
+ struct clone_race *race = arg;
+ unsigned int i;
+
+ for (i = 0; i < 8; i++) {
+ if (__atomic_load_n(race->ram, __ATOMIC_RELAXED) != 0x31) {
+ write(race->report, "x", 1);
+ return KSFT_FAIL;
+ }
+ usleep(1000);
+ }
+ return KSFT_PASS;
+}
+
+static void *clone_race_thread(void *arg)
+{
+ struct clone_race *race = arg;
+ char *stack = mmap(NULL, 65536, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);
+ pid_t pid;
+
+ if (stack == MAP_FAILED) {
+ write(race->report, "x", 1);
+ atomic_store(&race->rounds, 4);
+ return NULL;
+ }
+ while (!atomic_load(&race->stop)) {
+ pid = clone(clone_race_child, stack + 65536,
+ CLONE_VM | SIGCHLD, race);
+ if (pid >= 0) {
+ if (wait_child(pid))
+ write(race->report, "x", 1);
+ } else if (errno != EBUSY && errno != EINTR) {
+ write(race->report, "x", 1);
+ }
+ atomic_fetch_add(&race->rounds, 1);
+ usleep(1000);
+ }
+ munmap(stack, 65536);
+ return NULL;
+}
+
+/* A supervisor collects errors from any shared-mm child surviving exec. */
+static int clone_exec_race(void)
+{
+ int report[2], ret;
+ struct pollfd pfd;
+ char failed;
+ pid_t pid;
+
+ if (pipe(report))
+ return KSFT_FAIL;
+ pid = fork();
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ size_t size = sysconf(_SC_PAGESIZE);
+ struct clone_race race = { .report = report[1] };
+ pthread_t thread;
+ unsigned int attempt;
+
+ close(report[0]);
+ race.ram = preserved_ram(size);
+ if (!race.ram)
+ _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ if (pthread_create(&thread, NULL, clone_race_thread, &race))
+ _exit(KSFT_FAIL);
+ while (atomic_load(&race.rounds) < 4)
+ usleep(1000);
+ for (attempt = 0; attempt < 500; attempt++) {
+ if (check_exec(race.ram, size, 1, false) != EBUSY)
+ break;
+ if (check_bytes(race.ram, size, 0x31))
+ break;
+ usleep(1000);
+ }
+ atomic_store(&race.stop, true);
+ pthread_join(thread, NULL);
+ _exit(KSFT_FAIL);
+ }
+ close(report[1]);
+ ret = wait_child(pid);
+ pfd = (struct pollfd) { .fd = report[0], .events = POLLIN | POLLHUP };
+ if (poll(&pfd, 1, 3000) <= 0 || read(report[0], &failed, 1) != 0)
+ ret = KSFT_FAIL;
+ close(report[0]);
+ return ret;
+}
+
+/* Opt-in sweep of allocation failures, using only the child's fail-nth. */
+static int allocation_failures(void)
+{
+ const size_t size = 256ul << 20, stride = 2ul << 20;
+ struct rlimit no_core = { 0, 0 };
+ unsigned int early = 0, fatal = 0, success = 0, loader = 0;
+ unsigned int nth;
+ pid_t pid;
+ int status;
+ pid_t waited;
+
+ if (!getenv("DOEXEC_FAULT_INJECTION") || access("/proc/self/fail-nth", W_OK))
+ return KSFT_SKIP;
+ for (nth = 1; nth <= 256; nth++) {
+ pid = fork();
+ if (pid < 0)
+ return KSFT_FAIL;
+ if (!pid) {
+ char *ram, number[32];
+ size_t i;
+ int fd, len, err;
+
+ setrlimit(RLIMIT_CORE, &no_core);
+ ram = mmap(NULL, size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (ram == MAP_FAILED || madvise(ram, size, MADV_NOHUGEPAGE))
+ _exit(KSFT_FAIL);
+ for (i = 0; i < size; i += stride)
+ ram[i] = 0x31;
+ if (madvise(ram, size, MADV_DOEXEC))
+ _exit(errno == EINVAL ? KSFT_SKIP : KSFT_FAIL);
+ fd = open("/proc/self/fail-nth", O_WRONLY | O_CLOEXEC);
+ len = snprintf(number, sizeof(number), "%u", nth);
+ if (fd < 0 || write(fd, number, len) != len)
+ _exit(KSFT_FAIL);
+ err = check_exec(ram, size, stride, false);
+ /* Disarm before checking mappings or exiting. */
+ pwrite(fd, "0", 1, 0);
+ for (i = 0; i < size; i += stride)
+ if (ram[i] != 0x31)
+ _exit(KSFT_FAIL);
+ _exit(err == ENOMEM || err == EFAULT || err == EAGAIN ? 10 : KSFT_FAIL);
+ }
+ do {
+ waited = waitpid(pid, &status, 0);
+ } while (waited < 0 && errno == EINTR);
+ if (waited < 0)
+ return KSFT_FAIL;
+ if (WIFSIGNALED(status) && WTERMSIG(status) == SIGSEGV)
+ fatal++;
+ else if (WIFEXITED(status) && WEXITSTATUS(status) == 10)
+ early++;
+ else if (WIFEXITED(status) && WEXITSTATUS(status) == KSFT_PASS)
+ success++;
+ /* A dynamic loader may exit when a post-exec allocation fails. */
+ else if (WIFEXITED(status) && WEXITSTATUS(status) == 127)
+ loader++;
+ else if (WIFEXITED(status) && WEXITSTATUS(status) == KSFT_SKIP)
+ return KSFT_SKIP;
+ else
+ return KSFT_FAIL;
+ }
+ ksft_print_msg("faults: %u recoverable, %u SIGSEGV, %u success, %u loader exits\n",
+ early, fatal, success, loader);
+ /* A sweep that injected no recoverable/fatal errors provides no coverage. */
+ return early && fatal ? KSFT_PASS : KSFT_SKIP;
+}
+
+int main(int argc, char **argv)
+{
+ pid_t pid;
+ int kind, ret;
+
+ if (argc == 7 && !strcmp(argv[1], "--exec"))
+ return after_exec(atoi(argv[2]),
+ (char *)(uintptr_t)strtoull(argv[3], NULL, 10),
+ strtoull(argv[4], NULL, 10), atoi(argv[5]), atoi(argv[6]));
+ if (argc == 6 && !strcmp(argv[1], "--cow"))
+ return after_cow_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
+ strtoull(argv[3], NULL, 10), atoi(argv[4]), atoi(argv[5]));
+ if (argc == 5 && !strcmp(argv[1], "--reject"))
+ return KSFT_FAIL;
+ if (argc == 5 && !strcmp(argv[1], "--proc"))
+ return proc_exec((char *)(uintptr_t)strtoull(argv[2], NULL, 10),
+ strtoull(argv[3], NULL, 10), atoi(argv[4]));
+ if (argc == 5 && !strcmp(argv[1], "--check")) {
+ char *ram = (char *)(uintptr_t)strtoull(argv[2], NULL, 10);
+ size_t size = strtoull(argv[3], NULL, 10);
+ size_t stride = strtoull(argv[4], NULL, 10), i;
+ int fd = open("/proc/self/fail-nth", O_WRONLY);
+
+ if (fd >= 0) {
+ write(fd, "0", 1);
+ close(fd);
+ }
+ for (i = 0; i < size; i += stride)
+ if (ram[i] != 0x31)
+ return KSFT_FAIL;
+ return KSFT_PASS;
+ }
+ ksft_print_header();
+ ksft_set_plan(NR_TESTS + 8);
+ for (kind = 0; kind < NR_TESTS; kind++) {
+ fflush(stdout);
+ pid = fork();
+ if (!pid)
+ _exit(before_exec(kind));
+ ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
+ if (ret == KSFT_SKIP)
+ ksft_test_result_skip("%s: DOEXEC, GUP test or mapping unavailable\n",
+ names[kind]);
+ else
+ ksft_test_result(ret == KSFT_PASS, "%s: repeated exec and fork\n",
+ names[kind]);
+ }
+ fflush(stdout);
+ pid = fork();
+ if (!pid)
+ _exit(before_cow_exec());
+ ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
+ if (ret == KSFT_SKIP)
+ ksft_test_result_skip("pre-existing COW: DOEXEC unavailable\n");
+ else
+ ksft_test_result(ret == KSFT_PASS, "pre-existing COW preserved\n");
+ for (kind = 0; kind < 7; kind++) {
+ static const char * const extra_names[] = {
+ "CLONE_VM: recoverable EBUSY and retry",
+ "own threads: preserving exec allowed",
+ "vhost owner: reset before preserving exec",
+ "allocation failure sweep (opt-in)",
+ "non-leader thread: preserving exec allowed",
+ "proc readers: repeated preserving exec allowed",
+ "concurrent CLONE_VM: shared memory is not stolen",
+ };
+
+ fflush(stdout);
+ pid = fork();
+ if (!pid) {
+ switch (kind) {
+ case 0:
+ case 1:
+ _exit(shared_mm_exec(kind));
+ case 2:
+ _exit(vhost_owner_exec());
+ case 3:
+ _exit(allocation_failures());
+ case 4:
+ _exit(nonleader_exec());
+ case 5:
+ _exit(proc_readers_exec());
+ case 6:
+ _exit(clone_exec_race());
+ }
+ }
+ ret = pid < 0 ? KSFT_FAIL : wait_child(pid);
+ if (ret == KSFT_SKIP)
+ ksft_test_result_skip("%s: unavailable or disabled\n", extra_names[kind]);
+ else
+ ksft_test_result(ret == KSFT_PASS, "%s\n", extra_names[kind]);
+ }
+ ksft_finished();
+}
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 6ac8ba7e2447..9643b39762b8 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -296,6 +296,7 @@ fi
# Dump pages 0, 19, and 4096, using pin_user_pages:
CATEGORY="gup_test" run_test ./gup_test -ct -F 0x1 0 19 0x1000
CATEGORY="gup_test" run_test ./gup_longterm
+CATEGORY="gup_test" run_test ./doexec
CATEGORY="userfaultfd" run_test ./uffd-unit-tests
uffd_stress_bin=./uffd-stress
--
2.47.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] mm: allow MADV_DOEXEC on shared PFN mappings of device memory
@ 2026-10-09 14:39 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 14:39 UTC (permalink / raw)
To: Andrey Drobyshev, devel
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] mm: MADV_DOEXEC dups any shared PFNMAP VMA, sharing vm_private_data across mms
- [Medium] mm: vma_dup() links pfnmap VMA into i_mmap before PTE copy, so zap can miss them
- [Low] mm: vma_dup() keeps inherited VM_PAT on PFNMAP copy, unlike dup_mmap()
--
commit 2e5fc626303801168dc124d9f0c079510fb5d4fd
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
mm: allow MADV_DOEXEC on shared PFN mappings of device memory
This patch lets MADV_DOEXEC also preserve shared VM_PFNMAP file mappings
such as VFIO BARs. vma_dup() duplicates them the way fork does for a shared
file mapping: it takes a file reference, links the VMA into i_mmap, calls
vm_ops->open() and copies the PTEs.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-103860
> diff --git a/include/linux/mm.h b/include/linux/mm.h
> --- a/include/linux/mm.h
> +++ b/include/linux/mm.h
> @@ -1018,6 +1018,18 @@ static inline bool vma_is_anonymous(struct vm_area_struct *vma)
> return !vma->vm_ops;
> }
>
> +/*
> + * Shared mapping of memory without struct pages, such as a device BAR
> + * mapped through VFIO. Such a VMA may still have no vm_ops (when the
> + * driver populates it with remap_pfn_range() at mmap time), so it must
> + * be tested before vma_is_anonymous().
> + */
> +static inline bool vma_is_shared_pfnmap(struct vm_area_struct *vma)
> +{
> + return vma->vm_file &&
> + (vma->vm_flags & (VM_SHARED | VM_PFNMAP)) == (VM_SHARED | VM_PFNMAP);
> +}
> +
[Severity: High]
The eligibility test here and in madvise_vma_behavior() is purely
structural: a file plus VM_SHARED|VM_PFNMAP. There is no driver opt-in, and
VM_DONTCOPY and VM_IO are not excluded. Can this let unprivileged userspace
opt any driver PFN mapping into exec-time duplication?
vma_dup() copies the VMA with vm_area_dup(), so vm_private_data is shared
verbatim between the old and new VMA. Only vm_ops->open() is called, never
->mmap(). Drivers that free per-mapping state in ->close() without a
refcounting ->open() then get two VMAs and two ->close() calls.
For example, gru_file_mmap() sets VM_IO|VM_PFNMAP|VM_DONTCOPY and stores
vdata in vm_private_data. gru_vma_close() does:
vdata = vma->vm_private_data;
vma->vm_private_data = NULL;
...
kfree(vdata);
There is no ->open(). The duplicated VMA keeps the stale vdata pointer, so
can its later close double free vdata?
genwqe_mmap() uses remap_pfn_range(), and genwqe_vma_open() is an empty
stub. genwqe_vma_close() runs at exec teardown of the old mm:
__genwqe_del_mapping(cfile, dma_map);
__genwqe_free_consistent(cd, dma_map->size, dma_map->k_vaddr,
dma_map->dma_addr);
kfree(dma_map);
Can this free the coherent DMA buffer while the duplicated PFNMAP VMA in the
new mm still maps it, leaving userspace with access to freed memory?
uacce sets VM_DONTCOPY|VM_WIPEONFORK and kfree()s its qfr on close. Its
->mmap() uniqueness check is bypassed by the duplication, so can the
hardware mapping outlive its qfr?
Drivers set VM_DONTCOPY to say the VMA should not be duplicated.
MADV_DOFORK rejects VM_IO. Should this path require an explicit driver
opt-in, or at least reject VM_DONTCOPY and VM_IO VMAs, or require ->open()
to be defined?
> diff --git a/mm/mmap.c b/mm/mmap.c
> --- a/mm/mmap.c
> +++ b/mm/mmap.c
[ ... ]
> int vma_dup(struct vm_area_struct *old_vma, struct mm_struct *mm)
> {
> struct vm_area_struct *vma;
> unsigned long npages;
> + bool pfnmap = vma_is_shared_pfnmap(old_vma);
> int ret = -ENOMEM;
[ ... ]
> vma->vm_mm = mm;
> - ret = anon_vma_fork(vma, old_vma);
> - if (ret)
> - goto fail_nomem_anon_vma_fork;
> + if (pfnmap) {
> + get_file(vma->vm_file);
> + } else {
[Severity: Low]
In dup_mmap(), a VM_PFNMAP VMA gets this before copy_page_range():
/* track_pfn_copy() will later take care of copying internal state. */
if (unlikely(tmp->vm_flags & VM_PFNMAP))
untrack_pfn_clear(tmp);
The pfnmap branch here has no equivalent, so the vm_area_dup() copy keeps
the inherited VM_PAT flag without owning a reservation. Can this leave an
inconsistent PAT state on the destination VMA?
Consider copy_page_range() failing in track_pfn_copy() (in get_pat_info()
or reserve_pfn_range()) before it reaches vm_flags_set(dst_vma, VM_PAT).
The exec then fails and the new mm is torn down, and unmap_single_vma()
calls untrack_pfn() on a VMA that still has the inherited VM_PAT.
get_pat_info() runs on empty destination page tables. Can this hit
WARN_ON_ONCE(1) on the exec error path, and trigger a panic with
panic_on_warn? Should vma_dup() call untrack_pfn_clear() on the new VMA
before copy_page_range() as dup_mmap() does?
[ ... ]
> + /* Also links a file VMA into the file's i_mmap, for unmap_mapping_range() */
> ret = vma_link(mm, vma);
> if (ret)
> goto fail_nomem_vma_link;
>
> + if (pfnmap && vma->vm_ops && vma->vm_ops->open)
> + vma->vm_ops->open(vma);
> +
[ ... ]
> + /* For PFN maps this copies the device PTEs and the PAT memtype */
> + if (pfnmap)
> + ret = copy_page_range(vma, old_vma);
> + else
[Severity: Medium]
vma_link() makes the new VMA visible in i_mmap via vma_link_file(), which
takes i_mmap_rwsem for write only around the insert. copy_page_range() then
runs with no i_mmap_rwsem held. Can unmap_mapping_range() miss the copied
PTEs?
unmap_mapping_pages() takes only i_mmap_rwsem for read and no mmap_lock.
One possible interleaving:
exec thread unmap_mapping_range()
vma_link() (new VMA inserted)
walks i_mmap, zaps empty new VMA
copy_page_range() (copies PTEs
from old_vma, not yet zapped)
zaps old_vma only
The new mm would then keep live device MMIO PTEs after the revoke. fork()
avoids this with:
vma_interval_tree_insert_after(tmp, mpnt, &mapping->i_mmap);
so the walker sees the parent before the child. vma_link_file() uses a
plain insert with no such ordering. Is there something that closes this
window, such as keeping i_mmap_rwsem held across the PTE copy or ordering
the new VMA after old_vma?
> #ifdef CONFIG_PADATA
> - ret = copy_page_range_mt(vma, old_vma);
> + ret = copy_page_range_mt(vma, old_vma);
> #else
> - ret = copy_page_range(vma, old_vma);
> + ret = copy_page_range(vma, old_vma);
> #endif
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009143227.492150-1-andrey.drobyshev@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] dm-ploop: fix u32 overflow of the delta BAT byte size
@ 2026-10-09 14:28 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 14:28 UTC (permalink / raw)
To: Alexander Ivanov, den; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] dm-ploop: ploop_bat_clu_to_page_nr() still multiplies in 32 bits, wraps on 32-bit builds
--
commit 4d982aa9720b349cfdf860a1b465d9c33f3db9ff
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: fix u32 overflow of the delta BAT byte size
This patch computes the header and BAT byte size in u64 via
ploop_nr_bat_clusters() and in ploop_read_delta_metadata(). It also
rejects headers and resizes whose hb_nr exceeds PLOOP_MAX_HB_NR.
> diff --git a/drivers/md/dm-ploop-bat.c b/drivers/md/dm-ploop-bat.c
> --- a/drivers/md/dm-ploop-bat.c
> +++ b/drivers/md/dm-ploop-bat.c
[ ... ]
> @@ -339,7 +340,7 @@ int ploop_read_delta_metadata(struct ploop *ploop, struct file *file,
> if (ret)
> goto out;
>
> - size = (PLOOP_MAP_OFFSET + delta_nr_be) * sizeof(map_index_t);
> + size = ((u64)delta_nr_be + PLOOP_MAP_OFFSET) * sizeof(map_index_t);
> size = ALIGN(size, PAGE_SIZE); /* file may be open as direct */
> nr_segs = size / PAGE_SIZE;
>
[Severity: Medium]
Is the u64 conversion complete? ploop_bat_clu_to_page_nr() in
dm-ploop.h still has the same wrap this patch fixes elsewhere:
static inline u32 ploop_bat_clu_to_page_nr(u32 clu)
{
u64 byte;
byte = (clu + PLOOP_MAP_OFFSET) * sizeof(map_index_t);
return byte >> PAGE_SHIFT;
}
Here clu is u32 and PLOOP_MAP_OFFSET is int, so (clu + 16) is a 32-bit
value. It is multiplied by sizeof(map_index_t), a size_t. On a 32-bit
build size_t is 32 bits, so the product wraps for clu >= 2^30 - 16
before it is widened to u64 for byte. On 64-bit this does not wrap.
The sizes this patch now accepts reach that range. ploop_setup_metadata()
and ploop_resize() only reject sizes above PLOOP_MAX_HB_NR, which is
about 2^32 - 32. The commit message also describes m_Size >= 2^30 -
PLOOP_MAP_OFFSET as legal.
For such a size, the page count used by ploop_prealloc_md_pages() would
come from the wrapped helper:
ploop_prealloc_md_pages()
new_nr_pages = ploop_bat_clu_to_page_nr(new_nr_bat_entries - 1) + 1;
nr_segs in ploop_read_delta_metadata() is now computed from the u64
size. Could nr_segs then exceed the number of md pages that were
preallocated?
If so, the rb_root_for_each_md_page() loop would fill only part of the
bvec array. The remaining bvecs, allocated with kvmalloc(), would be
passed uninitialised to vfs_iter_read().
init_be_iter() also takes last_page from the same helper:
init_be_iter()
u32 rem, last_page = ploop_bat_clu_to_page_nr(nr_be - 1);
Could that give a wrong end index for large nr_be? Callers such as
ploop_md_page_find() users could also fail to find a page, and the
BUG_ON(!md) in the lookup path at the start of ploop_bat_clu_to_page_nr()
callers would then be reachable.
Would it make sense to widen the operand in ploop_bat_clu_to_page_nr()
the same way, for example ((u64)clu + PLOOP_MAP_OFFSET) *
sizeof(map_index_t)? Alternatively, 32-bit builds could be limited to
sizes the helper can represent.
The delta header comes from an image file and resize is command-driven,
so the large m_Size can be supplied externally.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009142316.801288-1-alexander.ivanov@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 3/4] ms/timens: Remove dependency on the vDSO
@ 2026-10-09 14:06 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 14:06 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] timens: TIME_NS now selectable on SPARC, whose private vDSO ignores offsets
--
commit 07e6081c6b5ec1169db1364534dcb49acce62383
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
ms/timens: Remove dependency on the vDSO
This backport drops "depends on GENERIC_GETTIMEOFDAY" from TIME_NS. It adds
a TIME_NS_VDSO symbol that gates the vDSO-specific code, and it stubs out
timens_commit() and find_timens_vvar_page() when TIME_NS_VDSO is not set.
Link: https://patch.msgid.link/20260326-vdso-timens-decoupling-v2-2-c82693a7775f@linutronix.de
> diff --git a/init/Kconfig b/init/Kconfig
> --- a/init/Kconfig
> +++ b/init/Kconfig
> @@ -1306,12 +1306,14 @@ config UTS_NS
>
> config TIME_NS
> bool "TIME namespace"
> - depends on GENERIC_GETTIMEOFDAY
> default y
> help
> In this namespace boottime and monotonic clocks can be set.
> The time will keep going with the same pace.
>
> +config TIME_NS_VDSO
> + def_bool TIME_NS && GENERIC_GETTIMEOFDAY
> +
[Severity: Medium]
Can TIME_NS now be enabled on SPARC with a vDSO that ignores the offsets?
The commit message says "Now that all architectures using a real vDSO are
using the generic library, that dependency is not necessary anymore."
That does not appear to hold for SPARC in this tree. arch/sparc/Kconfig
selects only GENERIC_TIME_VSYSCALL. It does not select
GENERIC_GETTIMEOFDAY or HAVE_GENERIC_VDSO. The tree still contains
arch/sparc/vdso/vclock_gettime.c and
arch/sparc/vdso/vdso32/vclock_gettime.c.
Those files read the host vvar page directly:
arch/sparc/vdso/vclock_gettime.c:__vdso_clock_gettime() {
...
case CLOCK_MONOTONIC:
if (unlikely(vvd->vclock_mode == VCLOCK_NONE))
break;
return do_monotonic(vvd, ts);
...
}
They have no time namespace offset handling. Before this patch, the
GENERIC_GETTIMEOFDAY dependency kept TIME_NS (default y) off on SPARC.
With the dependency gone, TIME_NS is on by default there.
TIME_NS_VDSO is then n, so these two stubs do nothing:
include/linux/time_namespace.h:
#else /* !CONFIG_TIME_NS_VDSO */
static inline void timens_commit(struct task_struct *tsk, struct time_namespace *ns)
{
}
static inline struct page *find_timens_vvar_page(struct vm_area_struct *vma)
{
return NULL;
}
On SPARC, unshare(CLONE_NEWTIME), setns() and writes to
/proc/<pid>/timens_offsets would all succeed. A task in a non-init time
namespace would still get host CLOCK_MONOTONIC and CLOCK_MONOTONIC_COARSE
from the vDSO, with no offset applied. The syscall path, /proc/uptime and
timerfd do apply the offset, so the two paths would disagree, and nothing
reports an error.
Should TIME_NS keep a dependency that excludes architectures whose vDSO is
not the generic one, such as SPARC? Alternatively, should the commit
message be corrected?
> diff --git a/include/linux/time_namespace.h b/include/linux/time_namespace.h
> --- a/include/linux/time_namespace.h
> +++ b/include/linux/time_namespace.h
[ ... ]
> +#ifdef CONFIG_TIME_NS_VDSO
> +extern void timens_commit(struct task_struct *tsk, struct time_namespace *ns);
> +struct page *find_timens_vvar_page(struct vm_area_struct *vma);
> +#else /* !CONFIG_TIME_NS_VDSO */
> +static inline void timens_commit(struct task_struct *tsk, struct time_namespace *ns)
> +{
> +}
> +
> +static inline struct page *find_timens_vvar_page(struct vm_area_struct *vma)
> +{
> + return NULL;
> +}
> +#endif /* CONFIG_TIME_NS_VDSO */
> +
> #endif /* _LINUX_TIMENS_H */
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009135929.3274885-1-eva.kurchatova@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 4/4] timens: freeze the offsets on the first join without the vDSO too
@ 2026-10-09 14:06 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 14:06 UTC (permalink / raw)
To: Eva Kurchatova, khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] timens: lockless frozen_offsets fast path lacks acquire/release ordering
--
commit 0c3eee5060109c37a2e00993e54d077407012d3c
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
timens: freeze the offsets on the first join without the vDSO too
This moves the freezing of a time namespace's offsets out of the vDSO
page setup code and into timens_commit(), so that kernels without a
generic vDSO also freeze the offsets on the first task join. The vDSO
page is now filled from the same place, where one exists.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148112
> diff --git a/kernel/time/namespace.c b/kernel/time/namespace.c
> --- a/kernel/time/namespace.c
> +++ b/kernel/time/namespace.c
[ ... ]
> +static void timens_freeze_offsets(struct time_namespace *ns)
> +{
> + if (ns == &init_time_ns)
> + return;
> +
> + /* Fast-path, taken by every task in namespace except the first. */
> + if (likely(ns->frozen_offsets))
> + return;
> +
> + guard(mutex)(&timens_offset_lock);
> + if (ns->frozen_offsets)
> + return;
> +
> + timens_vdso_setup_vvar_page(ns);
> + ns->frozen_offsets = true;
> +}
[Severity: Low]
Is the lockless fast path in timens_freeze_offsets() ordered against the
vvar page setup on weakly ordered CPUs?
The first joiner runs timens_vdso_setup_vvar_page() and then publishes the
flag with a plain store:
timens_vdso_setup_vvar_page(ns);
ns->frozen_offsets = true;
A second task joining the same namespace (via timens_on_fork() or
commit_nsset()) reads the flag without taking the mutex:
if (likely(ns->frozen_offsets))
return;
Neither side has release/acquire semantics. On arm64, powerpc or riscv,
could the second task observe frozen_offsets == true before the stores done
in timens_vdso_setup_vvar_page() (seq, clock_mode and the offsets set up by
timens_setup_vdso_clock_data()) are visible to it?
If so, that task would go on to read an un-namespaced or zeroed vvar page,
and its clocks would be wrong. This would be wrong output rather than
memory corruption. The window is very small and incidental
synchronisation, such as passing the namespace reference between tasks,
probably hides it in practice, so this may be speculative.
Would it make sense to use smp_store_release(&ns->frozen_offsets, true) for
the publication and smp_load_acquire(&ns->frozen_offsets) on the fast path?
> diff --git a/kernel/time/namespace_vdso.c b/kernel/time/namespace_vdso.c
> --- a/kernel/time/namespace_vdso.c
> +++ b/kernel/time/namespace_vdso.c
[ ... ]
> +/* Called once per namespace, under timens_offset_lock, before any task reads */
> +void timens_vdso_setup_vvar_page(struct time_namespace *ns)
> {
> + struct vdso_time_data *vdata = page_address(ns->vvar_page);
> + struct vdso_clock *vc = vdata->clock_data;
> unsigned int i;
>
> for (i = 0; i < CS_BASES; i++)
> timens_setup_vdso_clock_data(&vc[i], ns);
> }
[Severity: Low]
These are the plain stores to the shared vvar page that must be visible to
any task that sees frozen_offsets set through the lockless fast path in
timens_freeze_offsets(), as described above.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009135929.3274885-1-eva.kurchatova@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 1/4] ms/vdso: Drop Kconfig GENERIC_VDSO_TIME_NS
@ 2026-10-09 14:04 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-09 14:04 UTC (permalink / raw)
To: Eva Kurchatova, khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] config: stale CONFIG_GENERIC_VDSO_TIME_NS=y left in shipped Virtuozzo configs
--
commit 6f298fef5110add3238fa534d536cbb04d9561a1
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
ms/vdso: Drop Kconfig GENERIC_VDSO_TIME_NS
This is a backport of the upstream change that removes the
GENERIC_VDSO_TIME_NS Kconfig symbol. TIME_NS now depends on
GENERIC_GETTIMEOFDAY instead, and the per-architecture selects and the
selftests config entry are dropped.
Link: https://lore.kernel.org/all/20250826-vdso-cleanups-v1-10-d9b65750e49f@linutronix.de
> diff --git a/lib/vdso/Kconfig b/lib/vdso/Kconfig
> index 45df764b49ad6..a1cc90133cb9e 100644
> --- a/lib/vdso/Kconfig
> +++ b/lib/vdso/Kconfig
> @@ -24,12 +24,6 @@ config GENERIC_COMPAT_VDSO
> help
> This config option enables the compat VDSO layer.
>
> -config GENERIC_VDSO_TIME_NS
> - bool
> - help
> - Selected by architectures which support time namespaces in the
> - VDSO
> -
[Severity: Low]
This removes the Kconfig symbol, but the shipped Virtuozzo config files
still set it:
config.vz10.minimal:
CONFIG_GENERIC_VDSO_TIME_NS=y
configs/kernel-6.12.0-x86_64.config:
CONFIG_GENERIC_VDSO_TIME_NS=y
configs/kernel-6.12.0-x86_64-debug.config:
CONFIG_GENERIC_VDSO_TIME_NS=y
Should these three lines be dropped in this patch as well?
As it stands, make oldconfig or olddefconfig will silently discard the
unknown symbol. The next config regeneration would then produce a spurious
diff, and any check comparing the shipped configs against regenerated
output would report a mismatch.
The effective x86 configuration should not change, since TIME_NS now
depends on GENERIC_GETTIMEOFDAY, which x86 selects unconditionally. The
upstream commit has no equivalent files, so this gap appears specific to
the downstream backport.
The later patches in the series do not appear to remove these references
either.
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261009135929.3274885-1-eva.kurchatova@virtuozzo.com?part=1
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* [RFC HCI-8.1 PATCH 5/5] qemu update: refuse PCI hostdevs on non-shared guest RAM #VSTOR-103860
2026-10-09 14:04 10% ` [RFC HCI-8.1 PATCH 1/5] qemu update: allow qemu-update for VMs with PCI and USB hostdevs #VSTOR-103860 #VSTOR-137800 Andrey Drobyshev
2026-10-09 14:04 7% ` [RFC HCI-8.1 PATCH 4/5] qemu: share auxiliary RAM of VMs with DMA-pinning devices #VSTOR-103860 Andrey Drobyshev
@ 2026-10-09 14:04 8% ` Andrey Drobyshev
2 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:04 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, efim.shevrin, den
New domains with PCI hostdevs get shared memfd guest RAM since the
previous commits, but not all running domains have it: those started
by an older libvirt, those with explicitly configured private memory
backing, and those that got a PCI hostdev hotplugged while running on
anonymous RAM. For them qemu-update would succeed while silently
breaking the device: DMA keeps going to the pages pinned before the
exec, which the guest stops seeing on its first write to them.
Refuse qemu-update for a domain with PCI hostdevs unless all of its
guest RAM is shared. A cold restart gives such a domain shared memfd
RAM, unless its memory backing is configured otherwise.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
src/qemu/qemu_driver.c | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c
index b3902f10c65..c39e0133ebb 100644
--- a/src/qemu/qemu_driver.c
+++ b/src/qemu/qemu_driver.c
@@ -25194,6 +25194,16 @@ qemuDomainGuestQemuUpdatePrechecks(virDomainObj *vm)
return -1;
}
+ /* Device DMA keeps going to the pages pinned before the exec, which the
+ * guest stops seeing on its first write if the RAM is private. */
+ if (virDomainDefHasPCIHostdev(vm->def) &&
+ !qemuDomainDefGuestRAMIsShared(vm->def)) {
+ virReportError(VIR_ERR_OPERATION_INVALID,
+ _("VM '%1$s' has PCI hostdev on non-shared guest memory, qemu-update cancelled; a cold restart switches it to shared memory"),
+ vm->def->name);
+ return -1;
+ }
+
if (virDomainDefHasNVMeDisk(vm->def)) {
virReportError(VIR_ERR_OPERATION_INVALID,
_("VM '%1$s' has NVMe disk, qemu-update cancelled"),
--
2.47.1
^ permalink raw reply [relevance 8%]
* [RFC HCI-8.1 PATCH 4/5] qemu: share auxiliary RAM of VMs with DMA-pinning devices #VSTOR-103860
2026-10-09 14:04 10% ` [RFC HCI-8.1 PATCH 1/5] qemu update: allow qemu-update for VMs with PCI and USB hostdevs #VSTOR-103860 #VSTOR-137800 Andrey Drobyshev
@ 2026-10-09 14:04 7% ` Andrey Drobyshev
2026-10-09 14:04 8% ` [RFC HCI-8.1 PATCH 5/5] qemu update: refuse PCI hostdevs on non-shared guest RAM #VSTOR-103860 Andrey Drobyshev
2 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:04 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, efim.shevrin, den
Besides guest RAM, VFIO also maps and pins auxiliary RAM that is visible
in the guest address space: VGA VRAM, firmware and option ROMs. QEMU
allocates it as anonymous private memory unless the machine property
'aux-ram-share' is on, so with shared memfd guest RAM from the previous
commit it is the only memory left that qemu-update preserves with
MADV_DOEXEC and that can thus diverge between the device and the guest.
ROMs are never written after boot, so in practice only VRAM is at risk,
and only if the device does DMA to it; still, close the gap.
When a new domain starts with DMA-pinning devices and all of its guest
RAM shared, and QEMU knows 'aux-ram-share', add aux-ram-share=on to
-machine.
The decision is taken once at start and kept in the domain private data
(<auxRamShare/> in the status XML). The qemu-update command line is
built from it rather than recomputed, because it must match the running
QEMU even after a libvirt upgrade changed the rules. Incoming migration
and restore don't share auxiliary RAM, as the source may not have.
XML-to-argv outputs of the hostdev, mdev, NVMe and vDPA tests with QEMU
10.0 capabilities gain aux-ram-share=on; the status XML round-trip test
covers the new element.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
src/qemu/qemu_command.c | 3 ++
src/qemu/qemu_domain.c | 36 +++++++++++++++++++
src/qemu/qemu_domain.h | 6 ++++
src/qemu/qemu_process.c | 36 ++++++++++++++++++-
tests/qemustatusxml2xmldata/modern-in.xml | 1 +
.../disk-nvme.x86_64-latest.args | 2 +-
.../disk-slices.x86_64-latest.args | 2 +-
...tdev-mdev-display-ramfb.x86_64-latest.args | 2 +-
...play-spice-egl-headless.x86_64-latest.args | 2 +-
...ev-display-spice-opengl.x86_64-latest.args | 2 +-
...isplay-vnc-egl-headless.x86_64-latest.args | 2 +-
...ostdev-mdev-display-vnc.x86_64-latest.args | 2 +-
.../hostdev-mdev-display.x86_64-latest.args | 2 +-
...hostdev-mdev-precreated.x86_64-latest.args | 2 +-
...tdev-pci-address-device.x86_64-latest.args | 2 +-
...-pci-address-unassigned.x86_64-latest.args | 2 +-
.../hostdev-pci-address.x86_64-latest.args | 2 +-
...stdev-pci-display-ramfb.x86_64-latest.args | 2 +-
...stdev-pci-multifunction.x86_64-latest.args | 2 +-
...tdev-subsys-mdev-vfio-ap.s390x-latest.args | 2 +-
...ubsys-mdev-vfio-ccw-boot.s390x-latest.args | 2 +-
...dev-subsys-mdev-vfio-ccw.s390x-latest.args | 2 +-
...ostdev-vfio-multidomain.x86_64-latest.args | 2 +-
...o-zpci-autogenerate-fids.s390x-latest.args | 2 +-
...o-zpci-autogenerate-uids.s390x-latest.args | 2 +-
...v-vfio-zpci-autogenerate.s390x-latest.args | 2 +-
...dev-vfio-zpci-boundaries.s390x-latest.args | 2 +-
...vfio-zpci-ccw-memballoon.s390x-latest.args | 2 +-
...io-zpci-multidomain-many.s390x-latest.args | 2 +-
.../hostdev-vfio-zpci.s390x-latest.args | 2 +-
.../hostdev-vfio.x86_64-latest.args | 2 +-
...machine-loadparm-hostdev.s390x-latest.args | 2 +-
.../net-hostdev-bootorder.x86_64-latest.args | 2 +-
...net-hostdev-multidomain.x86_64-latest.args | 2 +-
...ostdev-vfio-multidomain.x86_64-latest.args | 2 +-
.../net-hostdev-vfio.x86_64-latest.args | 2 +-
.../net-hostdev.x86_64-latest.args | 2 +-
.../net-vdpa-multiqueue.x86_64-latest.args | 2 +-
.../net-vdpa.x86_64-latest.args | 2 +-
...-virtio-teaming-hostdev.x86_64-latest.args | 2 +-
...-virtio-teaming-network.x86_64-latest.args | 2 +-
.../net-virtio-teaming.x86_64-latest.args | 2 +-
.../pci-rom.x86_64-latest.args | 2 +-
.../pseries-hostdevs-1.ppc64-latest.args | 2 +-
.../pseries-hostdevs-2.ppc64-latest.args | 2 +-
.../pseries-hostdevs-3.ppc64-latest.args | 2 +-
46 files changed, 122 insertions(+), 42 deletions(-)
diff --git a/src/qemu/qemu_command.c b/src/qemu/qemu_command.c
index fcc0a64ed0d..7c995a58b88 100644
--- a/src/qemu/qemu_command.c
+++ b/src/qemu/qemu_command.c
@@ -7399,6 +7399,9 @@ qemuBuildMachineCommandLine(virCommand *cmd,
if (qemuAppendDomainMemoryMachineParams(&buf, cfg, def, qemuCaps) < 0)
return -1;
+ if (priv && priv->auxRamShare)
+ virBufferAddLit(&buf, ",aux-ram-share=on");
+
if (cpu && cpu->model &&
cpu->mode == VIR_CPU_MODE_HOST_MODEL &&
qemuDomainIsPSeries(def)) {
diff --git a/src/qemu/qemu_domain.c b/src/qemu/qemu_domain.c
index 551c214c774..791c833fc13 100644
--- a/src/qemu/qemu_domain.c
+++ b/src/qemu/qemu_domain.c
@@ -1933,6 +1933,7 @@ qemuDomainObjPrivateDataClear(qemuDomainObjPrivate *priv)
g_clear_pointer(&priv->namespaces, virBitmapFree);
priv->rememberOwner = false;
+ priv->auxRamShare = false;
priv->reconnectBlockjobs = VIR_TRISTATE_BOOL_ABSENT;
priv->allowReboot = VIR_TRISTATE_BOOL_ABSENT;
@@ -2881,6 +2882,9 @@ qemuDomainObjPrivateXMLFormat(virBuffer *buf,
if (priv->rememberOwner)
virBufferAddLit(buf, "<rememberOwner/>\n");
+ if (priv->auxRamShare)
+ virBufferAddLit(buf, "<auxRamShare/>\n");
+
qemuDomainObjPrivateXMLFormatAllowReboot(buf, priv->allowReboot);
qemuDomainObjPrivateXMLFormatPR(buf, priv);
@@ -3605,6 +3609,7 @@ qemuDomainObjPrivateXMLParse(xmlXPathContextPtr ctxt,
}
priv->rememberOwner = virXPathBoolean("count(./rememberOwner) > 0", ctxt);
+ priv->auxRamShare = virXPathBoolean("count(./auxRamShare) > 0", ctxt);
if ((n = virXPathNodeSet("./vcpus/vcpu", ctxt, &nodes)) < 0)
return -1;
@@ -9421,6 +9426,37 @@ qemuDomainNeedsVFIO(const virDomainDef *def)
}
+/**
+ * qemuDomainDefGuestRAMIsShared:
+ * @def: domain definition
+ *
+ * Returns true if all guest RAM is shared, i.e. its memory access mode is
+ * 'shared' globally or for every guest NUMA node. Shared RAM is backed by
+ * a file or memfd mapped with MAP_SHARED.
+ */
+bool
+qemuDomainDefGuestRAMIsShared(const virDomainDef *def)
+{
+ size_t nnodes = virDomainNumaGetNodeCount(def->numa);
+ size_t i;
+
+ if (nnodes == 0)
+ return def->mem.access == VIR_DOMAIN_MEMORY_ACCESS_SHARED;
+
+ for (i = 0; i < nnodes; i++) {
+ virDomainMemoryAccess access =
+ virDomainNumaGetNodeMemoryAccessMode(def->numa, i);
+
+ if (access == VIR_DOMAIN_MEMORY_ACCESS_DEFAULT)
+ access = def->mem.access;
+ if (access != VIR_DOMAIN_MEMORY_ACCESS_SHARED)
+ return false;
+ }
+
+ return true;
+}
+
+
/**
* qemuDomainGetHostdevPath:
* @dev: host device definition
diff --git a/src/qemu/qemu_domain.h b/src/qemu/qemu_domain.h
index c28ce473152..87b458ece38 100644
--- a/src/qemu/qemu_domain.h
+++ b/src/qemu/qemu_domain.h
@@ -293,6 +293,10 @@ struct _qemuDomainObjPrivate {
/* true if libvirt remembers the original owner for files */
bool rememberOwner;
+ /* true if QEMU runs with -machine aux-ram-share=on; decided once when
+ * the domain starts and kept for qemu-update */
+ bool auxRamShare;
+
/* true if global -mem-prealloc appears on cmd line */
bool memPrealloc;
@@ -1103,6 +1107,8 @@ bool qemuDomainSupportsVideoVga(const virDomainVideoDef *video,
bool qemuDomainNeedsVFIO(const virDomainDef *def);
+bool qemuDomainDefGuestRAMIsShared(const virDomainDef *def);
+
int qemuDomainGetHostdevPath(virDomainHostdevDef *dev,
char **path,
int *perms);
diff --git a/src/qemu/qemu_process.c b/src/qemu/qemu_process.c
index cea6957ad1a..b4859a5dba5 100644
--- a/src/qemu/qemu_process.c
+++ b/src/qemu/qemu_process.c
@@ -8025,6 +8025,34 @@ qemuProcessPrepareDomainMemoryBacking(virDomainObj *vm)
}
+/**
+ * qemuProcessPrepareDomainAuxRamShare:
+ * @vm: domain object
+ *
+ * Besides guest RAM, VFIO also maps and pins auxiliary RAM visible in the
+ * guest address space: VGA VRAM, firmware and option ROMs. QEMU allocates
+ * it as anonymous private memory unless -machine aux-ram-share=on is set,
+ * so it would keep the qemu-update problem described at
+ * qemuProcessPrepareDomainMemoryBacking() even with shared guest RAM.
+ *
+ * Decide whether to share it. The decision is kept in the status XML and
+ * reused for qemu-update, whose QEMU command line must match the running
+ * one regardless of what a newer libvirt would decide.
+ */
+static void
+qemuProcessPrepareDomainAuxRamShare(virDomainObj *vm)
+{
+ virDomainDef *def = vm->def;
+ qemuDomainObjPrivate *priv = vm->privateData;
+
+ priv->auxRamShare = (qemuDomainNeedsVFIO(def) ||
+ virDomainDefHasVDPANet(def)) &&
+ qemuDomainDefGuestRAMIsShared(def) &&
+ virQEMUCapsGet(priv->qemuCaps,
+ QEMU_CAPS_MACHINE_AUX_RAM_SHARE);
+}
+
+
static int
qemuProcessPrepareDomainHostdevs(virDomainObj *vm,
qemuDomainObjPrivate *priv)
@@ -8240,8 +8268,14 @@ qemuProcessPrepareDomain(virQEMUDriver *driver,
return -1;
/* Never on qemu-update: the new QEMU must match the running one. */
- if (!(flags & VIR_QEMU_PROCESS_START_X_QEMU_UPDATE))
+ if (!(flags & VIR_QEMU_PROCESS_START_X_QEMU_UPDATE)) {
qemuProcessPrepareDomainMemoryBacking(vm);
+ qemuProcessPrepareDomainAuxRamShare(vm);
+ }
+ } else if (!(flags & VIR_QEMU_PROCESS_START_X_QEMU_UPDATE)) {
+ /* Incoming migration or restore: keep QEMU's default, the source
+ * didn't necessarily share its auxiliary RAM either. */
+ priv->auxRamShare = false;
}
for (i = 0; i < vm->def->nchannels; i++) {
diff --git a/tests/qemustatusxml2xmldata/modern-in.xml b/tests/qemustatusxml2xmldata/modern-in.xml
index 3b3e8317599..7fae6e82a14 100644
--- a/tests/qemustatusxml2xmldata/modern-in.xml
+++ b/tests/qemustatusxml2xmldata/modern-in.xml
@@ -260,6 +260,7 @@
<channelTargetDir path='/var/lib/libvirt/qemu/channel/target/domain-1-upstream'/>
<memoryBackingDir path='/var/lib/libvirt/qemu/ram/1-upstream'/>
<chardevStdioLogd/>
+ <auxRamShare/>
<allowReboot value='yes'/>
<nodename index='123'/>
<fdset index='321'/>
diff --git a/tests/qemuxmlconfdata/disk-nvme.x86_64-latest.args b/tests/qemuxmlconfdata/disk-nvme.x86_64-latest.args
index 31fc6d517bf..a47609d454b 100644
--- a/tests/qemuxmlconfdata/disk-nvme.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/disk-nvme.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/disk-slices.x86_64-latest.args b/tests/qemuxmlconfdata/disk-slices.x86_64-latest.args
index 2124e6b129e..8db12aac833 100644
--- a/tests/qemuxmlconfdata/disk-slices.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/disk-slices.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-mdev-display-ramfb.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-mdev-display-ramfb.x86_64-latest.args
index ffcc5562b6d..23ac8d00dba 100644
--- a/tests/qemuxmlconfdata/hostdev-mdev-display-ramfb.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-mdev-display-ramfb.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest2/.config \
-name guest=QEMUGuest2,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest2/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-mdev-display-spice-egl-headless.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-mdev-display-spice-egl-headless.x86_64-latest.args
index ef8c0121ff6..2a011750637 100644
--- a/tests/qemuxmlconfdata/hostdev-mdev-display-spice-egl-headless.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-mdev-display-spice-egl-headless.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest2/.config \
-name guest=QEMUGuest2,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest2/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-mdev-display-spice-opengl.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-mdev-display-spice-opengl.x86_64-latest.args
index 4a863af3abd..fcd3a0ed3e4 100644
--- a/tests/qemuxmlconfdata/hostdev-mdev-display-spice-opengl.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-mdev-display-spice-opengl.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest2/.config \
-name guest=QEMUGuest2,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest2/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-mdev-display-vnc-egl-headless.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-mdev-display-vnc-egl-headless.x86_64-latest.args
index a5ffde5a48e..4be783e8e02 100644
--- a/tests/qemuxmlconfdata/hostdev-mdev-display-vnc-egl-headless.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-mdev-display-vnc-egl-headless.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest2/.config \
-name guest=QEMUGuest2,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest2/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-mdev-display-vnc.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-mdev-display-vnc.x86_64-latest.args
index c4b59a37c1a..f3305762ad3 100644
--- a/tests/qemuxmlconfdata/hostdev-mdev-display-vnc.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-mdev-display-vnc.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest2/.config \
-name guest=QEMUGuest2,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest2/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-mdev-display.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-mdev-display.x86_64-latest.args
index c4b59a37c1a..f3305762ad3 100644
--- a/tests/qemuxmlconfdata/hostdev-mdev-display.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-mdev-display.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest2/.config \
-name guest=QEMUGuest2,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest2/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-mdev-precreated.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-mdev-precreated.x86_64-latest.args
index 9c8064c7d3b..afcecdbd5f8 100644
--- a/tests/qemuxmlconfdata/hostdev-mdev-precreated.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-mdev-precreated.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest2/.config \
-name guest=QEMUGuest2,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest2/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-pci-address-device.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-pci-address-device.x86_64-latest.args
index 80970358b9c..0597b08ea4f 100644
--- a/tests/qemuxmlconfdata/hostdev-pci-address-device.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-pci-address-device.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest2/.config \
-name guest=QEMUGuest2,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest2/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-pci-address-unassigned.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-pci-address-unassigned.x86_64-latest.args
index 0328d441dce..3c7d7a54371 100644
--- a/tests/qemuxmlconfdata/hostdev-pci-address-unassigned.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-pci-address-unassigned.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-delete/.config \
-name guest=delete,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-delete/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel kvm \
-cpu qemu64 \
-m size=262144k \
diff --git a/tests/qemuxmlconfdata/hostdev-pci-address.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-pci-address.x86_64-latest.args
index 18dfa2d17d5..5961e551dc2 100644
--- a/tests/qemuxmlconfdata/hostdev-pci-address.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-pci-address.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest2/.config \
-name guest=QEMUGuest2,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest2/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-pci-display-ramfb.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-pci-display-ramfb.x86_64-latest.args
index 332fff8b84a..d1d31c04fc2 100644
--- a/tests/qemuxmlconfdata/hostdev-pci-display-ramfb.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-pci-display-ramfb.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest2/.config \
-name guest=QEMUGuest2,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest2/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-pci-multifunction.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-pci-multifunction.x86_64-latest.args
index 12d10aed715..990f700f968 100644
--- a/tests/qemuxmlconfdata/hostdev-pci-multifunction.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-pci-multifunction.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-delete/.config \
-name guest=delete,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-delete/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel kvm \
-cpu qemu64 \
-m size=262144k \
diff --git a/tests/qemuxmlconfdata/hostdev-subsys-mdev-vfio-ap.s390x-latest.args b/tests/qemuxmlconfdata/hostdev-subsys-mdev-vfio-ap.s390x-latest.args
index 7c923d99f10..5817f9ddacf 100644
--- a/tests/qemuxmlconfdata/hostdev-subsys-mdev-vfio-ap.s390x-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-subsys-mdev-vfio-ap.s390x-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram \
+-machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram,aux-ram-share=on \
-accel tcg \
-cpu qemu \
-m size=524288k \
diff --git a/tests/qemuxmlconfdata/hostdev-subsys-mdev-vfio-ccw-boot.s390x-latest.args b/tests/qemuxmlconfdata/hostdev-subsys-mdev-vfio-ccw-boot.s390x-latest.args
index 6ed62006e3a..03c038b537c 100644
--- a/tests/qemuxmlconfdata/hostdev-subsys-mdev-vfio-ccw-boot.s390x-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-subsys-mdev-vfio-ccw-boot.s390x-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram \
+-machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram,aux-ram-share=on \
-accel tcg \
-cpu qemu \
-m size=524288k \
diff --git a/tests/qemuxmlconfdata/hostdev-subsys-mdev-vfio-ccw.s390x-latest.args b/tests/qemuxmlconfdata/hostdev-subsys-mdev-vfio-ccw.s390x-latest.args
index dc225816355..4a3a1ad9eeb 100644
--- a/tests/qemuxmlconfdata/hostdev-subsys-mdev-vfio-ccw.s390x-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-subsys-mdev-vfio-ccw.s390x-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram \
+-machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram,aux-ram-share=on \
-accel tcg \
-cpu qemu \
-m size=524288k \
diff --git a/tests/qemuxmlconfdata/hostdev-vfio-multidomain.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-vfio-multidomain.x86_64-latest.args
index ad08f942abe..000de5f65ff 100644
--- a/tests/qemuxmlconfdata/hostdev-vfio-multidomain.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-vfio-multidomain.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest2/.config \
-name guest=QEMUGuest2,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest2/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-vfio-zpci-autogenerate-fids.s390x-latest.args b/tests/qemuxmlconfdata/hostdev-vfio-zpci-autogenerate-fids.s390x-latest.args
index d6c1855a015..a688f06fd38 100644
--- a/tests/qemuxmlconfdata/hostdev-vfio-zpci-autogenerate-fids.s390x-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-vfio-zpci-autogenerate-fids.s390x-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram \
+-machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram,aux-ram-share=on \
-accel tcg \
-cpu qemu \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-vfio-zpci-autogenerate-uids.s390x-latest.args b/tests/qemuxmlconfdata/hostdev-vfio-zpci-autogenerate-uids.s390x-latest.args
index c71df6ab57f..ef9871783d9 100644
--- a/tests/qemuxmlconfdata/hostdev-vfio-zpci-autogenerate-uids.s390x-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-vfio-zpci-autogenerate-uids.s390x-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram \
+-machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram,aux-ram-share=on \
-accel tcg \
-cpu qemu \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-vfio-zpci-autogenerate.s390x-latest.args b/tests/qemuxmlconfdata/hostdev-vfio-zpci-autogenerate.s390x-latest.args
index 75bfe10700f..f22c212d403 100644
--- a/tests/qemuxmlconfdata/hostdev-vfio-zpci-autogenerate.s390x-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-vfio-zpci-autogenerate.s390x-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram \
+-machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram,aux-ram-share=on \
-accel tcg \
-cpu qemu \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-vfio-zpci-boundaries.s390x-latest.args b/tests/qemuxmlconfdata/hostdev-vfio-zpci-boundaries.s390x-latest.args
index 93f15c430b6..2b8f6590a6f 100644
--- a/tests/qemuxmlconfdata/hostdev-vfio-zpci-boundaries.s390x-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-vfio-zpci-boundaries.s390x-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram \
+-machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram,aux-ram-share=on \
-accel tcg \
-cpu qemu \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-vfio-zpci-ccw-memballoon.s390x-latest.args b/tests/qemuxmlconfdata/hostdev-vfio-zpci-ccw-memballoon.s390x-latest.args
index 37635a3e25c..bd28e8bcb9e 100644
--- a/tests/qemuxmlconfdata/hostdev-vfio-zpci-ccw-memballoon.s390x-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-vfio-zpci-ccw-memballoon.s390x-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-KVMGuest1/.config \
-name guest=KVMGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-KVMGuest1/master-key.aes"}' \
--machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram \
+-machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram,aux-ram-share=on \
-accel kvm \
-cpu gen16a-base,nnpa=on,aen=on,cmmnt=on,vxpdeh=on,aefsi=on,diag318=on,csske=on,mepoch=on,msa9=on,msa8=on,msa7=on,msa6=on,msa5=on,msa4=on,msa3=on,msa2=on,msa1=on,sthyi=on,edat=on,ri=on,deflate=on,edat2=on,etoken=on,vx=on,ipter=on,pai=on,paie=on,mepochptff=on,ap=on,vxeh=on,vxpd=on,esop=on,msa9_pckmo=on,vxeh2=on,esort=on,appv=on,apqi=on,apft=on,els=on,iep=on,appvi=on,apqci=on,cte=on,ais=on,bpb=on,ctop=on,gs=on,ppa15=on,zpci=on,rdp=on,sea_esop2=on,beareh=on,te=on,cmm=on,vxpdeh2=on \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-vfio-zpci-multidomain-many.s390x-latest.args b/tests/qemuxmlconfdata/hostdev-vfio-zpci-multidomain-many.s390x-latest.args
index 948bb876fed..6a4605310e3 100644
--- a/tests/qemuxmlconfdata/hostdev-vfio-zpci-multidomain-many.s390x-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-vfio-zpci-multidomain-many.s390x-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram \
+-machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram,aux-ram-share=on \
-accel tcg \
-cpu qemu \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-vfio-zpci.s390x-latest.args b/tests/qemuxmlconfdata/hostdev-vfio-zpci.s390x-latest.args
index 1e923207ca5..e53d44129dd 100644
--- a/tests/qemuxmlconfdata/hostdev-vfio-zpci.s390x-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-vfio-zpci.s390x-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram \
+-machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram,aux-ram-share=on \
-accel tcg \
-cpu qemu \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/hostdev-vfio.x86_64-latest.args b/tests/qemuxmlconfdata/hostdev-vfio.x86_64-latest.args
index 7054582818e..48798b83553 100644
--- a/tests/qemuxmlconfdata/hostdev-vfio.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/hostdev-vfio.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest2/.config \
-name guest=QEMUGuest2,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest2/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/machine-loadparm-hostdev.s390x-latest.args b/tests/qemuxmlconfdata/machine-loadparm-hostdev.s390x-latest.args
index fa4084739ed..31747c1e982 100644
--- a/tests/qemuxmlconfdata/machine-loadparm-hostdev.s390x-latest.args
+++ b/tests/qemuxmlconfdata/machine-loadparm-hostdev.s390x-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram \
+-machine s390-ccw-virtio,usb=off,dump-guest-core=off,memory-backend=s390.ram,aux-ram-share=on \
-accel tcg \
-cpu qemu \
-m size=524288k \
diff --git a/tests/qemuxmlconfdata/net-hostdev-bootorder.x86_64-latest.args b/tests/qemuxmlconfdata/net-hostdev-bootorder.x86_64-latest.args
index 432627f037f..40dd5309ba8 100644
--- a/tests/qemuxmlconfdata/net-hostdev-bootorder.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/net-hostdev-bootorder.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/net-hostdev-multidomain.x86_64-latest.args b/tests/qemuxmlconfdata/net-hostdev-multidomain.x86_64-latest.args
index f9c46a9df79..9a295d59ee4 100644
--- a/tests/qemuxmlconfdata/net-hostdev-multidomain.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/net-hostdev-multidomain.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/net-hostdev-vfio-multidomain.x86_64-latest.args b/tests/qemuxmlconfdata/net-hostdev-vfio-multidomain.x86_64-latest.args
index bd59a926816..e2a56874246 100644
--- a/tests/qemuxmlconfdata/net-hostdev-vfio-multidomain.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/net-hostdev-vfio-multidomain.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/net-hostdev-vfio.x86_64-latest.args b/tests/qemuxmlconfdata/net-hostdev-vfio.x86_64-latest.args
index 1f8d5357d95..77c56946978 100644
--- a/tests/qemuxmlconfdata/net-hostdev-vfio.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/net-hostdev-vfio.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/net-hostdev.x86_64-latest.args b/tests/qemuxmlconfdata/net-hostdev.x86_64-latest.args
index 263811a3486..938ae3270f9 100644
--- a/tests/qemuxmlconfdata/net-hostdev.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/net-hostdev.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/net-vdpa-multiqueue.x86_64-latest.args b/tests/qemuxmlconfdata/net-vdpa-multiqueue.x86_64-latest.args
index c4f618c1c06..91ccc9bf28a 100644
--- a/tests/qemuxmlconfdata/net-vdpa-multiqueue.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/net-vdpa-multiqueue.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/net-vdpa.x86_64-latest.args b/tests/qemuxmlconfdata/net-vdpa.x86_64-latest.args
index 15e9cf0a341..65b50faf7f0 100644
--- a/tests/qemuxmlconfdata/net-vdpa.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/net-vdpa.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/net-virtio-teaming-hostdev.x86_64-latest.args b/tests/qemuxmlconfdata/net-virtio-teaming-hostdev.x86_64-latest.args
index fe6521e61b1..ddc5acdb3a2 100644
--- a/tests/qemuxmlconfdata/net-virtio-teaming-hostdev.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/net-virtio-teaming-hostdev.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/net-virtio-teaming-network.x86_64-latest.args b/tests/qemuxmlconfdata/net-virtio-teaming-network.x86_64-latest.args
index 0255d1b586d..9bd650f61b3 100644
--- a/tests/qemuxmlconfdata/net-virtio-teaming-network.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/net-virtio-teaming-network.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/net-virtio-teaming.x86_64-latest.args b/tests/qemuxmlconfdata/net-virtio-teaming.x86_64-latest.args
index a4cf2b9db94..3027e6d3b95 100644
--- a/tests/qemuxmlconfdata/net-virtio-teaming.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/net-virtio-teaming.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest1/.config \
-name guest=QEMUGuest1,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest1/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/pci-rom.x86_64-latest.args b/tests/qemuxmlconfdata/pci-rom.x86_64-latest.args
index 70556423ac5..0f1a470f7dd 100644
--- a/tests/qemuxmlconfdata/pci-rom.x86_64-latest.args
+++ b/tests/qemuxmlconfdata/pci-rom.x86_64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-QEMUGuest2/.config \
-name guest=QEMUGuest2,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-QEMUGuest2/master-key.aes"}' \
--machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,acpi=off \
+-machine pc,usb=off,dump-guest-core=off,memory-backend=pc.ram,aux-ram-share=on,acpi=off \
-accel tcg \
-cpu qemu64 \
-m size=219136k \
diff --git a/tests/qemuxmlconfdata/pseries-hostdevs-1.ppc64-latest.args b/tests/qemuxmlconfdata/pseries-hostdevs-1.ppc64-latest.args
index 57fb8128b39..55227197ae2 100644
--- a/tests/qemuxmlconfdata/pseries-hostdevs-1.ppc64-latest.args
+++ b/tests/qemuxmlconfdata/pseries-hostdevs-1.ppc64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-guest/.config \
-name guest=guest,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-guest/master-key.aes"}' \
--machine pseries,usb=off,dump-guest-core=off,memory-backend=ppc_spapr.ram \
+-machine pseries,usb=off,dump-guest-core=off,memory-backend=ppc_spapr.ram,aux-ram-share=on \
-accel tcg \
-cpu POWER10 \
-m size=524288k \
diff --git a/tests/qemuxmlconfdata/pseries-hostdevs-2.ppc64-latest.args b/tests/qemuxmlconfdata/pseries-hostdevs-2.ppc64-latest.args
index d7c97f4dae8..7807414e174 100644
--- a/tests/qemuxmlconfdata/pseries-hostdevs-2.ppc64-latest.args
+++ b/tests/qemuxmlconfdata/pseries-hostdevs-2.ppc64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-guest/.config \
-name guest=guest,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-guest/master-key.aes"}' \
--machine pseries,usb=off,dump-guest-core=off,memory-backend=ppc_spapr.ram \
+-machine pseries,usb=off,dump-guest-core=off,memory-backend=ppc_spapr.ram,aux-ram-share=on \
-accel tcg \
-cpu POWER10 \
-m size=524288k \
diff --git a/tests/qemuxmlconfdata/pseries-hostdevs-3.ppc64-latest.args b/tests/qemuxmlconfdata/pseries-hostdevs-3.ppc64-latest.args
index 90157f61c25..6b5e7c4ce2e 100644
--- a/tests/qemuxmlconfdata/pseries-hostdevs-3.ppc64-latest.args
+++ b/tests/qemuxmlconfdata/pseries-hostdevs-3.ppc64-latest.args
@@ -10,7 +10,7 @@ XDG_CONFIG_HOME=/var/lib/libvirt/qemu/domain--1-guest/.config \
-name guest=guest,debug-threads=on \
-S \
-object '{"qom-type":"secret","id":"masterKey0","format":"raw","file":"/var/lib/libvirt/qemu/domain--1-guest/master-key.aes"}' \
--machine pseries,usb=off,dump-guest-core=off,memory-backend=ppc_spapr.ram \
+-machine pseries,usb=off,dump-guest-core=off,memory-backend=ppc_spapr.ram,aux-ram-share=on \
-accel tcg \
-cpu POWER10 \
-m size=524288k \
--
2.47.1
^ permalink raw reply [relevance 7%]
* [RFC HCI-8.1 PATCH 1/5] qemu update: allow qemu-update for VMs with PCI and USB hostdevs #VSTOR-103860 #VSTOR-137800
@ 2026-10-09 14:04 10% ` Andrey Drobyshev
2026-10-09 14:04 7% ` [RFC HCI-8.1 PATCH 4/5] qemu: share auxiliary RAM of VMs with DMA-pinning devices #VSTOR-103860 Andrey Drobyshev
2026-10-09 14:04 8% ` [RFC HCI-8.1 PATCH 5/5] qemu update: refuse PCI hostdevs on non-shared guest RAM #VSTOR-103860 Andrey Drobyshev
2 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-09 14:04 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, efim.shevrin, den
Previous commit 23ba7842b6 ("qemu update: disable update if hostdev
device is added to VM #VSTOR-97345") refused qemu-update for any domain
carrying a <hostdev>, because QEMU of that time could not preserve
passthrough devices across cpr-exec. QEMU now does: VFIO devices
survive the update via the CPR FD preservation machinery, and USB
hostdevs are covered by QEMU as well.
Replace the blanket check with an allowlist. PCI hostdevs (VFIO; this
also covers <interface type='hostdev'> SR-IOV VFs, which libvirt keeps
in the hostdevs array too) and USB hostdevs pass. Other hostdev types
(mdev, scsi, scsi_host) haven't been validated with cpr-exec and are
still refused, now naming the offending type in the error.
NVMe disks (QEMU's userspace NVMe driver on top of VFIO) and vDPA
interfaces pin guest memory for device DMA just like VFIO devices, but
they are not hostdevs and thus never went through the hostdev check.
Refuse them explicitly, each with its own error.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
src/qemu/qemu_driver.c | 34 ++++++++++++++++++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c
index 911690e3909..b3902f10c65 100644
--- a/src/qemu/qemu_driver.c
+++ b/src/qemu/qemu_driver.c
@@ -25158,6 +25158,7 @@ qemuDomainGuestQemuUpdatePrechecks(virDomainObj *vm)
qemuDomainObjPrivate *priv = vm->privateData;
g_autoptr(GHashTable) blockjobstats = NULL;
g_autoptr(virJSONValue) array = NULL;
+ size_t i;
if (virDomainObjGetState(vm, NULL) != VIR_DOMAIN_RUNNING) {
virReportError(VIR_ERR_OPERATION_INVALID,
@@ -25172,9 +25173,38 @@ qemuDomainGuestQemuUpdatePrechecks(virDomainObj *vm)
return -1;
}
- if (vm->def->nhostdevs > 0) {
+ /* Only PCI (VFIO) and USB hostdevs are known to survive cpr-exec */
+ for (i = 0; i < vm->def->nhostdevs; i++) {
+ virDomainHostdevDef *hostdev = vm->def->hostdevs[i];
+ const char *type;
+
+ if (virHostdevIsPCIDevice(hostdev) ||
+ (hostdev->mode == VIR_DOMAIN_HOSTDEV_MODE_SUBSYS &&
+ hostdev->source.subsys.type == VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_USB))
+ continue;
+
+ if (hostdev->mode == VIR_DOMAIN_HOSTDEV_MODE_SUBSYS)
+ type = virDomainHostdevSubsysTypeToString(hostdev->source.subsys.type);
+ else
+ type = virDomainHostdevModeTypeToString(hostdev->mode);
+
virReportError(VIR_ERR_OPERATION_INVALID,
- _("VM '%1$s' has hostdevs, qemu-update cancelled"), vm->def->name);
+ _("VM '%1$s' has hostdev of unsupported type '%2$s', qemu-update cancelled"),
+ vm->def->name, type);
+ return -1;
+ }
+
+ if (virDomainDefHasNVMeDisk(vm->def)) {
+ virReportError(VIR_ERR_OPERATION_INVALID,
+ _("VM '%1$s' has NVMe disk, qemu-update cancelled"),
+ vm->def->name);
+ return -1;
+ }
+
+ if (virDomainDefHasVDPANet(vm->def)) {
+ virReportError(VIR_ERR_OPERATION_INVALID,
+ _("VM '%1$s' has vDPA interface, qemu-update cancelled"),
+ vm->def->name);
return -1;
}
--
2.47.1
^ permalink raw reply [relevance 10%]
* [Devel] [PATCH hci-8.1 2/4] ms/vdso/timens: Move functions to new file
@ 2026-10-09 13:58 2% ` Eva Kurchatova
3 siblings, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-10-09 13:58 UTC (permalink / raw)
To: khorenko; +Cc: devel
As a preparation of the untangling of time namespaces and the vDSO, move
the glue functions between those subsystems into a new file.
While at it, switch the mutex lock and mmap_read_lock() in the vDSO
namespace code to guard().
Signed-off-by: Thomas Weißschuh <thomas.weissschuh@linutronix.de>
Signed-off-by: Thomas Gleixner <tglx@kernel.org>
Link: https://patch.msgid.link/20260326-vdso-timens-decoupling-v2-1-c82693a7775f@linutronix.de
(cherry picked from commit 5dc9cf835aba73c882348aa4f99be83b6e45ad9b)
Rebase note: this kernel has no auxiliary clocks, so the vdso data has
no aux_clock_data to set up. The offset lock was made visible as
offset_lock by 51a228b021a0 ("ve: Add interface for
ve::clock_[monotonic|bootbased] adjustment") and nothing uses it
outside namespace.c, so it takes its mainline name and home in
namespace_internal.h.
https://virtuozzo.atlassian.net/browse/VSTOR-148112
Feature: fix ms/timens
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
MAINTAINERS | 2 +
include/linux/time_namespace.h | 10 ---
kernel/time/Makefile | 2 +-
kernel/time/namespace.c | 119 +-------------------------
kernel/time/namespace_internal.h | 13 +++
kernel/time/namespace_vdso.c | 141 +++++++++++++++++++++++++++++++
lib/vdso/datastore.c | 25 ------
7 files changed, 161 insertions(+), 151 deletions(-)
create mode 100644 kernel/time/namespace_internal.h
create mode 100644 kernel/time/namespace_vdso.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 9523da3c767c..ac649ee0c43c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -9683,6 +9683,7 @@ S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/tip/tip.git timers/vdso
F: include/asm-generic/vdso/vsyscall.h
F: include/vdso/
+F: kernel/time/namespace_vdso.c
F: kernel/time/vsyscall.c
F: lib/vdso/
F: tools/testing/selftests/vDSO/
@@ -18576,6 +18577,7 @@ F: include/trace/events/timer*
F: kernel/time/itimer.c
F: kernel/time/posix-*
F: kernel/time/namespace.c
+F: kernel/time/namespace_vdso.c
POWER MANAGEMENT CORE
M: "Rafael J. Wysocki" <rafael@kernel.org>
diff --git a/include/linux/time_namespace.h b/include/linux/time_namespace.h
index 3d3b529c5a98..63d0051927f1 100644
--- a/include/linux/time_namespace.h
+++ b/include/linux/time_namespace.h
@@ -32,11 +32,7 @@ struct time_namespace {
extern struct time_namespace init_time_ns;
-extern struct mutex offset_lock;
-
#ifdef CONFIG_TIME_NS
-extern int vdso_join_timens(struct task_struct *task,
- struct time_namespace *ns);
extern void timens_commit(struct task_struct *tsk, struct time_namespace *ns);
static inline struct time_namespace *get_time_ns(struct time_namespace *ns)
@@ -110,12 +106,6 @@ static inline ktime_t timens_ktime_to_host(clockid_t clockid, ktime_t tim)
}
#else
-static inline int vdso_join_timens(struct task_struct *task,
- struct time_namespace *ns)
-{
- return 0;
-}
-
static inline void timens_commit(struct task_struct *tsk,
struct time_namespace *ns)
{
diff --git a/kernel/time/Makefile b/kernel/time/Makefile
index fe0ae82124fe..494bd4e41781 100644
--- a/kernel/time/Makefile
+++ b/kernel/time/Makefile
@@ -23,6 +23,6 @@ endif
obj-$(CONFIG_HAVE_GENERIC_VDSO) += vsyscall.o
obj-$(CONFIG_DEBUG_FS) += timekeeping_debug.o
obj-$(CONFIG_TEST_UDELAY) += test_udelay.o
-obj-$(CONFIG_TIME_NS) += namespace.o
+obj-$(CONFIG_TIME_NS) += namespace.o namespace_vdso.o
obj-$(CONFIG_TEST_CLOCKSOURCE_WATCHDOG) += clocksource-wdtest.o
obj-$(CONFIG_TIME_KUNIT_TEST) += time_test.o
diff --git a/kernel/time/namespace.c b/kernel/time/namespace.c
index e85f947d4063..c82dc9a5d1de 100644
--- a/kernel/time/namespace.c
+++ b/kernel/time/namespace.c
@@ -18,7 +18,7 @@
#include <linux/err.h>
#include <linux/mm.h>
-#include <vdso/datapage.h>
+#include "namespace_internal.h"
ktime_t do_timens_ktime_to_host(clockid_t clockid, ktime_t tim,
struct timens_offsets *ns_offsets)
@@ -139,112 +139,7 @@ struct time_namespace *copy_time_ns(unsigned long flags,
return clone_time_ns(user_ns, old_ns);
}
-static struct timens_offset offset_from_ts(struct timespec64 off)
-{
- struct timens_offset ret;
-
- ret.sec = off.tv_sec;
- ret.nsec = off.tv_nsec;
-
- return ret;
-}
-
-/*
- * A time namespace VVAR page has the same layout as the VVAR page which
- * contains the system wide VDSO data.
- *
- * For a normal task the VVAR pages are installed in the normal ordering:
- * VVAR
- * PVCLOCK
- * HVCLOCK
- * TIMENS <- Not really required
- *
- * Now for a timens task the pages are installed in the following order:
- * TIMENS
- * PVCLOCK
- * HVCLOCK
- * VVAR
- *
- * The check for vdso_clock->clock_mode is in the unlikely path of
- * the seq begin magic. So for the non-timens case most of the time
- * 'seq' is even, so the branch is not taken.
- *
- * If 'seq' is odd, i.e. a concurrent update is in progress, the extra check
- * for vdso_clock->clock_mode is a non-issue. The task is spin waiting for the
- * update to finish and for 'seq' to become even anyway.
- *
- * Timens page has vdso_clock->clock_mode set to VDSO_CLOCKMODE_TIMENS which
- * enforces the time namespace handling path.
- */
-static void timens_setup_vdso_clock_data(struct vdso_clock *vc,
- struct time_namespace *ns)
-{
- struct timens_offset *offset = vc->offset;
- struct timens_offset monotonic = offset_from_ts(ns->offsets.monotonic);
- struct timens_offset boottime = offset_from_ts(ns->offsets.boottime);
-
- vc->seq = 1;
- vc->clock_mode = VDSO_CLOCKMODE_TIMENS;
- offset[CLOCK_MONOTONIC] = monotonic;
- offset[CLOCK_MONOTONIC_RAW] = monotonic;
- offset[CLOCK_MONOTONIC_COARSE] = monotonic;
- offset[CLOCK_BOOTTIME] = boottime;
- offset[CLOCK_BOOTTIME_ALARM] = boottime;
-}
-
-struct page *find_timens_vvar_page(struct vm_area_struct *vma)
-{
- if (likely(vma->vm_mm == current->mm))
- return current->nsproxy->time_ns->vvar_page;
-
- /*
- * VM_PFNMAP | VM_IO protect .fault() handler from being called
- * through interfaces like /proc/$pid/mem or
- * process_vm_{readv,writev}() as long as there's no .access()
- * in special_mapping_vmops().
- * For more details check_vma_flags() and __access_remote_vm()
- */
-
- WARN(1, "vvar_page accessed remotely");
-
- return NULL;
-}
-
-/*
- * Protects possibly multiple offsets writers racing each other
- * and tasks entering the namespace.
- */
-DEFINE_MUTEX(offset_lock);
-
-static void timens_set_vvar_page(struct task_struct *task,
- struct time_namespace *ns)
-{
- struct vdso_time_data *vdata;
- struct vdso_clock *vc;
- unsigned int i;
-
- if (ns == &init_time_ns)
- return;
-
- /* Fast-path, taken by every task in namespace except the first. */
- if (likely(ns->frozen_offsets))
- return;
-
- mutex_lock(&offset_lock);
- /* Nothing to-do: vvar_page has been already initialized. */
- if (ns->frozen_offsets)
- goto out;
-
- ns->frozen_offsets = true;
- vdata = page_address(ns->vvar_page);
- vc = vdata->clock_data;
-
- for (i = 0; i < CS_BASES; i++)
- timens_setup_vdso_clock_data(&vc[i], ns);
-
-out:
- mutex_unlock(&offset_lock);
-}
+DEFINE_MUTEX(timens_offset_lock);
void free_time_ns(struct time_namespace *ns)
{
@@ -297,12 +192,6 @@ static void timens_put(struct ns_common *ns)
put_time_ns(to_time_ns(ns));
}
-void timens_commit(struct task_struct *tsk, struct time_namespace *ns)
-{
- timens_set_vvar_page(tsk, ns);
- vdso_join_timens(tsk, ns);
-}
-
static int timens_install(struct nsset *nsset, struct ns_common *new)
{
struct nsproxy *nsproxy = nsset->nsproxy;
@@ -427,7 +316,7 @@ int proc_timens_set_offset(struct file *file, struct task_struct *p,
goto out;
}
- mutex_lock(&offset_lock);
+ mutex_lock(&timens_offset_lock);
if (time_ns->frozen_offsets) {
err = -EACCES;
goto out_unlock;
@@ -452,7 +341,7 @@ int proc_timens_set_offset(struct file *file, struct task_struct *p,
}
out_unlock:
- mutex_unlock(&offset_lock);
+ mutex_unlock(&timens_offset_lock);
out:
put_time_ns(time_ns);
diff --git a/kernel/time/namespace_internal.h b/kernel/time/namespace_internal.h
new file mode 100644
index 000000000000..e85da11abb4d
--- /dev/null
+++ b/kernel/time/namespace_internal.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _TIME_NAMESPACE_INTERNAL_H
+#define _TIME_NAMESPACE_INTERNAL_H
+
+#include <linux/mutex.h>
+
+/*
+ * Protects possibly multiple offsets writers racing each other
+ * and tasks entering the namespace.
+ */
+extern struct mutex timens_offset_lock;
+
+#endif /* _TIME_NAMESPACE_INTERNAL_H */
diff --git a/kernel/time/namespace_vdso.c b/kernel/time/namespace_vdso.c
new file mode 100644
index 000000000000..558bf5208588
--- /dev/null
+++ b/kernel/time/namespace_vdso.c
@@ -0,0 +1,141 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Author: Andrei Vagin <avagin@openvz.org>
+ * Author: Dmitry Safonov <dima@arista.com>
+ */
+
+#include <linux/cleanup.h>
+#include <linux/mm.h>
+#include <linux/time_namespace.h>
+#include <linux/time.h>
+#include <linux/vdso_datastore.h>
+
+#include <vdso/clocksource.h>
+#include <vdso/datapage.h>
+
+#include "namespace_internal.h"
+
+static struct timens_offset offset_from_ts(struct timespec64 off)
+{
+ struct timens_offset ret;
+
+ ret.sec = off.tv_sec;
+ ret.nsec = off.tv_nsec;
+
+ return ret;
+}
+
+/*
+ * A time namespace VVAR page has the same layout as the VVAR page which
+ * contains the system wide VDSO data.
+ *
+ * For a normal task the VVAR pages are installed in the normal ordering:
+ * VVAR
+ * PVCLOCK
+ * HVCLOCK
+ * TIMENS <- Not really required
+ *
+ * Now for a timens task the pages are installed in the following order:
+ * TIMENS
+ * PVCLOCK
+ * HVCLOCK
+ * VVAR
+ *
+ * The check for vdso_clock->clock_mode is in the unlikely path of
+ * the seq begin magic. So for the non-timens case most of the time
+ * 'seq' is even, so the branch is not taken.
+ *
+ * If 'seq' is odd, i.e. a concurrent update is in progress, the extra check
+ * for vdso_clock->clock_mode is a non-issue. The task is spin waiting for the
+ * update to finish and for 'seq' to become even anyway.
+ *
+ * Timens page has vdso_clock->clock_mode set to VDSO_CLOCKMODE_TIMENS which
+ * enforces the time namespace handling path.
+ */
+static void timens_setup_vdso_clock_data(struct vdso_clock *vc,
+ struct time_namespace *ns)
+{
+ struct timens_offset *offset = vc->offset;
+ struct timens_offset monotonic = offset_from_ts(ns->offsets.monotonic);
+ struct timens_offset boottime = offset_from_ts(ns->offsets.boottime);
+
+ vc->seq = 1;
+ vc->clock_mode = VDSO_CLOCKMODE_TIMENS;
+ offset[CLOCK_MONOTONIC] = monotonic;
+ offset[CLOCK_MONOTONIC_RAW] = monotonic;
+ offset[CLOCK_MONOTONIC_COARSE] = monotonic;
+ offset[CLOCK_BOOTTIME] = boottime;
+ offset[CLOCK_BOOTTIME_ALARM] = boottime;
+}
+
+struct page *find_timens_vvar_page(struct vm_area_struct *vma)
+{
+ if (likely(vma->vm_mm == current->mm))
+ return current->nsproxy->time_ns->vvar_page;
+
+ /*
+ * VM_PFNMAP | VM_IO protect .fault() handler from being called
+ * through interfaces like /proc/$pid/mem or
+ * process_vm_{readv,writev}() as long as there's no .access()
+ * in special_mapping_vmops().
+ * For more details check_vma_flags() and __access_remote_vm()
+ */
+
+ WARN(1, "vvar_page accessed remotely");
+
+ return NULL;
+}
+
+static void timens_set_vvar_page(struct task_struct *task,
+ struct time_namespace *ns)
+{
+ struct vdso_time_data *vdata;
+ struct vdso_clock *vc;
+ unsigned int i;
+
+ if (ns == &init_time_ns)
+ return;
+
+ /* Fast-path, taken by every task in namespace except the first. */
+ if (likely(ns->frozen_offsets))
+ return;
+
+ guard(mutex)(&timens_offset_lock);
+ /* Nothing to-do: vvar_page has been already initialized. */
+ if (ns->frozen_offsets)
+ return;
+
+ ns->frozen_offsets = true;
+ vdata = page_address(ns->vvar_page);
+ vc = vdata->clock_data;
+
+ for (i = 0; i < CS_BASES; i++)
+ timens_setup_vdso_clock_data(&vc[i], ns);
+}
+
+/*
+ * The vvar page layout depends on whether a task belongs to the root or
+ * non-root time namespace. Whenever a task changes its namespace, the VVAR
+ * page tables are cleared and then they will be re-faulted with a
+ * corresponding layout.
+ * See also the comment near timens_setup_vdso_clock_data() for details.
+ */
+static int vdso_join_timens(struct task_struct *task, struct time_namespace *ns)
+{
+ struct mm_struct *mm = task->mm;
+ struct vm_area_struct *vma;
+ VMA_ITERATOR(vmi, mm, 0);
+
+ guard(mmap_read_lock)(mm);
+ for_each_vma(vmi, vma) {
+ if (vma_is_special_mapping(vma, &vdso_vvar_mapping))
+ zap_vma_pages(vma);
+ }
+ return 0;
+}
+
+void timens_commit(struct task_struct *tsk, struct time_namespace *ns)
+{
+ timens_set_vvar_page(tsk, ns);
+ vdso_join_timens(tsk, ns);
+}
diff --git a/lib/vdso/datastore.c b/lib/vdso/datastore.c
index 46192c1d8542..7713fd392dd0 100644
--- a/lib/vdso/datastore.c
+++ b/lib/vdso/datastore.c
@@ -102,28 +102,3 @@ struct vm_area_struct *vdso_install_vvar_mapping(struct mm_struct *mm, unsigned
VM_READ | VM_MAYREAD | VM_IO | VM_DONTDUMP | VM_PFNMAP,
&vdso_vvar_mapping);
}
-
-#ifdef CONFIG_TIME_NS
-/*
- * The vvar page layout depends on whether a task belongs to the root or
- * non-root time namespace. Whenever a task changes its namespace, the VVAR
- * page tables are cleared and then they will be re-faulted with a
- * corresponding layout.
- * See also the comment near timens_setup_vdso_clock_data() for details.
- */
-int vdso_join_timens(struct task_struct *task, struct time_namespace *ns)
-{
- struct mm_struct *mm = task->mm;
- struct vm_area_struct *vma;
- VMA_ITERATOR(vmi, mm, 0);
-
- mmap_read_lock(mm);
- for_each_vma(vmi, vma) {
- if (vma_is_special_mapping(vma, &vdso_vvar_mapping))
- zap_vma_pages(vma);
- }
- mmap_read_unlock(mm);
-
- return 0;
-}
-#endif
--
2.56.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 2%]
* Re: [Devel] [PATCH hci-8.1 v4 3/3] dm-qcow2: truncate unused tail of image on suspend
@ 2026-10-08 17:08 3% ` no-reply
2026-10-10 12:42 0% ` Alexander Ivanov
0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-08 17:08 UTC (permalink / raw)
To: den, Alexander Ivanov; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] dm-qcow2: truncate_preallocations() trusts unvalidated on-disk refcounts as file length
--
commit e3b5d03194a42ef95bbb361e199ba3c183aed97e
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-qcow2: truncate unused tail of image on suspend
This patch adds qcow2_calc_used_end(), which scans the refcount blocks backward and returns the end of the last cluster with a non-zero refcount. qcow2_truncate_preallocations() now truncates the image file to that value on postsuspend. If the scan fails, or md writeback failed, it falls back to the old behaviour.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
> --- a/drivers/md/dm-qcow2-map.c
> +++ b/drivers/md/dm-qcow2-map.c
[ ... ]
> +loff_t qcow2_calc_used_end(struct qcow2 *qcow2)
> +{
[ ... ]
> + for (i = r2.index_in_page; i >= 0; i--, pos -= clu_size) {
> + /* Last cluster is partial if size isn't cluster-aligned */
> + if (get_r2_entry(qcow2, r2.md, i))
> + return min_t(loff_t, pos + clu_size,
> + qcow2->file_preallocated_area_start);
> + }
> + }
> +
> + return -EIO;
> +}
[Severity: Medium]
The used end here comes only from the on-disk refcounts. Can it be wrong when those refcounts are stale or corrupt?
An image written by QEMU with lazy refcounts, or one left dirty after a crash, can legitimately have zero refcounts for clusters that are still in use. A corrupt or crafted image can too.
In that case qcow2_calc_used_end() returns an end below clusters that are still referenced. Nothing cross-checks the result against the L1/L2 tables, the snapshot extents, or the header and refcount table/refblock offsets.
Before this patch the truncate went only down to file_preallocated_area_start. That value is set to file_size at load and is only raised by this driver's own preallocation, so the old code never cut off referenced data.
> diff --git a/drivers/md/dm-qcow2-target.c b/drivers/md/dm-qcow2-target.c
> --- a/drivers/md/dm-qcow2-target.c
> +++ b/drivers/md/dm-qcow2-target.c
[ ... ]
> + /* Refcounts on disk are stale after failed md writeback */
> + if (!tgt->md_writeback_error) {
> + used_end = qcow2_calc_used_end(qcow2);
> + if (used_end < 0)
> + QC_ERR(ti, "Can't find used end: %pe", ERR_PTR(used_end));
> + else
> + end = used_end;
> + }
> +
> if (end == qcow2->file_size)
> return;
>
> ret = qcow2_truncate_safe(qcow2->file, end);
[Severity: Medium]
In qcow2_truncate_preallocations(), the value from qcow2_calc_used_end() is passed straight to qcow2_truncate_safe() as the new file length. The fallback only covers scan errors (-EIO and similar). It does not cover refcounts that are consistent to parse but logically wrong.
The md_writeback_error check only covers this driver's own writeback failures. It does not cover refcounts that were already stale when the image was loaded.
Is there a way to validate the result before truncating? For example, the shrink could be skipped when the dirty or lazy-refcount flag is set, or the used end could be checked against the L1/L2 and metadata extents.
This ties into qcow2_check_convert_hdr(), where the dirty-bit check is commented out:
// if (!is_ro && kernel_sets_dirty_bit !=
// !(hdr->incompatible_features & INCOMPATIBLE_FEATURES_DIRTY_BIT))
// return kernel_sets_dirty_bit ? -EUCLEAN : -ENOLCK;
With that check disabled, images with stale refcounts are accepted at load time. I also don't see any other refcount consistency validation at load.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261008170200.646208-1-alexander.ivanov@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 v3 2/5] dm-ploop: set delta is_raw before applying its mappings
@ 2026-10-08 14:09 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-08 14:09 UTC (permalink / raw)
To: Alexander Ivanov, den; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] dm-ploop: if (nr_be) guard in ploop_add_delta() skips the top delta header copy
--
commit fadb633bf83be4165d2be5c5a7e26f5f3b0c5554
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: set delta is_raw before applying its mappings
This commit sets deltas[level].is_raw before ploop_apply_delta_mappings()
runs, so that raw deltas are no longer handled as non-raw ones. It also
skips applying mappings when the delta is empty (nr_be == 0).
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop-bat.c b/drivers/md/dm-ploop-bat.c
> --- a/drivers/md/dm-ploop-bat.c
> +++ b/drivers/md/dm-ploop-bat.c
[ ... ]
> @@ -501,7 +501,10 @@ int ploop_add_delta(struct ploop *ploop, u32 level, struct file *file, bool is_r
> if (level != ploop_top_level(ploop) && nr_be > deltas[level + 1].nr_be)
> goto out;
>
> - ploop_apply_delta_mappings(ploop, deltas, level, &md_root, nr_be);
> + deltas[level].is_raw = is_raw;
> + /* Empty delta has no mappings, and BAT iterators can't handle it */
> + if (nr_be)
> + ploop_apply_delta_mappings(ploop, deltas, level, &md_root, nr_be);
[Severity: Low]
Can this unconditional if (nr_be) guard skip the top-level header copy?
The commit message only justifies the guard for an empty raw delta.
In ploop_apply_delta_mappings(), the on-disk header is copied into md0
only here:
if (is_top_level && md->id == 0 && !is_raw) {
/* bat_entries before PLOOP_MAP_OFFSET is hdr */
memcpy(bat_entries, d_bat_entries,
sizeof(struct ploop_pvd_header));
}
A non-raw top delta with m_Size == 0 appears to reach this path.
ploop_add_deltas_stack() calls ploop_check_top_delta() and then
ploop_add_delta(). ploop_check_top_delta() does not reject m_Size == 0,
and ploop_read_delta_metadata() can then return nr_be == 0.
Before the patch, ploop->bat_entries held md page 0 in this case, so the
loop body ran once and the memcpy was executed. With the guard, the whole
function is skipped.
ploop_check_top_delta() only allocates md pages through
ploop_prealloc_md_pages(). Those pages are filled with BAT_ENTRY_NONE and
do not get the header.
Would md0 then keep the BAT_ENTRY_NONE fill instead of the header?
ploop_resize() reads hdr->m_SizeInSectors_v2 from md0, and writeback or
grow writes it back.
This looks limited to crafted or corrupt images, since a device with
nr_bat_entries == 0 cannot serve normal I/O.
Would it be better to apply the guard only for the raw case, or to still
copy the header for a non-raw top delta? The later ploop_read_delta() and
ploop_reload_metadata() changes in the series keep the same if (nr_be)
guard, so it does not look like this is fixed later.
>
> deltas[level].file = file;
> deltas[level].mtfile = kcalloc(ploop->nkt_runners, sizeof(*file),
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261008140333.614909-1-alexander.ivanov@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 5/6] dm-ploop: reload metadata on resume
@ 2026-10-08 7:54 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-08 7:54 UTC (permalink / raw)
To: Alexander Ivanov, den; +Cc: devel
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] dm-ploop: lost wakeup in ploop_allocator() can hang ploop_postsuspend() in wait_event()
- [Medium] dm-ploop: ploop_reload_metadata() uses GFP_KERNEL allocs with no NOIO scope
- [Medium] dm-ploop: postsuspend wait_event on prealloc can hang on allocator lost wakeup
- [Low] dm-ploop: ploop_reload_metadata() restore path leaves m_Sig/m_Type/m_Sectors modified
--
commit 17ae791b0a37f669147d370ddae2ae7351e7b6de
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: reload metadata on resume
This patch makes ploop_preresume() re-read the top delta header, the BAT
of every delta and the delta file sizes, and rebuild the BAT tree and
holes_bitmap. It moves preallocation from ploop_ctr() to ploop_preresume(),
makes ploop_postsuspend() wait for pending preallocation, and takes
ctl_rwsem in ploop_llseek_hole().
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop-bat.c b/drivers/md/dm-ploop-bat.c
> --- a/drivers/md/dm-ploop-bat.c
> +++ b/drivers/md/dm-ploop-bat.c
[ ... ]
> +int ploop_reload_metadata(struct ploop *ploop)
> +{
> + struct rb_root old_bat_entries = ploop->bat_entries;
> + void *old_holes_bitmap = ploop->holes_bitmap;
> + u32 old_nr_bat_entries = ploop->nr_bat_entries;
> + u32 old_hb_nr = ploop->hb_nr;
[ ... ]
> + old_deltas = kmemdup(ploop->deltas,
> + sizeof(*old_deltas) * ploop->nr_deltas,
> + GFP_KERNEL);
> + page = alloc_page(GFP_KERNEL);
[Severity: Medium]
Can these GFP_KERNEL allocations deadlock under memory pressure?
ploop_preresume() runs in the DM ioctl task while the device is still
suspended, and no memalloc_noio scope is set on that path. Direct reclaim
with __GFP_IO and __GFP_FS could write back dirty pages of a filesystem
mounted on this ploop device. Those bios are held until resume completes,
but the resume is the task waiting in the allocation.
The same applies to the other allocations reached from here:
ploop_reload_metadata()
ploop_setup_metadata()
ploop_setup_holes_bitmap()
ploop->holes_bitmap = kvmalloc(size, GFP_KERNEL);
and to ploop_prealloc_md_pages(). The ploop kthreads avoid this by running
with PF_MEMALLOC_NOIO (PLOOP_PFLAGS in dm-ploop-map.c), but the ioctl task
has no such protection.
Would it make sense to wrap ploop_reload_metadata() in
memalloc_noio_save()/memalloc_noio_restore(), or to use GFP_NOIO?
[ ... ]
> + ploop->bat_entries = RB_ROOT;
> + ploop->holes_bitmap = NULL;
> +
> + /* Image is already checked for being in use in ploop_ctr() */
> + ret = ploop_setup_metadata(ploop, page, false);
> + if (ret)
> + goto restore;
[ ... ]
> +restore:
> + ploop_free_md_pages_tree(&ploop->bat_entries);
> + kvfree(ploop->holes_bitmap);
> + ploop->bat_entries = old_bat_entries;
> + ploop->holes_bitmap = old_holes_bitmap;
> + ploop->nr_bat_entries = old_nr_bat_entries;
> + ploop->hb_nr = old_hb_nr;
[Severity: Low]
The commit message says "On error the old metadata is kept and resume
fails." Is that fully true here?
ploop_setup_metadata() overwrites these fields before its later checks
('custom FirstBlockOffset', 'Too short BAT', ploop_setup_holes_bitmap()
returning -ENOMEM):
memcpy(ploop->m_Sig, m_hdr->m_Sig, sizeof(ploop->m_Sig));
ploop->m_Type = le32_to_cpu(m_hdr->m_Type);
ploop->m_Sectors = le32_to_cpu(m_hdr->m_Sectors);
The restore label puts back bat_entries, holes_bitmap, nr_bat_entries,
hb_nr and deltas[], but not m_Sig, m_Type or m_Sectors. The same applies
when ploop_prealloc_md_pages() or ploop_read_delta() fails.
m_Sig and m_Sectors are validated, so in practice only m_Type could differ,
and only if userspace rewrote the header. ploop_delta_check_header()
compares against these fields, so a later delta check could then use a value
that does not match the retained old BAT.
Should these three fields be saved and restored along with the others?
> diff --git a/drivers/md/dm-ploop-map.c b/drivers/md/dm-ploop-map.c
> --- a/drivers/md/dm-ploop-map.c
> +++ b/drivers/md/dm-ploop-map.c
> @@ -1245,6 +1245,12 @@ static int ploop_no_pending_prealloc(struct ploop *ploop)
> return ret;
> }
>
> +void ploop_wait_prealloc(struct ploop *ploop)
> +{
> + wait_event(ploop->dispatcher_wq_prealloc,
> + ploop_no_pending_prealloc(ploop));
> +}
[Severity: Medium]
Can this wait hang forever on a lost wakeup in the allocator?
This is an unbounded, uninterruptible wait whose only waker is the
wake_up() in ploop_allocator() after a preallocation completes. In
ploop_allocator(), prealloc_size is tested locklessly before the task state
is set:
if (ploop->prealloc_size) {
...
}
if (kthread_should_stop()) {...}
set_current_state(TASK_INTERRUPTIBLE);
schedule();
Consider this interleaving:
allocator: reads prealloc_size == 0, not yet in set_current_state()
requester: ploop_req_prealloc() sets prealloc_size under bat_lock
requester: wake_up_process(ploop->kt_allocator->task) /* TASK_RUNNING */
allocator: set_current_state(TASK_INTERRUPTIBLE); schedule();
The wakeup is a no-op and the allocator sleeps with prealloc_size != 0.
ploop_req_prealloc() returns early when the requested size is already
covered, so later IO would not re-kick it, and suspend has quiesced IO
anyway. ploop_no_pending_prealloc() then never becomes true.
The race in the allocator predates this patch. Before it, a lost wakeup
only delayed preallocation until the next request. Now it can hang
ploop_postsuspend(). The new ploop_should_prealloc() call in
ploop_preresume() also adds a new place to trigger it.
Could the allocator re-check prealloc_size after set_current_state(), or
could the wait be made bounded or killable?
[ ... ]
> diff --git a/drivers/md/dm-ploop-target.c b/drivers/md/dm-ploop-target.c
> --- a/drivers/md/dm-ploop-target.c
> +++ b/drivers/md/dm-ploop-target.c
[ ... ]
> static void ploop_postsuspend(struct dm_target *ti)
> {
> + struct ploop *ploop = ti->private;
> +
> + /*
> + * Let the next resumed ploop (maybe of reloaded table)
> + * see final size of the top delta.
> + */
> + ploop_wait_prealloc(ploop);
> ploop_set_suspended(ti, true);
> }
[Severity: Medium]
Can ploop_postsuspend() block here indefinitely?
If the allocator loses the wakeup described above, ploop_wait_prealloc()
never returns and dmsetup suspend hangs in D state. This is a new
dependency of the suspend path on the allocator making progress.
The follow-up patch "dm-ploop: truncate unused tail of top delta on
suspend" does not touch ploop_allocator() or ploop_wait_prealloc(). It
makes ploop_postsuspend() call ploop_wait_prealloc() under
down_write(&ctl_rwsem), so in the final series state the hang would also
block every other ctl_rwsem user (message, status, ploop_llseek_hole()).
Would it be possible to fix the allocator wakeup, or to avoid waiting while
holding ctl_rwsem for write?
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261008073837.510678-1-alexander.ivanov@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 v2 6/6] dm-ploop: truncate unused tail of top delta on suspend
@ 2026-10-08 7:52 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-08 7:52 UTC (permalink / raw)
To: Alexander Ivanov, den; +Cc: devel
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] dm-ploop: unvalidated m_FormatExtensionOffset can truncate delta to unaligned size
- [Medium] dm-ploop: GFP_KERNEL BAT read in postsuspend() can deadlock via reclaim IO
- [Low] dm-ploop: WARN_ON_ONCE in ploop_calc_used_end() can fire on a benign inflight_md race
- [Low] dm-ploop: tail truncation assumes CBT header is the last extension cluster
--
commit 209cb17124d4cf7236cb1d3d9e28ea5cc4361bbf
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: truncate unused tail of top delta on suspend
This patch adds ploop_calc_used_end(), which reads the on-disk BAT and the
format extension offset to find the end of the used part of the top delta.
ploop_postsuspend() now truncates the top delta to that end under
ctl_rwsem. The header field m_Reserved is renamed to m_FormatExtensionOffset.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop-bat.c b/drivers/md/dm-ploop-bat.c
> --- a/drivers/md/dm-ploop-bat.c
> +++ b/drivers/md/dm-ploop-bat.c
[ ... ]
> +loff_t ploop_calc_used_end(struct ploop *ploop, struct file *file)
> +{
[ ... ]
> + /* BAT on disk may miss allocated clusters until writeback is done */
> + if (WARN_ON_ONCE(!ploop_md_pages_are_clean(ploop)))
> + return -EBUSY;
[Severity: Low]
Can this WARN_ON_ONCE() fire on a benign race? The commit message
describes the not-clean case as something to skip, but here it raises a
warning backtrace (and would panic with panic_on_warn).
In ploop_put_piwb(), the data pios are ended first, and inflight_md is
decremented only afterwards:
ploop_bat_write_finish(piwb->pio, piwb, piwb->bi_status);
...
if (atomic_dec_and_test(&piwb->ploop->inflight_md))
wake_up_interruptible(&piwb->ploop->inflight_md_wq);
dm suspend can see those bios complete and reach ploop_postsuspend()
while inflight_md is still 1. ploop_postsuspend() only calls
ploop_wait_prealloc(), and does not wait on inflight_md_wq. So
ploop_md_pages_are_clean() can return false with no I/O error involved.
ploop_truncate_unused_tail() then also prints "Can't find used end of top
delta: -16" and skips the truncation. Should this be a plain condition,
or should ploop_postsuspend() wait on inflight_md_wq first? The same
pattern is already in ploop_reload_metadata(), but this adds a second
site.
[ ... ]
> + ret = ploop_read_delta_metadata(ploop, file, &md_root, &nr_be);
> + if (ret)
> + return ret;
[Severity: Medium]
Can this full BAT read deadlock when called from ploop_postsuspend()?
ploop_read_delta_metadata() allocates with GFP_KERNEL, both through
ploop_prealloc_md_pages() (alloc_page()) and in:
bvec = kvmalloc(sizeof(*bvec) * nr_segs, GFP_KERNEL);
dm_suspend() calls dm_table_postsuspend_targets() after IO to the device
has been stopped, with suspend_lock held and no memalloc_noio scope.
Direct reclaim here could write back dirty pages of a filesystem on the
ploop device, or swap out to it. Those bios are deferred because the
device is suspended, so the reclaiming task waits for a resume. The
resume needs md->suspend_lock, which the suspending task still holds.
The old ploop_postsuspend() did no allocations. Would wrapping the call
in memalloc_noio_save()/memalloc_noio_restore() be appropriate? The same
hazard already exists in ploop_reload_metadata() from ploop_preresume(),
but this patch adds the postsuspend site.
[ ... ]
> + /* Format extension header is the last cluster of extension */
> + hdr = ploop_md_first_entry(&md_root)->kmpage;
> + ext_off = le64_to_cpu(hdr->m_FormatExtensionOffset);
> + if (ext_off) {
> + ext_end = to_bytes(ext_off) + CLU_SIZE(ploop);
[Severity: Medium]
Can an unvalidated m_FormatExtensionOffset lead to an unaligned
truncation? This field used to be m_Reserved[8] and was never
interpreted. ploop_delta_check_header() and ploop_setup_metadata() do not
look at it.
If ext_off is sector-aligned but not cluster-aligned, ext_end is not
cluster-aligned. If it is also above the BAT-derived used_end and below
top->file_size, ploop_truncate_unused_tail() passes it to do_ftruncate().
The next ploop_preresume() -> ploop_reload_metadata() ->
ploop_read_delta() -> ploop_check_delta_length() then hits:
if (loff & (CLU_SIZE(ploop) - 1))
return -EPROTO;
and the device cannot be resumed.
Also, to_bytes() is an unsigned long shift by 9, so a large ext_off such
as 2^55 or more wraps, and the wrapped value can land in the same range.
Should ext_off be required to be cluster-aligned and checked for
overflow, with truncation skipped when it is out of range?
> + if (ext_end > used_end)
> + used_end = ext_end;
> + }
[Severity: Low]
Is it guaranteed that the format extension header is the last cluster of
the extension? The code assumes the extension ends one cluster after its
offset:
ext_end = to_bytes(ext_off) + CLU_SIZE(ploop);
If a CBT extension ever has data after its header cluster, anything beyond
ext_end that lies past the last BAT-referenced cluster is cut off by
do_ftruncate() in ploop_truncate_unused_tail(). That loss is irreversible.
The only support for this layout is the commit message ("as userspace
does") and the code comment. Could the layout be documented in the code or
in a header comment?
[ ... ]
> diff --git a/drivers/md/dm-ploop-target.c b/drivers/md/dm-ploop-target.c
> --- a/drivers/md/dm-ploop-target.c
> +++ b/drivers/md/dm-ploop-target.c
[ ... ]
> + end = ploop_calc_used_end(ploop, top->file);
> + if (end < 0) {
> + PL_ERR("Can't find used end of top delta: %lld", end);
> + return;
> + }
[Severity: Low]
This isn't a bug, but when ploop_calc_used_end() returns -EBUSY for the
not-clean skip case, this path reports it as an error on top of the
WARN_ON_ONCE() in ploop_calc_used_end(). Should the skip case be silent?
[ ... ]
> + ret = do_ftruncate(top->file, end, 0);
[ ... ]
> + ploop_wait_prealloc(ploop);
> + ploop_truncate_unused_tail(ploop);
> + ploop->suspended = true;
> + up_write(&ploop->ctl_rwsem);
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261008073837.510678-1-alexander.ivanov@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 v2 3/6] dm-ploop: set delta is_raw before applying its mappings
@ 2026-10-08 7:46 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-08 7:46 UTC (permalink / raw)
To: den, Alexander Ivanov; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] dm-ploop: zero-length raw base delta underflows nr_be - 1 in init_be_iter()
--
commit 8d9d7ed186e03b4389524be1bcdc31e23ab48455
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-ploop: set delta is_raw before applying its mappings
This patch moves the assignment of deltas[level].is_raw in
ploop_add_delta() so that it happens before
ploop_apply_delta_mappings() runs. Previously a raw delta was handled
as a non-raw one, which dereferenced a NULL d_md.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-ploop-bat.c b/drivers/md/dm-ploop-bat.c
> index a7849de8aaa6..76fcfa64f875 100644
> --- a/drivers/md/dm-ploop-bat.c
> +++ b/drivers/md/dm-ploop-bat.c
[ ... ]
> @@ -501,6 +501,7 @@ int ploop_add_delta(struct ploop *ploop, u32 level, struct file *file, bool is_r
> if (level != ploop_top_level(ploop) && nr_be > deltas[level + 1].nr_be)
> goto out;
>
> + deltas[level].is_raw = is_raw;
> ploop_apply_delta_mappings(ploop, deltas, level, &md_root, nr_be);
[Severity: Low]
With this change the raw branch of ploop_apply_delta_mappings() becomes
reachable, and it looks like it can misbehave for a zero-length raw base
delta.
For a raw delta, nr_be comes straight from the file size:
ploop_add_delta()
nr_be = POS_TO_CLU(ploop, file_size);
ploop_check_delta_length() only rejects sizes that are not cluster
aligned, so a size of 0 passes. The check against deltas[level + 1].nr_be
cannot fail for 0 either.
Then, with nr_be == 0, init_be_iter() computes:
u32 rem, last_page = ploop_bat_clu_to_page_nr(nr_be - 1);
nr_be - 1 is 0xFFFFFFFF, and the u32 wrap in ploop_bat_clu_to_page_nr()
gives last_page = 0. Page 0 then gets start=16 and end=15 and is
skipped, while every other page iterates in full.
In ploop_apply_delta_mappings() the stop condition can never trigger:
if (clu == nr_be - 1)
stop = true;
...
dst_clu = clu;
so can identity mappings be written at the raw level for every
unpopulated slot on pages above 0, past EOF of the raw file, including
tail slots of the last page beyond nr_bat_entries?
Should ploop_add_delta() reject nr_be == 0 for a raw delta (or should
init_be_iter() handle it)?
The BAT arrays do not appear to be overrun since the loop stays within
each md page. Reads of those clusters would hit EOF on the raw file and
likely return zeros, like a hole. I did not trace the write/COW path for
these mappings. Reaching this requires a table load with an empty
"raw@fd" base, so it needs CAP_SYS_ADMIN and is an unusual
misconfiguration. I did not find a check for this in later patches of
the series.
>
> deltas[level].file = file;
[ ... ]
> @@ -521,7 +522,6 @@ int ploop_add_delta(struct ploop *ploop, u32 level, struct file *file, bool is_r
> deltas[level].file_size = file_size;
> deltas[level].file_preallocated_area_start = file_size;
> deltas[level].nr_be = nr_be;
> - deltas[level].is_raw = is_raw;
> ret = 0;
> out:
> ploop_free_md_pages_tree(&md_root);
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261008073837.510678-1-alexander.ivanov@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 2/6] dm-qcow2: truncate unused tail of image on suspend
@ 2026-10-08 7:44 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-08 7:44 UTC (permalink / raw)
To: den, Alexander Ivanov; +Cc: devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] dm-qcow2: qcow2_calc_used_end() can return an end beyond file_size, so suspend grows the file
- [Low] dm-qcow2: duplicated prefix in used-end error message in qcow2_truncate_preallocations()
--
commit ce35554959d7d4a8d76e3913a469c5465e287b33
Author: Alexander Ivanov <alexander.ivanov@virtuozzo.com>
dm-qcow2: truncate unused tail of image on suspend
This patch adds qcow2_calc_used_end(), which scans refcount blocks
backward from file_preallocated_area_start and returns the end of the
last cluster with a non-zero refcount. On postsuspend the image file is
then truncated to that end instead of only to
file_preallocated_area_start. The scan is skipped if md writeback
failed, and the old behaviour is used if the scan fails.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148171
> diff --git a/drivers/md/dm-qcow2-map.c b/drivers/md/dm-qcow2-map.c
> --- a/drivers/md/dm-qcow2-map.c
> +++ b/drivers/md/dm-qcow2-map.c
[ ... ]
> +loff_t qcow2_calc_used_end(struct qcow2 *qcow2)
> +{
> + u32 clu_size = qcow2->clu_size;
> + struct qcow2_map_item r1, r2;
> + struct md_page *r1_md = NULL;
> + loff_t pos;
> + s32 i;
> + int ret;
> +
> + pos = round_down(qcow2->file_preallocated_area_start - 1, clu_size);
[ ... ]
> + for (i = r2.index_in_page; i >= 0; i--, pos -= clu_size) {
> + if (get_r2_entry(qcow2, r2.md, i))
> + return pos + clu_size;
> + }
[Severity: Medium]
Can the value returned here exceed qcow2->file_size?
file_preallocated_area_start is only page aligned after
qcow2_parse_metadata(), not aligned to clu_size. When clu_size is larger
than PAGE_SIZE and the file ends in the middle of a cluster, the first
cluster examined starts below file_preallocated_area_start but extends
past it.
If that cluster has a non-zero refcount, the function returns
pos + clu_size, which is greater than file_size.
> diff --git a/drivers/md/dm-qcow2-target.c b/drivers/md/dm-qcow2-target.c
> --- a/drivers/md/dm-qcow2-target.c
> +++ b/drivers/md/dm-qcow2-target.c
> @@ -1042,11 +1042,21 @@ static void qcow2_truncate_preallocations(struct dm_target *ti)
[ ... ]
> + /* Refcounts on disk are stale after failed md writeback */
> + if (!tgt->md_writeback_error) {
> + used_end = qcow2_calc_used_end(qcow2);
> + if (used_end < 0)
> + QC_ERR(ti, "dm-qcow2: Can't find used end: %lld", used_end);
> + else
> + end = used_end;
> + }
> +
> if (end == qcow2->file_size)
> return;
>
> ret = qcow2_truncate_safe(qcow2->file, end);
[Severity: Medium]
Is the check against file_size sufficient here?
Only end == qcow2->file_size is tested, so with used_end greater than
file_size the code goes on to call qcow2_truncate_safe(), and ftruncate
extends the file. qcow2->file_size and file_preallocated_area_start are
then set to the larger value, although this patch is meant only to shrink
the image.
The growth is at most one cluster, but would it make sense to clamp with
min(used_end, qcow2->file_size), or to use end >= qcow2->file_size for the
early return?
[Severity: Low]
This isn't a bug, but QC_ERR already adds the "dm-qcow2: " prefix through
QCOW2_FMT, so this message prints the prefix twice:
dm-qcow2: <dev>: dm-qcow2: Can't find used end: -5
The raw negative errno is also printed with %lld. Neighbouring messages in
the same function repeat the prefix as well.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261008073837.510678-1-alexander.ivanov@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 v8 2/2] selftests: ve: introduce vtty pair teardown stress test
2026-10-08 3:52 6% ` [Devel] [PATCH hci-8.1 v8 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
@ 2026-10-08 4:03 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-08 4:03 UTC (permalink / raw)
To: Eva Kurchatova, khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Medium] selftests/ve: kmsg_check() fails on any unrelated host log record or ring overrun
- [Low] selftests/ve: master loop ignores early exit of slave child in main()
- [Low] selftests/ve: deadline uses 1s-granularity time(NULL) in main() and hammer_slave()
- [Low] selftests/ve: wait_child() leaves status 0 when waitpid() fails non-EINTR
- [Low] selftests/ve: stop flag check races with blocking waitpid()/read() in main()
--
commit 8ed5cf9c90a1b3d9d20bbf2774ae5c2adb6076ca
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
selftests: ve: introduce vtty pair teardown stress test
This adds a selftest that opens and closes both ends of a vtty pair
concurrently, to exercise teardown ownership and thread safety. The
slave end runs in a forked child inside a VE, the master end is opened
via the vzctl ioctl, and the kernel log is scanned for splats.
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..f4bdd81133b8
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_vtty_stress.c
[ ... ]
> +/* The handler interrupts a wait, the child is still to be reaped */
> +static void wait_child(pid_t pid, int *status)
> +{
> + *status = 0;
> + do {
> + if (stop)
> + kill(pid, SIGKILL);
> + } while (waitpid(pid, status, 0) < 0 && errno == EINTR);
> +}
[Severity: Low]
If waitpid() fails with an error other than EINTR (for example ECHILD),
the loop exits with *status still preset to 0. Callers then see
WIFEXITED(0) true and WEXITSTATUS(0) equal to 0, so slave_err is computed
as 0 and the child is treated as having exited cleanly. Could the
failure be reported to the caller instead of leaving a status that reads
as a clean exit? This path is unlikely, since the child is the parent's
own fork.
[Severity: Low]
Is the check of stop atomic with the blocking waitpid()? If SIGTERM or
SIGINT arrives after the "if (stop)" test but before waitpid() is
entered, the handler (installed with sa_flags = 0) sets stop but nothing
interrupts the wait. The parent then blocks until the child reaches its
own deadline. This is a delay bounded by secs, but if the runner
escalates to SIGKILL in the meantime, the cgroup, the mknod node and the
tmp directory are left behind. Would blocking the signals and using
ppoll()/pselect(), signalfd or a pidfd close this window?
[ ... ]
> +/*
> + * 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: ",
> + };
[ ... ]
> + 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]
Can this give wrong results in both directions? kmsg_open() seeks to the
end of /dev/kmsg, and kmsg_check() then scans every later record on the
host, matching generic substrings such as "WARNING: ", "Oops",
"INFO: task ", "rcu: INFO: " and "soft lockup - " with strstr() anywhere
in the record. There is no filtering by vtty, by tty, or by the pid of
the test.
A warning from an unrelated subsystem or container during the run is
therefore counted as a vtty complaint, and main() lets a complaint
outrank how the run ended. An EPIPE ring overrun is also counted as a
failure (found++), and since the log is drained only about once per
second in main(), a chatty host can overrun it.
In the other direction, without KASAN or a similar detector, a
use-after-free or a double release_tty() that logs nothing passes
silently, because the log scan is the only detector. Could the matching
be narrowed to vtty-related records, and could an overrun be treated
differently from a vtty failure?
[ ... ]
> + deadline = time(NULL) + secs;
[Severity: Low]
Is a 1 second granularity deadline intended here? time(NULL) truncates
to whole seconds, so with VE_VTTY_SECS=1 the stress window can be almost
empty. The parent in main() and the child in hammer_slave() also
evaluate "time(NULL) < deadline" independently and stop at slightly
different instants, and time() is subject to wall-clock steps. Would
clock_gettime(CLOCK_MONOTONIC) be a better fit?
[ ... ]
> + while (!stop && (now = time(NULL)) < deadline) {
> + int fd = open_master(ctl, veid);
> +
> + if (fd >= 0) {
> + master_opened++;
> + close(fd);
> + } else if (!expected_errno(errno)) {
> + master_err = errno;
> + break;
> + }
[Severity: Low]
Does this master loop notice when the slave child has already exited?
hammer_slave() returns on the first unexpected errno (for example EPERM
or EACCES from a device policy), and the child then writes its count and
exits. The loop condition only checks stop and the deadline, so the
master keeps hammering alone until the deadline, and only afterwards does
main() read count[0] and call wait_child().
In a denied-slave environment this means the full 30 second default
passes before the SKIP is reported, and a real slave-side failure is only
reported at the end, with the rest of the run exercising only one end of
the pair. Would polling the child with waitpid(WNOHANG), or poll() on
count[0], inside this loop help?
[ ... ]
> + } 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;
> + }
[Severity: Low]
Is the same window present here? The blocking read(count[0], ...) has no
check of stop that is atomic with it, so a SIGTERM or SIGINT delivered
after the master loop exits but before read() is entered is not noticed
until the child finishes at its own deadline.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261008035335.2823931-1-eva.kurchatova@virtuozzo.com?part=2
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* [Devel] [PATCH hci-8.1 v8 2/2] selftests: ve: introduce vtty pair teardown stress test
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
@ 2026-10-08 3:52 6% ` Eva Kurchatova
2026-10-08 4:03 3% ` no-reply
1 sibling, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-10-08 3:52 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 the
test takes a random one high above the hand-given range, the way a
container gets its id from a uuid.
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 selftests
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 | 415 ++++++++++++++++++++
3 files changed, 418 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..f4bdd81133b8
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_vtty_stress.c
@@ -0,0 +1,415 @@
+// 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/random.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;
+ do {
+ if (stop)
+ kill(pid, SIGKILL);
+ } while (waitpid(pid, status, 0) < 0 && errno == EINTR);
+}
+
+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)
+{
+ /* 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;
+ unsigned int veid;
+ 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);
+
+ 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: a random high one, as a container gets */
+ if (getrandom(&veid, sizeof(veid), 0) != sizeof(veid)) {
+ status = errno;
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("cannot draw a veid: %s\n", strerror(status));
+ }
+ veid = (veid >> 2) | (1u << 30);
+
+ /* The ve.veid must match between master / slave */
+ snprintf(path, sizeof(path), "%d/ve.veid", ctid);
+ snprintf(val, sizeof(val), "%u", veid);
+ 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));
+ }
+
+ status = open_master(ctl, veid);
+ if (status < 0) {
+ status = errno;
+ destroy_ve(cg, ctid);
+ /* Absent or denied is the host's doing, anything else the kernel's */
+ if (status == ENOTTY || status == ENODEV || status == ENOENT ||
+ status == EPERM || status == EACCES)
+ ksft_exit_skip("no vtty master: %s\n", strerror(status));
+ ksft_exit_fail_msg("cannot open the 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 %u, pair %d, %ld seconds\n", veid, 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, veid);
+
+ 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);
+
+ close_namespace_fds(&ns);
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ /* The last close frees its tty from a work item, let that run first */
+ usleep(100000);
+ status = complaints + kmsg_check(kmsg);
+
+ /* 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%]
* [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
2026-10-08 3:52 6% ` [Devel] [PATCH hci-8.1 v8 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
1 sibling, 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%]
* 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%]
* 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 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 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%]
* [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 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%]
* 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%]
* [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 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%]
* 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: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 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%]
* [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%]
* [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 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%]
* 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
2026-10-11 22:45 0% ` Vladimir Riabchun
0 siblings, 1 reply; 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%]
* 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
2026-10-11 22:38 0% ` Vladimir Riabchun
0 siblings, 1 reply; 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 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
2026-10-11 21:35 0% ` Vladimir Riabchun
0 siblings, 1 reply; 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 2/6] ve/printk: Report potentially leaking usage of ve_printk functions
@ 2026-10-06 21:59 3% ` no-reply
2026-10-11 21:18 0% ` Vladimir Riabchun
0 siblings, 1 reply; 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 1/6] ve/printk: Add sanity checks in ve_printk functions
@ 2026-10-06 21:58 4% ` no-reply
2026-10-11 20:58 0% ` Vladimir Riabchun
0 siblings, 1 reply; 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%]
* [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%]
* [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%]
* 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 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 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 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: [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%]
* 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 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 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 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 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 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 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%]
* [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%]
* [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 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 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 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%]
* Re: [PATCH QEMU HCI-8.0 v7 12/14] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
@ 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%]
* 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%]
Results 1-200 of ~800 next (older) | reverse | sort options + mbox downloads above
-- links below jump to the message on this page --
2026-10-06 15:35 [PATCH QEMU HCI-8.0 v7 00/14] usb-host: support CPR migration Andrey Drobyshev
2026-10-06 15:36 ` [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 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-11 20:58 0% ` Vladimir Riabchun
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-11 21:18 0% ` Vladimir Riabchun
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-11 21:35 0% ` Vladimir Riabchun
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-11 22:38 0% ` Vladimir Riabchun
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-11 22:45 0% ` Vladimir Riabchun
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
2026-10-08 3:52 6% ` [Devel] [PATCH hci-8.1 v8 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-10-08 4:03 3% ` no-reply
2026-10-08 7:38 [Devel] [PATCH hci-8.1 v2 0/6] truncate unused tail of images on suspend Alexander Ivanov
2026-10-08 7:38 ` [Devel] [PATCH hci-8.1 v2 2/6] dm-qcow2: truncate unused tail of image " Alexander Ivanov
2026-10-08 7:44 3% ` no-reply
2026-10-08 7:38 ` [Devel] [PATCH hci-8.1 v2 3/6] dm-ploop: set delta is_raw before applying its mappings Alexander Ivanov
2026-10-08 7:46 3% ` no-reply
2026-10-08 7:38 ` [Devel] [PATCH hci-8.1 v2 5/6] dm-ploop: reload metadata on resume Alexander Ivanov
2026-10-08 7:54 3% ` no-reply
2026-10-08 7:38 ` [Devel] [PATCH hci-8.1 v2 6/6] dm-ploop: truncate unused tail of top delta on suspend Alexander Ivanov
2026-10-08 7:52 3% ` no-reply
2026-10-08 14:03 [Devel] [PATCH hci-8.1 v3 0/5] " Alexander Ivanov
2026-10-08 14:03 ` [Devel] [PATCH hci-8.1 v3 2/5] dm-ploop: set delta is_raw before applying its mappings Alexander Ivanov
2026-10-08 14:09 3% ` no-reply
2026-10-08 17:01 [Devel] [PATCH hci-8.1 v4 0/3] dm-qcow2: truncate unused tail of image on suspend Alexander Ivanov
2026-10-08 17:02 ` [Devel] [PATCH hci-8.1 v4 3/3] " Alexander Ivanov
2026-10-08 17:08 3% ` no-reply
2026-10-10 12:42 0% ` Alexander Ivanov
2026-10-09 13:58 [Devel] [PATCH hci-8.1 0/4] timens: Remove dependency on the vDSO Eva Kurchatova
2026-10-09 13:58 ` [Devel] [PATCH hci-8.1 1/4] ms/vdso: Drop Kconfig GENERIC_VDSO_TIME_NS Eva Kurchatova
2026-10-09 14:04 4% ` no-reply
2026-10-09 13:58 2% ` [Devel] [PATCH hci-8.1 2/4] ms/vdso/timens: Move functions to new file Eva Kurchatova
2026-10-09 13:58 ` [Devel] [PATCH hci-8.1 3/4] ms/timens: Remove dependency on the vDSO Eva Kurchatova
2026-10-09 14:06 3% ` no-reply
2026-10-09 13:58 ` [Devel] [PATCH hci-8.1 4/4] timens: freeze the offsets on the first join without the vDSO too Eva Kurchatova
2026-10-09 14:06 3% ` no-reply
2026-10-09 14:00 [Devel] [PATCH RFC hci-8.1 0/4] mm: keep DMA-pinned MADV_DOEXEC memory working across exec Andrey Drobyshev
2026-10-09 14:00 ` [Devel] [PATCH RFC PATCH hci-8.1 1/4] mm: support moving anonymous page tables between address spaces Andrey Drobyshev
2026-10-09 15:02 3% ` no-reply
2026-10-09 14:00 ` [Devel] [PATCH RFC PATCH hci-8.1 2/4] exec: protect preserved memory against surviving shared-mm tasks Andrey Drobyshev
2026-10-09 14:57 3% ` no-reply
2026-10-09 14:00 ` [Devel] [PATCH RFC PATCH hci-8.1 3/4] mm: move MADV_DOEXEC memory instead of copying it Andrey Drobyshev
2026-10-09 15:08 4% ` no-reply
2026-10-09 14:00 4% ` [Devel] [PATCH RFC PATCH hci-8.1 4/4] selftests/mm: test memory preservation across exec Andrey Drobyshev
2026-10-09 15:07 3% ` no-reply
2026-10-09 14:04 [RFC HCI-8.1 PATCH 0/5] qemu: allow qemu-update with VFIO hostdevs on shared memfd RAM #VSTOR-103860 Andrey Drobyshev
2026-10-09 14:04 10% ` [RFC HCI-8.1 PATCH 1/5] qemu update: allow qemu-update for VMs with PCI and USB hostdevs #VSTOR-103860 #VSTOR-137800 Andrey Drobyshev
2026-10-09 14:04 7% ` [RFC HCI-8.1 PATCH 4/5] qemu: share auxiliary RAM of VMs with DMA-pinning devices #VSTOR-103860 Andrey Drobyshev
2026-10-09 14:04 8% ` [RFC HCI-8.1 PATCH 5/5] qemu update: refuse PCI hostdevs on non-shared guest RAM #VSTOR-103860 Andrey Drobyshev
2026-10-09 14:23 [Devel] [PATCH hci-8.1] dm-ploop: fix u32 overflow of the delta BAT byte size Alexander Ivanov
2026-10-09 14:28 3% ` no-reply
2026-10-09 14:32 [Devel] [PATCH hci-8.1] mm: allow MADV_DOEXEC on shared PFN mappings of device memory Andrey Drobyshev
2026-10-09 14:39 4% ` no-reply
2026-10-09 14:55 [PATCH QEMU HCI-8.1 00/21] vfio: support qemu-update with passed through GPU's and vGPU's #VSTOR-103860 Andrey Drobyshev
2026-10-09 14:56 13% ` [PATCH QEMU HCI-8.1 01/21] migration: Plug memory leaks after migrate_set_error() Andrey Drobyshev
2026-10-09 15:09 0% ` no-reply
2026-10-09 14:56 5% ` [PATCH QEMU HCI-8.1 05/21] vfio/pci: Initialize rom_read_failed in vfio_pci_load_rom() Andrey Drobyshev
2026-10-09 14:56 4% ` [PATCH QEMU HCI-8.1 07/21] q35: Fix migration of SMRAM state Andrey Drobyshev
2026-10-09 15:12 3% ` no-reply
2026-10-09 14:56 12% ` [PATCH QEMU HCI-8.1 09/21] migration: Fix regression of passing error_fatal into vmstate_load_state() Andrey Drobyshev
2026-10-09 15:17 5% ` no-reply
2026-10-09 14:56 20% ` [PATCH QEMU HCI-8.1 10/21] migration: yank: Move register instance earlier Andrey Drobyshev
2026-10-09 15:16 3% ` no-reply
2026-10-09 14:56 19% ` [PATCH QEMU HCI-8.1 11/21] migration: Free cpr-transfer MigrationAddress along with gsource Andrey Drobyshev
2026-10-09 14:56 28% ` [PATCH QEMU HCI-8.1 12/21] migration: Move CPR HUP watch to cpr-transfer.c Andrey Drobyshev
2026-10-09 15:18 4% ` no-reply
2026-10-09 14:56 23% ` [PATCH QEMU HCI-8.1 13/21] migration: Fix crash on second migration when cancel early Andrey Drobyshev
2026-10-09 15:20 4% ` no-reply
2026-10-09 14:56 22% ` [PATCH QEMU HCI-8.1 14/21] kvm: tolerate ioctls after kvm_close() Andrey Drobyshev
2026-10-09 15:22 7% ` no-reply
2026-10-09 14:56 ` [PATCH QEMU HCI-8.1 16/21] vfio/migration: don't migrate device state during CPR Andrey Drobyshev
2026-10-09 15:29 3% ` no-reply
2026-10-09 14:56 ` [PATCH QEMU HCI-8.1 17/21] vfio/listener: skip dirty page tracking " Andrey Drobyshev
2026-10-09 15:27 4% ` no-reply
2026-10-09 14:56 ` [PATCH QEMU HCI-8.1 18/21] VZ: physmem: align fd-backed RAM like anonymous RAM #VSTOR-103860 Andrey Drobyshev
2026-10-09 15:31 3% ` no-reply
2026-10-09 14:56 6% ` [PATCH QEMU HCI-8.1 19/21] VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved Andrey Drobyshev
2026-10-09 15:31 3% ` no-reply
2026-10-09 14:56 9% ` [PATCH QEMU HCI-8.1 20/21] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860 Andrey Drobyshev
2026-10-09 15:36 5% ` no-reply
2026-10-09 14:56 ` [PATCH QEMU HCI-8.1 21/21] VZ: migration/cpr-exec: allow cpr-exec with VFIO devices #VSTOR-103860 Andrey Drobyshev
2026-10-09 15:34 4% ` no-reply
2026-10-11 22:52 [Devel] [PATCH hci-8.1 v3 0/6] ve/printk: Fix logs leakage via ve_printk called from IRQ context Vladimir Riabchun
2026-10-11 22:52 ` [Devel] [PATCH hci-8.1 v3 2/6] ve/printk: Report potentially leaking usage of ve_printk functions Vladimir Riabchun
2026-10-11 22:59 3% ` no-reply
2026-10-11 22:52 15% ` [Devel] [PATCH hci-8.1 v3 3/6] ve/printk: Specify ve in printks Vladimir Riabchun
2026-10-11 23:02 3% ` no-reply
2026-10-11 22:52 4% ` [Devel] [PATCH hci-8.1 v3 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
2026-10-11 23:03 3% ` no-reply
2026-10-11 22:52 8% ` [Devel] [PATCH hci-8.1 v3 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
2026-10-11 22:52 9% ` [Devel] [PATCH hci-8.1 v3 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
2026-10-12 7:59 0% ` Vasileios Almpanis
2026-10-12 10:20 [Devel] [PATCH vz10] fs: let the protection flags of a mount be locked from userspace Mirian Shilakadze
2026-10-12 10:28 3% ` no-reply
2026-10-12 13:26 [Devel] [PATCH hci-8.1 v4 00/11] dm-ploop: give back unused tail of top delta on suspend Alexander Ivanov
2026-10-12 13:26 ` [Devel] [PATCH hci-8.1 v4 01/11] dm-ploop: fix u32 overflow of the delta BAT byte size Alexander Ivanov
2026-10-12 13:33 3% ` no-reply
2026-10-12 13:26 ` [Devel] [PATCH hci-8.1 v4 09/11] dm-ploop: fix use-after-free on the last inflight_md decrement Alexander Ivanov
2026-10-12 13:40 3% ` no-reply
2026-10-12 13:26 ` [Devel] [PATCH hci-8.1 v4 10/11] dm-ploop: reload metadata on resume Alexander Ivanov
2026-10-12 13:44 3% ` no-reply
2026-10-12 13:26 ` [Devel] [PATCH hci-8.1 v4 11/11] dm-ploop: truncate unused tail of top delta on suspend Alexander Ivanov
2026-10-12 13:47 3% ` no-reply
2026-10-13 7:55 [Devel] [PATCH hci-8.1 v5 00/11] dm-ploop: give back " Alexander Ivanov
2026-10-13 7:55 ` [Devel] [PATCH hci-8.1 v5 01/11] dm-ploop: fix u32 overflow of the delta BAT byte size Alexander Ivanov
2026-10-13 8:02 3% ` no-reply
2026-10-13 7:55 ` [Devel] [PATCH hci-8.1 v5 10/11] dm-ploop: reload metadata on resume Alexander Ivanov
2026-10-13 8:16 2% ` no-reply
2026-10-13 7:55 ` [Devel] [PATCH hci-8.1 v5 11/11] dm-ploop: truncate unused tail of top delta on suspend Alexander Ivanov
2026-10-13 8:20 3% ` no-reply
2026-10-13 11:15 [Devel] [PATCH hci-8.1 v6 00/12] dm-ploop: give back " Alexander Ivanov
2026-10-13 11:15 ` [Devel] [PATCH hci-8.1 v6 01/12] dm-ploop: fix u32 overflow of the delta BAT byte size Alexander Ivanov
2026-10-13 11:23 3% ` no-reply
2026-10-13 11:16 ` [Devel] [PATCH hci-8.1 v6 11/12] dm-ploop: reload metadata on resume Alexander Ivanov
2026-10-13 11:35 3% ` no-reply
2026-10-13 12:46 0% ` Alexander Ivanov
2026-10-13 11:16 ` [Devel] [PATCH hci-8.1 v6 12/12] dm-ploop: truncate unused tail of top delta on suspend Alexander Ivanov
2026-10-13 11:38 3% ` no-reply
2026-10-13 12:53 0% ` Alexander Ivanov
2026-10-13 15:28 [PATCH QEMU HCI-8.1 v2 00/24] vfio: support qemu-update with passed through GPU's and vGPU's #VSTOR-103860 Andrey Drobyshev
2026-10-13 15:28 13% ` [PATCH QEMU HCI-8.1 v2 01/24] migration: Plug memory leaks after migrate_set_error() Andrey Drobyshev
2026-10-13 15:30 0% ` no-reply
2026-10-13 15:28 5% ` [PATCH QEMU HCI-8.1 v2 05/24] vfio/pci: Initialize rom_read_failed in vfio_pci_load_rom() Andrey Drobyshev
2026-10-13 15:28 4% ` [PATCH QEMU HCI-8.1 v2 07/24] q35: Fix migration of SMRAM state Andrey Drobyshev
2026-10-13 15:35 3% ` no-reply
2026-10-13 15:28 12% ` [PATCH QEMU HCI-8.1 v2 09/24] migration: Fix regression of passing error_fatal into vmstate_load_state() Andrey Drobyshev
2026-10-13 15:36 5% ` no-reply
2026-10-13 15:28 11% ` [PATCH QEMU HCI-8.1 v2 10/24] migration: vmstate_save_state_v: fix double error_setg Andrey Drobyshev
2026-10-13 15:38 3% ` no-reply
2026-10-13 15:28 21% ` [PATCH QEMU HCI-8.1 v2 11/24] migration: yank: Move register instance earlier Andrey Drobyshev
2026-10-13 15:41 3% ` no-reply
2026-10-13 15:28 19% ` [PATCH QEMU HCI-8.1 v2 12/24] migration: Free cpr-transfer MigrationAddress along with gsource Andrey Drobyshev
2026-10-13 15:28 28% ` [PATCH QEMU HCI-8.1 v2 13/24] migration: Move CPR HUP watch to cpr-transfer.c Andrey Drobyshev
2026-10-13 15:28 23% ` [PATCH QEMU HCI-8.1 v2 14/24] migration: Fix crash on second migration when cancel early Andrey Drobyshev
2026-10-13 15:45 6% ` no-reply
2026-10-13 15:28 23% ` [PATCH QEMU HCI-8.1 v2 15/24] kvm: tolerate ioctls after kvm_close() Andrey Drobyshev
2026-10-13 15:44 7% ` no-reply
2026-10-13 15:28 ` [PATCH QEMU HCI-8.1 v2 18/24] vfio/migration: don't migrate device state during CPR Andrey Drobyshev
2026-10-13 15:53 5% ` no-reply
2026-10-13 15:28 ` [PATCH QEMU HCI-8.1 v2 19/24] vfio/listener: skip dirty page tracking " Andrey Drobyshev
2026-10-13 15:50 3% ` no-reply
2026-10-13 15:28 6% ` [PATCH QEMU HCI-8.1 v2 21/24] VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved Andrey Drobyshev
2026-10-13 15:55 6% ` no-reply
2026-10-13 15:28 4% ` [PATCH QEMU HCI-8.1 v2 22/24] VZ: migration/cpr-exec: take the vhost-net devices back if exec fails Andrey Drobyshev
2026-10-13 15:55 5% ` no-reply
2026-10-13 15:28 8% ` [PATCH QEMU HCI-8.1 v2 23/24] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860 Andrey Drobyshev
2026-10-13 16:00 5% ` no-reply
2026-10-14 11:16 [Devel] [PATCH hci-8.1 0/4] sched: restore Container iowait accounting Vasileios Almpanis
2026-10-14 11:16 ` [Devel] [PATCH hci-8.1 3/4] sched: account sleep and block time of a task group entity Vasileios Almpanis
2026-10-14 11:28 6% ` no-reply
2026-10-14 11:36 0% ` Vasileios Almpanis
2026-10-14 17:11 [Devel] [PATCH hci-8.1 0/8] mm: keep DMA-pinned MADV_DOEXEC memory working across exec Konstantin Khorenko
2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 6/8] oracle/mm: zap the preserved ranges of the old mm after copying them Konstantin Khorenko
2026-10-14 17:24 3% ` no-reply
2026-10-14 17:11 ` [Devel] [PATCH hci-8.1 7/8] exec: protect preserved memory against surviving shared-mm tasks Konstantin Khorenko
2026-10-14 17:24 3% ` no-reply
2026-10-14 17:11 4% ` [Devel] [PATCH hci-8.1 8/8] selftests/mm: test memory preservation across exec Konstantin Khorenko
2026-10-14 17:33 3% ` no-reply
2026-10-14 17:12 [Devel] [PATCH hci-8.1 0/7] mm, exec: fix the MADV_DOEXEC code we carry from Oracle Konstantin Khorenko
2026-10-14 17:12 2% ` [Devel] [PATCH hci-8.1 6/7] selftests/mm: check that a setuid exec refuses preserved memory Konstantin Khorenko
2026-10-14 17:12 2% ` [Devel] [PATCH hci-8.1 7/7] selftests/mm: check MADV_DOEXEC on /dev/zero and exec permission Konstantin Khorenko
2026-10-14 17:13 [Devel] [PATCH hci-8.1 0/2] mm: allow MADV_DOEXEC on VFIO BAR mappings Konstantin Khorenko
2026-10-14 17:14 5% ` [Devel] [PATCH hci-8.1 2/2] selftests/mm: check MADV_DOEXEC on VFIO BARs and other PFN mappings Konstantin Khorenko
2026-10-14 17:14 [Devel] [PATCH hci-8.1 0/7] selftests/mm: broader MADV_DOEXEC coverage Konstantin Khorenko
2026-10-14 17:14 10% ` [Devel] [PATCH hci-8.1 1/7] selftests/mm: cover the MADV_DOEXEC interface and VMA attributes Konstantin Khorenko
2026-10-14 17:15 4% ` [Devel] [PATCH hci-8.1 2/7] selftests/mm: cover the MADV_DOEXEC opt-in note and the new stack Konstantin Khorenko
2026-10-14 17:15 7% ` [Devel] [PATCH hci-8.1 7/7] selftests/mm: say why a doexec case fails Konstantin Khorenko
2026-10-16 14:34 [Devel] [PATCH hci-8.1] block/blk-cbt: account a write request also when dm starts it Pavel Tikhomirov
2026-10-16 14:41 3% ` no-reply
2026-10-16 15:25 [PATCH QEMU HCI-8.1 v3 00/28] vfio: support qemu-update with passed through GPU's and vGPU's #VSTOR-103860 Andrey Drobyshev
2026-10-16 15:25 6% ` [PATCH QEMU HCI-8.1 v3 01/28] VZ: migration/cpr-exec: fix Error leak in preserve_ram() Andrey Drobyshev
2026-10-16 15:26 6% ` [PATCH QEMU HCI-8.1 v3 02/28] migration/cpr-exec: restore RAM write access if exec fails Andrey Drobyshev
2026-10-16 15:26 13% ` [PATCH QEMU HCI-8.1 v3 04/28] migration: Plug memory leaks after migrate_set_error() Andrey Drobyshev
2026-10-16 15:29 0% ` no-reply
2026-10-16 15:26 5% ` [PATCH QEMU HCI-8.1 v3 08/28] vfio/pci: Initialize rom_read_failed in vfio_pci_load_rom() Andrey Drobyshev
2026-10-16 15:26 4% ` [PATCH QEMU HCI-8.1 v3 10/28] q35: Fix migration of SMRAM state Andrey Drobyshev
2026-10-16 15:33 0% ` no-reply
2026-10-16 15:26 12% ` [PATCH QEMU HCI-8.1 v3 12/28] migration: Fix regression of passing error_fatal into vmstate_load_state() Andrey Drobyshev
2026-10-16 15:35 3% ` no-reply
2026-10-16 15:26 11% ` [PATCH QEMU HCI-8.1 v3 13/28] migration: vmstate_save_state_v: fix double error_setg Andrey Drobyshev
2026-10-16 15:38 6% ` no-reply
2026-10-16 15:26 22% ` [PATCH QEMU HCI-8.1 v3 14/28] migration: yank: Move register instance earlier Andrey Drobyshev
2026-10-16 15:41 5% ` no-reply
2026-10-16 15:26 19% ` [PATCH QEMU HCI-8.1 v3 15/28] migration: Free cpr-transfer MigrationAddress along with gsource Andrey Drobyshev
2026-10-16 15:26 28% ` [PATCH QEMU HCI-8.1 v3 16/28] migration: Move CPR HUP watch to cpr-transfer.c Andrey Drobyshev
2026-10-16 15:26 23% ` [PATCH QEMU HCI-8.1 v3 17/28] migration: Fix crash on second migration when cancel early Andrey Drobyshev
2026-10-16 15:44 5% ` no-reply
2026-10-16 15:26 23% ` [PATCH QEMU HCI-8.1 v3 18/28] kvm: tolerate ioctls after kvm_close() Andrey Drobyshev
2026-10-16 15:44 7% ` no-reply
2026-10-16 15:26 ` [PATCH QEMU HCI-8.1 v3 20/28] migration/cpr: add cpr_is_in_progress() Andrey Drobyshev
2026-10-16 15:46 3% ` no-reply
2026-10-16 15:26 ` [PATCH QEMU HCI-8.1 v3 21/28] vfio/migration: don't migrate device state during CPR Andrey Drobyshev
2026-10-16 15:55 3% ` no-reply
2026-10-16 15:26 ` [PATCH QEMU HCI-8.1 v3 22/28] vfio/listener: skip dirty page tracking " Andrey Drobyshev
2026-10-16 15:50 3% ` no-reply
2026-10-16 15:26 6% ` [PATCH QEMU HCI-8.1 v3 24/28] VZ: migration/cpr-exec: abort cpr-exec if memory can't be preserved Andrey Drobyshev
2026-10-16 15:26 10% ` [PATCH QEMU HCI-8.1 v3 25/28] VZ: migration/cpr-exec: take the vhost-net devices back if exec fails Andrey Drobyshev
2026-10-16 15:58 3% ` no-reply
2026-10-16 15:26 16% ` [PATCH QEMU HCI-8.1 v3 26/28] VZ: migration/cpr-exec: don't exec if a vhost-net device can't be released Andrey Drobyshev
2026-10-16 15:26 6% ` [PATCH QEMU HCI-8.1 v3 27/28] VZ: vfio/region: preserve BAR mappings across cpr-exec #VSTOR-103860 Andrey Drobyshev
2026-10-16 16:02 4% ` no-reply
2026-10-16 21:08 7% [Devel] [PATCH hci-8.1] ms/ipv4: validate checksum_start before completing checksum Konstantin Khorenko
2026-10-16 21:13 0% ` no-reply
2026-10-16 21:33 [Devel] [PATCH hci-8.1 0/2] ms/checkpatch: accept the Assisted-by: LLM tag Konstantin Khorenko
2026-10-16 21:33 ` [Devel] [PATCH hci-8.1 1/2] ms/checkpatch: add support for Assisted-by tag Konstantin Khorenko
2026-10-16 21:37 4% ` no-reply
2026-10-16 21:33 3% ` [Devel] [PATCH hci-8.1 2/2] ms/checkpatch.pl: adapt to new Assisted-by: format Konstantin Khorenko
2026-10-16 21:35 [Devel] [PATCH hci-8.1 v2 0/2] mm: allow MADV_DOEXEC on VFIO BAR mappings Konstantin Khorenko
2026-10-16 21:35 5% ` [Devel] [PATCH hci-8.1 v2 2/2] selftests/mm: check MADV_DOEXEC on VFIO BARs and other PFN mappings Konstantin Khorenko
2026-10-19 0:05 [Devel] [PATCH hci-8.1 v2 0/6] timens: Remove dependency on the vDSO Eva Kurchatova
2026-10-19 0:05 ` [Devel] [PATCH hci-8.1 v2 1/6] ms/vdso: Drop Kconfig GENERIC_VDSO_TIME_NS Eva Kurchatova
2026-10-19 0:12 4% ` no-reply
2026-10-19 0:05 3% ` [Devel] [PATCH hci-8.1 v2 2/6] ms/vdso/timens: Move functions to new file Eva Kurchatova
2026-10-19 0:12 4% ` no-reply
2026-10-19 0:05 ` [Devel] [PATCH hci-8.1 v2 4/6] timens: freeze the offsets on the first join without the vDSO too Eva Kurchatova
2026-10-19 0:14 3% ` no-reply
2026-10-19 18:26 [Devel] [PATCH hci-8.1 0/2] ms/kbuild: GCOV follow-ups to the prefer-atomic fix Konstantin Khorenko
2026-10-19 18:26 10% ` [Devel] [PATCH hci-8.1 2/2] ms/kbuild: move GCOV flags to scripts/Makefile.gcov Konstantin Khorenko
2026-10-20 4:12 [Devel] [PATCH hci-8.1] selftests/bpf: run the xsk veth tests in a network namespace of their own Eva Kurchatova
2026-10-20 4:18 3% ` no-reply
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.