* Re: [PATCH QEMU HCI-8.0 v7 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800
2026-10-06 15:36 29% ` [PATCH QEMU HCI-8.0 v7 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:52 6% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:52 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: den, andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Medium] usb-host: cpr_handed_off set before migration outcome is known, skips cleanup on failure
- [Medium] usb-host: cpr_is_incoming() in realize() stops global event reaping with no restart
- [Medium] usb-host: unrealize() clears handoff flag without restarting the global event reaping
- [Low] usb-host: unrealize() forces full reset even if the target may already own the device
- [Low] usb-host: commit message does not match where reaping is stopped and restarted
--
commit d5825316f822699756346d45e052e4ba605a47fe
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
usb-host: hand off the device across cpr-transfer #VSTOR-137800
This patch hands the usbfs FD and URB reaping off from the source QEMU to
the target QEMU during cpr-transfer. The source stops reaping at quiesce
time and skips device reset, interface release and driver rebind on exit.
The target stops reaping in realize() and restarts it in post_load().
> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c
[ ... ]
> +static void usb_host_stop_events(void)
> +{
> + const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
> +
> + libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
> + if (poll) {
> + for (int i = 0; poll[i] != NULL; i++) {
> + usb_host_del_fd(poll[i]->fd, ctx);
> + }
> + free(poll);
> + }
> +}
[ ... ]
> static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
> {
> if (usb_host_cpr_drain_xfers(s) < 0) {
> return -1;
> }
> + if (mode == MIG_MODE_CPR_TRANSFER) {
> + usb_host_stop_events();
> + s->cpr_handed_off = true;
> + }
> return 0;
> }
[Severity: Medium]
Is it safe to set cpr_handed_off here, before the outcome of the migration
is known?
The flag is cleared only in usb_host_cpr_reissue(), which runs on a running
transition, and in usb_host_unrealize().
Consider the case where usb_host_pre_save() calls usb_host_cpr_quiesce() as
the fallback for an already-paused VM, and the migration then fails. Nothing
resumes the VM, so cpr_handed_off stays true.
usb_host_exit_notifier() then skips the following on every later quit and
only calls libusb_close():
usb_host_abort_xfers(s);
usb_host_release_interfaces(s);
libusb_reset_device(s->dh);
usb_host_attach_kernel(s);
Can this leave the device without a reset and without the host kernel
driver rebound after a failed or cancelled cpr-transfer?
The commit message says the skip happens only "On success", but the exit
notifier cannot tell success from failure.
[ ... ]
> @@ -1371,16 +1402,19 @@ static int usb_host_close(USBHostDevice *s)
>
> trace_usb_host_close(s->bus_num, s->addr);
>
> - usb_host_abort_xfers(s);
> - usb_host_iso_free_all(s);
> + if (!s->cpr_handed_off) {
> + usb_host_abort_xfers(s);
> + usb_host_iso_free_all(s);
>
> - if (udev->attached) {
> - usb_device_detach(udev);
> + if (udev->attached) {
> + usb_device_detach(udev);
> + }
> +
> + usb_host_release_interfaces(s);
> + libusb_reset_device(s->dh);
> + usb_host_attach_kernel(s);
> }
>
> - usb_host_release_interfaces(s);
> - libusb_reset_device(s->dh);
> - usb_host_attach_kernel(s);
> libusb_close(s->dh);
> s->dh = NULL;
> s->dev = NULL;
[Severity: Medium]
The cpr_handed_off gate here also skips the QEMU-local steps
usb_host_abort_xfers(), usb_host_iso_free_all() and usb_device_detach(),
not only the reset, release and rebind that must not run under the target.
If usb_host_close() is reached while the flag is stale, for example via
usb_host_nodev_bh() or usb_host_auto_check(), s->dh and s->dev are still set
to NULL. Can that leave a guest-attached device with no libusb handle?
Should the gate cover only the device-affecting calls (release, reset,
rebind)?
[ ... ]
> @@ -1487,6 +1528,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
> + if (cpr_is_incoming()) {
> + /*
> + * The source still owns the shared fd until it hands off. Don't
> + * reap URBs on it yet, or we would steal the source's in-flight
> + * completions; .post_load() restarts events once we take over.
> + */
> + usb_host_stop_events();
> + }
[Severity: Medium]
Can this stop event reaping permanently for a device that is not migrated?
cpr_is_incoming() is "incoming_mode != MIG_MODE_NONE", and incoming_mode
does not appear to be reset after the migration completes.
If a usb-host device with hostdevice= is hotplugged on the target after the
CPR migration, usb_host_realize() takes this branch and calls
usb_host_stop_events(), which clears the pollfd notifiers and fd handlers on
the shared libusb ctx.
usb_host_post_load() is the only place that restarts events, and a hotplugged
device has no vmstate section to load. Would URB completions then stop being
reaped for that device and for every other usb-host device?
[Severity: Low]
This isn't a functional problem, but the commit message and the code differ
in a few places:
The target doesn't reap until it takes over: it stops events right
after opening the FD in .realize() and restarts in .post_load().
The condition used is cpr_is_incoming(), which is true for any incoming CPR
mode, including cpr-exec, where the fd is not shared.
The source stops reaping in .pre_save() and, since it stays alive,
restarts reaping from the vm-state handler's resume branch on failure.
The stop happens in usb_host_cpr_quiesce(), which usb_host_cpr_vm_state()
calls on the FINISH_MIGRATE transition. usb_host_pre_save() reaches it only
as a fallback for an already-paused VM. Could the commit message be updated
to describe this?
[ ... ]
> @@ -1550,6 +1599,12 @@ static void usb_host_unrealize(USBDevice *udev)
> + /*
> + * An explicit device_del means we still own the device (a successful
> + * cpr-transfer handoff exits the process instead), so clear the handoff
> + * flag to force a full reset/release in usb_host_close().
> + */
> + s->cpr_handed_off = false;
> usb_host_close(s);
> migrate_del_blocker(&s->cpr_blocker);
> if (s->cpr_vmse) {
> qemu_del_vm_change_state_handler(s->cpr_vmse);
> }
[Severity: Medium]
Does this path leave event reaping stopped for the other usb-host devices?
Suppose usb_host_cpr_quiesce() has called usb_host_stop_events() on the whole
ctx, and the migration then fails or is cancelled while the VM is paused. No
running transition occurs, so usb_host_cpr_reissue() never calls
usb_host_start_events().
A later device_del reaches usb_host_unrealize(), which sets
cpr_handed_off = false and removes cpr_vmse without calling
usb_host_start_events(). usb_host_init() returns early once ctx exists, so
nothing else restarts reaping.
Can transfers on the other usb-host devices (hostdevice=, autoscan or
bus/addr) hang here, since the stop applies to the shared context? The
commit message says only one hostdevice= device is supported, but nothing in
the code appears to enforce that.
[Severity: Low]
Can this unconditional clearing of cpr_handed_off cause a reset of a device
the target already owns?
The comment assumes that unrealize means the source still owns the device.
During the handoff window the source has cpr_handed_off = true and the target
may already be reaping. If a device_del or guest eject reaches
usb_host_unrealize() then, usb_host_close() runs usb_host_abort_xfers(),
usb_host_release_interfaces(), libusb_reset_device() and
usb_host_attach_kernel() on the shared fd.
Is it intended that this disrupts the target's session?
[ ... ]
> @@ -2033,6 +2088,7 @@ static int usb_host_post_load(void *opaque, int version_id)
> if (cpr_is_incoming()) {
> + usb_host_start_events();
> return 0;
> }
This is the only restart point on the target, and it runs only for devices
whose vmstate section is loaded, as noted in the usb_host_realize() comment
above.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=10
^ permalink raw reply [relevance 6%]
* Re: [PATCH QEMU HCI-8.0 v7 11/14] usb: migrate the interface altsetting #VSTOR-137800
@ 2026-10-06 15:51 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:51 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] usb: altsetting subsection in generic vmstate_usb_device has no consumer for emulated devices
--
commit c0dc143b080568d7b0883600b0776ce496b368ca
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
usb: migrate the interface altsetting #VSTOR-137800
This patch adds a vmstate subsection to the shared USBDevice vmstate that
carries the altsetting[] array. The subsection is emitted only in the CPR
migration modes and only when some altsetting[] entry is non-zero.
> diff --git a/hw/usb/bus.c b/hw/usb/bus.c
> --- a/hw/usb/bus.c
> +++ b/hw/usb/bus.c
[ ... ]
> +static bool usb_altsetting_needed(void *opaque)
> +{
> + USBDevice *dev = opaque;
> + int i;
> +
> + /*
> + * Only the CPR target consumes this, to rebuild its endpoints; other
> + * migrations reopen the device and reconstruct the altsetting.
> + */
> + if (!cpr_incoming_needed(NULL)) {
> + return false;
> + }
> + for (i = 0; i < USB_MAX_INTERFACES; i++) {
> + if (dev->altsetting[i]) {
> + return true;
> + }
> + }
> + return false;
> +}
[Severity: Low]
Is this gate too broad? usb_altsetting_needed() checks only
cpr_incoming_needed() and whether any dev->altsetting[i] is non-zero. It
does not check the device type, but the stated reason (the CPR target
rebuilds its endpoints) seems to hold only for usb-host.
[ ... ]
> + .subsections = (const VMStateDescription * const []) {
> + &vmstate_usb_device_altsetting,
> + NULL
> }
The subsection is attached to the shared vmstate_usb_device, so it is
emitted and loaded for every USB device type, including emulated devices.
For an emulated device, usb_desc_set_interface() in hw/usb/desc.c updates
the three together:
old = dev->altsetting[index];
dev->altsetting[index] = value;
dev->ifaces[index] = iface;
usb_desc_ep_init(dev);
After a CPR load, only altsetting[] is restored. ifaces[] and the endpoint
table stay at altsetting 0.
Can GET_INTERFACE then report the guest's non-zero altsetting while the
device model behaves as if altsetting 0 were active? Before this patch the
state was wrong but self-consistent. It is still wrong after the patch, so
emulated devices gain nothing from it.
The consumer in this series is only in usb_host_post_load() in
hw/usb/host-libusb.c, which calls usb_host_cpr_reclaim_interfaces() and
usb_host_ep_update(). Nothing re-applies ifaces[] or calls
usb_desc_ep_init() for emulated devices.
I did not verify which emulated devices have multiple altsettings or can be
CPR-migrated, so this may be of limited reach. Would it make sense to limit
the subsection to usb-host, or to restore ifaces[] and the endpoints in a
generic post_load?
Separately, the commit message says "The active altsetting of each
interface is host controller state". Is that accurate? altsetting[] is a
field of USBDevice, not of the host controller.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=11
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.0 v7 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800
2026-10-06 15:35 22% ` [PATCH QEMU HCI-8.0 v7 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:47 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:47 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: den, andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] usb-host: cpr_is_incoming() guard drops product string for any open
- [Low] usb-host: only the string read is skipped; other control I/O in open still runs
--
commit e0b84d4b8555ddc11f7b8275a12f5907c2ba320f
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
usb-host: skip product-string read on CPR incoming #VSTOR-137800
This patch skips the libusb product string descriptor read in
usb_host_open() when cpr_is_incoming() is true. It uses the synthetic
"host:bus.addr" name for product_desc instead.
> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> index d90117e4292d..271456abb0a1 100644
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c
[ ... ]
> @@ -1111,7 +1111,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
> udev->speed = speed_map[libusb_speed];
> usb_host_speed_compat(s);
>
> - if (s->ddesc.iProduct) {
> + if (s->ddesc.iProduct && !cpr_is_incoming()) {
[Severity: Low]
Is this guard too broad? cpr_is_incoming() is defined as
"incoming_mode != MIG_MODE_NONE", so it is true for the whole
incoming-migration phase. It is not a per-device check.
usb_host_open() is reached from several paths:
usb_host_realize() (hostfd and bus/addr paths)
usb_host_auto_check() (autoscan)
Any device opened while the flag is set takes the synthetic branch. That
includes autoscan or hotplug matches of devices that are not being handed
over.
product_desc is set once in usb_host_open() and is not refreshed. Can
those devices end up keeping the "host:bus.addr" name after the migration
completes?
[Severity: Low]
Is the product string read the only call in usb_host_open() that can block
during the handover? The neighbouring calls still run unguarded:
usb_host_detach_kernel(s);
...
usb_host_speed_compat(s); /* libusb_get_config_descriptor() */
The fail: path also calls libusb_reset_device().
If synchronous control transfers can block on the CPR target, could the
incoming migration still hang in one of these calls? The commit message
doesn't say what causes the blocking, so it is unclear whether skipping
only the string read is enough.
> libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
> (unsigned char *)udev->product_desc,
> sizeof(udev->product_desc));
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=8
^ permalink raw reply [relevance 5%]
* Re: [PATCH QEMU HCI-8.0 v7 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
2026-10-06 15:35 24% ` [PATCH QEMU HCI-8.0 v7 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:46 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:46 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] usb-host: replay_all queues completed URBs, so aborted CPR re-sends or drops data
--
commit 2947a35eaf543430e9227a58d6c2b6610a15e890
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
This patch records the packets suppressed during the CPR drain on a
cpr_inflight list. If CPR is aborted, the vm-state handler's resume branch
re-issues them on the preserved fd. Control transfers are completed with an
error instead of being re-issued.
> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c
[ ... ]
> @@ -450,6 +453,9 @@ static bool usb_host_req_cpr_replay(USBHostRequest *r,
> return false;
> }
> }
> + if (r->p->state == USB_PACKET_ASYNC) {
> + r->host->cpr_inflight = g_slist_append(r->host->cpr_inflight, r->p);
> + }
> return true;
> }
[Severity: Medium]
When cpr_replay_all is set by the paused-VM usb_host_pre_save() fallback,
the status and actual_length checks above are skipped. Does
usb_host_req_cpr_replay() then queue every reaped URB on cpr_inflight,
including URBs that completed successfully?
The request is then freed without its data being copied into the packet.
[ ... ]
> + for (it = s->cpr_inflight; it; it = it->next) {
> + p = it->data;
> + /*
> + * Replay from the start of the TD. A transfer that already moved some
> + * data is re-run in full, so an OUT fragment the device consumed may be
> + * sent twice. Only the paused-VM pre_save fallback queues such partial
> + * transfers here; the normal drain delivers them as short transfers.
> + */
> + p->actual_length = 0;
> + if (p->ep->nr == 0) {
> + /* control endpoint: can't be reconstructed, so error it out */
> + p->status = USB_RET_IOERROR;
> + usb_generic_async_ctrl_complete(udev, p);
> + } else {
> + usb_host_handle_data(udev, p);
If CPR aborts after that fallback, usb_host_cpr_reissue() re-submits these
fully completed transfers from the start:
- A completed OUT transfer can be sent to the physical device a second
time.
- A completed IN transfer has its data discarded, and a new read is
issued.
- A completed control transfer is failed with USB_RET_IOERROR, even
though the device already executed it.
The commit message and the comment above only mention a partial OUT
fragment being sent twice. Could they, or the queueing in
usb_host_req_cpr_replay(), be changed to cover fully completed transfers
too?
This could matter for non-idempotent streams such as mass storage, serial
or printer traffic.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=7
^ permalink raw reply [relevance 4%]
* Re: [PATCH QEMU HCI-8.0 v7 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800
2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:45 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:45 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: den, andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] usb-host: CPR drain suppresses completed URBs and reports short OUT as full
- [Medium] usb-host: iso drain cancels in-flight URBs once, so resubmitted URBs survive
- [Medium] usb-host: timeout path completes packets mid-iteration then drops live URBs
- [Medium] usb-host: usb_host_req_cpr_replay() not gated on an active CPR drain
--
commit e57108ef005a6c96c6eda4382d78b25d2524c377
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
usb-host: drain in-flight URBs across CPR #VSTOR-137800
This patch registers a vm_change_state handler that cancels and reaps all
pending usb-host URBs when the VM stops for CPR. It also adds a
.pre_save() fallback for a VM that is already paused, and cancels and
reaps isochronous URBs on their own rings. Transfers that moved no data
are suppressed so the target can replay them.
> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c
[ ... ]
> +/*
> + * Classify a reap during the CPR drain (r->p is still set). The paused-VM
> + * fallback (cpr_replay_all) can't deliver completions, so it suppresses every
> + * reap for the target to replay; otherwise one that moved no data is suppressed
> + * and one that moved some is kept as a short transfer. Returns true if
> + * suppressed, so the caller frees the request.
> + */
> +static bool usb_host_req_cpr_replay(USBHostRequest *r,
> + struct libusb_transfer *xfer)
> +{
> + if (!r->host->cpr_replay_all) {
> + if (xfer->status != LIBUSB_TRANSFER_CANCELLED) {
> + return false;
> + }
> + if (xfer->actual_length != 0) {
> + /* deliver as a short transfer */
> + xfer->status = LIBUSB_TRANSFER_COMPLETED;
> + return false;
> + }
> + }
> + return true;
> +}
[Severity: Medium]
Does this function lose data or duplicate side effects across CPR?
In the paused-VM fallback, usb_host_pre_save() sets cpr_replay_all = true.
usb_host_req_cpr_replay() then skips its status check and returns true
for every reap, including LIBUSB_TRANSFER_COMPLETED ones.
The packet is suppressed and replayed by the target. An IN URB that had
already received data loses it, since the device will not resend. A bulk
OUT or control URB that already executed is sent again.
This also seems to contradict the commit message statement "A transfer
that already completed in the kernel is delivered normally".
The final series state still has this behaviour. The later patch
"usb-host: re-issue drained URBs if CPR is aborted" only documents it in a
comment ("an OUT fragment the device consumed may be sent twice") and does
not change it.
On the normal vm_change_state path, a CANCELLED URB with actual_length != 0
is rewritten to COMPLETED:
if (xfer->actual_length != 0) {
/* deliver as a short transfer */
xfer->status = LIBUSB_TRANSFER_COMPLETED;
return false;
}
usb_host_req_complete_data() only trims the packet length for IN:
r->p->status = status_map[xfer->status];
if (r->in && xfer->actual_length) {
usb_packet_copy(r->p, r->buffer, xfer->actual_length);
}
For an OUT packet, p->actual_length was already set to the full length
when the guest data was copied at submit time. Can the guest be told that
the whole OUT TD succeeded when the device received only
xfer->actual_length bytes, with nothing replaying the remainder?
[Severity: Medium]
Separately, is usb_host_req_cpr_replay() meant to apply outside an active
CPR drain?
usb_host_req_complete_ctrl() and usb_host_req_complete_data() now call it
unconditionally for every completion with r->p != NULL. With
cpr_replay_all false, nothing checks cpr_drained or any other drain-active
state.
A transfer that arrives with LIBUSB_TRANSFER_CANCELLED outside a CPR drain
would behave differently:
- actual_length == 0: the request is freed and the packet is never
completed, which could leave it in USB_PACKET_ASYNC.
- actual_length != 0: the status is rewritten to COMPLETED.
Previously such a transfer was mapped by status_map[] to USB_RET_IOERROR.
Guest-initiated cancels clear r->p in usb_host_cancel_packet() and
usb_host_req_abort(), so they are not affected.
I could not confirm that libusb reports CANCELLED for a transfer with a
live packet outside CPR, so this one is speculative. Should the logic be
gated on an active drain?
[ ... ]
> +static int usb_host_cpr_drain_xfers(USBHostDevice *s)
> +{
[ ... ]
> + libusb_handle_events_timeout(ctx, &tv);
> + if (--limit == 0) {
> + /*
> + * The device won't release these URBs. Error the packets out so
> + * the resumed guest retries them, leak the requests (libusb still
> + * owns their transfers) and abort the migration.
> + */
> + QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
> + usb_host_req_complete_err(r, USB_RET_IOERROR);
> + }
> + usb_host_drop_requests(s);
> + return -1;
> + }
> + }
[Severity: Medium]
Can this timeout path leave live, uncancelled URBs behind?
The call chain is:
usb_host_cpr_drain_xfers()
usb_host_req_complete_err()
usb_packet_complete()
usb_process_one()
usb_host_handle_data()
libusb_submit_transfer()
For a non-ep0 packet, usb_packet_complete() pops the next queued packet on
the endpoint and processes it. That can allocate a new request, append it
to s->requests, and submit a new URB while QTAILQ_FOREACH_SAFE is running.
usb_host_drop_requests() then unlinks every request, including the newly
submitted ones, without cancelling them. Would such a URB stay on the
preserved fd with no tracking request, and later complete into a callback
with stale r->p or r->host?
This needs the 100-iteration timeout plus a queued packet behind the failed
one on the same endpoint, which xHCI can have.
[ ... ]
> + QTAILQ_FOREACH(ring, &s->isorings, next) {
> + QTAILQ_FOREACH(xfer, &ring->inflight, next) {
> + libusb_cancel_transfer(xfer->xfer);
> + }
> + }
> +
> + /*
> + * Cap the reap at 2x a full ring:
> + * iso_urb_count URBs x iso_urb_frames packets each
> + */
> + limit = 2 * s->iso_urb_count * s->iso_urb_frames;
> + while (usb_host_iso_inflight(s)) {
> + struct timeval tv = { .tv_usec = 1000 };
> + libusb_handle_events_timeout(ctx, &tv);
> + if (limit-- == 0) {
> + return -1;
> + }
> + }
> + usb_host_iso_free_all(s);
> + return 0;
> +}
[Severity: Medium]
Can iso URBs be resubmitted while this reap loop runs, and then escape
cancellation?
The iso URBs on ring->inflight are cancelled in a single pass, and the
reap loop does not cancel again. For an IN endpoint,
usb_host_req_complete_iso() moves the xfer to ring->copy and calls
usb_wakeup():
QTAILQ_INSERT_TAIL(&xfer->ring->copy, xfer, next);
usb_wakeup(xfer->ring->ep, 0);
usb_wakeup() reaches xhci_wakeup_endpoint(), which calls xhci_kick_ep()
and xhci_kick_epctx() without checking the VM run state. The kick can
reprocess an iso IN packet through usb_host_handle_data() and
usb_host_iso_data_in(), which moves ring->unused entries to inflight with
libusb_submit_transfer().
If that happens, the reap loop either runs out its limit and returns -1,
or it exits with new iso URBs from the old process still submitted on the
preserved fd. The control/bulk loop above re-cancels on every iteration,
but this one does not.
I did not prove that a guest iso TD is always available at that point.
Should the iso phase re-cancel inflight URBs on each iteration as well?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=6
^ permalink raw reply [relevance 4%]
* Re: [PATCH QEMU HCI-8.0 v7 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
2026-10-06 15:35 30% ` [PATCH QEMU HCI-8.0 v7 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:38 0% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:38 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev
> The FD passed into usb_host_open() for a wrapped device is only stored in
> s->hostfd after libusb_wrap_sys_device() succeeds, and the failure path
> doesn't close it. if wrapping fails, or a later step of the open sequence
> fails (e.g. usb_device_attach), the FD is leaked, as usb_host_close() only
> runs for fully opened devices, and the realize error path doesn't clean it
> up either.
>
> Store the FD in s->hostfd before wrapping and close it on the failure
> path.
>
> Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com?part=1
^ permalink raw reply [relevance 0%]
* [PATCH QEMU HCI-8.0 v7 14/14] usb-host: make CPR migration blocker conditional #VSTOR-137800
` (10 preceding siblings ...)
2026-10-06 15:36 10% ` [PATCH QEMU HCI-8.0 v7 12/14] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:36 20% ` Andrey Drobyshev
11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:36 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
A previous commit added an unconditional blocker for CPR migrations.
That is to refuse CPR gracefully instead of crashing and for the previous
commits to remain bisectable.
Now the restore path is complete: the FD is preserved, in-flight URBs
are drained and replayed, the device is handed off across cpr-transfer,
and the endpoints and isochronous streams are reconstructed on the
target. So let's lift the CPR blocker for the devices which satisfy the
preconditions.
Still, the remaining preconditions for CPR to work are:
* We must open host device FD ourselves, so that libusb owns the FD.
This way we're able to preserve the FD during CPR;
* Device ID must be provided as a unique stable key for CPR FD registry.
Use them to guard the blocker addition. Apart from that, prohibit CPR on
WIN32 platform and on hosts with older libusb versions.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 39 +++++++++++++++++++++++++++++++--------
1 file changed, 31 insertions(+), 8 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index cbe1b91c46d..d86657f5eb3 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1563,16 +1563,39 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
usb_host_auto_check(NULL);
}
- error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+#ifdef CONFIG_WIN32
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "WIN32 platform is not supported",
DEVICE(s)->id ?: "(anonymous)");
- if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
- MIG_MODE_CPR_TRANSFER,
- MIG_MODE_CPR_EXEC, -1) < 0) {
- if (s->needs_autoscan) {
- QTAILQ_REMOVE(&hostdevs, s, next);
+#elif LIBUSB_API_VERSION < 0x01000107
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "libusb API version 0x%08x is too old",
+ DEVICE(s)->id ?: "(anonymous)", (unsigned) LIBUSB_API_VERSION);
+#else
+ if (!(s->hostdevice && DEVICE(s)->id)) {
+ /*
+ * CPR needs the host fd owned by libusb (the hostdevice= property)
+ * and a device id to key the CPR fd registry.
+ */
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "hostdevice= and a device id are required",
+ DEVICE(s)->id ?: "(anonymous)");
+ }
+#endif
+
+ if (s->cpr_blocker) {
+ if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1) < 0) {
+ if (s->needs_autoscan) {
+ QTAILQ_REMOVE(&hostdevs, s, next);
+ }
+ usb_host_close(s);
+ return;
}
- usb_host_close(s);
- return;
}
s->exit.notify = usb_host_exit_notifier;
--
2.47.1
^ permalink raw reply [relevance 20%]
* [PATCH QEMU HCI-8.0 v7 12/14] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
` (9 preceding siblings ...)
@ 2026-10-06 15:36 10% ` Andrey Drobyshev
2026-10-06 15:36 20% ` [PATCH QEMU HCI-8.0 v7 14/14] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:36 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
On CPR incoming we skip the usb_host_open() rescan, so libusb only sets
up the endpoints from the device's default configuration. If the guest
had enabled an endpoint by selecting a non-zero altsetting (e.g. an
isochronous streaming endpoint), that endpoint is now missing, and the
guest's transfers to it simply stall.
The altsetting itself is restored by vmload now, so let's call
usb_host_ep_update() on the CPR target to rebuild the endpoints out of
it.
The kernel claims are still there on the preserved FD, but libusb's own
handle knows nothing about them, so a later SET_INTERFACE would fail with
LIBUSB_ERROR_NOT_FOUND. Let's re-claim the interfaces on the target as
well to bring libusb's view back in sync - on the preserved FD this is a
no-op at the usbfs level anyway.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 72 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 387040d6e1a..cbe1b91c46d 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -976,7 +976,7 @@ static void usb_host_ep_update(USBHostDevice *s)
alt = udev->altsetting[intf->bInterfaceNumber];
if (alt != 0) {
- assert(alt < conf->interface[i].num_altsetting);
+ assert(alt >= 0 && alt < conf->interface[i].num_altsetting);
intf = &conf->interface[i].altsetting[alt];
}
@@ -2079,6 +2079,69 @@ static void usb_host_post_load_bh(void *opaque)
usb_host_auto_check(NULL);
}
+/*
+ * Reconfigure the CPR target after vmload: reject a migrated altsetting the
+ * device can't honour, then re-claim the interfaces so libusb's view matches
+ * the preserved fd (which already holds the claims).
+ */
+static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
+{
+ USBDevice *udev = USB_DEVICE(s);
+ struct libusb_config_descriptor *conf;
+ int rc;
+
+ rc = libusb_get_active_config_descriptor(s->dev, &conf);
+ if (rc == LIBUSB_ERROR_NOT_FOUND) {
+ /* device is unconfigured (address state), nothing to reclaim */
+ return 0;
+ }
+ if (rc != 0) {
+ usb_host_libusb_error("libusb_get_active_config_descriptor", rc);
+ return -1;
+ }
+
+ /*
+ * Validate all migrated interface/altsetting numbers before touching the
+ * device, so garbage migration data can't leave it half-claimed.
+ */
+ for (int i = 0; i < conf->bNumInterfaces; i++) {
+ int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
+ int alt;
+
+ if (ifnum >= USB_MAX_INTERFACES) {
+ error_report("usb-host: interface number %d out of range", ifnum);
+ libusb_free_config_descriptor(conf);
+ return -1;
+ }
+ alt = udev->altsetting[ifnum];
+ if (alt < 0 || alt >= conf->interface[i].num_altsetting) {
+ error_report("usb-host: migrated altsetting %d out of range for "
+ "interface %d", alt, ifnum);
+ libusb_free_config_descriptor(conf);
+ return -1;
+ }
+ }
+
+ /* Data is sane: claim each interface on the fd and record it. */
+ for (int i = 0; i < conf->bNumInterfaces; i++) {
+ int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
+
+ rc = libusb_claim_interface(s->dh, ifnum);
+ if (rc != 0) {
+ usb_host_libusb_error("libusb_claim_interface", rc);
+ libusb_free_config_descriptor(conf);
+ return -1;
+ }
+ s->ifs[ifnum].claimed = true;
+ }
+
+ udev->ninterfaces = conf->bNumInterfaces;
+ udev->configuration = conf->bConfigurationValue;
+ libusb_free_config_descriptor(conf);
+
+ return 0;
+}
+
static int usb_host_post_load(void *opaque, int version_id)
{
USBHostDevice *dev = opaque;
@@ -2088,6 +2151,14 @@ static int usb_host_post_load(void *opaque, int version_id)
* rescan in usb_host_post_load_bh() -- those are for cross-host migration.
*/
if (cpr_is_incoming()) {
+ /*
+ * The device stayed open, so rebuild libusb's claims and endpoints,
+ * and start reaping now that the source has handed the fd off.
+ */
+ if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
+ return -1;
+ }
+ usb_host_ep_update(dev);
usb_host_start_events();
return 0;
}
--
2.47.1
^ permalink raw reply [relevance 10%]
* [PATCH QEMU HCI-8.0 v7 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800
` (7 preceding siblings ...)
2026-10-06 15:35 22% ` [PATCH QEMU HCI-8.0 v7 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:36 29% ` Andrey Drobyshev
2026-10-06 15:52 6% ` no-reply
` (2 subsequent siblings)
11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:36 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
Unlike cpr-exec, cpr-transfer keeps the source QEMU alive: it hands the
usbfs FD to the new QEMU over the migration channel and only exits once
the target is up. While the handoff is in flight both processes hold the
same FD, and only one may reap URB events on it - if both do, one reaps
the other's in-flight completions and wedges an isochronous transfer that
cannot be retried. So reaping is handed off along with the FD:
* The target doesn't reap until it takes over: it stops events right
after opening the FD in .realize() and restarts in .post_load().
* The source stops reaping in .pre_save() and, since it stays alive,
restarts reaping from the vm-state handler's resume branch on failure.
* On success the source's .exit() leaves the device untouched - no
reset, interface release or host-driver rebind - and just closes its
own copy of the FD.
Reaping is stopped and restarted on the whole libusb context, which is
shared by all usb-host devices, so cpr-transfer currently supports a
single hostdevice= usb-host at a time.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 80 ++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 68 insertions(+), 12 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 09ffb5d48f7..387040d6e1a 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -127,6 +127,7 @@ struct USBHostDevice {
GSList *cpr_inflight;
bool cpr_drained;
bool cpr_replay_all;
+ bool cpr_handed_off;
/* request queues */
QTAILQ_HEAD(, USBHostRequest) requests;
@@ -283,6 +284,23 @@ static void usb_host_start_events(void)
}
}
+/*
+ * cpr-transfer shares the usbfs fd with the new QEMU, so stop reaping URBs on
+ * it here -- only one process may drive the device.
+ */
+static void usb_host_stop_events(void)
+{
+ const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+ libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
+ if (poll) {
+ for (int i = 0; poll[i] != NULL; i++) {
+ usb_host_del_fd(poll[i]->fd, ctx);
+ }
+ free(poll);
+ }
+}
+
#else
static QEMUTimer *poll_timer;
@@ -306,6 +324,9 @@ static void usb_host_timer(void *opaque)
usb_host_timer_kick();
}
+static void usb_host_start_events(void) {}
+static void usb_host_stop_events(void) {}
+
#endif /* !CONFIG_WIN32 */
static int usb_host_init(void)
@@ -1272,13 +1293,18 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
/*
* Quiesce the URB data plane around a CPR switchover: drain the in-flight
* transfers before the device/irqchip snapshot, so completions raised during
- * the drain are migrated too.
+ * the drain are migrated too. For cpr-transfer, also hand reaping of the
+ * shared fd off to the target by stopping events on this side.
*/
static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
{
if (usb_host_cpr_drain_xfers(s) < 0) {
return -1;
}
+ if (mode == MIG_MODE_CPR_TRANSFER) {
+ usb_host_stop_events();
+ s->cpr_handed_off = true;
+ }
return 0;
}
@@ -1297,6 +1323,11 @@ static void usb_host_cpr_reissue(USBHostDevice *s)
return;
}
+ if (s->cpr_handed_off) {
+ usb_host_start_events();
+ s->cpr_handed_off = false;
+ }
+
for (it = s->cpr_inflight; it; it = it->next) {
p = it->data;
/*
@@ -1371,16 +1402,19 @@ static int usb_host_close(USBHostDevice *s)
trace_usb_host_close(s->bus_num, s->addr);
- usb_host_abort_xfers(s);
- usb_host_iso_free_all(s);
+ if (!s->cpr_handed_off) {
+ usb_host_abort_xfers(s);
+ usb_host_iso_free_all(s);
- if (udev->attached) {
- usb_device_detach(udev);
+ if (udev->attached) {
+ usb_device_detach(udev);
+ }
+
+ usb_host_release_interfaces(s);
+ libusb_reset_device(s->dh);
+ usb_host_attach_kernel(s);
}
- usb_host_release_interfaces(s);
- libusb_reset_device(s->dh);
- usb_host_attach_kernel(s);
libusb_close(s->dh);
s->dh = NULL;
s->dev = NULL;
@@ -1415,10 +1449,17 @@ static void usb_host_exit_notifier(struct Notifier *n, void *data)
USBHostDevice *s = container_of(n, USBHostDevice, exit);
if (s->dh) {
- usb_host_abort_xfers(s);
- usb_host_release_interfaces(s);
- libusb_reset_device(s->dh);
- usb_host_attach_kernel(s);
+ /*
+ * Handed to new QEMU across cpr-transfer: do not reset, release
+ * interfaces or rebind the host driver underneath it; just close
+ * our handle.
+ */
+ if (!s->cpr_handed_off) {
+ usb_host_abort_xfers(s);
+ usb_host_release_interfaces(s);
+ libusb_reset_device(s->dh);
+ usb_host_attach_kernel(s);
+ }
libusb_close(s->dh);
}
}
@@ -1487,6 +1528,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
error_setg(errp, "failed to open host usb device %s", s->hostdevice);
return;
}
+ if (cpr_is_incoming()) {
+ /*
+ * The source still owns the shared fd until it hands off. Don't
+ * reap URBs on it yet, or we would steal the source's in-flight
+ * completions; .post_load() restarts events once we take over.
+ */
+ usb_host_stop_events();
+ }
} else
#endif
if (s->match.addr && s->match.bus_num &&
@@ -1550,6 +1599,12 @@ static void usb_host_unrealize(USBDevice *udev)
if (s->needs_autoscan) {
QTAILQ_REMOVE(&hostdevs, s, next);
}
+ /*
+ * An explicit device_del means we still own the device (a successful
+ * cpr-transfer handoff exits the process instead), so clear the handoff
+ * flag to force a full reset/release in usb_host_close().
+ */
+ s->cpr_handed_off = false;
usb_host_close(s);
migrate_del_blocker(&s->cpr_blocker);
if (s->cpr_vmse) {
@@ -2033,6 +2088,7 @@ static int usb_host_post_load(void *opaque, int version_id)
* rescan in usb_host_post_load_bh() -- those are for cross-host migration.
*/
if (cpr_is_incoming()) {
+ usb_host_start_events();
return 0;
}
--
2.47.1
^ permalink raw reply [relevance 29%]
* [PATCH QEMU HCI-8.0 v7 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800
` (4 preceding siblings ...)
2026-10-06 15:35 25% ` [PATCH QEMU HCI-8.0 v7 05/14] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:35 26% ` Andrey Drobyshev
2026-10-06 15:45 4% ` no-reply
2026-10-06 15:35 24% ` [PATCH QEMU HCI-8.0 v7 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
` (5 subsequent siblings)
11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
URBs submitted by the old QEMU must not outlive it across a CPR
switchover: the kernel would complete them into buffers of an address
space that is gone after exec, and the new libusb context would reap URB
pointers it never submitted.
Register a vm_change_state handler that, when the VM stops for CPR
(RUN_STATE_FINISH_MIGRATE), cancels and reaps every pending transfer.
It runs before the vCPU/device snapshot, so a completion raised during
the drain lands in the still-live interrupt controller and is migrated
too. A transfer that already completed in the kernel is delivered
normally; one cancelled after moving some data is delivered as a short
transfer; one that moved nothing is suppressed, and the target replays
it from the start of the TD via usb_xhci_post_load(). A VM already
paused when migration starts never hits that transition, so .pre_save()
drains as a fallback, suppressing every URB for the target to replay.
Isochronous URBs live on their own rings; cancel and reap them too -- the
target reconstructs the stream from the guest's transfer ring.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 236 +++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 209 insertions(+), 27 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 1a79cdf9e4d..e631b683ed1 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -48,6 +48,7 @@
#include "qapi/error.h"
#include "migration/blocker.h"
#include "migration/cpr.h"
+#include "migration/misc.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -122,6 +123,9 @@ struct USBHostDevice {
/* CPR migration */
Error *cpr_blocker;
+ VMChangeStateEntry *cpr_vmse;
+ bool cpr_drained;
+ bool cpr_replay_all;
/* request queues */
QTAILQ_HEAD(, USBHostRequest) requests;
@@ -405,7 +409,10 @@ static void usb_host_req_free(USBHostRequest *r)
#ifdef CONFIG_WIN32
request_count--;
#endif
- QTAILQ_REMOVE(&r->host->requests, r, next);
+ /* Skip if a timed-out reap already unlinked it (avoid a double remove) */
+ if (QTAILQ_IN_USE(r, next)) {
+ QTAILQ_REMOVE(&r->host->requests, r, next);
+ }
libusb_free_transfer(r->xfer);
g_free(r->buffer);
g_free(r);
@@ -423,6 +430,29 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
return NULL;
}
+/*
+ * Classify a reap during the CPR drain (r->p is still set). The paused-VM
+ * fallback (cpr_replay_all) can't deliver completions, so it suppresses every
+ * reap for the target to replay; otherwise one that moved no data is suppressed
+ * and one that moved some is kept as a short transfer. Returns true if
+ * suppressed, so the caller frees the request.
+ */
+static bool usb_host_req_cpr_replay(USBHostRequest *r,
+ struct libusb_transfer *xfer)
+{
+ if (!r->host->cpr_replay_all) {
+ if (xfer->status != LIBUSB_TRANSFER_CANCELLED) {
+ return false;
+ }
+ if (xfer->actual_length != 0) {
+ /* deliver as a short transfer */
+ xfer->status = LIBUSB_TRANSFER_COMPLETED;
+ return false;
+ }
+ }
+ return true;
+}
+
static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
{
USBHostRequest *r = xfer->user_data;
@@ -432,6 +462,9 @@ static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
if (r->p == NULL) {
goto out; /* request was canceled */
}
+ if (usb_host_req_cpr_replay(r, xfer)) {
+ goto out; /* CPR drain: the target will replay this packet */
+ }
r->p->status = status_map[xfer->status];
r->p->actual_length = xfer->actual_length;
@@ -482,6 +515,9 @@ static void LIBUSB_CALL usb_host_req_complete_data(struct libusb_transfer *xfer)
if (r->p == NULL) {
goto out; /* request was canceled */
}
+ if (usb_host_req_cpr_replay(r, xfer)) {
+ goto out; /* CPR drain: the target will replay this packet */
+ }
r->p->status = status_map[xfer->status];
if (r->in && xfer->actual_length) {
@@ -502,22 +538,29 @@ out:
}
}
-static void usb_host_req_abort(USBHostRequest *r)
+/* Fail r's in-flight packet with an error status and detach it from r. */
+static void usb_host_req_complete_err(USBHostRequest *r, int status)
{
- USBHostDevice *s = r->host;
- bool inflight = (r->p && r->p->state == USB_PACKET_ASYNC);
+ USBHostDevice *s = r->host;
- if (inflight) {
- r->p->status = USB_RET_NODEV;
- trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
- r->p->status, r->p->actual_length);
- if (r->p->ep->nr == 0) {
- usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
- } else {
- usb_packet_complete(USB_DEVICE(s), r->p);
- }
- r->p = NULL;
+ if (!r->p || r->p->state != USB_PACKET_ASYNC) {
+ return;
+ }
+ r->p->status = status;
+ trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
+ r->p->status, r->p->actual_length);
+ if (r->p->ep->nr == 0) {
+ usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
+ } else {
+ usb_packet_complete(USB_DEVICE(s), r->p);
+ }
+ r->p = NULL;
+}
+static void usb_host_req_abort(USBHostRequest *r)
+{
+ if (r->p && r->p->state == USB_PACKET_ASYNC) {
+ usb_host_req_complete_err(r, USB_RET_NODEV);
libusb_cancel_transfer(r->xfer);
}
}
@@ -1098,9 +1141,18 @@ fail:
return -1;
}
-static void usb_host_reap_xfers(USBHostDevice *s)
+/* libusb still owns cancelled-but-unreaped transfers: unlink, don't free. */
+static void usb_host_drop_requests(USBHostDevice *s)
{
USBHostRequest *r, *rtmp;
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ QTAILQ_REMOVE(&s->requests, r, next);
+ }
+}
+
+static void usb_host_reap_xfers(USBHostDevice *s)
+{
int limit = 100;
while (QTAILQ_FIRST(&s->requests) != NULL) {
@@ -1110,15 +1162,10 @@ static void usb_host_reap_xfers(USBHostDevice *s)
libusb_handle_events_timeout(ctx, &tv);
if (--limit == 0) {
/*
- * Don't wait forever for libusb calling the complete
- * callback (which will unlink and free the request).
- *
- * Leaking memory here, to make sure libusb will not
- * access memory which we have released already.
+ * Don't wait forever for libusb to call the complete callback
+ * (which unlinks and frees the request).
*/
- QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
- QTAILQ_REMOVE(&s->requests, r, next);
- }
+ usb_host_drop_requests(s);
return;
}
}
@@ -1134,6 +1181,117 @@ static void usb_host_abort_xfers(USBHostDevice *s)
usb_host_reap_xfers(s);
}
+static bool usb_host_iso_inflight(USBHostDevice *s)
+{
+ USBHostIsoRing *ring;
+
+ QTAILQ_FOREACH(ring, &s->isorings, next) {
+ if (!QTAILQ_EMPTY(&ring->inflight)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/*
+ * Quiesce the device for a CPR switchover. Cancel and reap all URBs,
+ * so that no URB submitted by this process can outlive it.
+ */
+static int usb_host_cpr_drain_xfers(USBHostDevice *s)
+{
+ USBHostRequest *r, *rtmp;
+ USBHostIsoRing *ring;
+ USBHostIsoXfer *xfer;
+ int limit = 100;
+
+ s->cpr_drained = true;
+
+ /*
+ * Cancel and reap every URB. Completing a reaped URB can make xHCI queue
+ * the next TD, so re-cancel whatever reappears until the queue stays empty.
+ */
+ while (!QTAILQ_EMPTY(&s->requests)) {
+ struct timeval tv = { .tv_usec = 2500 };
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ libusb_cancel_transfer(r->xfer);
+ }
+ libusb_handle_events_timeout(ctx, &tv);
+ if (--limit == 0) {
+ /*
+ * The device won't release these URBs. Error the packets out so
+ * the resumed guest retries them, leak the requests (libusb still
+ * owns their transfers) and abort the migration.
+ */
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ usb_host_req_complete_err(r, USB_RET_IOERROR);
+ }
+ usb_host_drop_requests(s);
+ return -1;
+ }
+ }
+
+ /*
+ * Iso URBs are on the rings, not s->requests. Cancel them: one in
+ * flight when the guest stops may never complete on its own, and none
+ * may be left on the preserved fd. The target replays from the ring.
+ */
+ QTAILQ_FOREACH(ring, &s->isorings, next) {
+ QTAILQ_FOREACH(xfer, &ring->inflight, next) {
+ libusb_cancel_transfer(xfer->xfer);
+ }
+ }
+
+ /*
+ * Cap the reap at 2x a full ring:
+ * iso_urb_count URBs x iso_urb_frames packets each
+ */
+ limit = 2 * s->iso_urb_count * s->iso_urb_frames;
+ while (usb_host_iso_inflight(s)) {
+ struct timeval tv = { .tv_usec = 1000 };
+ libusb_handle_events_timeout(ctx, &tv);
+ if (limit-- == 0) {
+ return -1;
+ }
+ }
+ usb_host_iso_free_all(s);
+ return 0;
+}
+
+/*
+ * Quiesce the URB data plane around a CPR switchover: drain the in-flight
+ * transfers before the device/irqchip snapshot, so completions raised during
+ * the drain are migrated too.
+ */
+static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
+{
+ if (usb_host_cpr_drain_xfers(s) < 0) {
+ return -1;
+ }
+ return 0;
+}
+
+/*
+ * Run the CPR quiesce from the vm-state handler, on the stop-for-CPR
+ * transition before the device/irqchip snapshot.
+ */
+static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
+{
+ USBHostDevice *s = opaque;
+ MigMode mode;
+
+ if (running || !s->dh || state != RUN_STATE_FINISH_MIGRATE) {
+ return;
+ }
+ mode = migrate_mode();
+ if (mode != MIG_MODE_CPR_EXEC && mode != MIG_MODE_CPR_TRANSFER) {
+ return;
+ }
+ if (usb_host_cpr_quiesce(s, mode) < 0) {
+ migration_file_set_error(-EIO, NULL);
+ }
+}
+
static int usb_host_close(USBHostDevice *s)
{
USBDevice *udev = USB_DEVICE(s);
@@ -1301,6 +1459,8 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
+
+ s->cpr_vmse = qemu_add_vm_change_state_handler(usb_host_cpr_vm_state, s);
}
static void usb_host_instance_init(Object *obj)
@@ -1323,6 +1483,9 @@ static void usb_host_unrealize(USBDevice *udev)
}
usb_host_close(s);
migrate_del_blocker(&s->cpr_blocker);
+ if (s->cpr_vmse) {
+ qemu_del_vm_change_state_handler(s->cpr_vmse);
+ }
}
static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1792,10 +1955,8 @@ static int usb_host_post_load(void *opaque, int version_id)
USBHostDevice *dev = opaque;
/*
- * For CPR migration, device wasn't released/reset, and the guest
- * is unaware of the switchover. The detach/rescan performed in
- * usb_host_post_load_bh() only exists for cross-host migration.
- * Skip it for CPR.
+ * CPR keeps the device open and the guest unaware, so skip the detach and
+ * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
*/
if (cpr_is_incoming()) {
return 0;
@@ -1810,10 +1971,31 @@ static int usb_host_post_load(void *opaque, int version_id)
return 0;
}
+static int usb_host_pre_save(void *opaque)
+{
+ USBHostDevice *s = opaque;
+ MigMode mode = migrate_mode();
+
+ /*
+ * The vm-state handler drains on the stop-for-CPR transition, but a VM
+ * already paused never hits it. Drain here instead; the stopped guest
+ * can't take completions, so suppress every URB and let the target replay.
+ */
+ if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
+ s->dh && !s->cpr_drained) {
+ s->cpr_replay_all = true;
+ if (usb_host_cpr_quiesce(s, mode) < 0) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
static const VMStateDescription vmstate_usb_host = {
.name = "usb-host",
.version_id = 1,
.minimum_version_id = 1,
+ .pre_save = usb_host_pre_save,
.post_load = usb_host_post_load,
.fields = (const VMStateField[]) {
VMSTATE_USB_DEVICE(parent_obj, USBHostDevice),
--
2.47.1
^ permalink raw reply [relevance 26%]
* [PATCH QEMU HCI-8.0 v7 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
` (5 preceding siblings ...)
2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:35 24% ` Andrey Drobyshev
2026-10-06 15:46 4% ` no-reply
2026-10-06 15:35 22% ` [PATCH QEMU HCI-8.0 v7 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
` (4 subsequent siblings)
11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
If CPR fails, the source VM resumes with the drained packets still owned
by the host controller as in-flight, and nothing left to complete them.
Remember the packets suppressed during the drain on a list, and re-issue
them on the preserved fd from the vm-state handler's resume branch -- it
runs on any resume but only acts once we have actually drained
(cpr_drained). Control transfers cannot be reconstructed from the packet
alone, so complete those as errors and let the guest driver retry.
A re-issued transfer is replayed from the start of the TD, so a partial
transfer loses its progress -- an OUT fragment already consumed by the
device may be sent twice. Only the paused-VM .pre_save() fallback queues
partial transfers for re-issue; the normal running-VM drain delivers them
as short transfers and is unaffected.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 79 ++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 74 insertions(+), 5 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index e631b683ed1..d90117e4292 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -124,6 +124,7 @@ struct USBHostDevice {
/* CPR migration */
Error *cpr_blocker;
VMChangeStateEntry *cpr_vmse;
+ GSList *cpr_inflight;
bool cpr_drained;
bool cpr_replay_all;
@@ -169,6 +170,7 @@ static void usb_host_release_interfaces(USBHostDevice *s);
static void usb_host_nodev(USBHostDevice *s);
static void usb_host_detach_kernel(USBHostDevice *s);
static void usb_host_attach_kernel(USBHostDevice *s);
+static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
/* ------------------------------------------------------------------------ */
@@ -433,9 +435,10 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
/*
* Classify a reap during the CPR drain (r->p is still set). The paused-VM
* fallback (cpr_replay_all) can't deliver completions, so it suppresses every
- * reap for the target to replay; otherwise one that moved no data is suppressed
- * and one that moved some is kept as a short transfer. Returns true if
- * suppressed, so the caller frees the request.
+ * reap; otherwise one that moved no data is suppressed and one that moved some
+ * is kept as a short transfer. Suppressed packets are queued on cpr_inflight
+ * for the target to replay. Returns true if suppressed, so the caller frees
+ * the request.
*/
static bool usb_host_req_cpr_replay(USBHostRequest *r,
struct libusb_transfer *xfer)
@@ -450,6 +453,9 @@ static bool usb_host_req_cpr_replay(USBHostRequest *r,
return false;
}
}
+ if (r->p->state == USB_PACKET_ASYNC) {
+ r->host->cpr_inflight = g_slist_append(r->host->cpr_inflight, r->p);
+ }
return true;
}
@@ -1271,16 +1277,74 @@ static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
return 0;
}
+/*
+ * If CPR is aborted the source resumes with the drained packets still owned
+ * by the controller as in-flight; re-issue them on the preserved fd. Control
+ * transfers can't be reconstructed from the packet, so error those out.
+ */
+static void usb_host_cpr_reissue(USBHostDevice *s)
+{
+ USBDevice *udev = USB_DEVICE(s);
+ GSList *it;
+ USBPacket *p;
+
+ if (!s->cpr_drained) {
+ return;
+ }
+
+ for (it = s->cpr_inflight; it; it = it->next) {
+ p = it->data;
+ /*
+ * Replay from the start of the TD. A transfer that already moved some
+ * data is re-run in full, so an OUT fragment the device consumed may be
+ * sent twice. Only the paused-VM pre_save fallback queues such partial
+ * transfers here; the normal drain delivers them as short transfers.
+ */
+ p->actual_length = 0;
+ if (p->ep->nr == 0) {
+ /* control endpoint: can't be reconstructed, so error it out */
+ p->status = USB_RET_IOERROR;
+ usb_generic_async_ctrl_complete(udev, p);
+ } else {
+ usb_host_handle_data(udev, p);
+ if (p->status != USB_RET_ASYNC) {
+ /* completed synchronously with an error */
+ if (usb_host_use_combining(p->ep)) {
+ usb_combined_input_packet_complete(udev, p);
+ } else {
+ usb_packet_complete(udev, p);
+ }
+ }
+ }
+ }
+ g_slist_free(s->cpr_inflight);
+ s->cpr_inflight = NULL;
+ s->cpr_replay_all = false;
+ s->cpr_drained = false;
+}
+
/*
* Run the CPR quiesce from the vm-state handler, on the stop-for-CPR
- * transition before the device/irqchip snapshot.
+ * transition before the device/irqchip snapshot, and re-issue the drained
+ * packets on resume if CPR was aborted.
*/
static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
{
USBHostDevice *s = opaque;
MigMode mode;
- if (running || !s->dh || state != RUN_STATE_FINISH_MIGRATE) {
+ if (running) {
+ /*
+ * Resume: re-issue drained packets and reset state, even if the
+ * device was lost between the drain and now (dh == NULL).
+ */
+ usb_host_cpr_reissue(s);
+ return;
+ }
+ if (!s->dh) {
+ return;
+ }
+ if (state != RUN_STATE_FINISH_MIGRATE) {
return;
}
mode = migrate_mode();
@@ -1486,6 +1550,8 @@ static void usb_host_unrealize(USBDevice *udev)
if (s->cpr_vmse) {
qemu_del_vm_change_state_handler(s->cpr_vmse);
}
+ g_slist_free(s->cpr_inflight);
+ s->cpr_inflight = NULL;
}
static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1493,6 +1559,9 @@ static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
USBHostDevice *s = USB_HOST_DEVICE(udev);
USBHostRequest *r;
+ /* Packet is being cancelled -> it mustn't be re-issued on failed CPR */
+ s->cpr_inflight = g_slist_remove(s->cpr_inflight, p);
+
if (p->combined) {
usb_combined_packet_cancel(udev, p);
return;
--
2.47.1
^ permalink raw reply [relevance 24%]
* [PATCH QEMU HCI-8.0 v7 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800
` (6 preceding siblings ...)
2026-10-06 15:35 24% ` [PATCH QEMU HCI-8.0 v7 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:35 22% ` Andrey Drobyshev
2026-10-06 15:47 5% ` no-reply
2026-10-06 15:36 29% ` [PATCH QEMU HCI-8.0 v7 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
` (3 subsequent siblings)
11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
usb_host_open() reads the product string descriptor from the device
with a synchronous control transfer. On the CPR target this runs while
the device is being handed over, and the transfer can block there,
hanging the incoming migration.
The string only fills the cosmetic product_desc, so skip the read when
we're the CPR target and fall back to the synthetic "host:bus.addr"
name.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index d90117e4292..271456abb0a 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1111,7 +1111,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
udev->speed = speed_map[libusb_speed];
usb_host_speed_compat(s);
- if (s->ddesc.iProduct) {
+ if (s->ddesc.iProduct && !cpr_is_incoming()) {
libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
(unsigned char *)udev->product_desc,
sizeof(udev->product_desc));
--
2.47.1
^ permalink raw reply [relevance 22%]
* [PATCH QEMU HCI-8.0 v7 05/14] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800
` (3 preceding siblings ...)
2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:35 25% ` Andrey Drobyshev
2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
` (6 subsequent siblings)
11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
usb_host_abort_xfers() cancels every pending request and then pumps
libusb events until all of them are reaped, with a bounded wait. Split
that reap-wait loop out into usb_host_reap_xfers(). No functional
change.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index beab8edc619..1a79cdf9e4d 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1098,15 +1098,11 @@ fail:
return -1;
}
-static void usb_host_abort_xfers(USBHostDevice *s)
+static void usb_host_reap_xfers(USBHostDevice *s)
{
USBHostRequest *r, *rtmp;
int limit = 100;
- QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
- usb_host_req_abort(r);
- }
-
while (QTAILQ_FIRST(&s->requests) != NULL) {
struct timeval tv;
memset(&tv, 0, sizeof(tv));
@@ -1128,6 +1124,16 @@ static void usb_host_abort_xfers(USBHostDevice *s)
}
}
+static void usb_host_abort_xfers(USBHostDevice *s)
+{
+ USBHostRequest *r, *rtmp;
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ usb_host_req_abort(r);
+ }
+ usb_host_reap_xfers(s);
+}
+
static int usb_host_close(USBHostDevice *s)
{
USBDevice *udev = USB_DEVICE(s);
--
2.47.1
^ permalink raw reply [relevance 25%]
* [PATCH QEMU HCI-8.0 v7 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800
` (2 preceding siblings ...)
2026-10-06 15:35 13% ` [PATCH QEMU HCI-8.0 v7 03/14] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:35 26% ` Andrey Drobyshev
2026-10-06 15:35 25% ` [PATCH QEMU HCI-8.0 v7 05/14] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
` (7 subsequent siblings)
11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
In .realize(), save freshly opened host device FD into the CPR registry,
and then reuse it on CPR target. Also, add deletion of that FD from
registry to usb_host_open() / usb_host_close() cleanup paths.
For this to work, we also need to skip the .post-load() code which
closes, detaches the device, and then rescans the host bus to reopen
matching hostdevs. For the CPR-case migration we don't want any of that
as the hostdev FD stays preserved, and the guest shouldn't notice the
switchover.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 33 ++++++++++++++++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index e3bd7bcf749..beab8edc619 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -47,6 +47,7 @@
#include "qapi/error.h"
#include "migration/blocker.h"
+#include "migration/cpr.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -949,6 +950,21 @@ static void usb_host_ep_update(USBHostDevice *s)
libusb_free_config_descriptor(conf);
}
+static char *usb_host_cpr_fd_name(USBHostDevice *s)
+{
+ /* Prefix the key so it can't clash with another CPR label */
+ return DEVICE(s)->id ? g_strdup_printf("usb-host/%s", DEVICE(s)->id)
+ : NULL;
+}
+
+static void usb_host_cpr_delete_fd(USBHostDevice *s)
+{
+ g_autofree char *name = usb_host_cpr_fd_name(s);
+ if (name) {
+ cpr_delete_fd(name, 0);
+ }
+}
+
static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
{
USBDevice *udev = USB_DEVICE(s);
@@ -1075,6 +1091,7 @@ fail:
s->dev = NULL;
}
if (s->hostfd != -1) {
+ usb_host_cpr_delete_fd(s);
close(s->hostfd);
s->hostfd = -1;
}
@@ -1136,6 +1153,7 @@ static int usb_host_close(USBHostDevice *s)
s->dev = NULL;
if (s->hostfd != -1) {
+ usb_host_cpr_delete_fd(s);
close(s->hostfd);
s->hostfd = -1;
}
@@ -1222,9 +1240,12 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
if (s->hostdevice) {
+ g_autofree char *name = usb_host_cpr_fd_name(s);
int fd;
+
s->needs_autoscan = false;
- fd = qemu_open(s->hostdevice, O_RDWR, errp);
+ fd = name ? cpr_open_fd(s->hostdevice, O_RDWR, name, 0, errp)
+ : qemu_open(s->hostdevice, O_RDWR, errp);
if (fd < 0) {
return;
}
@@ -1764,6 +1785,16 @@ static int usb_host_post_load(void *opaque, int version_id)
{
USBHostDevice *dev = opaque;
+ /*
+ * For CPR migration, device wasn't released/reset, and the guest
+ * is unaware of the switchover. The detach/rescan performed in
+ * usb_host_post_load_bh() only exists for cross-host migration.
+ * Skip it for CPR.
+ */
+ if (cpr_is_incoming()) {
+ return 0;
+ }
+
if (!dev->bh_postld) {
dev->bh_postld = qemu_bh_new_guarded(usb_host_post_load_bh, dev,
&DEVICE(dev)->mem_reentrancy_guard);
--
2.47.1
^ permalink raw reply [relevance 26%]
* [PATCH QEMU HCI-8.0 v7 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
@ 2026-10-06 15:35 30% ` Andrey Drobyshev
2026-10-06 15:38 0% ` no-reply
2026-10-06 15:35 6% ` [PATCH QEMU HCI-8.0 v7 02/14] usb-host: don't leak config descriptor on claim " Andrey Drobyshev
` (10 subsequent siblings)
11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
The FD passed into usb_host_open() for a wrapped device is only stored in
s->hostfd after libusb_wrap_sys_device() succeeds, and the failure path
doesn't close it. if wrapping fails, or a later step of the open sequence
fails (e.g. usb_device_attach), the FD is leaked, as usb_host_close() only
runs for fully opened devices, and the realize error path doesn't clean it
up either.
Store the FD in s->hostfd before wrapping and close it on the failure
path.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b74670ae256..d4a8c68e6d4 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -961,6 +961,11 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
goto fail;
}
+ /* Track the caller-owned FD so the fail: path frees it. */
+ if (!dev) {
+ s->hostfd = hostfd;
+ }
+
if (dev) {
bus_num = libusb_get_bus_number(dev);
addr = libusb_get_device_address(dev);
@@ -978,7 +983,6 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
if (rc != 0) {
goto fail;
}
- s->hostfd = hostfd;
dev = libusb_get_device(s->dh);
bus_num = libusb_get_bus_number(dev);
addr = libusb_get_device_address(dev);
@@ -1066,6 +1070,10 @@ fail:
s->dh = NULL;
s->dev = NULL;
}
+ if (s->hostfd != -1) {
+ close(s->hostfd);
+ s->hostfd = -1;
+ }
return -1;
}
--
2.47.1
^ permalink raw reply [relevance 30%]
* [PATCH QEMU HCI-8.0 v7 03/14] usb-host: add migration blocker for CPR modes #VSTOR-137800
2026-10-06 15:35 30% ` [PATCH QEMU HCI-8.0 v7 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:35 6% ` [PATCH QEMU HCI-8.0 v7 02/14] usb-host: don't leak config descriptor on claim " Andrey Drobyshev
@ 2026-10-06 15:35 13% ` Andrey Drobyshev
2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
` (8 subsequent siblings)
11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
We're about to add support for CPR migration to usb-host in the upcoming
commits. Right now CPR is crashing with usb-host devices. Let's add
migration blocker upfront, for both cpr-exec and cpr-transfer. The
blocker is added unconditionally, so that we refuse CPR migration instead
of crashing on it. That is for the sake of bisectability of the upcoming
commits. It is going to be lifted (conditioned) in a following patch
once CPR support is added. Also, blocker is added at the end of
usb_host_realize(), so we have to cleanup all the side effects done
earlier in .realize() in case migrate_add_blocker_modes() fails.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 7c8b65ad5af..e3bd7bcf749 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -46,6 +46,7 @@
#endif
#include "qapi/error.h"
+#include "migration/blocker.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -118,6 +119,9 @@ struct USBHostDevice {
bool bh_postld_pending;
Notifier exit;
+ /* CPR migration */
+ Error *cpr_blocker;
+
/* request queues */
QTAILQ_HEAD(, USBHostRequest) requests;
QTAILQ_HEAD(, USBHostIsoRing) isorings;
@@ -1256,6 +1260,18 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
usb_host_auto_check(NULL);
}
+ error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+ DEVICE(s)->id ?: "(anonymous)");
+ if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1) < 0) {
+ if (s->needs_autoscan) {
+ QTAILQ_REMOVE(&hostdevs, s, next);
+ }
+ usb_host_close(s);
+ return;
+ }
+
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
}
@@ -1279,6 +1295,7 @@ static void usb_host_unrealize(USBDevice *udev)
QTAILQ_REMOVE(&hostdevs, s, next);
}
usb_host_close(s);
+ migrate_del_blocker(&s->cpr_blocker);
}
static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
--
2.47.1
^ permalink raw reply [relevance 13%]
* [PATCH QEMU HCI-8.0 v7 02/14] usb-host: don't leak config descriptor on claim failure #VSTOR-137800
2026-10-06 15:35 30% ` [PATCH QEMU HCI-8.0 v7 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 15:35 6% ` Andrey Drobyshev
2026-10-06 15:35 13% ` [PATCH QEMU HCI-8.0 v7 03/14] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
` (9 subsequent siblings)
11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 15:35 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
usb_host_claim_interfaces() fetches the active config descriptor but
frees it only on the success path. When not every interface can be
claimed it returns USB_RET_STALL without freeing it, leaking the
descriptor.
Free it before the error return.
Fixes: 896b6757f9c4 ("usb-host: support devices with sparse/non-sequential USB interfaces")
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index d4a8c68e6d4..7c8b65ad5af 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1381,6 +1381,7 @@ static int usb_host_claim_interfaces(USBHostDevice *s, int configuration)
}
}
if (claimed != conf->bNumInterfaces) {
+ libusb_free_config_descriptor(conf);
return USB_RET_STALL;
}
--
2.47.1
^ permalink raw reply [relevance 6%]
* Re: [Devel] [PATCH hci-8.1 1/5] selftests/ve: check CPUID override emulation for VE tasks
2026-10-06 14:56 3% ` [Devel] [PATCH hci-8.1 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
@ 2026-10-06 15:07 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:07 UTC (permalink / raw)
To: Pavel Tikhomirov, Vasileios Almpanis, Konstantin Khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [Medium] selftests/ve: child may run CPUID before faulting is armed, so tests are racy
- [Low] selftests/ve: ve_cpuid_override_test binary missing from .gitignore
- [Low] selftests/ve: x86-only cpuid asm built unconditionally for all ARCH
- [Low] selftests/ve: 4096-byte snapshot of cpuid_override can truncate the table, so restore fails
- [Low] selftests/ve: VE cgroup leaks when an ASSERT fails in FIXTURE_SETUP after make_ve()
- [Low] selftests/ve: leaf 1 EAX compared against single-CPU sample while child is unpinned
--
commit 931878a5cb863ada037d978eae5d17e57f54cba3
Author: Konstantin Khorenko <khorenko@virtuozzo.com>
selftests/ve: check CPUID override emulation for VE tasks
This commit adds a selftest that writes a one-entry /proc/vz/cpuid_override
table and checks that a task in a fresh VE sees the table values while the
host sees the hardware values. A second test flushes the table after the
task was armed and expects the hardware values again.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148843
> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
> --- a/tools/testing/selftests/ve/Makefile
> +++ b/tools/testing/selftests/ve/Makefile
> @@ -5,5 +5,6 @@ CFLAGS += -g -Wall -O2
> TEST_GEN_PROGS += ve_ns_owner_test
> TEST_GEN_PROGS += ve_perms_test
> TEST_GEN_PROGS += ve_mount_accounting_test
> +TEST_GEN_PROGS += ve_cpuid_override_test
[Severity: Low]
This isn't a bug in functionality, but the new binary ve_cpuid_override_test
is not added to tools/testing/selftests/ve/.gitignore, which lists only
ve_ns_owner_test, ve_perms_test and ve_mount_accounting_test.
Running make in tools/testing/selftests/ve will leave this binary untracked
in git status. Should it get an entry there?
[Severity: Low]
The test is added unconditionally for every architecture, and do_cpuid()
below uses x86-only inline asm.
Since tools/testing/selftests/Makefile has an unconditional "TARGETS += ve",
can this break the build of the whole ve selftest directory on non-x86 ARCH,
and on 32-bit PIC x86 because of the "=b" constraint? Would an arch check in
this Makefile be appropriate?
[ ... ]
> diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
> new file mode 100644
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c
[ ... ]
> +static int cpuid_child(struct child_ctx *ctx)
> +{
> + struct cpuid_regs r;
> + char go;
> +
> + close(ctx->result_pipe[0]);
> + close(ctx->go_pipe[1]);
> +
> + if (unshare(CLONE_NEWVE) < 0)
> + return 21;
> + if (write(ctx->result_pipe[1], "R", 1) != 1)
> + return 22;
> + if (read(ctx->go_pipe[0], &go, 1) != 1)
> + return 23;
> +
> + do_cpuid(CPUID_LEAF, &r);
[Severity: Medium]
Is the child guaranteed to be armed before it executes CPUID here?
CPUID faulting is only switched on in __switch_to_xtra() at the next
context switch, when prev_cpuid != next_cpuid:
arch/x86/kernel/process.c:__switch_to_xtra() {
...
prev_cpuid = (tifp & _TIF_NOCPUID) || (tifp & _TIF_CPUID_OVERRIDE);
next_cpuid = (tifn & _TIF_NOCPUID) || (tifn & _TIF_CPUID_OVERRIDE);
if (prev_cpuid != next_cpuid)
set_cpuid_faulting(next_cpuid);
...
}
The only blocking point of the child is read(ctx->go_pipe[0]). In
ve_sees_table_host_sees_hardware() the parent writes "G" right after
spawn_cpuid_child() has read "R".
On SMP the byte can already be in the pipe when the child calls read(). The
read then returns without sleeping, no context switch occurs, and the child
executes CPUID natively. It would then report the hardware EDX and fail
EXPECT_EQ(r.edx, self->host.edx & ~self->bit).
In flushed_table_falls_back_to_hardware() the parent calls
write_cpuid_override("\n") right after reading "R", and nothing waits for
the child to sleep. Can the child end up never being armed, so that the test
passes without exercising the armed-then-flushed path?
Later patches in the series (including "selftests/ve: check CPUID override
right after entering a VE") keep this handshake, and the final series state
still has the "R" then immediate "G" pattern with no wait for the child to
sleep, so the race is not fixed there either.
> + if (write(ctx->result_pipe[1], &r, sizeof(r)) != sizeof(r))
> + return 24;
> + return 0;
> +}
[ ... ]
> +FIXTURE_SETUP(ve_cpuid_override)
> +{
> + int fd;
> +
> + fd = open(CPUID_OVERRIDE_PATH, O_RDONLY);
> + if (fd < 0)
> + SKIP(return, "%s: %s", CPUID_OVERRIDE_PATH, strerror(errno));
> + close(fd);
> +
> + ASSERT_GE(read_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->saved,
> + sizeof(self->saved)), 0);
[Severity: Low]
Can this snapshot be truncated? self->saved is CPUID_TABLE_MAX (4096) bytes,
and read_file_at() fills at most buflen-1 bytes without reporting an error.
With up to 128 entries of roughly 45 to 64 bytes each, a large pre-existing
table can exceed 4095 bytes. If the cut falls mid-entry, the restore write in
FIXTURE_TEARDOWN_PARENT() fails with -EINVAL. If it falls on an entry
boundary, a shorter table is installed.
> +
> + self->cgv2_fd = mount_cg2_fd();
> + ASSERT_GE(self->cgv2_fd, 0);
> + ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
> + VE_CONTROLLERS), 0);
> + self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
> + ASSERT_GE(self->ctid, 0);
> +
> + /* Mask one feature bit the host has; SSE2 when present. */
> + do_cpuid(CPUID_LEAF, &self->host);
> + self->bit = 1u << 26;
> + if (!(self->host.edx & self->bit))
> + self->bit = self->host.edx & -self->host.edx;
> + ASSERT_NE(self->bit, 0);
[Severity: Low]
If an ASSERT fails in FIXTURE_SETUP after make_ve() (for example
ASSERT_NE(self->bit, 0)), will the VE cgroup be leaked?
The harness runs teardown only when setup has completed, so the statement in
the commit message that "The teardown runs in the parent because the harness
skips a child-side teardown once an ASSERT has fired" only covers the test
body, not a failure in setup. The ASSERT_NE path is very unlikely on x86, so
the impact looks small.
[ ... ]
> +FIXTURE_TEARDOWN_PARENT(ve_cpuid_override)
> +{
> + int cgv2_fd;
> +
> + write_cpuid_override(self->saved[0] ? self->saved : "\n");
[Severity: Low]
Should the return value of write_cpuid_override() be checked here? This is
the only restore of host state, and if it fails, the test's one-entry table
stays installed with no diagnostic.
[ ... ]
> +TEST_F(ve_cpuid_override, flushed_table_falls_back_to_hardware)
> +{
> + struct child_ctx ctx;
> + struct cpuid_regs r;
> + pid_t pid;
> +
> + ASSERT_EQ(write_cpuid_override(self->table), 0);
> +
> + pid = spawn_cpuid_child(_metadata, self->cgv2_fd, self->ctid, &ctx);
> +
> + ASSERT_EQ(write_cpuid_override("\n"), 0);
> + ASSERT_EQ(write(ctx.go_pipe[1], "G", 1), 1);
> + collect_cpuid_child(_metadata, pid, &ctx, &r);
> +
> + EXPECT_EQ(r.eax, self->host.eax);
> + EXPECT_NE(r.edx & self->bit, 0);
[Severity: Low]
Can EXPECT_EQ(r.eax, self->host.eax) fail spuriously on hybrid CPUs?
In this test the child's CPUID falls through to the real hardware, but
self->host was sampled by the parent in FIXTURE_SETUP(), and neither process
is pinned with sched_setaffinity(). If leaf 1 EAX (family/model/stepping)
differs between core types and the child runs on a different core type, the
comparison can fail without any kernel bug.
> +}
> +
> +TEST_HARNESS_MAIN
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-b4-prep-khorenko-vstor-148656-cpuid-v1-0-741609f4f03a@virtuozzo.com?part=1
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH hci-8.1 3/5] selftests/ve: check CPUID override right after entering a VE
@ 2026-10-06 15:03 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 15:03 UTC (permalink / raw)
To: Pavel Tikhomirov, Vasileios Almpanis, Konstantin Khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] selftests/ve: faulting child in wait_go=0 case fails at ready-read, not diagnosed
- [Low] selftests/ve: armed_before_first_reschedule cannot guarantee no reschedule, may falsely pass
--
commit 56789e953f13472fc78d80df081028a09179dc56
Author: Konstantin Khorenko <khorenko@virtuozzo.com>
selftests/ve: check CPUID override right after entering a VE
This commit adds a test case in which the child executes CPUID straight
after unshare(CLONE_NEWVE), with no blocking in between. It expects the
table values. A new wait_go field in struct child_ctx selects between the
old blocking flow and the new flow.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-148843
> diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
> --- a/tools/testing/selftests/ve/ve_cpuid_override_test.c
> +++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c
[ ... ]
> @@ -73,12 +74,18 @@ static int cpuid_child(struct child_ctx *ctx)
>
> if (unshare(CLONE_NEWVE) < 0)
> return 21;
> - if (write(ctx->result_pipe[1], "R", 1) != 1)
> - return 22;
> - if (read(ctx->go_pipe[0], &go, 1) != 1)
> - return 23;
> -
> + if (ctx->wait_go) {
> + if (write(ctx->result_pipe[1], "R", 1) != 1)
> + return 22;
> + if (read(ctx->go_pipe[0], &go, 1) != 1)
> + return 23;
> + }
> +
> + /* Without wait_go nothing wakes the parent before the CPUID. */
> do_cpuid(CPUID_LEAF, &r);
> +
> + if (!ctx->wait_go && write(ctx->result_pipe[1], "R", 1) != 1)
> + return 22;
[Severity: Low]
If the kernel does not emulate the CPUID fault, can the child die with
SIGSEGV inside do_cpuid() in the wait_go=0 case, before it writes the "R"
byte?
In that case the parent's read() of the ready byte in spawn_cpuid_child()
would see EOF, and ASSERT_EQ(read(...), 1) would abort the test there.
collect_cpuid_child() would then not be reached, so the "child killed by
signal" TH_LOG is not printed and waitpid() is never called. The child is
left as a zombie until the harness exits.
The test still fails, but with a misleading assertion. The later patch
"selftests/ve: check CPUID override emulation for a prefixed CPUID" keeps
the same ordering of "R" after CPUID for wait_go=0, so this does not appear
to be fixed later in the series.
> if (write(ctx->result_pipe[1], &r, sizeof(r)) != sizeof(r))
> return 24;
> return 0;
[ ... ]
> +/* No context switch between entering the VE and the CPUID. */
> +TEST_F(ve_cpuid_override, armed_before_first_reschedule)
> +{
> + struct child_ctx ctx = { .wait_go = 0 };
> + struct cpuid_regs r;
> + pid_t pid;
> +
> + ASSERT_EQ(write_cpuid_override(self->table), 0);
> +
> + pid = spawn_cpuid_child(_metadata, self->cgv2_fd, self->ctid, &ctx);
> + collect_cpuid_child(_metadata, pid, &ctx, &r);
> +
> + EXPECT_EQ(r.edx, self->host.edx & ~self->bit);
> +}
[Severity: Low]
Can this test pass on a kernel without the immediate-arming fix?
The comment says "No context switch between entering the VE and the
CPUID", but cpuid_child() only avoids voluntary blocking between
unshare(CLONE_NEWVE) and do_cpuid(). Nothing pins the child to a CPU or
prevents preemption, a timer tick, or a migration in that window.
Any such context switch would apply the CPUID-faulting state through
__switch_to_xtra(). The child would then see the table values even without
the fix.
The test is a single run with no retries, so it can miss the bug on a
loaded host. The later patches "x86/traps: decode the faulting CPUID with
the instruction decoder" and "selftests/ve: check CPUID override emulation
for a prefixed CPUID" add neither iterations nor pinning to this case.
Should the comment be softened, or should the test repeat the sequence?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-b4-prep-khorenko-vstor-148656-cpuid-v1-0-741609f4f03a@virtuozzo.com?part=3
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* [Devel] [PATCH hci-8.1 1/5] selftests/ve: check CPUID override emulation for VE tasks
@ 2026-10-06 14:56 3% ` Konstantin Khorenko
2026-10-06 15:07 3% ` no-reply
1 sibling, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-10-06 14:56 UTC (permalink / raw)
To: Pavel Tikhomirov, Vasileios Almpanis; +Cc: devel
Write a one-entry /proc/vz/cpuid_override table that clears one leaf 1
feature bit and compare what a task in a fresh VE and the host see:
the VE task must get the table values, the host the hardware ones.
A second test flushes the table after the task was armed and expects the
hardware values again, since faulting stays on for the task and the
emulation has to fall back to the real CPUID.
The child blocks before executing CPUID: faulting is switched on for a
task at its next context switch, not when the flag is set.
Without the fix both children die with SIGSEGV. The tests skip on CPUs
without CPUID faulting and restore the previous table on exit. The
teardown runs in the parent because the harness skips a child-side
teardown once an ASSERT has fired.
https://virtuozzo.atlassian.net/browse/VSTOR-148843
Feature: x86: cpu pools (cpuid override)
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/ve/Makefile | 1 +
.../testing/selftests/ve/ve_cpuid_override_test.c | 227 +++++++++++++++++++++
2 files changed, 228 insertions(+)
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index c6efe7c4b4fba..e7932408b1305 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -5,5 +5,6 @@ CFLAGS += -g -Wall -O2
TEST_GEN_PROGS += ve_ns_owner_test
TEST_GEN_PROGS += ve_perms_test
TEST_GEN_PROGS += ve_mount_accounting_test
+TEST_GEN_PROGS += ve_cpuid_override_test
include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_cpuid_override_test.c b/tools/testing/selftests/ve/ve_cpuid_override_test.c
new file mode 100644
index 0000000000000..c066d67d72a82
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_cpuid_override_test.c
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * /proc/vz/cpuid_override selftests
+ *
+ * A non-empty override table turns CPUID faulting on for every task that
+ * enters a VE, and the #GP handler has to emulate the instruction for
+ * them. The tests check that a task in a VE sees the table values, that
+ * the host keeps seeing the hardware values, and that a task which was
+ * armed while the table was set falls back to the hardware once the
+ * table is flushed. A kernel that does not emulate the fault kills the
+ * child with SIGSEGV instead.
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/wait.h>
+
+#include "../kselftest_harness.h"
+#include "ve_selftest.h"
+
+#define CPUID_OVERRIDE_PATH "/proc/vz/cpuid_override"
+#define CPUID_LEAF 1
+#define CPUID_TABLE_LEN 128
+#define CPUID_TABLE_MAX 4096
+
+struct cpuid_regs {
+ unsigned int eax, ebx, ecx, edx;
+};
+
+static void do_cpuid(unsigned int leaf, struct cpuid_regs *r)
+{
+ asm volatile("cpuid"
+ : "=a" (r->eax), "=b" (r->ebx), "=c" (r->ecx), "=d" (r->edx)
+ : "a" (leaf), "c" (0));
+}
+
+static int write_cpuid_override(const char *table)
+{
+ int fd, ret;
+
+ fd = open(CPUID_OVERRIDE_PATH, O_WRONLY);
+ if (fd < 0)
+ return -1;
+ ret = write(fd, table, strlen(table));
+ close(fd);
+ return ret == (int)strlen(table) ? 0 : -1;
+}
+
+/*
+ * The child enters a fresh VE, so it is armed with the table state of that
+ * moment, then reports its view of the leaf to the parent over a pipe. It
+ * blocks between the two steps: faulting is switched on for the task at its
+ * next context switch, and the parent may change the table meanwhile.
+ */
+struct child_ctx {
+ int result_pipe[2];
+ int go_pipe[2];
+};
+
+static int cpuid_child(struct child_ctx *ctx)
+{
+ struct cpuid_regs r;
+ char go;
+
+ close(ctx->result_pipe[0]);
+ close(ctx->go_pipe[1]);
+
+ if (unshare(CLONE_NEWVE) < 0)
+ return 21;
+ if (write(ctx->result_pipe[1], "R", 1) != 1)
+ return 22;
+ if (read(ctx->go_pipe[0], &go, 1) != 1)
+ return 23;
+
+ do_cpuid(CPUID_LEAF, &r);
+ if (write(ctx->result_pipe[1], &r, sizeof(r)) != sizeof(r))
+ return 24;
+ return 0;
+}
+
+FIXTURE(ve_cpuid_override)
+{
+ int cgv2_fd;
+ int ctid;
+ struct cpuid_regs host;
+ unsigned int bit;
+ char table[CPUID_TABLE_LEN];
+ char saved[CPUID_TABLE_MAX];
+};
+
+FIXTURE_SETUP(ve_cpuid_override)
+{
+ int fd;
+
+ fd = open(CPUID_OVERRIDE_PATH, O_RDONLY);
+ if (fd < 0)
+ SKIP(return, "%s: %s", CPUID_OVERRIDE_PATH, strerror(errno));
+ close(fd);
+
+ ASSERT_GE(read_file_at(AT_FDCWD, CPUID_OVERRIDE_PATH, self->saved,
+ sizeof(self->saved)), 0);
+
+ self->cgv2_fd = mount_cg2_fd();
+ ASSERT_GE(self->cgv2_fd, 0);
+ ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
+ VE_CONTROLLERS), 0);
+ self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
+ ASSERT_GE(self->ctid, 0);
+
+ /* Mask one feature bit the host has; SSE2 when present. */
+ do_cpuid(CPUID_LEAF, &self->host);
+ self->bit = 1u << 26;
+ if (!(self->host.edx & self->bit))
+ self->bit = self->host.edx & -self->host.edx;
+ ASSERT_NE(self->bit, 0);
+
+ snprintf(self->table, sizeof(self->table),
+ "0x%08x: 0x%08x 0x%08x 0x%08x 0x%08x\n", CPUID_LEAF,
+ self->host.eax, self->host.ebx, self->host.ecx,
+ self->host.edx & ~self->bit);
+}
+
+/*
+ * Runs in the parent: a child-side teardown is skipped when an ASSERT fires,
+ * and the table must be restored even then. The fd from setup belongs to the
+ * child, so the cgroup2 tree is mounted again here.
+ */
+FIXTURE_TEARDOWN_PARENT(ve_cpuid_override)
+{
+ int cgv2_fd;
+
+ write_cpuid_override(self->saved[0] ? self->saved : "\n");
+
+ cgv2_fd = mount_cg2_fd();
+ if (cgv2_fd < 0)
+ return;
+ destroy_ve(cgv2_fd, self->ctid);
+ close(cgv2_fd);
+}
+
+static pid_t spawn_cpuid_child(struct __test_metadata *_metadata,
+ int cgv2_fd, int ctid, struct child_ctx *ctx)
+{
+ char ready;
+ pid_t pid;
+
+ ASSERT_EQ(pipe(ctx->result_pipe), 0);
+ ASSERT_EQ(pipe(ctx->go_pipe), 0);
+ ASSERT_GE(enter_cgroup(cgv2_fd, ctid), 0);
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0)
+ _exit(cpuid_child(ctx));
+
+ ASSERT_GE(enter_cgroup(cgv2_fd, 0), 0);
+ close(ctx->result_pipe[1]);
+ close(ctx->go_pipe[0]);
+
+ ASSERT_EQ(read(ctx->result_pipe[0], &ready, 1), 1);
+ ASSERT_EQ(ready, 'R');
+ return pid;
+}
+
+static void collect_cpuid_child(struct __test_metadata *_metadata, pid_t pid,
+ struct child_ctx *ctx, struct cpuid_regs *r)
+{
+ int status, len;
+
+ len = read(ctx->result_pipe[0], r, sizeof(*r));
+ ASSERT_GE(waitpid(pid, &status, 0), 0);
+ if (WIFSIGNALED(status))
+ TH_LOG("child killed by signal %d", WTERMSIG(status));
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), 0);
+ ASSERT_EQ(len, sizeof(*r));
+
+ close(ctx->result_pipe[0]);
+ close(ctx->go_pipe[1]);
+}
+
+TEST_F(ve_cpuid_override, ve_sees_table_host_sees_hardware)
+{
+ struct child_ctx ctx;
+ struct cpuid_regs r;
+ pid_t pid;
+
+ ASSERT_EQ(write_cpuid_override(self->table), 0);
+
+ pid = spawn_cpuid_child(_metadata, self->cgv2_fd, self->ctid, &ctx);
+ ASSERT_EQ(write(ctx.go_pipe[1], "G", 1), 1);
+ collect_cpuid_child(_metadata, pid, &ctx, &r);
+
+ EXPECT_EQ(r.eax, self->host.eax);
+ EXPECT_EQ(r.ebx, self->host.ebx);
+ EXPECT_EQ(r.ecx, self->host.ecx);
+ EXPECT_EQ(r.edx, self->host.edx & ~self->bit);
+
+ do_cpuid(CPUID_LEAF, &r);
+ EXPECT_NE(r.edx & self->bit, 0);
+}
+
+TEST_F(ve_cpuid_override, flushed_table_falls_back_to_hardware)
+{
+ struct child_ctx ctx;
+ struct cpuid_regs r;
+ pid_t pid;
+
+ ASSERT_EQ(write_cpuid_override(self->table), 0);
+
+ pid = spawn_cpuid_child(_metadata, self->cgv2_fd, self->ctid, &ctx);
+
+ ASSERT_EQ(write_cpuid_override("\n"), 0);
+ ASSERT_EQ(write(ctx.go_pipe[1], "G", 1), 1);
+ collect_cpuid_child(_metadata, pid, &ctx, &r);
+
+ EXPECT_EQ(r.eax, self->host.eax);
+ EXPECT_NE(r.edx & self->bit, 0);
+}
+
+TEST_HARNESS_MAIN
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.0 v6 11/12] hcd-xhci: resync isochronous endpoints after CPR #VSTOR-137800
@ 2026-10-06 10:57 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:57 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev, den
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] usb-xhci: CPR iso resync fakes a missed-service error and drops the TD without fixing iso state
- [Low] usb-xhci: cpr_iso_resync flag is never cleared on idle, stop or reset, so a later in-sync iso TD is dropped
--
commit ff20d366f72942427c2474f20e5f7aa84692f640
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
hcd-xhci: resync isochronous endpoints after CPR #VSTOR-137800
This patch adds a cpr_iso_resync flag to XHCIEPContext. usb_xhci_post_load() sets it for running isochronous endpoints when CPR is incoming. The next isochronous TD submitted through xhci_submit() is then completed with CC_MISSED_SERVICE_ERROR without being sent to the device. The aim is to make the guest driver resync after resume.
> diff --git a/hw/usb/hcd-xhci.c b/hw/usb/hcd-xhci.c
> --- a/hw/usb/hcd-xhci.c
> +++ b/hw/usb/hcd-xhci.c
[ ... ]
> @@ -1811,6 +1813,19 @@ static int xhci_submit(XHCIState *xhci, XHCITransfer *xfer, XHCIEPContext *epctx
> xfer->pkts = 1;
> xfer->iso_xfer = true;
> xfer->timed_xfer = true;
> + if (epctx->cpr_iso_resync) {
> + /*
> + * First iso TD after a CPR resume: our transfer state was
> + * rebuilt from guest memory and no longer lines up with what
> + * the guest consumed. Report a missed service, like real HW,
> + * to kick the driver's skip logic into resyncing.
> + */
> + epctx->cpr_iso_resync = false;
> + xfer->status = CC_MISSED_SERVICE_ERROR;
> + xhci_xfer_report(xfer);
> + xfer->complete = 1;
> + return 0;
> + }
[Severity: Medium]
Can this early return leave the isochronous scheduling state unrebuilt?
The branch returns before xhci_calc_iso_kick() and
xhci_check_intr_iso_kick(), and before xhci_setup_packet() and
usb_handle_packet(). So epctx->mfindex_last is not updated for the
dropped TD and stays at its initial zero value. The TD is also never
passed to the device. An ISO OUT frame is lost, and an ISO IN buffer is
never filled.
Does the resync then depend entirely on the guest driver reacting to
CC_MISSED_SERVICE_ERROR? The commit message only describes the Linux
behaviour. Would other guests handle the synthetic error the same way?
[Severity: Low]
Can the flag stay set for longer than "the first isochronous transfer
after a CPR resume"?
The flag is consumed only here in xhci_submit(), on whichever isochronous
TD is submitted next. Consider xhci_kick_epctx():
length = xhci_ring_chain_length(xhci, ring);
if (length <= 0) {
if (epctx->type == ET_ISO_OUT || epctx->type == ET_ISO_IN) {
... CC_RING_OVERRUN / CC_RING_UNDERRUN event
}
break;
}
If the ring is empty at resume time, the loop breaks before xhci_submit().
The same happens if xhci_slot_ok() fails or the endpoint is halted.
The flag then survives the resume kick. The stop, reset and set-dequeue
paths do not clear it either.
Can a later isochronous TD that is already in sync then be dropped with a
spurious CC_MISSED_SERVICE_ERROR?
[ ... ]
> @@ -3510,6 +3525,11 @@ static int usb_xhci_post_load(void *opaque, int version_id)
> xhci_init_epctx(epctx, pctx, ep_ctx);
> epctx->state = state;
> if (state == EP_RUNNING) {
> + if (cpr_is_incoming() &&
> + (epctx->type == ET_ISO_IN || epctx->type == ET_ISO_OUT)) {
> + /* only iso streams need resyncing; see xhci_submit() */
> + epctx->cpr_iso_resync = true;
> + }
[Severity: Medium]
Why is the resync restricted to cpr_is_incoming()?
usb_xhci_post_load() rebuilds the endpoint contexts through
xhci_alloc_epctx() and xhci_init_epctx() in the same way for ordinary
live migration. The commit message says that "we rebuild the controller's
transient transfer and scheduling state out of guest memory" across CPR.
A normal-migration destination gets the same rebuilt state but no resync.
Could the commit message explain why only CPR is affected?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=11
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.0 v6 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
2026-10-06 10:34 10% ` [PATCH QEMU HCI-8.0 v6 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:57 6% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:57 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: andrey.drobyshev, den
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] usb-host: CPR post_load fails on an unconfigured device (NOT_FOUND treated as fatal)
- [Medium] usb-host: re-claim loop uses array index instead of bInterfaceNumber
- [Medium] usb-host: device-supplied bInterfaceNumber indexes altsetting[16] unchecked; ninterfaces unclamped
- [Low] usb-host: usb_host_ep_update() result ignored on CPR path, so partial rebuild reports success
--
commit 7fc1fbcbe45c4e899a6393d8acc4e2e9151dac29
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
This commit makes the CPR incoming path in usb_host_post_load() re-claim the
interfaces through libusb and call usb_host_ep_update(). The goal is to
rebuild the endpoints for a non-zero altsetting restored by vmload. It also
adds an "alt >= 0" check to an assert in usb_host_ep_update().
> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c
[ ... ]
> +static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
> +{
> + USBDevice *udev = USB_DEVICE(s);
> + struct libusb_config_descriptor *conf;
> + int n, rc;
> +
> + rc = libusb_get_active_config_descriptor(s->dev, &conf);
> + if (rc != 0) {
> + usb_host_libusb_error("libusb_get_active_config_descriptor", rc);
> + return -1;
> + }
[Severity: Medium]
Can this abort the whole incoming CPR vmload for a device that has no active
configuration?
For a device in the address state (for example after SET_CONFIGURATION(0) or
a device reset), libusb_get_active_config_descriptor() returns
LIBUSB_ERROR_NOT_FOUND. Here any rc != 0 returns -1, and
usb_host_post_load() propagates it:
if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
return -1;
}
Before this patch the CPR branch always returned 0. The existing
usb_host_claim_interfaces() treats the same condition as benign:
if (rc == LIBUSB_ERROR_NOT_FOUND) {
/* address state - ignore */
return USB_RET_SUCCESS;
}
and usb_host_ep_update() simply returns. Should NOT_FOUND be tolerated here
as well?
Also, if the host still has an active configuration but the guest never
configured the device, does this function claim every interface and then
overwrite the guest-visible state?
udev->ninterfaces = conf->bNumInterfaces;
udev->configuration = conf->bConfigurationValue;
The source had both of these at 0. The later patch "usb-host: make CPR
migration blocker conditional" only changes the blocker and does not touch
this helper.
> +
> + for (int i = 0; i < conf->bNumInterfaces; i++) {
> + int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
> + int alt = udev->altsetting[ifnum];
[Severity: Medium]
Can this read past the end of udev->altsetting[]?
ifnum comes from the device's bInterfaceNumber (a uint8_t, up to 255), while
altsetting[] has USB_MAX_INTERFACES (16) entries. This validation loop runs
over all conf->bNumInterfaces entries with no bound on ifnum.
The claim loop below is clamped with MIN(conf->bNumInterfaces,
USB_MAX_INTERFACES), but the function then stores the unclamped value and
returns success:
udev->ninterfaces = conf->bNumInterfaces;
Can a device with more than 16 interfaces then trip this assert in a later
usb_desc_set_config()?
assert(dev->ninterfaces <= USB_MAX_INTERFACES);
usb_host_claim_interfaces() would have returned a stall in that case.
usb_host_ep_update(), which is now also called from the CPR path, has the
same unchecked access:
alt = udev->altsetting[intf->bInterfaceNumber];
This isn't a bug introduced by this patch, but the new call site makes it
reachable on the CPR path. The patch only adds "alt >= 0" to the assert
there.
> +
> + if (alt < 0 || alt >= conf->interface[i].num_altsetting) {
> + error_report("usb-host: migrated altsetting %d out of range for "
> + "interface %d", alt, ifnum);
> + libusb_free_config_descriptor(conf);
> + return -1;
> + }
> + }
> +
> + n = MIN(conf->bNumInterfaces, USB_MAX_INTERFACES);
> + for (int i = 0; i < n; i++) {
> + rc = libusb_claim_interface(s->dh, i);
> + if (rc != 0) {
> + usb_host_libusb_error("libusb_claim_interface", rc);
> + libusb_free_config_descriptor(conf);
> + return -1;
> + }
> + s->ifs[i].claimed = true;
> + }
[Severity: Medium]
Is the loop index i the right value to pass to libusb_claim_interface()?
i is the position in conf->interface[], but libusb and usbfs expect the
interface number, which is altsetting[0].bInterfaceNumber. The validation
loop above already uses bInterfaceNumber for this. The two can differ for
devices whose interface numbers don't start at 0 or aren't contiguous, such
as {0,2} or {1,2}.
Claiming a non-existent number fails, so the function returns -1 and the
migration aborts. Where the claim does succeed, the real interfaces are not
recorded in libusb's claimed set, so a later SET_INTERFACE can still fail
with LIBUSB_ERROR_NOT_FOUND. That is the problem this patch sets out to fix.
s->ifs[i].claimed is also indexed by position, so usb_host_release_interfaces()
would act on the wrong numbers.
By contrast, usb_host_claim_interfaces() probes by interface number and
tolerates gaps:
for (i = 0; i < USB_MAX_INTERFACES; i++) {
trace_usb_host_claim_interface(s->bus_num, s->addr, configuration, i);
rc = libusb_claim_interface(s->dh, i);
if (rc == 0) {
s->ifs[i].claimed = true;
Should the new code claim by bInterfaceNumber and index ifs[] the same way?
The later patches in the series do not change this.
[ ... ]
> if (cpr_is_incoming()) {
> + /*
> + * The device stayed open, so rebuild libusb's claims and endpoints,
> + * and start reaping now that the source has handed the fd off.
> + */
> + if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
> + return -1;
> + }
> + usb_host_ep_update(dev);
> usb_host_cpr_restart_events();
> return 0;
> }
[Severity: Low]
Should the result of usb_host_ep_update() be checked here?
usb_host_ep_update() is void. After usb_ep_reset() it returns silently on
"invalid endpoint address", "duplicate endpoint address", or a missing
active configuration, which leaves a partial endpoint table.
usb_host_post_load() then returns 0, so guest transfers to the missing
endpoints would stall without any report.
usb_host_ep_update() also calls usb_ep_set_halted(udev, pid, ep, 0) for every
endpoint, so any halted state the guest had on the source is lost, since it
is not migrated. This second point may be speculative. The failure
conditions would normally already have occurred in usb_host_open() on the
same device, so this is probably a rare edge case.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=10
^ permalink raw reply [relevance 6%]
* Re: [PATCH QEMU HCI-8.0 v6 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800
2026-10-06 10:34 29% ` [PATCH QEMU HCI-8.0 v6 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:54 6% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:54 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] usb-host: cpr_handed_off is set before the handoff outcome is known and never cleared on close
- [Medium] usb-host: target restarts URB reaping in post_load() before migration is committed
- [Medium] usb-host: per-device CPR handoff stops and restarts events for the whole libusb context
- [Low] usb-host: commit message says .exit() closes the FD, but exit notifier only calls libusb_close()
--
commit d075dcc6f67ba19d35e41dafb7069f178e496bbf
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
usb-host: hand off the device across cpr-transfer #VSTOR-137800
This patch makes the usb-host device hand off URB reaping together with the
usbfs fd across cpr-transfer. The target stops reaping until post_load. The
source stops reaping when it quiesces, restarts on resume after a failure,
and skips device teardown at exit after a successful handoff.
> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c
[ ... ]
> +static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
> +{
> + if (usb_host_cpr_drain_xfers(s) < 0) {
> + return -1;
> + }
> + if (mode == MIG_MODE_CPR_TRANSFER) {
> + usb_host_cpr_stop_events();
> + s->cpr_handed_off = true;
> + }
> + return 0;
> +}
[Severity: Medium]
Is cpr_handed_off being set too early here? It is set from the vm-state
FINISH_MIGRATE branch or from usb_host_pre_save(), before the outcome of
the migration is known. The flag then means "handed off, awaiting outcome"
rather than "handoff succeeded".
The only place that clears it is usb_host_cpr_reissue(), which runs only
when the VM resumes with cpr_drained set and s->dh non-NULL.
Consider a failed or cancelled cpr-transfer where the VM stays paused, and
the user then quits or does device_del. Both usb_host_exit_notifier() and
usb_host_close() take the skip path:
if (!s->cpr_handed_off) {
usb_host_abort_xfers(s);
...
usb_host_attach_kernel(s);
}
The source is still the only owner of the device in that case. Does this
leave the interfaces claimed, skip libusb_reset_device(), and never rebind
the host driver? In usb_host_close() the guest port also stays attached,
and the iso rings are not freed.
usb_host_close() also sets s->dh = NULL without clearing the flag. After
that, usb_host_cpr_vm_state() returns early on !s->dh, so
usb_host_cpr_reissue() can never clear it. Would a later reopen of the same
USBHostDevice through autoscan then inherit cpr_handed_off == true and
cpr_drained?
Could the flag be cleared in usb_host_close(), or the skip decision be
based on a state that is only set once the migration has completed?
[ ... ]
> @@ -1344,10 +1365,17 @@ static void usb_host_exit_notifier(struct Notifier *n, void *data)
> USBHostDevice *s = container_of(n, USBHostDevice, exit);
>
> if (s->dh) {
> + /*
> + * Handed to new QEMU across cpr-transfer: do not reset, release
> + * interfaces or rebind the host driver underneath it; just close
> + * our handle.
> + */
> + if (!s->cpr_handed_off) {
> + usb_host_abort_xfers(s);
> + usb_host_release_interfaces(s);
> + libusb_reset_device(s->dh);
> + usb_host_attach_kernel(s);
> + }
> libusb_close(s->dh);
> }
> }
[Severity: Low]
This isn't a bug, but the commit message says "On success the source's
.exit() leaves the device untouched ... and just closes its own copy of the
FD."
usb_host_exit_notifier() only calls libusb_close(s->dh). That does not
close the application-owned usbfs fd of a libusb_wrap_sys_device() handle.
s->hostfd is closed only in usb_host_close(). Could the message be
corrected?
The message also says "The source stops reaping in .pre_save()". Normally
this happens in the vm-state handler through usb_host_cpr_quiesce().
pre_save only does it as a fallback when the VM was already paused.
[ ... ]
> @@ -2000,6 +2042,7 @@ static int usb_host_post_load(void *opaque, int version_id)
> * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
> */
> if (cpr_is_incoming()) {
> + usb_host_cpr_restart_events();
> return 0;
> }
[Severity: Medium]
Can the target start reaping too early here? The target calls
usb_host_cpr_restart_events() when the device section is loaded, not when
migration has completed.
Suppose another device section fails to load, or the channel breaks, while
the target is still alive. The source then resumes, and usb_host_cpr_reissue()
calls usb_host_cpr_restart_events() on its side:
if (s->cpr_handed_off) {
usb_host_cpr_restart_events();
s->cpr_handed_off = false;
}
Both processes could then reap URB completions on the shared usbfs fd. That
is the isochronous wedge this patch is meant to prevent. Is anything making
the target's takeover conditional on the source having committed? This
window is narrow and depends on the target surviving a failed migration.
[ ... ]
> +static void usb_host_cpr_stop_events(void)
> +{
> + const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
> +
> + libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
> + if (poll) {
> + for (int i = 0; poll[i] != NULL; i++) {
> + usb_host_del_fd(poll[i]->fd, ctx);
> + }
> + free(poll);
> + }
> +}
> +
> +static void usb_host_cpr_restart_events(void)
> +{
> + const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
> +
> + libusb_set_pollfd_notifiers(ctx, usb_host_add_fd, usb_host_del_fd, ctx);
> + if (poll) {
> + for (int i = 0; poll[i] != NULL; i++) {
> + usb_host_add_fd(poll[i]->fd, poll[i]->events, ctx);
> + }
> + free(poll);
> + }
> +}
[Severity: Medium]
Do these helpers act on more than the one device being handed off? Both
call libusb_set_pollfd_notifiers(ctx, ...) and walk all of
libusb_get_pollfds(ctx) on the static libusb context, while they are driven
from per-device hooks (usb_host_realize(), usb_host_post_load(),
usb_host_pre_save() and the vm-state handler) and the handoff state
(cpr_handed_off) is per device.
With more than one hostdevice= usb-host device:
On the target, the first device's usb_host_post_load() calls
usb_host_cpr_restart_events() and re-enables reaping for every device,
including devices whose post_load has not yet run and whose source side may
not be handed off.
On the source, the first device's usb_host_cpr_reissue() after a failure
restarts events for devices that are still handed off.
While the notifiers are NULL, fds of devices realized or hot-added in that
window are never registered either.
Is the multi-device case intended to be supported? The conditional blocker
added later in the series ("usb-host: make CPR migration blocker
conditional") still seems to allow several hostdevice= devices with ids.
[ ... ]
> @@ -1455,6 +1489,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
> error_setg(errp, "failed to open host usb device %s", s->hostdevice);
> return;
> }
> + if (cpr_is_incoming()) {
> + /*
> + * The source still owns the shared fd until it hands off. Don't
> + * reap URBs on it yet, or we would steal the source's in-flight
> + * completions; .post_load() restarts events once we take over.
> + */
> + usb_host_cpr_stop_events();
> + }
In usb_host_realize() on the target, does this disable event handling for
the whole libusb context whenever any hostdevice= device is realized? See
the previous comment on usb_host_cpr_stop_events().
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=8
^ permalink raw reply [relevance 6%]
* Re: [PATCH QEMU HCI-8.0 v6 09/12] usb: migrate the interface altsetting #VSTOR-137800
@ 2026-10-06 10:52 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:52 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] usb: altsetting subsection in generic vmstate_usb_device has no consumer for emulated devices
--
commit 6d71f98dc9c0687ea03b267e596a32a451816b68
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
usb: migrate the interface altsetting #VSTOR-137800
This commit adds a vmstate subsection to the generic vmstate_usb_device
that migrates the USBDevice altsetting[] array. The subsection is emitted
only in the CPR migration modes and only when some interface is on a
non-zero altsetting.
> diff --git a/hw/usb/bus.c b/hw/usb/bus.c
> --- a/hw/usb/bus.c
> +++ b/hw/usb/bus.c
[ ... ]
> +static bool usb_altsetting_needed(void *opaque)
> +{
> + USBDevice *dev = opaque;
> + int i;
> +
> + /*
> + * Only the CPR target consumes this, to rebuild its endpoints; other
> + * migrations reopen the device and reconstruct the altsetting.
> + */
> + if (!cpr_incoming_needed(NULL)) {
> + return false;
> + }
> + for (i = 0; i < USB_MAX_INTERFACES; i++) {
> + if (dev->altsetting[i]) {
> + return true;
> + }
> + }
> + return false;
> +}
> +
> +static const VMStateDescription vmstate_usb_device_altsetting = {
> + .name = "USBDevice/altsetting",
[Severity: Low]
Is this gate specific enough to the devices that consume the state?
usb_altsetting_needed() checks only cpr_incoming_needed() and whether
some dev->altsetting[i] is non-zero. It does not check whether the
device is a usb-host device.
The subsection lives in the generic vmstate_usb_device, so an emulated
USB device that supports SET_INTERFACE and sits on a non-zero altsetting
(an audio-style device, for example) will also emit and load
altsetting[] in CPR modes.
The generic usb_device_post_load() only recomputes dev->attached:
if (dev->state == USB_STATE_NOTATTACHED) {
dev->attached = false;
} else {
dev->attached = true;
}
Nothing there rebuilds dev->ifaces[] or calls usb_desc_ep_init() from
the loaded altsetting[]. On the CPR target such a device could report
the restored altsetting via GET_INTERFACE while its endpoints still
follow altsetting 0.
No emulated device that triggers this today has been identified, so this
is speculative.
The later patch in the series, "usb-host: reconfigure endpoints on CPR
incoming", adds usb_host_cpr_reclaim_interfaces(), which range-checks the
migrated altsetting. It also adds a usb_host_ep_update() call in
usb_host_post_load() and changes the assert in usb_host_ep_update() to
check alt >= 0. That covers the usb-host consumer and the unvalidated
index concern. The series adds no consumer for non-usb-host devices.
Should the subsection be restricted to usb-host devices, or should the
generic post_load rebuild the interface and endpoint state from
altsetting[]?
> + .version_id = 1,
> + .minimum_version_id = 1,
> + .needed = usb_altsetting_needed,
> + .fields = (const VMStateField[]) {
> + VMSTATE_INT32_ARRAY(altsetting, USBDevice, USB_MAX_INTERFACES),
> + VMSTATE_END_OF_LIST(),
> + }
> +};
[ ... ]
> + .subsections = (const VMStateDescription * const []) {
> + &vmstate_usb_device_altsetting,
> + NULL
> }
> };
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=9
^ permalink raw reply [relevance 3%]
* Re: [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800
2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:48 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:48 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: den, andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Medium] usb-host: xHCI replay of suppressed URBs relies on a dequeue pointer that is already advanced
- [Medium] usb-host: pre_save fallback drain can deliver completions after xHCI state is saved
- [Medium] usb-host: iso drain cancels in-flight URBs once but completions can resubmit new ones
- [Medium] usb-host: a cancelled OUT or control URB that moved data is reported as a short success
- [Medium] usb-host: drain timeout path can leave newly submitted URBs live and untracked
--
commit e9cd7ce570abec8bcd0aa45a09ea274defa372c5
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
usb-host: drain in-flight URBs across CPR #VSTOR-137800
This patch registers a vm_change_state handler that cancels and reaps all
pending libusb transfers when the VM stops for CPR. It also adds a
.pre_save() fallback for VMs that are already paused, and cancels and
reaps isochronous URBs as well. Cancelled URBs that moved no data are
suppressed so the target can replay them.
> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c
[ ... ]
> +/*
> + * Classify a reap during the CPR drain (r->p is still set). One that moved no
> + * data is suppressed for the target to replay; one that moved some is kept as a
> + * short transfer. Returns true if suppressed, so the caller frees the request.
> + */
> +static bool usb_host_req_cpr_replay(USBHostRequest *r,
> + struct libusb_transfer *xfer)
> +{
> + if (xfer->status != LIBUSB_TRANSFER_CANCELLED) {
> + return false;
> + }
[Severity: Medium]
The commit message says the .pre_save() fallback suppresses "every URB for
the target to replay". Is that accurate?
In usb_host_req_cpr_replay(), any status other than
LIBUSB_TRANSFER_CANCELLED returns false, so a URB that completed in the
kernel during the fallback drain is still delivered:
usb_host_req_complete_data()
-> usb_packet_complete()
Can this run after the xHCI and interrupt controller state has already
been saved? usb_host_pre_save() runs as part of device-state save, not
before the snapshot as the vm-state handler does. If so, event ring,
interrupter and endpoint state changed by that completion might be missing
from the migrated image.
> + if (xfer->actual_length != 0 && !r->host->cpr_replay_all) {
> + /* deliver as a short transfer */
> + xfer->status = LIBUSB_TRANSFER_COMPLETED;
> + return false;
> + }
> + return true;
> +}
[Severity: Medium]
Can this corrupt an OUT stream? The status is rewritten to
LIBUSB_TRANSFER_COMPLETED for every direction and transfer type, with no
check of r->in or the control stage.
For a bulk or interrupt OUT, or a control write, that was cancelled after
moving some bytes, the device has consumed a fragment. The guest is then
told the transfer ended short, and the target can neither replay it nor
see an error. A CBW or a data stage could be affected this way.
Should this conversion be limited to IN transfers?
[Severity: Medium]
When usb_host_req_cpr_replay() returns true, the completion is suppressed
and the request is freed. The replay then relies on the xHCI endpoint
context in guest RAM still pointing at the start of this TD.
Is that the case when several TDs are outstanding on one endpoint?
xhci_kick_epctx() advances ring->dequeue via xhci_ring_fetch() for each TD
it fires. xhci_complete() calls xhci_kick_epctx(), which calls
xhci_set_ep_state(EP_RUNNING) at its start:
xhci_complete()
-> xhci_kick_epctx()
-> xhci_set_ep_state()
ctx[2] = ring->dequeue | ring->ccs;
That writes the advanced dequeue to guest memory while later TDs are
still queued or in flight. usb_xhci_post_load() rebuilds the ring from
that value. Would a suppressed TD2 then be skipped on the target, so that
its completion event is never generated?
[ ... ]
> +/*
> + * Quiesce the device for a CPR switchover. Cancel and reap all URBs,
> + * so that no URB submitted by this process can outlive it.
> + */
> +static int usb_host_cpr_drain_xfers(USBHostDevice *s)
> +{
[ ... ]
> + if (--limit == 0) {
> + /*
> + * The device won't release these URBs. Error the packets out so
> + * the resumed guest retries them, leak the requests (libusb still
> + * owns their transfers) and abort the migration.
> + */
> + QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
> + usb_host_req_complete_err(r, USB_RET_IOERROR);
> + }
> + usb_host_drop_requests(s);
> + return -1;
> + }
[Severity: Medium]
On this timeout path, can usb_host_drop_requests() orphan live URBs?
usb_host_req_complete_err() calls usb_packet_complete(), which can process
queued packets on the endpoint:
usb_host_req_complete_err()
-> usb_packet_complete()
-> usb_process_one()
-> usb_host_handle_data() / control handler
-> libusb_submit_transfer()
Those paths append new entries to s->requests. usb_host_drop_requests()
then unlinks every entry without cancelling it, so the new URBs stay live
on the preserved fd and are never reaped.
> + /*
> + * Iso URBs are on the rings, not s->requests. Cancel them: one in
> + * flight when the guest stops may never complete on its own, and none
> + * may be left on the preserved fd. The target replays from the ring.
> + */
> + QTAILQ_FOREACH(ring, &s->isorings, next) {
> + QTAILQ_FOREACH(xfer, &ring->inflight, next) {
> + libusb_cancel_transfer(xfer->xfer);
> + }
> + }
> +
> + /*
> + * Cap the reap at 2x a full ring:
> + * iso_urb_count URBs x iso_urb_frames packets each
> + */
> + limit = 2 * s->iso_urb_count * s->iso_urb_frames;
> + while (usb_host_iso_inflight(s)) {
> + struct timeval tv = { .tv_usec = 1000 };
> + libusb_handle_events_timeout(ctx, &tv);
> + if (limit-- == 0) {
> + return -1;
> + }
> + }
[Severity: Medium]
Are the iso URBs cancelled only once here? Completing a cancelled IN iso
URB runs usb_host_req_complete_iso(), which does:
QTAILQ_INSERT_TAIL(&xfer->ring->copy, xfer, next);
usb_wakeup(xfer->ring->ep, 0);
That can reach xhci_wakeup_endpoint(), then a kick, then
usb_host_iso_data_in(), which submits every xfer on ring->unused into
ring->inflight. The loop above never re-cancels those, and the bulk loop
earlier in usb_host_cpr_drain_xfers() has already finished, so it does not
recheck s->requests.
Can this either time out and return -1, which aborts the migration, or
leave freshly submitted iso or bulk URBs live across exec?
[ ... ]
> +static int usb_host_pre_save(void *opaque)
> +{
[ ... ]
> + if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
> + s->dh && !s->cpr_drained) {
> + s->cpr_replay_all = true;
> + if (usb_host_cpr_drain_xfers(s) < 0) {
> + return -1;
> + }
> + }
[Severity: Medium]
Is it safe to run the drain from usb_host_pre_save()? Packet completions
raised here can modify xHCI state that may already have been serialized,
depending on the order of the xHCI and usb-host vmstate entries in the
save sequence. Could that ordering be confirmed, or the drain be moved
earlier?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=5
^ permalink raw reply [relevance 5%]
* Re: [PATCH QEMU HCI-8.0 v6 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800
2026-10-06 10:34 22% ` [PATCH QEMU HCI-8.0 v6 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:47 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:47 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: den, andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] usb-host: cpr_is_incoming() guard in usb_host_open() skips product string for all opens
- [Low] usb-host: CPR product-string skip is an uncommented workaround for an unexplained hang
--
commit b936df5f0185ffcfa3c56baf845d05beba3ac351
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
usb-host: skip product-string read on CPR incoming #VSTOR-137800
This patch skips the synchronous libusb product string read in
usb_host_open() when the process is the CPR target. In that case it uses
the synthetic "host:bus.addr" name for product_desc instead.
> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> index 85509dc2d83e..020a685aa419 100644
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c
[ ... ]
> @@ -1106,7 +1106,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
> udev->speed = speed_map[libusb_speed];
> usb_host_speed_compat(s);
>
> - if (s->ddesc.iProduct) {
> + if (s->ddesc.iProduct && !cpr_is_incoming()) {
[Severity: Low]
Is this guard broader than the CPR hostfd hand-over it is meant to
cover?
cpr_is_incoming() returns incoming_mode != MIG_MODE_NONE, which is a
process-wide flag that stays set for the whole incoming migration. It
is not a per-device indicator.
Any usb_host_open() call in that window therefore takes the synthetic
name branch. That includes the autoscan path:
usb_host_auto_check()
usb_host_open(s, devs[i], 0)
It also includes the explicit bus/addr realize path. Neither of these
is the hostfd hand-over described in the commit message.
Since product_desc is only written in usb_host_open(), would those
devices keep the "host:bus.addr" name after migration completes?
Could the check be narrowed to the device that was actually handed over
by CPR, for example by keying off hostfd?
[Severity: Low]
This isn't a functional problem, but could a comment be added here
explaining why the product string read is skipped on the CPR target?
The commit message says only that the transfer "can block". The cause
is not recorded in the code, and other cpr_is_incoming() checks in this
file carry comments.
It is also unclear whether the other libusb calls that still run in
usb_host_open() before usb_device_attach() can block in the same way:
libusb_wrap_sys_device()
usb_host_detach_kernel()
usb_host_ep_update()
usb_host_speed_compat()
> libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
> (unsigned char *)udev->product_desc,
> sizeof(udev->product_desc));
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=7
^ permalink raw reply [relevance 5%]
* Re: [PATCH QEMU HCI-8.0 v6 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
2026-10-06 10:34 23% ` [PATCH QEMU HCI-8.0 v6 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:46 6% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:46 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev, den
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] usb-host: cpr_reissue() completes failed combined IN packets with plain usb_packet_complete()
- [Medium] usb-host: cpr_vm_state() returns on NULL dh before reissue, leaving CPR state stale
- [Medium] usb-host: replay from offset 0 under cpr_replay_all duplicates OUT data and loses IN data
--
commit cf25e5a0b05e40289cd96941160f3044c5ee49c9
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
This patch records the packets suppressed during the CPR drain on a new
cpr_inflight list. If CPR is aborted, the vm-state handler's resume
branch re-issues them on the preserved fd. Control transfers are
completed with an error so that the guest driver retries them.
> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c
[ ... ]
> @@ -446,6 +448,9 @@ static bool usb_host_req_cpr_replay(USBHostRequest *r,
> xfer->status = LIBUSB_TRANSFER_COMPLETED;
> return false;
> }
> + if (r->p->state == USB_PACKET_ASYNC) {
> + r->host->cpr_inflight = g_slist_append(r->host->cpr_inflight, r->p);
> + }
> return true;
> }
[Severity: Medium]
When cpr_replay_all is set by usb_host_pre_save(), a cancelled URB with
a non-zero actual_length is also suppressed and queued here:
if (xfer->actual_length != 0 && !r->host->cpr_replay_all) {
...
}
If CPR is then aborted, usb_host_cpr_reissue() restarts that packet from
offset 0:
/* Replay from the start of the TD */
p->actual_length = 0;
Can this duplicate data? For an OUT endpoint, the part already sent to
the device would be sent again, since usb_host_handle_data() copies the
whole payload and resubmits it. For an IN endpoint, the bytes already
read from the device would be discarded, so the guest would never see
them.
Also, cpr_inflight is filled in libusb reap order. Is that the same as
guest submission order when several URBs are pipelined on one endpoint?
I could not confirm this from the code.
The commit message does not mention this limitation.
[ ... ]
> @@ -1254,17 +1259,26 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
> static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
> {
> USBHostDevice *s = opaque;
> MigMode mode;
>
> - if (running || !s->dh || state != RUN_STATE_FINISH_MIGRATE) {
> + if (!s->dh) {
> + return;
> + }
> + if (running) {
> + usb_host_cpr_reissue(s);
> + return;
> + }
> + if (state != RUN_STATE_FINISH_MIGRATE) {
> return;
> }
[Severity: Medium]
Can this early return on a NULL dh leave stale CPR state? Before this
patch the !s->dh check only guarded the drain. It now also skips
usb_host_cpr_reissue() on resume.
Consider this sequence:
usb_host_cpr_drain_xfers()
suppresses requests and queues their packets on cpr_inflight
usb_host_nodev_bh() or unplug
usb_host_close()
s->dh = NULL
resume
usb_host_cpr_vm_state(running)
if (!s->dh) return;
Here cpr_drained and cpr_replay_all stay set. A later CPR attempt then
skips the drain, because usb_host_pre_save() tests
"s->dh && !s->cpr_drained".
In the base patch, usb_host_close() calls usb_device_detach(). The HCD
then cancels the packets through usb_host_cancel_packet(), which drops
them from cpr_inflight, so the packets are not stranded in that case.
With the later patch "usb-host: hand off the device across
cpr-transfer", usb_host_close() skips usb_host_abort_xfers() and
usb_device_detach() when cpr_handed_off is set. In cpr-transfer mode, a
device loss between the drain and the resume would then leave the guest
packets ASYNC with no request behind them and nothing to complete them.
cpr_handed_off would also stay true.
Should the resume branch run the reissue and the state reset even when
dh is NULL?
[ ... ]
> @@ -1356,6 +1370,45 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
> +static void usb_host_cpr_reissue(USBHostDevice *s)
> +{
[ ... ]
> + } else {
> + usb_host_handle_data(udev, p);
> + if (p->status != USB_RET_ASYNC) {
> + /* Completed synchronously with error */
> + usb_packet_complete(udev, p);
> + }
> + }
[Severity: Medium]
Does this completion handle combined packets correctly?
usb_host_handle_data() can return a non-ASYNC status synchronously:
- USB_RET_NODEV when libusb_submit_transfer() fails
- USB_RET_STALL on a halted endpoint
- USB_RET_STALL when streams are unsupported
For a bulk IN endpoint where usb_host_use_combining() is true, the
packet on cpr_inflight is the first packet of a USBCombinedPacket. A
plain usb_packet_complete() then completes only that first packet.
The normal completion path in usb_host_req_complete_data() does this
instead:
if (usb_host_use_combining(r->p->ep)) {
usb_combined_input_packet_complete(USB_DEVICE(s), r->p);
} else {
usb_packet_complete(USB_DEVICE(s), r->p);
}
Can the other sub-packets be left in USB_PACKET_ASYNC with p->combined
still set, and combined->first pointing at an already completed packet?
That could wedge the pipelined bulk-IN endpoint.
The existing usb_host_req_complete_err() uses the same plain call, but
this reissue error path is new in this patch. The later patch "usb-host:
make CPR migration blocker conditional" does not change this code.
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=6
^ permalink raw reply [relevance 6%]
* Re: [PATCH QEMU HCI-8.0 v6 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
2026-10-06 10:34 31% ` [PATCH QEMU HCI-8.0 v6 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:37 7% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-06 10:37 UTC (permalink / raw)
To: svt-core, Andrey Drobyshev; +Cc: andrey.drobyshev, den
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] usb-host: usb_host_open() stores hostfd before early checks that skip the new close
--
commit 0fc31623dd0ee74837ec2c01d3f3ced2e30b7f95
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
usb-host: don't leak hostdev FD on open failure #VSTOR-137800
This patch stores the caller-supplied hostfd in s->hostfd at the top of
usb_host_open() when no libusb_device is given. It also closes s->hostfd
on the fail: path, so the FD is released when libusb_wrap_sys_device() or
a later open step fails.
> diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
> index b74670ae25696..83645fbe0ced0 100644
> --- a/hw/usb/host-libusb.c
> +++ b/hw/usb/host-libusb.c
> @@ -954,6 +954,11 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
> int rc;
> Error *local_err = NULL;
>
> + /* Make sure failure path frees caller-owned FD */
> + if (!dev) {
> + s->hostfd = hostfd;
> + }
> +
> if (s->bh_postld_pending) {
> return -1;
> }
> if (s->dh != NULL) {
> goto fail;
> }
[Severity: Medium]
Can this leak the FD when usb_host_open() returns early?
The store of s->hostfd happens before the bh_postld_pending check:
if (!dev) {
s->hostfd = hostfd;
}
if (s->bh_postld_pending) {
return -1;
}
That early return bypasses the fail: label and its new close(s->hostfd)
block. The caller-owned FD is then neither closed nor released, and
s->hostfd keeps holding it.
usb_host_realize() does not close the FD either. It only calls
error_setg() and returns:
rc = usb_host_open(s, NULL, fd);
if (rc < 0) {
error_setg(errp, "failed to open host usb device %s", s->hostdevice);
return;
}
The s->dh != NULL case has a related problem. It does jump to fail:, but
s->hostfd has already been overwritten. The FD that s->hostfd previously
owned is lost, and the new caller's FD is closed instead.
Today the only caller that passes a real FD is usb_host_realize(). There
s->hostfd starts at -1 and s->dh is NULL, so both early exits look
unreachable and the problem is latent. I also checked the end of the
series. The store is still placed before the bh_postld_pending check, so
a later patch does not fix this.
Would it be better to assign s->hostfd only after those two early checks?
Alternatively, the early checks could be routed through the fail: path in
a way that does not clobber an FD that is already owned.
> @@ -978,7 +983,6 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
> if (rc != 0) {
> goto fail;
> }
> - s->hostfd = hostfd;
> dev = libusb_get_device(s->dh);
[ ... ]
> @@ -1066,6 +1070,10 @@ fail:
> s->dh = NULL;
> s->dev = NULL;
> }
> + if (s->hostfd != -1) {
> + close(s->hostfd);
> + s->hostfd = -1;
> + }
> return -1;
> }
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com?part=1
^ permalink raw reply [relevance 7%]
* [PATCH QEMU HCI-8.0 v6 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800
` (10 preceding siblings ...)
@ 2026-10-06 10:34 20% ` Andrey Drobyshev
11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
A previous commit added an unconditional blocker for CPR migrations.
That is to refuse CPR gracefully instead of crashing and for the previous
commits to remain bisectable.
Now the restore path is complete: the FD is preserved, in-flight URBs
are drained and replayed, the device is handed off across cpr-transfer,
and the endpoints and isochronous streams are reconstructed on the
target. So let's lift the CPR blocker for the devices which satisfy the
preconditions.
Still, the remaining preconditions for CPR to work are:
* We must open host device FD ourselves, so that libusb owns the FD.
This way we're able to preserve the FD during CPR;
* Device ID must be provided as a unique stable key for CPR FD registry.
Use them to guard the blocker addition. Apart from that, prohibit CPR on
WIN32 platform and on hosts with older libusb versions.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 39 +++++++++++++++++++++++++++++++--------
1 file changed, 31 insertions(+), 8 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b3c46aa8de0..258e1d5c471 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1524,16 +1524,39 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
usb_host_auto_check(NULL);
}
- error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+#ifdef CONFIG_WIN32
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "WIN32 platform is not supported",
DEVICE(s)->id ?: "(anonymous)");
- if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
- MIG_MODE_CPR_TRANSFER,
- MIG_MODE_CPR_EXEC, -1) < 0) {
- if (s->needs_autoscan) {
- QTAILQ_REMOVE(&hostdevs, s, next);
+#elif LIBUSB_API_VERSION < 0x01000107
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "libusb API version 0x%08x is too old",
+ DEVICE(s)->id ?: "(anonymous)", (unsigned) LIBUSB_API_VERSION);
+#else
+ if (!(s->hostdevice && DEVICE(s)->id)) {
+ /*
+ * CPR needs the host fd owned by libusb (the hostdevice= property)
+ * and a device id to key the CPR fd registry.
+ */
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "hostdevice= and a device id are required",
+ DEVICE(s)->id ?: "(anonymous)");
+ }
+#endif
+
+ if (s->cpr_blocker) {
+ if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1) < 0) {
+ if (s->needs_autoscan) {
+ QTAILQ_REMOVE(&hostdevs, s, next);
+ }
+ usb_host_close(s);
+ return;
}
- usb_host_close(s);
- return;
}
s->exit.notify = usb_host_exit_notifier;
--
2.47.1
^ permalink raw reply [relevance 20%]
* [PATCH QEMU HCI-8.0 v6 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
` (8 preceding siblings ...)
@ 2026-10-06 10:34 10% ` Andrey Drobyshev
2026-10-06 10:57 6% ` no-reply
2026-10-06 10:34 20% ` [PATCH QEMU HCI-8.0 v6 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
On CPR incoming we skip the usb_host_open() rescan, so libusb only sets
up the endpoints from the device's default configuration. If the guest
had enabled an endpoint by selecting a non-zero altsetting (e.g. an
isochronous streaming endpoint), that endpoint is now missing, and the
guest's transfers to it simply stall.
The altsetting itself is restored by vmload now, so let's call
usb_host_ep_update() on the CPR target to rebuild the endpoints out of
it.
The kernel claims are still there on the preserved FD, but libusb's own
handle knows nothing about them, so a later SET_INTERFACE would fail with
LIBUSB_ERROR_NOT_FOUND. Let's re-claim the interfaces on the target as
well to bring libusb's view back in sync - on the preserved FD this is a
no-op at the usbfs level anyway.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 56 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 492f0442974..b3c46aa8de0 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -946,7 +946,7 @@ static void usb_host_ep_update(USBHostDevice *s)
alt = udev->altsetting[intf->bInterfaceNumber];
if (alt != 0) {
- assert(alt < conf->interface[i].num_altsetting);
+ assert(alt >= 0 && alt < conf->interface[i].num_altsetting);
intf = &conf->interface[i].altsetting[alt];
}
@@ -2033,6 +2033,53 @@ static void usb_host_post_load_bh(void *opaque)
usb_host_auto_check(NULL);
}
+/*
+ * Reconfigure the CPR target after vmload: reject a migrated altsetting the
+ * device can't honour, then re-claim the interfaces so libusb's view matches
+ * the preserved fd (which already holds the claims).
+ */
+static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
+{
+ USBDevice *udev = USB_DEVICE(s);
+ struct libusb_config_descriptor *conf;
+ int n, rc;
+
+ rc = libusb_get_active_config_descriptor(s->dev, &conf);
+ if (rc != 0) {
+ usb_host_libusb_error("libusb_get_active_config_descriptor", rc);
+ return -1;
+ }
+
+ for (int i = 0; i < conf->bNumInterfaces; i++) {
+ int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
+ int alt = udev->altsetting[ifnum];
+
+ if (alt < 0 || alt >= conf->interface[i].num_altsetting) {
+ error_report("usb-host: migrated altsetting %d out of range for "
+ "interface %d", alt, ifnum);
+ libusb_free_config_descriptor(conf);
+ return -1;
+ }
+ }
+
+ n = MIN(conf->bNumInterfaces, USB_MAX_INTERFACES);
+ for (int i = 0; i < n; i++) {
+ rc = libusb_claim_interface(s->dh, i);
+ if (rc != 0) {
+ usb_host_libusb_error("libusb_claim_interface", rc);
+ libusb_free_config_descriptor(conf);
+ return -1;
+ }
+ s->ifs[i].claimed = true;
+ }
+
+ udev->ninterfaces = conf->bNumInterfaces;
+ udev->configuration = conf->bConfigurationValue;
+ libusb_free_config_descriptor(conf);
+
+ return 0;
+}
+
static int usb_host_post_load(void *opaque, int version_id)
{
USBHostDevice *dev = opaque;
@@ -2042,6 +2089,14 @@ static int usb_host_post_load(void *opaque, int version_id)
* rescan in usb_host_post_load_bh() -- those are for cross-host migration.
*/
if (cpr_is_incoming()) {
+ /*
+ * The device stayed open, so rebuild libusb's claims and endpoints,
+ * and start reaping now that the source has handed the fd off.
+ */
+ if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
+ return -1;
+ }
+ usb_host_ep_update(dev);
usb_host_cpr_restart_events();
return 0;
}
--
2.47.1
^ permalink raw reply [relevance 10%]
* [PATCH QEMU HCI-8.0 v6 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800
` (6 preceding siblings ...)
2026-10-06 10:34 22% ` [PATCH QEMU HCI-8.0 v6 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 29% ` Andrey Drobyshev
2026-10-06 10:54 6% ` no-reply
` (3 subsequent siblings)
11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
Unlike cpr-exec, cpr-transfer keeps the source QEMU alive: it hands the
usbfs FD to the new QEMU over the migration channel and only exits once
the target is up. While the handoff is in flight both processes hold the
same FD, and only one may reap URB events on it - if both do, one reaps
the other's in-flight completions and wedges an isochronous transfer that
cannot be retried. So reaping is handed off along with the FD:
* The target doesn't reap until it takes over: it stops events right
after opening the FD in .realize() and restarts in .post_load().
* The source stops reaping in .pre_save() and, since it stays alive,
restarts reaping from the vm-state handler's resume branch on failure.
* On success the source's .exit() leaves the device untouched - no
reset, interface release or host-driver rebind - and just closes its
own copy of the FD.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 109 +++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 96 insertions(+), 13 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 020a685aa41..492f0442974 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -127,6 +127,7 @@ struct USBHostDevice {
GSList *cpr_inflight;
bool cpr_drained;
bool cpr_replay_all;
+ bool cpr_handed_off;
/* request queues */
QTAILQ_HEAD(, USBHostRequest) requests;
@@ -1260,6 +1261,23 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
}
static void usb_host_cpr_reissue(USBHostDevice *s);
+static void usb_host_cpr_stop_events(void);
+
+/*
+ * Drain the device for a CPR switchover and, for cpr-transfer, hand reaping of
+ * the shared fd off to the target. Returns -1 if the drain couldn't complete.
+ */
+static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode)
+{
+ if (usb_host_cpr_drain_xfers(s) < 0) {
+ return -1;
+ }
+ if (mode == MIG_MODE_CPR_TRANSFER) {
+ usb_host_cpr_stop_events();
+ s->cpr_handed_off = true;
+ }
+ return 0;
+}
/*
* Quiesce the URB data plane around a CPR switchover: drain when the VM stops
@@ -1285,7 +1303,7 @@ static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
if (mode != MIG_MODE_CPR_EXEC && mode != MIG_MODE_CPR_TRANSFER) {
return;
}
- if (usb_host_cpr_drain_xfers(s) < 0) {
+ if (usb_host_cpr_quiesce(s, mode) < 0) {
migration_file_set_error(-EIO, NULL);
}
}
@@ -1300,16 +1318,19 @@ static int usb_host_close(USBHostDevice *s)
trace_usb_host_close(s->bus_num, s->addr);
- usb_host_abort_xfers(s);
- usb_host_iso_free_all(s);
+ if (!s->cpr_handed_off) {
+ usb_host_abort_xfers(s);
+ usb_host_iso_free_all(s);
- if (udev->attached) {
- usb_device_detach(udev);
+ if (udev->attached) {
+ usb_device_detach(udev);
+ }
+
+ usb_host_release_interfaces(s);
+ libusb_reset_device(s->dh);
+ usb_host_attach_kernel(s);
}
- usb_host_release_interfaces(s);
- libusb_reset_device(s->dh);
- usb_host_attach_kernel(s);
libusb_close(s->dh);
s->dh = NULL;
s->dev = NULL;
@@ -1344,10 +1365,17 @@ static void usb_host_exit_notifier(struct Notifier *n, void *data)
USBHostDevice *s = container_of(n, USBHostDevice, exit);
if (s->dh) {
- usb_host_abort_xfers(s);
- usb_host_release_interfaces(s);
- libusb_reset_device(s->dh);
- usb_host_attach_kernel(s);
+ /*
+ * Handed to new QEMU across cpr-transfer: do not reset, release
+ * interfaces or rebind the host driver underneath it; just close
+ * our handle.
+ */
+ if (!s->cpr_handed_off) {
+ usb_host_abort_xfers(s);
+ usb_host_release_interfaces(s);
+ libusb_reset_device(s->dh);
+ usb_host_attach_kernel(s);
+ }
libusb_close(s->dh);
}
}
@@ -1371,6 +1399,7 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
}
static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+static void usb_host_cpr_restart_events(void);
/*
* If CPR is aborted the source resumes with the drained packets still owned
@@ -1387,6 +1416,11 @@ static void usb_host_cpr_reissue(USBHostDevice *s)
return;
}
+ if (s->cpr_handed_off) {
+ usb_host_cpr_restart_events();
+ s->cpr_handed_off = false;
+ }
+
for (it = s->cpr_inflight; it; it = it->next) {
p = it->data;
/* Replay from the start of the TD */
@@ -1455,6 +1489,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
error_setg(errp, "failed to open host usb device %s", s->hostdevice);
return;
}
+ if (cpr_is_incoming()) {
+ /*
+ * The source still owns the shared fd until it hands off. Don't
+ * reap URBs on it yet, or we would steal the source's in-flight
+ * completions; .post_load() restarts events once we take over.
+ */
+ usb_host_cpr_stop_events();
+ }
} else
#endif
if (s->match.addr && s->match.bus_num &&
@@ -2000,6 +2042,7 @@ static int usb_host_post_load(void *opaque, int version_id)
* rescan in usb_host_post_load_bh() -- those are for cross-host migration.
*/
if (cpr_is_incoming()) {
+ usb_host_cpr_restart_events();
return 0;
}
@@ -2012,6 +2055,46 @@ static int usb_host_post_load(void *opaque, int version_id)
return 0;
}
+#ifndef CONFIG_WIN32
+
+/*
+ * cpr-transfer shares the usbfs fd with the new QEMU, so stop reaping URBs on
+ * it here -- only one process may drive the device. Restart if migration
+ * fails and the source resumes.
+ */
+static void usb_host_cpr_stop_events(void)
+{
+ const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+ libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
+ if (poll) {
+ for (int i = 0; poll[i] != NULL; i++) {
+ usb_host_del_fd(poll[i]->fd, ctx);
+ }
+ free(poll);
+ }
+}
+
+static void usb_host_cpr_restart_events(void)
+{
+ const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+ libusb_set_pollfd_notifiers(ctx, usb_host_add_fd, usb_host_del_fd, ctx);
+ if (poll) {
+ for (int i = 0; poll[i] != NULL; i++) {
+ usb_host_add_fd(poll[i]->fd, poll[i]->events, ctx);
+ }
+ free(poll);
+ }
+}
+
+#else
+
+static void usb_host_cpr_stop_events(void) {}
+static void usb_host_cpr_restart_events(void) {}
+
+#endif
+
static int usb_host_pre_save(void *opaque)
{
USBHostDevice *s = opaque;
@@ -2025,7 +2108,7 @@ static int usb_host_pre_save(void *opaque)
if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
s->dh && !s->cpr_drained) {
s->cpr_replay_all = true;
- if (usb_host_cpr_drain_xfers(s) < 0) {
+ if (usb_host_cpr_quiesce(s, mode) < 0) {
return -1;
}
}
--
2.47.1
^ permalink raw reply [relevance 29%]
* [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800
` (3 preceding siblings ...)
2026-10-06 10:34 25% ` [PATCH QEMU HCI-8.0 v6 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 26% ` Andrey Drobyshev
2026-10-06 10:48 5% ` no-reply
2026-10-06 10:34 23% ` [PATCH QEMU HCI-8.0 v6 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
` (6 subsequent siblings)
11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
URBs submitted by the old QEMU must not outlive it across a CPR
switchover: the kernel would complete them into buffers of an address
space that is gone after exec, and the new libusb context would reap URB
pointers it never submitted.
Register a vm_change_state handler that, when the VM stops for CPR
(RUN_STATE_FINISH_MIGRATE), cancels and reaps every pending transfer.
It runs before the vCPU/device snapshot, so a completion raised during
the drain lands in the still-live interrupt controller and is migrated
too. A transfer that already completed in the kernel is delivered
normally; one cancelled after moving some data is delivered as a short
transfer; one that moved nothing is suppressed, and the target replays
it from the start of the TD via usb_xhci_post_load(). A VM already
paused when migration starts never hits that transition, so .pre_save()
drains as a fallback, suppressing every URB for the target to replay.
Isochronous URBs live on their own rings; cancel and reap them too -- the
target reconstructs the stream from the guest's transfer ring.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 220 ++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 193 insertions(+), 27 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 5dc3308770e..d68fe0df03e 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -48,6 +48,7 @@
#include "qapi/error.h"
#include "migration/blocker.h"
#include "migration/cpr.h"
+#include "migration/misc.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -122,6 +123,9 @@ struct USBHostDevice {
/* CPR migration */
Error *cpr_blocker;
+ VMChangeStateEntry *cpr_vmse;
+ bool cpr_drained;
+ bool cpr_replay_all;
/* request queues */
QTAILQ_HEAD(, USBHostRequest) requests;
@@ -405,7 +409,10 @@ static void usb_host_req_free(USBHostRequest *r)
#ifdef CONFIG_WIN32
request_count--;
#endif
- QTAILQ_REMOVE(&r->host->requests, r, next);
+ /* Skip if a timed-out reap already unlinked it (avoid a double remove) */
+ if (QTAILQ_IN_USE(r, next)) {
+ QTAILQ_REMOVE(&r->host->requests, r, next);
+ }
libusb_free_transfer(r->xfer);
g_free(r->buffer);
g_free(r);
@@ -423,6 +430,25 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
return NULL;
}
+/*
+ * Classify a reap during the CPR drain (r->p is still set). One that moved no
+ * data is suppressed for the target to replay; one that moved some is kept as a
+ * short transfer. Returns true if suppressed, so the caller frees the request.
+ */
+static bool usb_host_req_cpr_replay(USBHostRequest *r,
+ struct libusb_transfer *xfer)
+{
+ if (xfer->status != LIBUSB_TRANSFER_CANCELLED) {
+ return false;
+ }
+ if (xfer->actual_length != 0 && !r->host->cpr_replay_all) {
+ /* deliver as a short transfer */
+ xfer->status = LIBUSB_TRANSFER_COMPLETED;
+ return false;
+ }
+ return true;
+}
+
static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
{
USBHostRequest *r = xfer->user_data;
@@ -432,6 +458,9 @@ static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
if (r->p == NULL) {
goto out; /* request was canceled */
}
+ if (usb_host_req_cpr_replay(r, xfer)) {
+ goto out; /* CPR drain: the target will replay this packet */
+ }
r->p->status = status_map[xfer->status];
r->p->actual_length = xfer->actual_length;
@@ -482,6 +511,9 @@ static void LIBUSB_CALL usb_host_req_complete_data(struct libusb_transfer *xfer)
if (r->p == NULL) {
goto out; /* request was canceled */
}
+ if (usb_host_req_cpr_replay(r, xfer)) {
+ goto out; /* CPR drain: the target will replay this packet */
+ }
r->p->status = status_map[xfer->status];
if (r->in && xfer->actual_length) {
@@ -502,22 +534,29 @@ out:
}
}
-static void usb_host_req_abort(USBHostRequest *r)
+/* Fail r's in-flight packet with an error status and detach it from r. */
+static void usb_host_req_complete_err(USBHostRequest *r, int status)
{
- USBHostDevice *s = r->host;
- bool inflight = (r->p && r->p->state == USB_PACKET_ASYNC);
+ USBHostDevice *s = r->host;
- if (inflight) {
- r->p->status = USB_RET_NODEV;
- trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
- r->p->status, r->p->actual_length);
- if (r->p->ep->nr == 0) {
- usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
- } else {
- usb_packet_complete(USB_DEVICE(s), r->p);
- }
- r->p = NULL;
+ if (!r->p || r->p->state != USB_PACKET_ASYNC) {
+ return;
+ }
+ r->p->status = status;
+ trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
+ r->p->status, r->p->actual_length);
+ if (r->p->ep->nr == 0) {
+ usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
+ } else {
+ usb_packet_complete(USB_DEVICE(s), r->p);
+ }
+ r->p = NULL;
+}
+static void usb_host_req_abort(USBHostRequest *r)
+{
+ if (r->p && r->p->state == USB_PACKET_ASYNC) {
+ usb_host_req_complete_err(r, USB_RET_NODEV);
libusb_cancel_transfer(r->xfer);
}
}
@@ -1098,9 +1137,18 @@ fail:
return -1;
}
-static void usb_host_reap_xfers(USBHostDevice *s)
+/* libusb still owns cancelled-but-unreaped transfers: unlink, don't free. */
+static void usb_host_drop_requests(USBHostDevice *s)
{
USBHostRequest *r, *rtmp;
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ QTAILQ_REMOVE(&s->requests, r, next);
+ }
+}
+
+static void usb_host_reap_xfers(USBHostDevice *s)
+{
int limit = 100;
while (QTAILQ_FIRST(&s->requests) != NULL) {
@@ -1110,15 +1158,10 @@ static void usb_host_reap_xfers(USBHostDevice *s)
libusb_handle_events_timeout(ctx, &tv);
if (--limit == 0) {
/*
- * Don't wait forever for libusb calling the complete
- * callback (which will unlink and free the request).
- *
- * Leaking memory here, to make sure libusb will not
- * access memory which we have released already.
+ * Don't wait forever for libusb to call the complete callback
+ * (which unlinks and frees the request).
*/
- QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
- QTAILQ_REMOVE(&s->requests, r, next);
- }
+ usb_host_drop_requests(s);
return;
}
}
@@ -1134,6 +1177,105 @@ static void usb_host_abort_xfers(USBHostDevice *s)
usb_host_reap_xfers(s);
}
+static bool usb_host_iso_inflight(USBHostDevice *s)
+{
+ USBHostIsoRing *ring;
+
+ QTAILQ_FOREACH(ring, &s->isorings, next) {
+ if (!QTAILQ_EMPTY(&ring->inflight)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/*
+ * Quiesce the device for a CPR switchover. Cancel and reap all URBs,
+ * so that no URB submitted by this process can outlive it.
+ */
+static int usb_host_cpr_drain_xfers(USBHostDevice *s)
+{
+ USBHostRequest *r, *rtmp;
+ USBHostIsoRing *ring;
+ USBHostIsoXfer *xfer;
+ int limit = 100;
+
+ s->cpr_drained = true;
+
+ /*
+ * Cancel and reap every URB. Completing a reaped URB can make xHCI queue
+ * the next TD, so re-cancel whatever reappears until the queue stays empty.
+ */
+ while (!QTAILQ_EMPTY(&s->requests)) {
+ struct timeval tv = { .tv_usec = 2500 };
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ libusb_cancel_transfer(r->xfer);
+ }
+ libusb_handle_events_timeout(ctx, &tv);
+ if (--limit == 0) {
+ /*
+ * The device won't release these URBs. Error the packets out so
+ * the resumed guest retries them, leak the requests (libusb still
+ * owns their transfers) and abort the migration.
+ */
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ usb_host_req_complete_err(r, USB_RET_IOERROR);
+ }
+ usb_host_drop_requests(s);
+ return -1;
+ }
+ }
+
+ /*
+ * Iso URBs are on the rings, not s->requests. Cancel them: one in
+ * flight when the guest stops may never complete on its own, and none
+ * may be left on the preserved fd. The target replays from the ring.
+ */
+ QTAILQ_FOREACH(ring, &s->isorings, next) {
+ QTAILQ_FOREACH(xfer, &ring->inflight, next) {
+ libusb_cancel_transfer(xfer->xfer);
+ }
+ }
+
+ /*
+ * Cap the reap at 2x a full ring:
+ * iso_urb_count URBs x iso_urb_frames packets each
+ */
+ limit = 2 * s->iso_urb_count * s->iso_urb_frames;
+ while (usb_host_iso_inflight(s)) {
+ struct timeval tv = { .tv_usec = 1000 };
+ libusb_handle_events_timeout(ctx, &tv);
+ if (limit-- == 0) {
+ return -1;
+ }
+ }
+ usb_host_iso_free_all(s);
+ return 0;
+}
+
+/*
+ * Quiesce the URB data plane around a CPR switchover: drain when the VM stops
+ * for CPR -- before the device/irqchip snapshot, so completions raised during
+ * the drain are migrated too.
+ */
+static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
+{
+ USBHostDevice *s = opaque;
+ MigMode mode;
+
+ if (running || !s->dh || state != RUN_STATE_FINISH_MIGRATE) {
+ return;
+ }
+ mode = migrate_mode();
+ if (mode != MIG_MODE_CPR_EXEC && mode != MIG_MODE_CPR_TRANSFER) {
+ return;
+ }
+ if (usb_host_cpr_drain_xfers(s) < 0) {
+ migration_file_set_error(-EIO, NULL);
+ }
+}
+
static int usb_host_close(USBHostDevice *s)
{
USBDevice *udev = USB_DEVICE(s);
@@ -1301,6 +1443,8 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
+
+ s->cpr_vmse = qemu_add_vm_change_state_handler(usb_host_cpr_vm_state, s);
}
static void usb_host_instance_init(Object *obj)
@@ -1323,6 +1467,9 @@ static void usb_host_unrealize(USBDevice *udev)
}
usb_host_close(s);
migrate_del_blocker(&s->cpr_blocker);
+ if (s->cpr_vmse) {
+ qemu_del_vm_change_state_handler(s->cpr_vmse);
+ }
}
static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1791,10 +1938,8 @@ static int usb_host_post_load(void *opaque, int version_id)
USBHostDevice *dev = opaque;
/*
- * For CPR migration, device wasn't released/reset, and the guest
- * is unaware of the switchover. The detach/rescan performed in
- * usb_host_post_load_bh() only exists for cross-host migration.
- * Skip it for CPR.
+ * CPR keeps the device open and the guest unaware, so skip the detach and
+ * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
*/
if (cpr_is_incoming()) {
return 0;
@@ -1809,10 +1954,31 @@ static int usb_host_post_load(void *opaque, int version_id)
return 0;
}
+static int usb_host_pre_save(void *opaque)
+{
+ USBHostDevice *s = opaque;
+ MigMode mode = migrate_mode();
+
+ /*
+ * The vm-state handler drains on the stop-for-CPR transition, but a VM
+ * already paused never hits it. Drain here instead; the stopped guest
+ * can't take completions, so suppress every URB and let the target replay.
+ */
+ if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
+ s->dh && !s->cpr_drained) {
+ s->cpr_replay_all = true;
+ if (usb_host_cpr_drain_xfers(s) < 0) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
static const VMStateDescription vmstate_usb_host = {
.name = "usb-host",
.version_id = 1,
.minimum_version_id = 1,
+ .pre_save = usb_host_pre_save,
.post_load = usb_host_post_load,
.fields = (const VMStateField[]) {
VMSTATE_USB_DEVICE(parent_obj, USBHostDevice),
--
2.47.1
^ permalink raw reply [relevance 26%]
* [PATCH QEMU HCI-8.0 v6 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800
` (2 preceding siblings ...)
2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 25% ` Andrey Drobyshev
2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
` (7 subsequent siblings)
11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
usb_host_abort_xfers() cancels every pending request and then pumps
libusb events until all of them are reaped, with a bounded wait. Split
that reap-wait loop out into usb_host_reap_xfers(). No functional
change.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index f601b85b4d9..5dc3308770e 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1098,15 +1098,11 @@ fail:
return -1;
}
-static void usb_host_abort_xfers(USBHostDevice *s)
+static void usb_host_reap_xfers(USBHostDevice *s)
{
USBHostRequest *r, *rtmp;
int limit = 100;
- QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
- usb_host_req_abort(r);
- }
-
while (QTAILQ_FIRST(&s->requests) != NULL) {
struct timeval tv;
memset(&tv, 0, sizeof(tv));
@@ -1128,6 +1124,16 @@ static void usb_host_abort_xfers(USBHostDevice *s)
}
}
+static void usb_host_abort_xfers(USBHostDevice *s)
+{
+ USBHostRequest *r, *rtmp;
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ usb_host_req_abort(r);
+ }
+ usb_host_reap_xfers(s);
+}
+
static int usb_host_close(USBHostDevice *s)
{
USBDevice *udev = USB_DEVICE(s);
--
2.47.1
^ permalink raw reply [relevance 25%]
* [PATCH QEMU HCI-8.0 v6 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
` (4 preceding siblings ...)
2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 23% ` Andrey Drobyshev
2026-10-06 10:46 6% ` no-reply
2026-10-06 10:34 22% ` [PATCH QEMU HCI-8.0 v6 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
` (5 subsequent siblings)
11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
If CPR fails, the source VM resumes with the drained packets still owned
by the host controller as in-flight, and nothing left to complete them.
Remember the packets suppressed during the drain on a list, and re-issue
them on the preserved fd from the vm-state handler's resume branch -- it
runs on any resume but only acts once we have actually drained
(cpr_drained). Control transfers cannot be reconstructed from the packet
alone, so complete those as errors and let the guest driver retry.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 66 ++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 62 insertions(+), 4 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index d68fe0df03e..85509dc2d83 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -124,6 +124,7 @@ struct USBHostDevice {
/* CPR migration */
Error *cpr_blocker;
VMChangeStateEntry *cpr_vmse;
+ GSList *cpr_inflight;
bool cpr_drained;
bool cpr_replay_all;
@@ -432,8 +433,9 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
/*
* Classify a reap during the CPR drain (r->p is still set). One that moved no
- * data is suppressed for the target to replay; one that moved some is kept as a
- * short transfer. Returns true if suppressed, so the caller frees the request.
+ * data is suppressed and queued on cpr_inflight for the target to replay; one
+ * that moved some is kept as a short transfer. Returns true if suppressed, so
+ * the caller frees the request.
*/
static bool usb_host_req_cpr_replay(USBHostRequest *r,
struct libusb_transfer *xfer)
@@ -446,6 +448,9 @@ static bool usb_host_req_cpr_replay(USBHostRequest *r,
xfer->status = LIBUSB_TRANSFER_COMPLETED;
return false;
}
+ if (r->p->state == USB_PACKET_ASYNC) {
+ r->host->cpr_inflight = g_slist_append(r->host->cpr_inflight, r->p);
+ }
return true;
}
@@ -1254,17 +1259,26 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
return 0;
}
+static void usb_host_cpr_reissue(USBHostDevice *s);
+
/*
* Quiesce the URB data plane around a CPR switchover: drain when the VM stops
* for CPR -- before the device/irqchip snapshot, so completions raised during
- * the drain are migrated too.
+ * the drain are migrated too -- and re-issue on resume if CPR was aborted.
*/
static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
{
USBHostDevice *s = opaque;
MigMode mode;
- if (running || !s->dh || state != RUN_STATE_FINISH_MIGRATE) {
+ if (!s->dh) {
+ return;
+ }
+ if (running) {
+ usb_host_cpr_reissue(s);
+ return;
+ }
+ if (state != RUN_STATE_FINISH_MIGRATE) {
return;
}
mode = migrate_mode();
@@ -1356,6 +1370,45 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
return ret;
}
+static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+
+/*
+ * If CPR is aborted the source resumes with the drained packets still owned
+ * by the controller as in-flight; re-issue them on the preserved fd. Control
+ * transfers can't be reconstructed from the packet, so error those out.
+ */
+static void usb_host_cpr_reissue(USBHostDevice *s)
+{
+ USBDevice *udev = USB_DEVICE(s);
+ GSList *it;
+ USBPacket *p;
+
+ if (!s->cpr_drained) {
+ return;
+ }
+
+ for (it = s->cpr_inflight; it; it = it->next) {
+ p = it->data;
+ /* Replay from the start of the TD */
+ p->actual_length = 0;
+ if (p->ep->nr == 0) {
+ /* control endpoint: can't be reconstructed, so error it out */
+ p->status = USB_RET_IOERROR;
+ usb_generic_async_ctrl_complete(udev, p);
+ } else {
+ usb_host_handle_data(udev, p);
+ if (p->status != USB_RET_ASYNC) {
+ /* Completed synchronously with error */
+ usb_packet_complete(udev, p);
+ }
+ }
+ }
+ g_slist_free(s->cpr_inflight);
+ s->cpr_inflight = NULL;
+ s->cpr_replay_all = false;
+ s->cpr_drained = false;
+}
+
static void usb_host_realize(USBDevice *udev, Error **errp)
{
USBHostDevice *s = USB_HOST_DEVICE(udev);
@@ -1470,6 +1523,8 @@ static void usb_host_unrealize(USBDevice *udev)
if (s->cpr_vmse) {
qemu_del_vm_change_state_handler(s->cpr_vmse);
}
+ g_slist_free(s->cpr_inflight);
+ s->cpr_inflight = NULL;
}
static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1477,6 +1532,9 @@ static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
USBHostDevice *s = USB_HOST_DEVICE(udev);
USBHostRequest *r;
+ /* Packet is being cancelled -> it mustn't be re-issued on failed CPR */
+ s->cpr_inflight = g_slist_remove(s->cpr_inflight, p);
+
if (p->combined) {
usb_combined_packet_cancel(udev, p);
return;
--
2.47.1
^ permalink raw reply [relevance 23%]
* [PATCH QEMU HCI-8.0 v6 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800
` (5 preceding siblings ...)
2026-10-06 10:34 23% ` [PATCH QEMU HCI-8.0 v6 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 22% ` Andrey Drobyshev
2026-10-06 10:47 5% ` no-reply
2026-10-06 10:34 29% ` [PATCH QEMU HCI-8.0 v6 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
` (4 subsequent siblings)
11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
usb_host_open() reads the product string descriptor from the device
with a synchronous control transfer. On the CPR target this runs while
the device is being handed over, and the transfer can block there,
hanging the incoming migration.
The string only fills the cosmetic product_desc, so skip the read when
we're the CPR target and fall back to the synthetic "host:bus.addr"
name.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 85509dc2d83..020a685aa41 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1106,7 +1106,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
udev->speed = speed_map[libusb_speed];
usb_host_speed_compat(s);
- if (s->ddesc.iProduct) {
+ if (s->ddesc.iProduct && !cpr_is_incoming()) {
libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
(unsigned char *)udev->product_desc,
sizeof(udev->product_desc));
--
2.47.1
^ permalink raw reply [relevance 22%]
* [PATCH QEMU HCI-8.0 v6 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800
2026-10-06 10:34 31% ` [PATCH QEMU HCI-8.0 v6 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:34 13% ` [PATCH QEMU HCI-8.0 v6 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 26% ` Andrey Drobyshev
2026-10-06 10:34 25% ` [PATCH QEMU HCI-8.0 v6 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
` (8 subsequent siblings)
11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
In .realize(), save freshly opened host device FD into the CPR registry,
and then reuse it on CPR target. Also, add deletion of that FD from
registry to usb_host_open() / usb_host_close() cleanup paths.
For this to work, we also need to skip the .post-load() code which
closes, detaches the device, and then rescans the host bus to reopen
matching hostdevs. For the CPR-case migration we don't want any of that
as the hostdev FD stays preserved, and the guest shouldn't notice the
switchover.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 33 ++++++++++++++++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 770bdaca4c8..f601b85b4d9 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -47,6 +47,7 @@
#include "qapi/error.h"
#include "migration/blocker.h"
+#include "migration/cpr.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -949,6 +950,21 @@ static void usb_host_ep_update(USBHostDevice *s)
libusb_free_config_descriptor(conf);
}
+static char *usb_host_cpr_fd_name(USBHostDevice *s)
+{
+ /* Prefix the key so it can't clash with another CPR label */
+ return DEVICE(s)->id ? g_strdup_printf("usb-host/%s", DEVICE(s)->id)
+ : NULL;
+}
+
+static void usb_host_cpr_delete_fd(USBHostDevice *s)
+{
+ g_autofree char *name = usb_host_cpr_fd_name(s);
+ if (name) {
+ cpr_delete_fd(name, 0);
+ }
+}
+
static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
{
USBDevice *udev = USB_DEVICE(s);
@@ -1075,6 +1091,7 @@ fail:
s->dev = NULL;
}
if (s->hostfd != -1) {
+ usb_host_cpr_delete_fd(s);
close(s->hostfd);
s->hostfd = -1;
}
@@ -1136,6 +1153,7 @@ static int usb_host_close(USBHostDevice *s)
s->dev = NULL;
if (s->hostfd != -1) {
+ usb_host_cpr_delete_fd(s);
close(s->hostfd);
s->hostfd = -1;
}
@@ -1222,9 +1240,12 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
if (s->hostdevice) {
+ g_autofree char *name = usb_host_cpr_fd_name(s);
int fd;
+
s->needs_autoscan = false;
- fd = qemu_open(s->hostdevice, O_RDWR, errp);
+ fd = name ? cpr_open_fd(s->hostdevice, O_RDWR, name, 0, errp)
+ : qemu_open(s->hostdevice, O_RDWR, errp);
if (fd < 0) {
return;
}
@@ -1763,6 +1784,16 @@ static int usb_host_post_load(void *opaque, int version_id)
{
USBHostDevice *dev = opaque;
+ /*
+ * For CPR migration, device wasn't released/reset, and the guest
+ * is unaware of the switchover. The detach/rescan performed in
+ * usb_host_post_load_bh() only exists for cross-host migration.
+ * Skip it for CPR.
+ */
+ if (cpr_is_incoming()) {
+ return 0;
+ }
+
if (!dev->bh_postld) {
dev->bh_postld = qemu_bh_new_guarded(usb_host_post_load_bh, dev,
&DEVICE(dev)->mem_reentrancy_guard);
--
2.47.1
^ permalink raw reply [relevance 26%]
* [PATCH QEMU HCI-8.0 v6 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
@ 2026-10-06 10:34 31% ` Andrey Drobyshev
2026-10-06 10:37 7% ` no-reply
2026-10-06 10:34 13% ` [PATCH QEMU HCI-8.0 v6 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
` (10 subsequent siblings)
11 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
The FD passed into usb_host_open() for a wrapped device is only stored in
s->hostfd after libusb_wrap_sys_device() succeeds, and the failure path
doesn't close it. if wrapping fails, or a later step of the open sequence
fails (e.g. usb_device_attach), the FD is leaked, as usb_host_close() only
runs for fully opened devices, and the realize error path doesn't clean it
up either.
Store the FD in s->hostfd before wrapping and close it on the failure
path.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b74670ae256..83645fbe0ce 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -954,6 +954,11 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
int rc;
Error *local_err = NULL;
+ /* Make sure failure path frees caller-owned FD */
+ if (!dev) {
+ s->hostfd = hostfd;
+ }
+
if (s->bh_postld_pending) {
return -1;
}
@@ -978,7 +983,6 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
if (rc != 0) {
goto fail;
}
- s->hostfd = hostfd;
dev = libusb_get_device(s->dh);
bus_num = libusb_get_bus_number(dev);
addr = libusb_get_device_address(dev);
@@ -1066,6 +1070,10 @@ fail:
s->dh = NULL;
s->dev = NULL;
}
+ if (s->hostfd != -1) {
+ close(s->hostfd);
+ s->hostfd = -1;
+ }
return -1;
}
--
2.47.1
^ permalink raw reply [relevance 31%]
* [PATCH QEMU HCI-8.0 v6 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800
2026-10-06 10:34 31% ` [PATCH QEMU HCI-8.0 v6 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-10-06 10:34 13% ` Andrey Drobyshev
2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
` (9 subsequent siblings)
11 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-06 10:34 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
We're about to add support for CPR migration to usb-host in the upcoming
commits. Right now CPR is crashing with usb-host devices. Let's add
migration blocker upfront, for both cpr-exec and cpr-transfer. The
blocker is added unconditionally, so that we refuse CPR migration instead
of crashing on it. That is for the sake of bisectability of the upcoming
commits. It is going to be lifted (conditioned) in a following patch
once CPR support is added. Also, blocker is added at the end of
usb_host_realize(), so we have to cleanup all the side effects done
earlier in .realize() in case migrate_add_blocker_modes() fails.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 83645fbe0ce..770bdaca4c8 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -46,6 +46,7 @@
#endif
#include "qapi/error.h"
+#include "migration/blocker.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -118,6 +119,9 @@ struct USBHostDevice {
bool bh_postld_pending;
Notifier exit;
+ /* CPR migration */
+ Error *cpr_blocker;
+
/* request queues */
QTAILQ_HEAD(, USBHostRequest) requests;
QTAILQ_HEAD(, USBHostIsoRing) isorings;
@@ -1256,6 +1260,18 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
usb_host_auto_check(NULL);
}
+ error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+ DEVICE(s)->id ?: "(anonymous)");
+ if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1) < 0) {
+ if (s->needs_autoscan) {
+ QTAILQ_REMOVE(&hostdevs, s, next);
+ }
+ usb_host_close(s);
+ return;
+ }
+
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
}
@@ -1279,6 +1295,7 @@ static void usb_host_unrealize(USBDevice *udev)
QTAILQ_REMOVE(&hostdevs, s, next);
}
usb_host_close(s);
+ migrate_del_blocker(&s->cpr_blocker);
}
static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
--
2.47.1
^ permalink raw reply [relevance 13%]
* Re: [Devel] [PATCH hci-8.1 v4 1/1] selftests: net: skip the veth GRO checks where they cannot run
@ 2026-10-05 11:29 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-05 11:29 UTC (permalink / raw)
To: khorenko, Eva Kurchatova; +Cc: devel
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] selftests: veth.sh udp_encap_in_use() probes only some UDP encap users
- [Low] selftests: veth.sh ss port regex is not anchored to local encap sockets
- [Low] selftests: veth.sh UDP_ENCAP flag is inverted and skips are not reported as skips
--
commit f9959728bd7c821cbe897be9079c649853692e5a
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
selftests: net: skip the veth GRO checks where they cannot run
This patch adds udp_encap_in_use() to veth.sh. It detects UDP
encapsulation users such as vxlan, geneve, bareudp, fou, l2tp and sockets
on ports 4789 and 6081. A new chk_gro_fwd() wrapper then skips the three
rx-udp-gro-forwarding aggregation checks when any of them is found.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-139651
> diff --git a/tools/testing/selftests/net/veth.sh b/tools/testing/selftests/net/veth.sh
> --- a/tools/testing/selftests/net/veth.sh
> +++ b/tools/testing/selftests/net/veth.sh
[ ... ]
> +udp_encap_in_use() {
> + local ns cmd t
> +
> + for ns in "" `ip netns list 2>/dev/null | awk '{print $1}'`; do
[Severity: Medium]
udp_encap_enable() does static_branch_inc(&udp_encap_needed_key), and
udp_gro_receive() tests that global key. The set of callers is much wider
than what this function probes. WireGuard (drivers/net/wireguard/socket.c),
GTP (drivers/net/gtp.c) and the UDP_ENCAP setsockopt path, which covers
ESP-in-UDP and NAT-T sockets, all reach it.
Consider a host with only one of those. None of the vxlan, geneve,
bareudp, fou, l2tp or port 4789/6081 probes match, so udp_encap_in_use()
returns 1. chk_gro_fwd() then calls chk_gro(), and the check still fails
with 10 packets where 1 is expected. Doesn't that leave the failure this
patch is meant to remove?
Only named namespaces from "ip netns list" are visited. Anonymous and
container-runtime namespaces are missed, even though the key is global.
Can the commit message wording "in any namespace" hold with this approach?
Errors from "ip fou show", "ip l2tp show tunnel" and "ss" are discarded
with 2>/dev/null. Doesn't a missing tool or an unloaded module then read
as "no encapsulation"?
The probe also runs only once, before the tests start. Tunnels created
later are not seen.
The commit message says "Any UDP encapsulation socket on the machine".
The comment above chk_gro_fwd() says "Only meaningful where nothing else
has turned udp_encap_needed_key on". Could both be reworded to match the
subset of users that is actually detected?
> + $cmd ss -uapn 2>/dev/null | \
> + grep -qE ':(4789|6081)([^0-9]|$)' && return 0
[Severity: Low]
ss -uapn prints both the local and the peer address:port columns. This
regex is not anchored to the local column. Can a plain UDP socket bound to
4789 or 6081, or a client connected to a peer on those ports, match here?
Such sockets do not enable udp_encap_needed_key, so the checks would be
skipped on hosts where they could have run.
Conversely, encap sockets on non-default ports are only caught through the
link type checks.
[ ... ]
> +# Only meaningful where nothing else has turned udp_encap_needed_key on
> +chk_gro_fwd() {
> + local msg="$1"
> + local expected=$2
> +
> + if [ $UDP_ENCAP -eq 0 ]; then
> + printf "%-60s" "$msg"
> + echo " skip - UDP encapsulation in use"
> + return
> + fi
[Severity: Low]
This isn't a bug, but UDP_ENCAP=0 meaning "encapsulation in use" comes
from storing the exit status of udp_encap_in_use() with UDP_ENCAP=$?. It
is easy to misread.
The skip path only prints text. It does not set the kselftest skip exit
code (4). Can the script then exit 0 when all three gated checks were
skipped?
> +
> + chk_gro "$msg" $expected
> +}
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261005112332.1989594-1-eva.kurchatova@virtuozzo.com?part=1
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter
2026-10-02 21:40 3% ` no-reply
@ 2026-10-05 10:02 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-05 10:02 UTC (permalink / raw)
To: vasileios.almpanis, Konstantin Khorenko; +Cc: devel
On 10/2/26 23:40, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Medium] net: neigh_alloc() drops table-specific overflow log; ARP and ND share one failcount
> --
>
> commit 83435d15f32b1a7fcb7f3aac09fb30774f63d147
> Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
>
> ve: add neigh_tbl failcounter
>
> This commit adds a per-VE neigh_tbl_failcount and exposes it in
> ve.failcount. In neigh_alloc(), the "neighbor table overflow!" message
> is replaced by a call to ve_failcount_inc() when the table limit is hit.
>
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-135520
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
>
>> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
>> --- a/kernel/ve/ve.c
>> +++ b/kernel/ve/ve.c
>
> [ ... ]
>
>> @@ -834,6 +835,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
>>
>> atomic_set(&ve->arp_neigh_nr, 0);
>> atomic_set(&ve->nd_neigh_nr, 0);
>> + atomic_set(&ve->neigh_tbl_failcount, 0);
>
> [Severity: Medium]
> ARP and ND have separate per-VE counters and limits (arp_neigh_nr and
> nd_neigh_nr, selected by get_perve_tbl_entries_counter()), but both now
> feed the single neigh_tbl_failcount.
This is true, but IMO it won't add much value.
I'll return net_info_ratelimited message with table id and ve name
for debugging purposes. It will go to the host only.
>
> Can the ve.failcount output tell an operator whether the ARP table or the
> ND table hit its limit?
>
> [ ... ]
>
>> diff --git a/net/core/neighbour.c b/net/core/neighbour.c
>> --- a/net/core/neighbour.c
>> +++ b/net/core/neighbour.c
>
> [ ... ]
>
>> @@ -520,8 +520,7 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
>> (glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
>> time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
>> if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
>> - net_info_ratelimited("%s: neighbor table overflow!\n",
>> - tbl->id);
>> + ve_failcount_inc(ve, neigh_tbl);
>> NEIGH_CACHE_STAT_INC(tbl, table_fulls);
>> goto out_entries;
>> }
>
> [Severity: Medium]
> The removed net_info_ratelimited() message named the table through
> tbl->id. The replacement message from ve_failcount_inc() is fixed text
> and has no table or family information:
>
> "CT<name>: hits the neigh_tbl limit, see the 'neigh_tbl' counter in
> ve.failcount of the container's ve cgroup"
>
> Is it intended to drop the arp_tbl versus nd_tbl distinction and the
> host-level overflow message? The commit message does not mention that the
> original message is being removed.
>
> Also, this path runs for ve0 as well. Would the host then log "CT0: hits
> the neigh_tbl limit ... of the container's ve cgroup", which seems
> misleading wording for the host?
>
> Finally, the ratelimit state in ve_failcount_inc() comes from
> __ve_pr_warn_ratelimited(), which is a single static state at the macro
> call site. Is it a concern that all VEs share it, so one container that
> overflows its table often could suppress the message for other containers?
>
--
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve
2026-10-02 21:38 5% ` no-reply
@ 2026-10-05 9:56 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-05 9:56 UTC (permalink / raw)
To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel
On 10/2/26 23:38, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Low] printk: !CONFIG_PRINTK __ve_printk_ratelimited() stub lacks the ve parameter
> --
>
> commit 446bb8b8f19e9083361fc5d794bba0859020d3a4
> Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
>
> ve/failcount: Use ve_printk with specified ve
>
> This patch adds __ve_printk_ratelimited() and __ve_pr_warn_ratelimited()
> helpers that take an explicit ve argument. ve_failcount_inc() now uses them
> so the message goes to the right VE when called from IRQ context.
>
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
>
>> diff --git a/include/linux/printk.h b/include/linux/printk.h
>> --- a/include/linux/printk.h
>> +++ b/include/linux/printk.h
>
> [ ... ]
>
>> #else
>> #define printk_ratelimited(fmt, ...) \
>> no_printk(fmt, ##__VA_ARGS__)
>> #define ve_printk_ratelimited(dst, fmt, ...) \
>> no_printk(fmt, ##__VA_ARGS__)
>> +#define __ve_printk_ratelimited(dst, fmt, ...) \
>> + no_printk(fmt, ##__VA_ARGS__)
>> #endif
>
> [Severity: Low]
> Is the parameter list of this !CONFIG_PRINTK stub correct? The
> CONFIG_PRINTK version of __ve_printk_ratelimited() takes
> (dst, ve, fmt, ...), but this stub takes only (dst, fmt, ...).
No, it is not. Will fix in v2.
>
> [ ... ]
>
>> +#define __ve_pr_warn_ratelimited(dst, ve, fmt, ...) \
>> + __ve_printk_ratelimited(dst, ve, KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
>
> This macro is defined outside the #ifdef and always passes ve as the second
> argument. With CONFIG_VE=y and CONFIG_PRINTK=n, it expands into the
> mismatched stub, so ve is bound to fmt.
>
>> diff --git a/include/linux/ve.h b/include/linux/ve.h
>> --- a/include/linux/ve.h
>> +++ b/include/linux/ve.h
>
> [ ... ]
>
>> + __ve_pr_warn_ratelimited(VE_LOG_BOTH, __ve, "CT%s: hits the " #name \
>> + " limit, see the '" #name "' counter in ve.failcount " \
>> + "of the container's ve cgroup\n", ve_name(__ve)); \
>
> With CONFIG_PRINTK=n, ve_failcount_inc() expands like this:
>
> ve_failcount_inc()
> __ve_pr_warn_ratelimited(VE_LOG_BOTH, __ve, "CT%s: ...", ve_name(__ve))
> __ve_printk_ratelimited(dst, ve, KERN_WARNING pr_fmt(fmt), ...)
> no_printk(<struct ve_struct *>, <real format>, ve_name(__ve))
>
> no_printk() then receives a struct ve_struct pointer as its const char *
> format argument. The real format string and ve_name(__ve) end up in the
> varargs.
>
> Can this produce an incompatible-pointer diagnostic, which is an error with
> -Werror or newer compilers? The format string also stops being checked.
>
> Should the stub be changed to take (dst, ve, fmt, ...)? As written, this
> can break the build when CONFIG_VE=y and CONFIG_PRINTK=n.
>
--
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [PATCH hci-8.0 v2 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304
2026-10-05 9:26 3% ` [PATCH hci-8.0 v2 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
@ 2026-10-05 9:30 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-05 9:30 UTC (permalink / raw)
To: Denis V. Lunev, svt-core; +Cc: andrey.drobyshev, den
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] chardev: log fd left stale in CPR registry on cpr-transfer target
--
commit 9181fad86181ebb4fb1b78687516f5494301eab0
Author: Denis V. Lunev <den@openvz.org>
chardev: preserve the logfile fd across cpr-exec #VSTOR-148304
This patch saves the chardev log fd in the CPR fd registry under the
chardev label. On cpr-exec incoming, qemu_char_open() takes the fd from
the registry instead of reopening the logfile path. char_finalize() and
char_cpr_claim_fds() are updated to keep the registry entry consistent.
> diff --git a/chardev/char.c b/chardev/char.c
> --- a/chardev/char.c
> +++ b/chardev/char.c
[ ... ]
> @@ -261,9 +262,21 @@ static void qemu_char_open(Chardev *chr, ChardevBackend *backend,
> } else {
> flags |= O_TRUNC;
> }
> - chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
> - if (chr->logfd < 0) {
> - return;
> + if (cpr_exec_is_incoming()) {
> + chr->logfd = cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID);
> + if (chr->logfd < 0) {
> + warn_report("chardev '%s': log file was not preserved, "
> + "continuing without it", chr->label);
> + }
> + } else {
> + chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
> + if (chr->logfd < 0) {
> + return;
> + }
> + if (!cpr_is_incoming()) {
> + cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
> + cpr_save_fd(chr->label, CPR_CHARDEV_LOG_ID, chr->logfd);
> + }
> }
> }
[Severity: Low]
On a cpr-transfer target, cpr_is_incoming() is true but
cpr_exec_is_incoming() is false. Does qemu_char_open() then take the
else branch and reopen the path with qemu_create()?
The log fd that the source saved with cpr_save_fd() is part of the
registry that cpr_state_save() serializes for MIG_MODE_CPR_TRANSFER as
well. On the target, that fd is neither used nor replaced, because the
cpr_save_fd() call is skipped by the !cpr_is_incoming() check.
Could this leave an unused log fd open and registered under
(label, CPR_CHARDEV_LOG_ID) until process exit? If the target later does
a CPR save, the stale entry would be serialized too.
Would the Permission denied failure described in the commit message also
still occur in cpr-transfer mode, since the path is reopened there?
[ ... ]
> @@ -315,11 +332,14 @@ static void char_finalize(Object *obj)
> if (chr->be) {
> chr->be->chr = NULL;
> }
> - g_free(chr->filename);
> - g_free(chr->label);
> if (chr->logfd != -1) {
> + if (cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID) == chr->logfd) {
> + cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
> + }
> close(chr->logfd);
> }
Related to the above: in char_finalize(), the registry entry is removed
only when cpr_find_fd() equals chr->logfd. On a cpr-transfer target, the
stale entry holds the fd received from the source, which differs from the
freshly opened chr->logfd. Does that entry then never get removed, so its
fd stays open?
I did not find any code that clears it, and the follow-up patch only adds
an iotest.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261005092635.3564485-1-den@openvz.org?part=2
^ permalink raw reply [relevance 3%]
* [PATCH hci-8.0 v2 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304
2026-10-05 9:26 7% ` [PATCH hci-8.0 v2 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304 Denis V. Lunev
@ 2026-10-05 9:26 3% ` Denis V. Lunev
2026-10-05 9:30 3% ` no-reply
1 sibling, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-10-05 9:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
A chardev with logfile= opens the log in qemu_char_open() on every
start, including the start of the new QEMU after cpr-exec. The backend
fds are taken from the cpr fd registry there, the log fd is not, so the
new process reopens the path itself.
libvirt normally opens the log through virtlogd and passes a pipe in an
fdset. The qemu-update command line cannot carry that fdset, so it
names the log file path. The exec'd QEMU, running as the qemu user,
then fails to create a log it has no write access to, e.g. a nova
console.log on node-local storage:
qemu-kvm: -chardev pty,id=charserial0,logfile=.../console.log,
logappend=on: Could not create '.../console.log': Permission denied
and the VM is destroyed. Where the open succeeds, the new QEMU writes
the file directly, behind virtlogd's back.
Save the log fd in the cpr registry under the chardev label and take it
from there on cpr-exec incoming, as the backends do. The logfile path
is not opened at all then. If the source did not save it, run without
a log rather than reopen the path. A failed chardev-change re-claims
the log through the base class cpr_claim_fds(), since the replacement's
log has displaced the entry.
Signed-off-by: Denis V. Lunev <den@openvz.org>
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
chardev/char.c | 30 +++++++++++++++++++++++++-----
include/migration/cpr.h | 1 +
2 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/chardev/char.c b/chardev/char.c
index b64767f574e..c037d2e7730 100644
--- a/chardev/char.c
+++ b/chardev/char.c
@@ -40,6 +40,7 @@
#include "qemu/id.h"
#include "qemu/coroutine.h"
#include "qemu/yank.h"
+#include "migration/cpr.h"
#include "chardev-internal.h"
@@ -261,9 +262,21 @@ static void qemu_char_open(Chardev *chr, ChardevBackend *backend,
} else {
flags |= O_TRUNC;
}
- chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
- if (chr->logfd < 0) {
- return;
+ if (cpr_exec_is_incoming()) {
+ chr->logfd = cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID);
+ if (chr->logfd < 0) {
+ warn_report("chardev '%s': log file was not preserved, "
+ "continuing without it", chr->label);
+ }
+ } else {
+ chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
+ if (chr->logfd < 0) {
+ return;
+ }
+ if (!cpr_is_incoming()) {
+ cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
+ cpr_save_fd(chr->label, CPR_CHARDEV_LOG_ID, chr->logfd);
+ }
}
}
@@ -297,6 +310,10 @@ static int null_chr_write(Chardev *chr, const uint8_t *buf, int len)
static void char_cpr_claim_fds(Chardev *chr)
{
+ if (chr->logfd >= 0) {
+ cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
+ cpr_save_fd(chr->label, CPR_CHARDEV_LOG_ID, chr->logfd);
+ }
}
static void char_class_init(ObjectClass *oc, const void *data)
@@ -315,11 +332,14 @@ static void char_finalize(Object *obj)
if (chr->be) {
chr->be->chr = NULL;
}
- g_free(chr->filename);
- g_free(chr->label);
if (chr->logfd != -1) {
+ if (cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID) == chr->logfd) {
+ cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
+ }
close(chr->logfd);
}
+ g_free(chr->filename);
+ g_free(chr->label);
qemu_mutex_destroy(&chr->chr_write_lock);
}
diff --git a/include/migration/cpr.h b/include/migration/cpr.h
index d74ff1ea602..8fb79eb0a28 100644
--- a/include/migration/cpr.h
+++ b/include/migration/cpr.h
@@ -15,6 +15,7 @@
#define CPR_LISTENSOCK_ID 0
#define CPR_CONNSOCK_ID 1
+#define CPR_CHARDEV_LOG_ID 2
#define QEMU_CPR_FILE_MAGIC 0x51435052
#define QEMU_CPR_FILE_VERSION 0x00000001
--
2.53.0
^ permalink raw reply [relevance 3%]
* [PATCH hci-8.0 v2 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304
@ 2026-10-05 9:26 7% ` Denis V. Lunev
2026-10-05 9:26 3% ` [PATCH hci-8.0 v2 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
1 sibling, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-10-05 9:26 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
Give TYPE_CHARDEV an empty cpr_claim_fds() and make the file, pty and
socket backends chain to their parent's. Every chardev class then has
the hook, so qmp_chardev_change() calls it without a NULL check, and
state owned by the base class can be re-claimed there as well.
No functional change.
Signed-off-by: Denis V. Lunev <den@openvz.org>
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
chardev/char-file.c | 3 +++
chardev/char-pty.c | 3 +++
chardev/char-socket.c | 3 +++
chardev/char.c | 13 +++++++------
include/chardev/char.h | 5 ++++-
5 files changed, 20 insertions(+), 7 deletions(-)
diff --git a/chardev/char-file.c b/chardev/char-file.c
index a90f4a77e13..89011f52e3e 100644
--- a/chardev/char-file.c
+++ b/chardev/char-file.c
@@ -124,7 +124,10 @@ out_close:
static void char_file_cpr_claim_fds(Chardev *chr)
{
FDChardev *s = FD_CHARDEV(chr);
+ ChardevClass *pc = CHARDEV_CLASS(
+ object_class_get_parent(object_class_by_name(TYPE_CHARDEV_FILE)));
+ pc->cpr_claim_fds(chr);
if (s->ioc_out) {
cpr_delete_fd(chr->label, 0);
cpr_save_fd(chr->label, 0, QIO_CHANNEL_FILE(s->ioc_out)->fd);
diff --git a/chardev/char-pty.c b/chardev/char-pty.c
index a0090e4c3d1..8058da92bb4 100644
--- a/chardev/char-pty.c
+++ b/chardev/char-pty.c
@@ -403,7 +403,10 @@ static void char_pty_open(Chardev *chr,
static void char_pty_cpr_claim_fds(Chardev *chr)
{
PtyChardev *s = PTY_CHARDEV(chr);
+ ChardevClass *pc = CHARDEV_CLASS(
+ object_class_get_parent(object_class_by_name(TYPE_CHARDEV_PTY)));
+ pc->cpr_claim_fds(chr);
if (s->ioc) {
cpr_delete_fd(chr->label, 0);
cpr_save_fd(chr->label, 0, QIO_CHANNEL_FILE(s->ioc)->fd);
diff --git a/chardev/char-socket.c b/chardev/char-socket.c
index d20aea764f9..c73a3dd0f7a 100644
--- a/chardev/char-socket.c
+++ b/chardev/char-socket.c
@@ -1681,7 +1681,10 @@ char_socket_get_connected(Object *obj, Error **errp)
static void char_socket_cpr_claim_fds(Chardev *chr)
{
SocketChardev *s = SOCKET_CHARDEV(chr);
+ ChardevClass *pc = CHARDEV_CLASS(
+ object_class_get_parent(object_class_by_name(TYPE_CHARDEV_SOCKET)));
+ pc->cpr_claim_fds(chr);
if (s->sioc) {
cpr_delete_fd(chr->label, CPR_CONNSOCK_ID);
cpr_save_fd(chr->label, CPR_CONNSOCK_ID, s->sioc->fd);
diff --git a/chardev/char.c b/chardev/char.c
index 87f40d0137e..b64767f574e 100644
--- a/chardev/char.c
+++ b/chardev/char.c
@@ -295,12 +295,17 @@ static int null_chr_write(Chardev *chr, const uint8_t *buf, int len)
return len;
}
+static void char_cpr_claim_fds(Chardev *chr)
+{
+}
+
static void char_class_init(ObjectClass *oc, const void *data)
{
ChardevClass *cc = CHARDEV_CLASS(oc);
cc->chr_write = null_chr_write;
cc->chr_be_event = chr_be_event;
+ cc->cpr_claim_fds = char_cpr_claim_fds;
}
static void char_finalize(Object *obj)
@@ -1164,9 +1169,7 @@ ChardevReturn *qmp_chardev_change(const char *id, ChardevBackend *backend,
backend, chr->gcontext, handover_yank_instance, errp);
if (!chr_new) {
/* the failed replacement's open() may have displaced our cpr fds */
- if (cc->cpr_claim_fds) {
- cc->cpr_claim_fds(chr);
- }
+ cc->cpr_claim_fds(chr);
return NULL;
}
@@ -1186,9 +1189,7 @@ ChardevReturn *qmp_chardev_change(const char *id, ChardevBackend *backend,
qemu_chr_be_event(chr, CHR_EVENT_OPENED);
}
/* chr_new's open() may have displaced our cpr fds; re-claim them */
- if (cc->cpr_claim_fds) {
- cc->cpr_claim_fds(chr);
- }
+ cc->cpr_claim_fds(chr);
object_unref(OBJECT(chr_new));
return NULL;
}
diff --git a/include/chardev/char.h b/include/chardev/char.h
index 431d07ad3c6..968766fd5d5 100644
--- a/include/chardev/char.h
+++ b/include/chardev/char.h
@@ -305,7 +305,10 @@ struct ChardevClass {
/* notify the backend of frontend open state */
void (*chr_set_fe_open)(Chardev *chr, int fe_open);
- /* (re-)register the backend's CPR fds, e.g. on a failed chardev-change */
+ /*
+ * (re-)register CPR fds, e.g. on a failed chardev-change; an override
+ * must call the parent's hook first
+ */
void (*cpr_claim_fds)(Chardev *chr);
/* handle various events */
--
2.53.0
^ permalink raw reply [relevance 7%]
* Re: [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization
2026-10-02 21:37 3% ` no-reply
@ 2026-10-05 8:45 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-05 8:45 UTC (permalink / raw)
To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel
On 10/2/26 23:37, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
> - [Medium] printk: ve_log_printk() path stamps records with zero or stale time_offset
> - [Medium] printk: unsynchronized log_state->time_offset used to pass author offsets
> - [Medium] printk: time_namespace get/put in update_log_time() on atomic printk paths
> - [Low] printk: 32-byte timens_offsets embedded in every printk_info grows ringbuffers and vmcoreinfo
> --
All of these are true. Two biggest issues seem to be printk_info
growth and synchronization troubles with log_state->time_offset.
The patch will be reworked in v2.
>
> commit eea8f8e8aad7cad7bcb35ee878f7d51794c27e83
> Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
>
> ve/printk: Fix time virtualization
>
> ...
>
--
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH hci-8.1 3/6] ve/printk: Specify ve in printks
2026-10-02 21:36 4% ` no-reply
@ 2026-10-03 13:49 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-03 13:49 UTC (permalink / raw)
To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel
On 10/2/26 23:36, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
> - [Medium] printk: host-log decision in ve_vprintk() uses get_exec_env() instead of the explicit ve
> - [Low] printk: ve_printk_ratelimited() passes get_exec_env() as explicit ve, bypassing in_task() guard
> - [Low] printk: !CONFIG_PRINTK stub ve_printk_deferred() keeps stale __printf(2, 3) attribute
> --
>
> commit 59948e2d94e2f343f89f9252383a061f24bb4a33
> Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
>
> ve/printk: Specify ve in printks
>
> This commit adds ve_printk helpers that take an explicit VE, so callers
> where get_exec_env() is unreliable can name the VE to log to. The
> ve_ratelimit() and ve_net_ratelimit() helpers now require a VE argument,
> and the netfilter callers pass net->owner_ve.
>
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
>
>> diff --git a/include/linux/printk.h b/include/linux/printk.h
>> --- a/include/linux/printk.h
>> +++ b/include/linux/printk.h
>
> [ ... ]
>
>> static inline __printf(3, 4) __cold
>> +int __ve_printk(int dst, struct ve_struct *ve, const char *s, ...)
>> +{
>> + return 0;
>> +}
>> static inline __printf(2, 3) __cold
>> -int ve_printk_deferred(int dst, const char *s, ...)
>> +int ve_printk_deferred(int dst, struct ve_struct *ve, const char *s, ...)
>> {
>> return 0;
>> }
>
> [Severity: Low]
> The stub ve_printk_deferred() gained the struct ve_struct *ve parameter,
> so the format string is now argument 3. It still carries
> __printf(2, 3), while the real prototype was updated to __printf(3, 4).
>
> With the old attribute, GCC treats the ve pointer as the format string.
> Could this produce a -Wformat warning (an error under -Werror) at call
> sites such as __ve_ratelimit() in lib/ratelimit.c when building with
> CONFIG_PRINTK=n?
>
> Should the stub use __printf(3, 4) to match?
Yes, correct. Will fix in v2.
>
> [ ... ]
>
>> @@ -747,7 +761,8 @@ struct pi_entry {
>> DEFAULT_RATELIMIT_BURST); \
>> \
>> - if (ve_ratelimit(dst, &_rs)) \
>> + WARN_ON_ONCE((dst & VE_LOG) && !in_task()); \
>> + if (ve_ratelimit(dst, get_exec_env(), &_rs)) \
>> ve_printk(dst, fmt, ##__VA_ARGS__); \
>> })
>
> [Severity: Low]
> Here get_exec_env() is passed as an explicit VE. When
> ve_printk_ratelimited(VE_LOG, ...) runs from softirq, the interval-reset
> path in __ve_ratelimit() calls:
>
> __ve_ratelimit()
> ve_printk_deferred(dst, ve, KERN_WARNING "%s: %d callbacks suppressed\n", ...)
>
> With a non-NULL ve, ve_vprintk_deferred() computes ve_provided as true,
> so this check is skipped:
>
> if (!ve_provided && WARN_ON_ONCE(!in_task()))
> goto done;
>
> Can the "callbacks suppressed" message then be written to the log of
> whichever VE belongs to the interrupted task? The in_task() guard no
> longer protects this path.
True. Will fix in v2 by passing ve0 value here in IRQ case.
>
> Would it be better to keep passing NULL here, or to use a variant that
> keeps the guard?
>
>> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
>> --- a/kernel/printk/printk.c
>> +++ b/kernel/printk/printk.c
>
> [ ... ]
>
>> -static asmlinkage int ve_vprintk(int dst, const char *fmt, va_list args)
>> +static asmlinkage int ve_vprintk(int dst, struct ve_struct *ve,
>> + const char *fmt, va_list args)
>> {
>> va_list args2;
>> - int r = 0;
>> + int r = 0, ve_provided = (ve != NULL);
>>
>> WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
>>
>> + if (!ve)
>> + ve = get_exec_env();
>> +
>> va_copy(args2, args);
>> if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>> r = vprintk(fmt, args);
>
> [Severity: Medium]
> The host-log decision here still uses ve_is_super(get_exec_env()), while
> the per-VE branch below uses ve_is_super(ve). When an explicit ve is
> passed, can the two destinations disagree?
>
> Take net_velog_ratelimited(net->owner_ve, ...) called from a netfilter
> softirq path such as xt_TCPMSS or nf_conntrack allocation:
>
> - If a container-owned VE_LOG-only message is emitted while the softirq
> interrupted a VE0 task, ve_is_super(get_exec_env()) is true. The
> message is then also printed to the host log.
>
> - If a VE0-owned message is emitted while the softirq interrupted a
> container task, ve_is_super(get_exec_env()) is false. If dst has no
> VE0_LOG, the host branch is skipped. The per-VE branch is skipped too,
> since ve_is_super(ve) is true. The message is lost.
>
> Should this test use ve_is_super(ve) instead of get_exec_env()?
True, missed it. Will fix in v2. Next one as well.
>
> [ ... ]
>
>> +static asmlinkage int ve_vprintk_deferred(int dst, struct ve_struct *ve,
>> + const char *fmt, va_list args)
>> {
>> va_list args2;
>> - int r = 0;
>> + int r = 0, ve_provided = (ve != NULL);
>>
>> WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
>>
>> + if (!ve)
>> + ve = get_exec_env();
>> +
>> va_copy(args2, args);
>> if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>> r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
>
> [Severity: Medium]
> The deferred variant has the same issue. __ve_ratelimit() calls
> ve_printk_deferred() from arbitrary contexts with an explicit ve, but the
> host-log condition still looks at get_exec_env().
>
> Can the "callbacks suppressed" message go to the host log, or be dropped,
> based on the interrupted task's VE rather than the ve passed in?
>
> Should this use ve_is_super(ve) as well?
>
--
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH hci-8.1 2/6] ve/printk: Report potentially leaking usage of ve_printk functions
2026-10-02 21:32 3% ` no-reply
@ 2026-10-03 12:26 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-03 12:26 UTC (permalink / raw)
To: vasileios.almpanis, Konstantin Khorenko; +Cc: devel
On 10/2/26 23:32, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
> - [Medium] printk: WARN_ON_ONCE() added to ve_vprintk_deferred() and ve_log_state() on printk-sensitive paths
> - [Low] ve: ve_dev_printk_emit() returns uninitialized r when VE_LOG is rejected in non-task context
> - [Low] printk: in_task() WARN is evaluated before ve_is_super(), so host-context calls warn spuriously
> - [Low] printk: host-log decision still uses get_exec_env() in non-task context
> --
>
> commit b1f1fbc536b0ab55f70c0064fc9579c59cbe13a9
> Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
>
> ve/printk: Report potentially leaking usage of ve_printk functions
>
> This patch adds WARN_ON_ONCE(!in_task()) checks to the ve printk helpers
> and to ve_log_state(). The goal is to avoid relying on get_exec_env() in
> non-task context, and either reject VE printing or fall back to the host
> log state.
>
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
>
>> diff --git a/drivers/base/core.c b/drivers/base/core.c
>> --- a/drivers/base/core.c
>> +++ b/drivers/base/core.c
>
> [ ... ]
>
>> @@ -4952,7 +4952,9 @@ int ve_dev_printk_emit(int dst, int level, const struct device *dev, const char
>>
>> if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>> r = dev_vprintk_emit(level, dev, fmt, args);
>> - if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
>> +
>> + /* Report IRQ usage here - printing to VE requires explicit ve reference */
>> + if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
>> r = ve_dev_vprintk_emit(level, dev, fmt, args2);
>
> [Severity: Low]
> Can ve_dev_printk_emit() return an uninitialized value here?
>
> The local variable is declared as "int r;" with no initializer. If
> ve_dev_printk_emit(VE_LOG, ...) is called from IRQ, softirq or NMI
> context while the interrupted task is in a non-super container, the
> first branch is not taken because ve_is_super() is false and VE0_LOG
> is not set. The second branch is now skipped because
> WARN_ON_ONCE(!in_task()) is true. The function then does "return r"
> with r never assigned.
>
> Before this patch every valid dst assigned r in at least one branch.
> The sibling functions ve_vprintk() and ve_vprintk_deferred() use
> "int r = 0". Should r be initialized the same way here?
True, will fix in v2.
>
> [Severity: Low]
> Is it intended that WARN_ON_ONCE(!in_task()) is evaluated before
> ve_is_super() in this condition?
>
> With the current ordering, ve_dev_printk_emit(VE_LOG, ...) called from
> IRQ context while the interrupted task is in the host (VE0) triggers
> the warning, even though a host caller cannot leak into a container.
> Because the warning is once per site, an early harmless hit could also
> suppress a later hit that really would leak. The same ordering appears
> in ve_vprintk() and ve_vprintk_deferred() below. Would testing
> !ve_is_super(get_exec_env()) first limit the warning to the
> potentially leaking case?
It doesn't matter that this hit was harmless. We report problematic
places, not problematic situations - interrupted task is a random value.
>
> [Severity: Low]
> Is the host-log decision in the preceding branch still based on
> get_exec_env() in non-task context?
>
> if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
> r = dev_vprintk_emit(level, dev, fmt, args);
>
> If ve_dev_printk_emit(VE_LOG, ...) is called from IRQ and the
> interrupted task is in the host, the message goes to the host log. If
> the interrupted task is in a container, the VE branch is rejected by
> the WARN and the message is dropped. The commit message says printing
> is either rejected or the host log state is returned, but this branch
> still depends on whichever task was interrupted. The same pattern
> exists in ve_vprintk() and ve_vprintk_deferred(). Is that routing
> intended?
We might miss some logs in the described case, and these logs might
go to the host log instead of container's one.
Both cases are fine IMO and commit allows both. This is a sort of
best-effort printing.
>
>> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
>> --- a/kernel/printk/printk.c
>> +++ b/kernel/printk/printk.c
>> @@ -560,7 +560,8 @@ struct log_state *ve_log_state(void)
>> {
>> struct log_state *log = &init_log_state;
>> #ifdef CONFIG_VE
>> - if (get_exec_env()->log_state)
>> + /* get_exec_env() is reliable only in task context */
>> + if (!WARN_ON_ONCE(!in_task()) && get_exec_env()->log_state)
>> log = get_exec_env()->log_state;
>> #endif
>> return log;
>
> [Severity: Medium]
> Can this WARN_ON_ONCE() recurse into printk?
It is fine.
>
> ve_log_state() is reached from ve_vprintk_emit() and then
> vprintk_emit_log(), so the WARN fires in the middle of the printk emit
> path. A WARN prints a message and backtrace through the regular
> non-deferred printk, and it can panic under panic_on_warn.
>
> [ ... ]
>
>> @@ -2540,7 +2541,9 @@ static asmlinkage int ve_vprintk(int dst, const char *fmt, va_list args)
>> va_copy(args2, args);
>> if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>> r = vprintk(fmt, args);
>> - if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
>> +
>> + /* Report IRQ usage here - printing to VE requires explicit ve reference */
>> + if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
>> r = ve_vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, fmt, args2);
>> va_end(args2);
>
> [ ... ]
>
>> @@ -2574,7 +2577,9 @@ static asmlinkage int ve_vprintk_deferred(int dst, const char *fmt, va_list args
>> va_copy(args2, args);
>> if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>> r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
>> - if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
>> +
>> + /* Report IRQ usage here - printing to VE requires explicit ve reference */
>> + if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
>> r = ve_vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args2);
>> va_end(args2);
>
> [Severity: Medium]
> Is a synchronous WARN_ON_ONCE() safe inside ve_vprintk_deferred()?
>
> This helper exists for contexts where regular printk is unsafe, such as
> scheduler code or code holding rq or timekeeping locks. If a caller
> uses ve_printk_deferred(VE_LOG, ...) from hardirq, softirq or
> scheduler-related context, the WARN fires inside
> ve_vprintk_deferred() and prints through the non-deferred printk with
> a backtrace. Can this deadlock or recurse into printk, or panic under
> panic_on_warn?
>
> The WARN added in ve_log_state() has the same concern on the
> ve_vprintk_emit() path used here.
It should be fine, WARNs are used in sched code. See reply to the
previous patch.
>
--
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH hci-8.1 1/6] ve/printk: Add sanity checks in ve_printk functions
2026-10-02 21:31 4% ` no-reply
@ 2026-10-03 12:26 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-10-03 12:26 UTC (permalink / raw)
To: vasileios.almpanis, Konstantin Khorenko; +Cc: devel
On 10/2/26 23:31, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Low] printk: WARN_ON_ONCE() in ve_vprintk_deferred() uses non-deferred printk path
> --
>
> commit 2b22fc2da5243bd2b5c695100f66ab107b28c158
> Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
>
> ve/printk: Add sanity checks in ve_printk functions
>
> This patch adds a sanity check on the dst argument in the ve_printk
> helpers. It also reorders the VE_LOG test so the dst bit is checked
> before ve_is_super().
>
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
>
>> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
>> --- a/kernel/printk/printk.c
>> +++ b/kernel/printk/printk.c
>
> [ ... ]
>
>> @@ -2567,10 +2569,12 @@ static asmlinkage int ve_vprintk_deferred(int dst, const char *fmt, va_list args
>> va_list args2;
>> int r = 0;
>>
>> + WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
>> +
>> va_copy(args2, args);
>> if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
>> r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
>
> [Severity: Low]
> This isn't a bug in the normal case, but is the new WARN_ON_ONCE() safe
> in ve_vprintk_deferred()?
>
> This function exists for contexts where the regular printk path is
> unsafe, which is why it emits at LOGLEVEL_SCHED through vprintk_emit().
> WARN_ON_ONCE() reports through the normal printk and console path. It
> can also panic if panic_on_warn is set.
>
> If a caller passes dst == 0 or a dst with bits outside VE_LOG_BOTH from
> such a context, could the warning recurse into scheduler or console
> locks that the deferred variant is meant to avoid?
>
> I did not find a caller that passes an invalid dst. The trigger would
> need a buggy caller, and the warning fires only once.
>
> Would a BUILD_BUG_ON() for constant arguments be more suitable here?
> Alternatively, the report could go through a deferred-safe mechanism
> such as printk_deferred().
We can't know passed values when compiling this file.
Also, WARN_ON doesn't print anything itself, instead it executes UD2,
leaving all the job to the exception handler.
And kernel does use WARNs in sched code: check
force_compatible_cpus_allowed_ptr,
it has both WARN_ON and printk_deferred.
>
--
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter
2026-10-02 21:26 4% ` [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
@ 2026-10-02 21:40 3% ` no-reply
2026-10-05 10:02 0% ` Vladimir Riabchun
0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-02 21:40 UTC (permalink / raw)
To: vasileios.almpanis, Konstantin Khorenko, Vladimir Riabchun; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] net: neigh_alloc() drops table-specific overflow log; ARP and ND share one failcount
--
commit 83435d15f32b1a7fcb7f3aac09fb30774f63d147
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
ve: add neigh_tbl failcounter
This commit adds a per-VE neigh_tbl_failcount and exposes it in
ve.failcount. In neigh_alloc(), the "neighbor table overflow!" message
is replaced by a call to ve_failcount_inc() when the table limit is hit.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-135520
Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
> --- a/kernel/ve/ve.c
> +++ b/kernel/ve/ve.c
[ ... ]
> @@ -834,6 +835,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
>
> atomic_set(&ve->arp_neigh_nr, 0);
> atomic_set(&ve->nd_neigh_nr, 0);
> + atomic_set(&ve->neigh_tbl_failcount, 0);
[Severity: Medium]
ARP and ND have separate per-VE counters and limits (arp_neigh_nr and
nd_neigh_nr, selected by get_perve_tbl_entries_counter()), but both now
feed the single neigh_tbl_failcount.
Can the ve.failcount output tell an operator whether the ARP table or the
ND table hit its limit?
[ ... ]
> diff --git a/net/core/neighbour.c b/net/core/neighbour.c
> --- a/net/core/neighbour.c
> +++ b/net/core/neighbour.c
[ ... ]
> @@ -520,8 +520,7 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
> (glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
> time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
> if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
> - net_info_ratelimited("%s: neighbor table overflow!\n",
> - tbl->id);
> + ve_failcount_inc(ve, neigh_tbl);
> NEIGH_CACHE_STAT_INC(tbl, table_fulls);
> goto out_entries;
> }
[Severity: Medium]
The removed net_info_ratelimited() message named the table through
tbl->id. The replacement message from ve_failcount_inc() is fixed text
and has no table or family information:
"CT<name>: hits the neigh_tbl limit, see the 'neigh_tbl' counter in
ve.failcount of the container's ve cgroup"
Is it intended to drop the arp_tbl versus nd_tbl distinction and the
host-level overflow message? The commit message does not mention that the
original message is being removed.
Also, this path runs for ve0 as well. Would the host then log "CT0: hits
the neigh_tbl limit ... of the container's ve cgroup", which seems
misleading wording for the host?
Finally, the ratelimit state in ve_failcount_inc() comes from
__ve_pr_warn_ratelimited(), which is a single static state at the macro
call site. Is it a concern that all VEs share it, so one container that
overflows its table often could suppress the message for other containers?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002-ve-printk-irq-v1-0-cb55896d455c@virtuozzo.com?part=6
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve
2026-10-02 21:26 8% ` [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
@ 2026-10-02 21:38 5% ` no-reply
2026-10-05 9:56 0% ` Vladimir Riabchun
0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-02 21:38 UTC (permalink / raw)
To: Vladimir Riabchun, Konstantin Khorenko, vasileios.almpanis; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] printk: !CONFIG_PRINTK __ve_printk_ratelimited() stub lacks the ve parameter
--
commit 446bb8b8f19e9083361fc5d794bba0859020d3a4
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
ve/failcount: Use ve_printk with specified ve
This patch adds __ve_printk_ratelimited() and __ve_pr_warn_ratelimited()
helpers that take an explicit ve argument. ve_failcount_inc() now uses them
so the message goes to the right VE when called from IRQ context.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
> diff --git a/include/linux/printk.h b/include/linux/printk.h
> --- a/include/linux/printk.h
> +++ b/include/linux/printk.h
[ ... ]
> #else
> #define printk_ratelimited(fmt, ...) \
> no_printk(fmt, ##__VA_ARGS__)
> #define ve_printk_ratelimited(dst, fmt, ...) \
> no_printk(fmt, ##__VA_ARGS__)
> +#define __ve_printk_ratelimited(dst, fmt, ...) \
> + no_printk(fmt, ##__VA_ARGS__)
> #endif
[Severity: Low]
Is the parameter list of this !CONFIG_PRINTK stub correct? The
CONFIG_PRINTK version of __ve_printk_ratelimited() takes
(dst, ve, fmt, ...), but this stub takes only (dst, fmt, ...).
[ ... ]
> +#define __ve_pr_warn_ratelimited(dst, ve, fmt, ...) \
> + __ve_printk_ratelimited(dst, ve, KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
This macro is defined outside the #ifdef and always passes ve as the second
argument. With CONFIG_VE=y and CONFIG_PRINTK=n, it expands into the
mismatched stub, so ve is bound to fmt.
> diff --git a/include/linux/ve.h b/include/linux/ve.h
> --- a/include/linux/ve.h
> +++ b/include/linux/ve.h
[ ... ]
> + __ve_pr_warn_ratelimited(VE_LOG_BOTH, __ve, "CT%s: hits the " #name \
> + " limit, see the '" #name "' counter in ve.failcount " \
> + "of the container's ve cgroup\n", ve_name(__ve)); \
With CONFIG_PRINTK=n, ve_failcount_inc() expands like this:
ve_failcount_inc()
__ve_pr_warn_ratelimited(VE_LOG_BOTH, __ve, "CT%s: ...", ve_name(__ve))
__ve_printk_ratelimited(dst, ve, KERN_WARNING pr_fmt(fmt), ...)
no_printk(<struct ve_struct *>, <real format>, ve_name(__ve))
no_printk() then receives a struct ve_struct pointer as its const char *
format argument. The real format string and ve_name(__ve) end up in the
varargs.
Can this produce an incompatible-pointer diagnostic, which is an error with
-Werror or newer compilers? The format string also stops being checked.
Should the stub be changed to take (dst, ve, fmt, ...)? As written, this
can break the build when CONFIG_VE=y and CONFIG_PRINTK=n.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002-ve-printk-irq-v1-0-cb55896d455c@virtuozzo.com?part=5
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 5%]
* Re: [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization
2026-10-02 21:26 3% ` [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
@ 2026-10-02 21:37 3% ` no-reply
2026-10-05 8:45 0% ` Vladimir Riabchun
0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-02 21:37 UTC (permalink / raw)
To: Konstantin Khorenko, Vladimir Riabchun, vasileios.almpanis; +Cc: devel
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] printk: ve_log_printk() path stamps records with zero or stale time_offset
- [Medium] printk: unsynchronized log_state->time_offset used to pass author offsets
- [Medium] printk: time_namespace get/put in update_log_time() on atomic printk paths
- [Low] printk: 32-byte timens_offsets embedded in every printk_info grows ringbuffers and vmcoreinfo
--
commit eea8f8e8aad7cad7bcb35ee878f7d51794c27e83
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
ve/printk: Fix time virtualization
This patch stores timens offsets in each printk_info record, so the
timestamp is shifted using the offsets of the context that emitted the
message. The offsets are copied into log_state on each log_state lookup
and then into the record in vprintk_store_log().
Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
> diff --git a/kernel/printk/internal.h b/kernel/printk/internal.h
> --- a/kernel/printk/internal.h
> +++ b/kernel/printk/internal.h
[ ... ]
> @@ -354,11 +355,16 @@ struct log_state {
> */
> struct latched_seq clear_seq;
> struct printk_ringbuffer *prb;
> + /* Updated on every log_state request */
> + struct timens_offsets time_offset;
> };
[Severity: Medium]
Can passing the author offsets through shared log_state storage produce
torn or stale values?
update_log_time() does a plain multi-word struct copy into
GET_VE_LOG(ve)->time_offset on every ve_log_state() or __ve_log_state()
call. That includes read-only callers such as devkmsg_poll(),
devkmsg_read() and do_syslog(), and they can run concurrently on other
CPUs or in IRQ context.
vprintk_store_log() later reads the field with another plain copy, after
vsnprintf and prb_reserve. Nothing orders the two accesses, since there
is no lock, no seqcount and no READ_ONCE()/WRITE_ONCE().
The offsets are normally the same for a given VE. They can differ across a
VE stop and start, which the commit message says can produce a new time
namespace. In that case a mixed or stale value could be stored in the
ring buffer, and KCSAN would likely flag the race.
Would it be possible to compute the offsets locally in the writer and
pass them down to vprintk_emit_log() by value or by pointer?
> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c
> @@ -556,13 +556,28 @@ DEFINE_STRUCT_MEMBER_ALIAS(clear_seq, init_log_state, clear_seq);
> DEFINE_STRUCT_MEMBER_ALIAS(prb, init_log_state, prb);
> #undef DEFINE_STRUCT_MEMBER_ALIAS
>
> +static void update_log_time(struct ve_struct *ve)
> +{
> + struct time_namespace *timens = ve_get_time_ns(ve);
> +
> + if (timens)
> + GET_VE_LOG(ve)->time_offset = timens->offsets;
> + else
> + GET_VE_LOG(ve)->time_offset = init_time_ns.offsets;
> +
> + if (timens)
> + put_time_ns(timens);
> +}
[Severity: Medium]
Can the get/put pair here run from atomic context?
__ve_vprintk_emit() calls __ve_log_state(ve), which has no in_task()
guard, so it can be reached from IRQ or other atomic context:
__ve_vprintk_emit()
__ve_log_state()
update_log_time()
ve_get_time_ns()
put_time_ns()
If VE teardown clears ve_nsproxy and drops its reference between the get
and the put, the put_time_ns() here would be the last one. free_time_ns()
would then run from atomic or scheduler-locked printk context. I could not
confirm that this race is reachable, so this part is speculative.
Is the reference needed at all? An rcu_read_lock() section that copies the
offsets looks like it would be enough. The old code only took the reference
when formatting output, whereas now every log_state lookup pays for the
refcount inc/dec and the struct copy, including the read-only syslog and
devkmsg paths.
> struct log_state *ve_log_state(void)
> {
> struct log_state *log = &init_log_state;
> #ifdef CONFIG_VE
> /* get_exec_env() is reliable only in task context */
> - if (!WARN_ON_ONCE(!in_task()) && get_exec_env()->log_state)
> + if (!WARN_ON_ONCE(!in_task()) && get_exec_env()->log_state) {
> + update_log_time(get_exec_env());
> log = get_exec_env()->log_state;
> + }
> #endif
> return log;
> }
[ ... ]
> @@ -1108,6 +1114,7 @@ void log_buf_vmcoreinfo_setup(void)
> VMCOREINFO_OFFSET(printk_info, text_len);
> VMCOREINFO_OFFSET(printk_info, caller_id);
> VMCOREINFO_OFFSET(printk_info, dev_info);
> + VMCOREINFO_OFFSET(printk_info, time_offset);
[Severity: Low]
Is the size of the new field intended? struct printk_info now embeds a
32-byte struct timens_offsets (two timespec64) in every descriptor, but
only the boottime offset is used.
That grows the static host __log_buf infos array and every per-VE
ringbuffer infos array. SIZE(printk_info) also changes and a new
OFFSET(printk_info.time_offset) is exported, so dump tools such as crash
and makedumpfile would need updating.
Would a per-log value, or a 16-byte boottime delta, be enough? The commit
message does not mention the size or vmcoreinfo change.
[ ... ]
> @@ -2402,6 +2411,8 @@ static int vprintk_store_log(struct log_state *log, int facility, int level,
> if (dev_info)
> memcpy(&r.info->dev_info, dev_info, sizeof(r.info->dev_info));
>
> + r.info->time_offset = log->time_offset;
> +
[Severity: Medium]
Can this store a zero or stale time_offset for records coming from
ve_log_printk()?
ve_log_printk() calls ve_log_vprintk(), which for a non-super VE calls
vprintk_emit_log() directly:
r = vprintk_emit_log(ve->log_state, 0, LOGLEVEL_DEFAULT, NULL,
fmt, args);
That path never goes through ve_log_state() or __ve_log_state(), which
are the only callers of update_log_time(). vprintk_store_log() then
copies whatever is in log->time_offset.
The field is zero from the kzalloc in ve_log_init() unless some other
lookup has updated it. The record would then be stored with an unshifted
host uptime, which is the leak this patch is meant to prevent.
Should the offsets be refreshed on this path as well?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002-ve-printk-irq-v1-0-cb55896d455c@virtuozzo.com?part=4
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH hci-8.1 3/6] ve/printk: Specify ve in printks
2026-10-02 21:26 15% ` [Devel] [PATCH hci-8.1 3/6] ve/printk: Specify ve in printks Vladimir Riabchun
@ 2026-10-02 21:36 4% ` no-reply
2026-10-03 13:49 0% ` Vladimir Riabchun
0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-02 21:36 UTC (permalink / raw)
To: Konstantin Khorenko, Vladimir Riabchun, vasileios.almpanis; +Cc: devel
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] printk: host-log decision in ve_vprintk() uses get_exec_env() instead of the explicit ve
- [Low] printk: ve_printk_ratelimited() passes get_exec_env() as explicit ve, bypassing in_task() guard
- [Low] printk: !CONFIG_PRINTK stub ve_printk_deferred() keeps stale __printf(2, 3) attribute
--
commit 59948e2d94e2f343f89f9252383a061f24bb4a33
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
ve/printk: Specify ve in printks
This commit adds ve_printk helpers that take an explicit VE, so callers
where get_exec_env() is unreliable can name the VE to log to. The
ve_ratelimit() and ve_net_ratelimit() helpers now require a VE argument,
and the netfilter callers pass net->owner_ve.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
> diff --git a/include/linux/printk.h b/include/linux/printk.h
> --- a/include/linux/printk.h
> +++ b/include/linux/printk.h
[ ... ]
> static inline __printf(3, 4) __cold
> +int __ve_printk(int dst, struct ve_struct *ve, const char *s, ...)
> +{
> + return 0;
> +}
> static inline __printf(2, 3) __cold
> -int ve_printk_deferred(int dst, const char *s, ...)
> +int ve_printk_deferred(int dst, struct ve_struct *ve, const char *s, ...)
> {
> return 0;
> }
[Severity: Low]
The stub ve_printk_deferred() gained the struct ve_struct *ve parameter,
so the format string is now argument 3. It still carries
__printf(2, 3), while the real prototype was updated to __printf(3, 4).
With the old attribute, GCC treats the ve pointer as the format string.
Could this produce a -Wformat warning (an error under -Werror) at call
sites such as __ve_ratelimit() in lib/ratelimit.c when building with
CONFIG_PRINTK=n?
Should the stub use __printf(3, 4) to match?
[ ... ]
> @@ -747,7 +761,8 @@ struct pi_entry {
> DEFAULT_RATELIMIT_BURST); \
> \
> - if (ve_ratelimit(dst, &_rs)) \
> + WARN_ON_ONCE((dst & VE_LOG) && !in_task()); \
> + if (ve_ratelimit(dst, get_exec_env(), &_rs)) \
> ve_printk(dst, fmt, ##__VA_ARGS__); \
> })
[Severity: Low]
Here get_exec_env() is passed as an explicit VE. When
ve_printk_ratelimited(VE_LOG, ...) runs from softirq, the interval-reset
path in __ve_ratelimit() calls:
__ve_ratelimit()
ve_printk_deferred(dst, ve, KERN_WARNING "%s: %d callbacks suppressed\n", ...)
With a non-NULL ve, ve_vprintk_deferred() computes ve_provided as true,
so this check is skipped:
if (!ve_provided && WARN_ON_ONCE(!in_task()))
goto done;
Can the "callbacks suppressed" message then be written to the log of
whichever VE belongs to the interrupted task? The in_task() guard no
longer protects this path.
Would it be better to keep passing NULL here, or to use a variant that
keeps the guard?
> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c
[ ... ]
> -static asmlinkage int ve_vprintk(int dst, const char *fmt, va_list args)
> +static asmlinkage int ve_vprintk(int dst, struct ve_struct *ve,
> + const char *fmt, va_list args)
> {
> va_list args2;
> - int r = 0;
> + int r = 0, ve_provided = (ve != NULL);
>
> WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
>
> + if (!ve)
> + ve = get_exec_env();
> +
> va_copy(args2, args);
> if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
> r = vprintk(fmt, args);
[Severity: Medium]
The host-log decision here still uses ve_is_super(get_exec_env()), while
the per-VE branch below uses ve_is_super(ve). When an explicit ve is
passed, can the two destinations disagree?
Take net_velog_ratelimited(net->owner_ve, ...) called from a netfilter
softirq path such as xt_TCPMSS or nf_conntrack allocation:
- If a container-owned VE_LOG-only message is emitted while the softirq
interrupted a VE0 task, ve_is_super(get_exec_env()) is true. The
message is then also printed to the host log.
- If a VE0-owned message is emitted while the softirq interrupted a
container task, ve_is_super(get_exec_env()) is false. If dst has no
VE0_LOG, the host branch is skipped. The per-VE branch is skipped too,
since ve_is_super(ve) is true. The message is lost.
Should this test use ve_is_super(ve) instead of get_exec_env()?
[ ... ]
> +static asmlinkage int ve_vprintk_deferred(int dst, struct ve_struct *ve,
> + const char *fmt, va_list args)
> {
> va_list args2;
> - int r = 0;
> + int r = 0, ve_provided = (ve != NULL);
>
> WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
>
> + if (!ve)
> + ve = get_exec_env();
> +
> va_copy(args2, args);
> if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
> r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
[Severity: Medium]
The deferred variant has the same issue. __ve_ratelimit() calls
ve_printk_deferred() from arbitrary contexts with an explicit ve, but the
host-log condition still looks at get_exec_env().
Can the "callbacks suppressed" message go to the host log, or be dropped,
based on the interrupted task's VE rather than the ve passed in?
Should this use ve_is_super(ve) as well?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002-ve-printk-irq-v1-0-cb55896d455c@virtuozzo.com?part=3
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* Re: [Devel] [PATCH hci-8.1 2/6] ve/printk: Report potentially leaking usage of ve_printk functions
@ 2026-10-02 21:32 3% ` no-reply
2026-10-03 12:26 0% ` Vladimir Riabchun
0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-02 21:32 UTC (permalink / raw)
To: vasileios.almpanis, Vladimir Riabchun, Konstantin Khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] printk: WARN_ON_ONCE() added to ve_vprintk_deferred() and ve_log_state() on printk-sensitive paths
- [Low] ve: ve_dev_printk_emit() returns uninitialized r when VE_LOG is rejected in non-task context
- [Low] printk: in_task() WARN is evaluated before ve_is_super(), so host-context calls warn spuriously
- [Low] printk: host-log decision still uses get_exec_env() in non-task context
--
commit b1f1fbc536b0ab55f70c0064fc9579c59cbe13a9
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
ve/printk: Report potentially leaking usage of ve_printk functions
This patch adds WARN_ON_ONCE(!in_task()) checks to the ve printk helpers
and to ve_log_state(). The goal is to avoid relying on get_exec_env() in
non-task context, and either reject VE printing or fall back to the host
log state.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
> diff --git a/drivers/base/core.c b/drivers/base/core.c
> --- a/drivers/base/core.c
> +++ b/drivers/base/core.c
[ ... ]
> @@ -4952,7 +4952,9 @@ int ve_dev_printk_emit(int dst, int level, const struct device *dev, const char
>
> if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
> r = dev_vprintk_emit(level, dev, fmt, args);
> - if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
> +
> + /* Report IRQ usage here - printing to VE requires explicit ve reference */
> + if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
> r = ve_dev_vprintk_emit(level, dev, fmt, args2);
[Severity: Low]
Can ve_dev_printk_emit() return an uninitialized value here?
The local variable is declared as "int r;" with no initializer. If
ve_dev_printk_emit(VE_LOG, ...) is called from IRQ, softirq or NMI
context while the interrupted task is in a non-super container, the
first branch is not taken because ve_is_super() is false and VE0_LOG
is not set. The second branch is now skipped because
WARN_ON_ONCE(!in_task()) is true. The function then does "return r"
with r never assigned.
Before this patch every valid dst assigned r in at least one branch.
The sibling functions ve_vprintk() and ve_vprintk_deferred() use
"int r = 0". Should r be initialized the same way here?
[Severity: Low]
Is it intended that WARN_ON_ONCE(!in_task()) is evaluated before
ve_is_super() in this condition?
With the current ordering, ve_dev_printk_emit(VE_LOG, ...) called from
IRQ context while the interrupted task is in the host (VE0) triggers
the warning, even though a host caller cannot leak into a container.
Because the warning is once per site, an early harmless hit could also
suppress a later hit that really would leak. The same ordering appears
in ve_vprintk() and ve_vprintk_deferred() below. Would testing
!ve_is_super(get_exec_env()) first limit the warning to the
potentially leaking case?
[Severity: Low]
Is the host-log decision in the preceding branch still based on
get_exec_env() in non-task context?
if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
r = dev_vprintk_emit(level, dev, fmt, args);
If ve_dev_printk_emit(VE_LOG, ...) is called from IRQ and the
interrupted task is in the host, the message goes to the host log. If
the interrupted task is in a container, the VE branch is rejected by
the WARN and the message is dropped. The commit message says printing
is either rejected or the host log state is returned, but this branch
still depends on whichever task was interrupted. The same pattern
exists in ve_vprintk() and ve_vprintk_deferred(). Is that routing
intended?
> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c
> @@ -560,7 +560,8 @@ struct log_state *ve_log_state(void)
> {
> struct log_state *log = &init_log_state;
> #ifdef CONFIG_VE
> - if (get_exec_env()->log_state)
> + /* get_exec_env() is reliable only in task context */
> + if (!WARN_ON_ONCE(!in_task()) && get_exec_env()->log_state)
> log = get_exec_env()->log_state;
> #endif
> return log;
[Severity: Medium]
Can this WARN_ON_ONCE() recurse into printk?
ve_log_state() is reached from ve_vprintk_emit() and then
vprintk_emit_log(), so the WARN fires in the middle of the printk emit
path. A WARN prints a message and backtrace through the regular
non-deferred printk, and it can panic under panic_on_warn.
[ ... ]
> @@ -2540,7 +2541,9 @@ static asmlinkage int ve_vprintk(int dst, const char *fmt, va_list args)
> va_copy(args2, args);
> if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
> r = vprintk(fmt, args);
> - if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
> +
> + /* Report IRQ usage here - printing to VE requires explicit ve reference */
> + if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
> r = ve_vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, fmt, args2);
> va_end(args2);
[ ... ]
> @@ -2574,7 +2577,9 @@ static asmlinkage int ve_vprintk_deferred(int dst, const char *fmt, va_list args
> va_copy(args2, args);
> if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
> r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
> - if ((dst & VE_LOG) && !ve_is_super(get_exec_env()))
> +
> + /* Report IRQ usage here - printing to VE requires explicit ve reference */
> + if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
> r = ve_vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args2);
> va_end(args2);
[Severity: Medium]
Is a synchronous WARN_ON_ONCE() safe inside ve_vprintk_deferred()?
This helper exists for contexts where regular printk is unsafe, such as
scheduler code or code holding rq or timekeeping locks. If a caller
uses ve_printk_deferred(VE_LOG, ...) from hardirq, softirq or
scheduler-related context, the WARN fires inside
ve_vprintk_deferred() and prints through the non-deferred printk with
a backtrace. Can this deadlock or recurse into printk, or panic under
panic_on_warn?
The WARN added in ve_log_state() has the same concern on the
ve_vprintk_emit() path used here.
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002-ve-printk-irq-v1-0-cb55896d455c@virtuozzo.com?part=2
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH hci-8.1 1/6] ve/printk: Add sanity checks in ve_printk functions
@ 2026-10-02 21:31 4% ` no-reply
2026-10-03 12:26 0% ` Vladimir Riabchun
0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-10-02 21:31 UTC (permalink / raw)
To: vasileios.almpanis, Vladimir Riabchun, Konstantin Khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] printk: WARN_ON_ONCE() in ve_vprintk_deferred() uses non-deferred printk path
--
commit 2b22fc2da5243bd2b5c695100f66ab107b28c158
Author: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
ve/printk: Add sanity checks in ve_printk functions
This patch adds a sanity check on the dst argument in the ve_printk
helpers. It also reorders the VE_LOG test so the dst bit is checked
before ve_is_super().
Link: https://virtuozzo.atlassian.net/browse/VSTOR-146839
> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c
[ ... ]
> @@ -2567,10 +2569,12 @@ static asmlinkage int ve_vprintk_deferred(int dst, const char *fmt, va_list args
> va_list args2;
> int r = 0;
>
> + WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
> +
> va_copy(args2, args);
> if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
> r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
[Severity: Low]
This isn't a bug in the normal case, but is the new WARN_ON_ONCE() safe
in ve_vprintk_deferred()?
This function exists for contexts where the regular printk path is
unsafe, which is why it emits at LOGLEVEL_SCHED through vprintk_emit().
WARN_ON_ONCE() reports through the normal printk and console path. It
can also panic if panic_on_warn is set.
If a caller passes dst == 0 or a dst with bits outside VE_LOG_BOTH from
such a context, could the warning recurse into scheduler or console
locks that the deferred variant is meant to avoid?
I did not find a caller that passes an invalid dst. The trigger would
need a buggy caller, and the warning fires only once.
Would a BUILD_BUG_ON() for constant arguments be more suitable here?
Alternatively, the report could go through a deferred-safe mechanism
such as printk_deferred().
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002-ve-printk-irq-v1-0-cb55896d455c@virtuozzo.com?part=1
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve
` (3 preceding siblings ...)
2026-10-02 21:26 3% ` [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
@ 2026-10-02 21:26 8% ` Vladimir Riabchun
2026-10-02 21:38 5% ` no-reply
2026-10-02 21:26 4% ` [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
5 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-02 21:26 UTC (permalink / raw)
To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel
ve_failcount_inc could be called from IRQ context, and we want the message
to go to the right place. Use new __ve_printk helpers for this.
https://virtuozzo.atlassian.net/browse/VSTOR-146839
Feature: ve printk
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
include/linux/printk.h | 18 ++++++++++++++----
include/linux/ve.h | 16 ++++++++--------
2 files changed, 22 insertions(+), 12 deletions(-)
diff --git a/include/linux/printk.h b/include/linux/printk.h
index 1478e47dc582..ce82623fb138 100644
--- a/include/linux/printk.h
+++ b/include/linux/printk.h
@@ -755,21 +755,28 @@ struct pi_entry {
printk(fmt, ##__VA_ARGS__); \
})
-#define ve_printk_ratelimited(dst, fmt, ...) \
+#define __ve_printk_ratelimited(dst, ve, fmt, ...) \
({ \
static DEFINE_RATELIMIT_STATE(_rs, \
DEFAULT_RATELIMIT_INTERVAL, \
DEFAULT_RATELIMIT_BURST); \
\
- WARN_ON_ONCE((dst & VE_LOG) && !in_task()); \
- if (ve_ratelimit(dst, get_exec_env(), &_rs)) \
- ve_printk(dst, fmt, ##__VA_ARGS__); \
+ if (ve_ratelimit(dst, ve, &_rs)) \
+ __ve_printk(dst, ve, fmt, ##__VA_ARGS__); \
+})
+
+#define ve_printk_ratelimited(dst, fmt, ...) \
+({ \
+ WARN_ON_ONCE((dst & VE_LOG) && !in_task()); \
+ __ve_printk_ratelimited(dst, get_exec_env(), fmt, ##__VA_ARGS__); \
})
#else
#define printk_ratelimited(fmt, ...) \
no_printk(fmt, ##__VA_ARGS__)
#define ve_printk_ratelimited(dst, fmt, ...) \
no_printk(fmt, ##__VA_ARGS__)
+#define __ve_printk_ratelimited(dst, fmt, ...) \
+ no_printk(fmt, ##__VA_ARGS__)
#endif
#define pr_emerg_ratelimited(fmt, ...) \
@@ -787,6 +794,7 @@ struct pi_entry {
#define pr_info_ratelimited(fmt, ...) \
printk_ratelimited(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
/* no pr_cont_ratelimited, don't do that... */
+
#define ve_pr_emerg_ratelimited(dst, fmt, ...) \
ve_printk_ratelimited(dst, KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)
#define ve_pr_alert_ratelimited(dst, fmt, ...) \
@@ -797,6 +805,8 @@ struct pi_entry {
ve_printk_ratelimited(dst, KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
#define ve_pr_warn_ratelimited(dst, fmt, ...) \
ve_printk_ratelimited(dst, KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
+#define __ve_pr_warn_ratelimited(dst, ve, fmt, ...) \
+ __ve_printk_ratelimited(dst, ve, KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
#define ve_pr_notice_ratelimited(dst, fmt, ...) \
ve_printk_ratelimited(dst, KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
#define ve_pr_info_ratelimited(dst, fmt, ...) \
diff --git a/include/linux/ve.h b/include/linux/ve.h
index a400d21593b2..946df4eebe89 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -156,14 +156,14 @@ extern int nr_ve;
#define BPF_PROG_MAX_NR_DEFAULT 256 /* number of loaded BPF progs per-VE */
#define MNT_MAX_NR_DEFAULT 4096 /* number of mounts per-VE */
-#define ve_failcount_inc(ve, name) \
-do { \
- struct ve_struct *__ve = (ve); \
- \
- atomic_inc(&__ve->name##_failcount); \
- ve_pr_warn_ratelimited(VE_LOG_BOTH, "CT%s: hits the " #name \
- " limit, see the '" #name "' counter in ve.failcount " \
- "of the container's ve cgroup\n", ve_name(__ve)); \
+#define ve_failcount_inc(ve, name) \
+do { \
+ struct ve_struct *__ve = (ve); \
+ \
+ atomic_inc(&__ve->name##_failcount); \
+ __ve_pr_warn_ratelimited(VE_LOG_BOTH, __ve, "CT%s: hits the " #name \
+ " limit, see the '" #name "' counter in ve.failcount " \
+ "of the container's ve cgroup\n", ve_name(__ve)); \
} while (0)
#ifdef CONFIG_VE
--
2.52.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 8%]
* [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter
` (4 preceding siblings ...)
2026-10-02 21:26 8% ` [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
@ 2026-10-02 21:26 4% ` Vladimir Riabchun
2026-10-02 21:40 3% ` no-reply
5 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-02 21:26 UTC (permalink / raw)
To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel
Count hits of the per-VE neigh_tbl limit in ve.failcount.
This one was delayed due to a high risk of IRQ-context hits,
when destination VE can't be obtained from get_exec_env.
https://virtuozzo.atlassian.net/browse/VSTOR-135520
https://virtuozzo.atlassian.net/browse/VSTOR-146839
Feature: per-ve failcounters
Feature: ve printk
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
include/linux/ve.h | 1 +
kernel/ve/ve.c | 3 +++
net/core/neighbour.c | 3 +--
3 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/include/linux/ve.h b/include/linux/ve.h
index 946df4eebe89..44e04dc75823 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -97,6 +97,7 @@ struct ve_struct {
atomic_t arp_neigh_nr;
atomic_t nd_neigh_nr;
+ atomic_t neigh_tbl_failcount;
unsigned long meminfo_val;
/*
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index 51cc635be2e2..35fad9658149 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -118,6 +118,7 @@ struct ve_struct ve0 = {
.arp_neigh_nr = ATOMIC_INIT(0),
.nd_neigh_nr = ATOMIC_INIT(0),
+ .neigh_tbl_failcount = ATOMIC_INIT(0),
.mnt_avail_nr = ATOMIC_INIT(INT_MAX),
.mnt_max_nr = INT_MAX,
.mnt_failcount = ATOMIC_INIT(0),
@@ -834,6 +835,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
atomic_set(&ve->arp_neigh_nr, 0);
atomic_set(&ve->nd_neigh_nr, 0);
+ atomic_set(&ve->neigh_tbl_failcount, 0);
ve->mnt_max_nr = MNT_MAX_NR_DEFAULT;
atomic_set(&ve->mnt_failcount, 0);
atomic_set(&ve->mnt_avail_nr, MNT_MAX_NR_DEFAULT);
@@ -1098,6 +1100,7 @@ static const struct ve_failcount_entry {
VE_FC_ENTRY(mnt),
VE_FC_ENTRY(netif),
VE_FC_ENTRY(bpf_prog),
+ VE_FC_ENTRY(neigh_tbl),
#ifdef CONFIG_AIO
VE_FC_ENTRY(aio),
#endif
diff --git a/net/core/neighbour.c b/net/core/neighbour.c
index f90deb17fb25..c7d5e6df50c2 100644
--- a/net/core/neighbour.c
+++ b/net/core/neighbour.c
@@ -520,8 +520,7 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
(glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
- net_info_ratelimited("%s: neighbor table overflow!\n",
- tbl->id);
+ ve_failcount_inc(ve, neigh_tbl);
NEIGH_CACHE_STAT_INC(tbl, table_fulls);
goto out_entries;
}
--
2.52.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization
` (2 preceding siblings ...)
2026-10-02 21:26 15% ` [Devel] [PATCH hci-8.1 3/6] ve/printk: Specify ve in printks Vladimir Riabchun
@ 2026-10-02 21:26 3% ` Vladimir Riabchun
2026-10-02 21:37 3% ` no-reply
2026-10-02 21:26 8% ` [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
2026-10-02 21:26 4% ` [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
5 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-02 21:26 UTC (permalink / raw)
To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel
The usage of get_exec_env() in ve_timens_add_boottime_ns can lead
to time travels - a printk from IRQ might use container time for
printing to host dmesg and vice versa.
Fix this by adding timens_offsets info to printk_info entry. This way
correct time is provided by the original printer of message, rather than
printing job, that may be delayed and called from a different context.
It seems complicated to keep time namespace in log_state, because
VE creation and namespace creation happen at different times and it
seems that one VE can have different time namespaces if it was stopped
and then started again.
So instead of tracking time namespaces, let's put actual time offsets
into log state.
https://virtuozzo.atlassian.net/browse/VSTOR-146839
Feature: ve printk
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
kernel/printk/internal.h | 6 +++++
kernel/printk/printk.c | 51 ++++++++++++++++++++++++---------------
kernel/printk/printk_ringbuffer.h | 2 ++
3 files changed, 39 insertions(+), 20 deletions(-)
diff --git a/kernel/printk/internal.h b/kernel/printk/internal.h
index d00ac40d8401..d0096aba5af7 100644
--- a/kernel/printk/internal.h
+++ b/kernel/printk/internal.h
@@ -4,6 +4,7 @@
*/
#include <linux/console.h>
#include <linux/percpu.h>
+#include <linux/time_namespace.h>
#include <linux/types.h>
#if defined(CONFIG_PRINTK) && defined(CONFIG_SYSCTL)
@@ -354,11 +355,16 @@ struct log_state {
*/
struct latched_seq clear_seq;
struct printk_ringbuffer *prb;
+ /* Updated on every log_state request */
+ struct timens_offsets time_offset;
};
+#define GET_VE_LOG(ve) ((struct log_state *) ve->log_state)
+
extern struct log_state init_log_state;
struct log_state *ve_log_state(void);
+struct log_state *__ve_log_state(struct ve_struct *ve);
bool printk_get_next_message(struct log_state *log,
struct printk_message *pmsg, u64 seq,
diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
index df7cf83fb4af..575bd3590198 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -556,13 +556,28 @@ DEFINE_STRUCT_MEMBER_ALIAS(clear_seq, init_log_state, clear_seq);
DEFINE_STRUCT_MEMBER_ALIAS(prb, init_log_state, prb);
#undef DEFINE_STRUCT_MEMBER_ALIAS
+static void update_log_time(struct ve_struct *ve)
+{
+ struct time_namespace *timens = ve_get_time_ns(ve);
+
+ if (timens)
+ GET_VE_LOG(ve)->time_offset = timens->offsets;
+ else
+ GET_VE_LOG(ve)->time_offset = init_time_ns.offsets;
+
+ if (timens)
+ put_time_ns(timens);
+}
+
struct log_state *ve_log_state(void)
{
struct log_state *log = &init_log_state;
#ifdef CONFIG_VE
/* get_exec_env() is reliable only in task context */
- if (!WARN_ON_ONCE(!in_task()) && get_exec_env()->log_state)
+ if (!WARN_ON_ONCE(!in_task()) && get_exec_env()->log_state) {
+ update_log_time(get_exec_env());
log = get_exec_env()->log_state;
+ }
#endif
return log;
}
@@ -571,8 +586,10 @@ struct log_state *__ve_log_state(struct ve_struct *ve)
{
struct log_state *log = &init_log_state;
#ifdef CONFIG_VE
- if (ve->log_state)
+ if (ve->log_state) {
+ update_log_time(ve);
log = ve->log_state;
+ }
#endif
return log;
}
@@ -698,21 +715,10 @@ static void append_char(char **pp, char *e, char c)
*(*pp)++ = c;
}
-static inline u64 ve_timens_add_boottime_ns(u64 nsec)
+static inline u64 ve_timens_add_boottime_ns(u64 nsec, const struct timens_offsets *off)
{
- struct time_namespace *ve_time_ns;
- struct timens_offsets *ns_offsets;
-
- ve_time_ns = ve_get_time_ns(get_exec_env());
- if (unlikely(!ve_time_ns)) {
- /* container not yet started */
- return nsec;
- }
-
- ns_offsets = &ve_time_ns->offsets;
- nsec += ns_offsets->boottime.tv_sec * NSEC_PER_SEC;
- nsec += ns_offsets->boottime.tv_nsec;
- put_time_ns(ve_time_ns);
+ nsec += off->boottime.tv_sec * NSEC_PER_SEC;
+ nsec += off->boottime.tv_nsec;
return nsec;
}
@@ -732,7 +738,7 @@ static ssize_t info_print_ext_header(char *buf, size_t size,
#endif
/* shift the timestamp on the Container uptime value */
- ts_usec = ve_timens_add_boottime_ns(ts_usec);
+ ts_usec = ve_timens_add_boottime_ns(ts_usec, &info->time_offset);
do_div(ts_usec, 1000);
return scnprintf(buf, size, "%u,%llu,%llu,%c%s;",
@@ -1108,6 +1114,7 @@ void log_buf_vmcoreinfo_setup(void)
VMCOREINFO_OFFSET(printk_info, text_len);
VMCOREINFO_OFFSET(printk_info, caller_id);
VMCOREINFO_OFFSET(printk_info, dev_info);
+ VMCOREINFO_OFFSET(printk_info, time_offset);
VMCOREINFO_STRUCT_SIZE(dev_printk_info);
VMCOREINFO_OFFSET(dev_printk_info, subsystem);
@@ -1219,6 +1226,7 @@ static unsigned int __init add_to_rb(struct printk_ringbuffer *rb,
dest_r.info->ts_nsec = r->info->ts_nsec;
dest_r.info->caller_id = r->info->caller_id;
memcpy(&dest_r.info->dev_info, &r->info->dev_info, sizeof(dest_r.info->dev_info));
+ dest_r.info->time_offset = r->info->time_offset;
prb_final_commit(&e);
@@ -1427,12 +1435,13 @@ static size_t print_syslog(unsigned int level, char *buf)
return sprintf(buf, "<%u>", level);
}
-static size_t print_time(u64 ts, char *buf)
+static size_t print_time(const struct printk_info *info, char *buf)
{
unsigned long rem_nsec;
+ u64 ts;
/* shift the timestamp on the Container uptime value */
- ts = ve_timens_add_boottime_ns(ts);
+ ts = ve_timens_add_boottime_ns(info->ts_nsec, &info->time_offset);
rem_nsec = do_div(ts, 1000000000);
@@ -1462,7 +1471,7 @@ static size_t info_print_prefix(const struct printk_info *info, bool syslog,
len = print_syslog((info->facility << 3) | info->level, buf);
if (time)
- len += print_time(info->ts_nsec, buf + len);
+ len += print_time(info, buf + len);
len += print_caller(info->caller_id, buf + len);
@@ -2402,6 +2411,8 @@ static int vprintk_store_log(struct log_state *log, int facility, int level,
if (dev_info)
memcpy(&r.info->dev_info, dev_info, sizeof(r.info->dev_info));
+ r.info->time_offset = log->time_offset;
+
/* A message without a trailing newline can be continued. */
if (!(flags & LOG_NEWLINE))
prb_commit(&e);
diff --git a/kernel/printk/printk_ringbuffer.h b/kernel/printk/printk_ringbuffer.h
index 4ef81349d9fb..19052ba005fc 100644
--- a/kernel/printk/printk_ringbuffer.h
+++ b/kernel/printk/printk_ringbuffer.h
@@ -7,6 +7,7 @@
#include <linux/bits.h>
#include <linux/dev_printk.h>
#include <linux/stddef.h>
+#include <linux/time_namespace.h>
#include <linux/types.h>
/*
@@ -25,6 +26,7 @@ struct printk_info {
u32 caller_id; /* thread id or processor id */
struct dev_printk_info dev_info;
+ struct timens_offsets time_offset; /* Author timens offsets */
};
/*
--
2.52.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* [Devel] [PATCH hci-8.1 3/6] ve/printk: Specify ve in printks
@ 2026-10-02 21:26 15% ` Vladimir Riabchun
2026-10-02 21:36 4% ` no-reply
2026-10-02 21:26 3% ` [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
` (2 subsequent siblings)
5 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-10-02 21:26 UTC (permalink / raw)
To: Konstantin Khorenko, vasileios.almpanis; +Cc: devel
Introduce ve_printk helpers that accept VE structure,
so we can provide correct VE where get_exec_env is unreliable.
This is true for ve_printk_deferred functions, and their users.
Now VE ratelimiting requires specified VE, and this requirement
is further forwarded to ve_net_ratelimited kind.
https://virtuozzo.atlassian.net/browse/VSTOR-146839
Feature: ve printk
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
include/linux/net.h | 16 ++++----
include/linux/printk.h | 23 ++++++++++--
include/linux/ratelimit_types.h | 6 ++-
kernel/printk/printk.c | 74 +++++++++++++++++++++++++++++++------
lib/ratelimit.c | 7 ++--
net/core/utils.c | 4 +-
net/netfilter/nf_conntrack_core.c | 2 +-
net/netfilter/nf_conntrack_expect.c | 2 +-
net/netfilter/xt_TCPMSS.c | 4 +-
9 files changed, 104 insertions(+), 34 deletions(-)
diff --git a/include/linux/net.h b/include/linux/net.h
index 3f3bbe9f149d..4c34b9a0386f 100644
--- a/include/linux/net.h
+++ b/include/linux/net.h
@@ -265,7 +265,7 @@ struct socket *sockfd_lookup(int fd, int *err);
struct socket *sock_from_file(struct file *file);
#define sockfd_put(sock) fput(sock->file)
int net_ratelimit(void);
-int ve_net_ratelimit(int dst);
+int ve_net_ratelimit(int dst, struct ve_struct *ve);
#define net_ratelimited_function(function, ...) \
do { \
@@ -273,10 +273,10 @@ do { \
function(__VA_ARGS__); \
} while (0)
-#define ve_net_ratelimited_function(function, dst, ...) \
+#define ve_net_ratelimited_function(function, dst, ve, ...) \
do { \
- if (ve_net_ratelimit(dst)) \
- function(dst, __VA_ARGS__); \
+ if (ve_net_ratelimit(dst, ve)) \
+ function(dst, ve, __VA_ARGS__); \
} while (0)
#define net_emerg_ratelimited(fmt, ...) \
@@ -311,10 +311,10 @@ do { \
no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif
-#define net_velog_ratelimited(fmt, ...) \
- ve_net_ratelimited_function(ve_printk, VE_LOG, fmt, ##__VA_ARGS__)
-#define net_veboth_ratelimited(fmt, ...) \
- ve_net_ratelimited_function(ve_printk, VE_LOG_BOTH, fmt, ##__VA_ARGS__)
+#define net_velog_ratelimited(ve, fmt, ...) \
+ ve_net_ratelimited_function(__ve_printk, VE_LOG, ve, fmt, ##__VA_ARGS__)
+#define net_veboth_ratelimited(ve, fmt, ...) \
+ ve_net_ratelimited_function(__ve_printk, VE_LOG_BOTH, ve, fmt, ##__VA_ARGS__)
#define net_get_random_once(buf, nbytes) \
get_random_once((buf), (nbytes))
diff --git a/include/linux/printk.h b/include/linux/printk.h
index 3b0af0deb6c3..1478e47dc582 100644
--- a/include/linux/printk.h
+++ b/include/linux/printk.h
@@ -76,6 +76,7 @@ extern int console_printk[];
#define VE0_LOG 1
#define VE_LOG 2
#define VE_LOG_BOTH (VE0_LOG | VE_LOG)
+struct ve_struct;
extern void console_verbose(void);
@@ -160,6 +161,11 @@ int ve_vprintk_emit(int facility, int level,
const struct dev_printk_info *dev_info,
const char *fmt, va_list args);
+asmlinkage __printf(5, 0)
+int __ve_vprintk_emit(struct ve_struct *ve, int facility, int level,
+ const struct dev_printk_info *dev_info,
+ const char *fmt, va_list args);
+
asmlinkage __printf(1, 0)
int vprintk(const char *fmt, va_list args);
__printf(1, 0)
@@ -171,8 +177,11 @@ int _printk(const char *fmt, ...);
asmlinkage __printf(2, 3) __cold
int ve_printk(int dst, const char *fmt, ...);
-asmlinkage __printf(2, 3) __cold
-int ve_printk_deferred(int dst, const char *fmt, ...);
+asmlinkage __printf(3, 4) __cold
+int __ve_printk(int dst, struct ve_struct *ve, const char *fmt, ...);
+
+asmlinkage __printf(3, 4) __cold
+int ve_printk_deferred(int dst, struct ve_struct *ve, const char *fmt, ...);
struct ve_struct;
int ve_log_init(struct ve_struct *ve);
@@ -248,8 +257,13 @@ int ve_printk(int dst, const char *s, ...)
{
return 0;
}
+static inline __printf(3, 4) __cold
+int __ve_printk(int dst, struct ve_struct *ve, const char *s, ...)
+{
+ return 0;
+}
static inline __printf(2, 3) __cold
-int ve_printk_deferred(int dst, const char *s, ...)
+int ve_printk_deferred(int dst, struct ve_struct *ve, const char *s, ...)
{
return 0;
}
@@ -747,7 +761,8 @@ struct pi_entry {
DEFAULT_RATELIMIT_INTERVAL, \
DEFAULT_RATELIMIT_BURST); \
\
- if (ve_ratelimit(dst, &_rs)) \
+ WARN_ON_ONCE((dst & VE_LOG) && !in_task()); \
+ if (ve_ratelimit(dst, get_exec_env(), &_rs)) \
ve_printk(dst, fmt, ##__VA_ARGS__); \
})
#else
diff --git a/include/linux/ratelimit_types.h b/include/linux/ratelimit_types.h
index 3902066f0fee..aa9eee2a2455 100644
--- a/include/linux/ratelimit_types.h
+++ b/include/linux/ratelimit_types.h
@@ -44,7 +44,9 @@ struct ratelimit_state {
extern int ___ratelimit(struct ratelimit_state *rs, const char *func);
#define __ratelimit(state) ___ratelimit(state, __func__)
-extern int __ve_ratelimit(int dst, struct ratelimit_state *rs, const char *func);
-#define ve_ratelimit(dst, state) __ve_ratelimit(dst, state, __func__)
+struct ve_struct;
+extern int __ve_ratelimit(int dst, struct ve_struct *ve,
+ struct ratelimit_state *rs, const char *func);
+#define ve_ratelimit(dst, ve, state) __ve_ratelimit(dst, ve, state, __func__)
#endif /* _LINUX_RATELIMIT_TYPES_H */
diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
index b06fc835d7c7..df7cf83fb4af 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -567,6 +567,16 @@ struct log_state *ve_log_state(void)
return log;
}
+struct log_state *__ve_log_state(struct ve_struct *ve)
+{
+ struct log_state *log = &init_log_state;
+#ifdef CONFIG_VE
+ if (ve->log_state)
+ log = ve->log_state;
+#endif
+ return log;
+}
+
void log_poll_wait(struct file *filp, poll_table *p)
{
poll_wait(filp, &ve_log_state()->wait, p);
@@ -2525,26 +2535,45 @@ asmlinkage int ve_vprintk_emit(int facility, int level,
}
EXPORT_SYMBOL(ve_vprintk_emit);
+asmlinkage int __ve_vprintk_emit(struct ve_struct *ve, int facility, int level,
+ const struct dev_printk_info *dev_info,
+ const char *fmt, va_list args)
+{
+ return vprintk_emit_log(__ve_log_state(ve),
+ facility, level, dev_info,
+ fmt, args);
+}
+EXPORT_SYMBOL(__ve_vprintk_emit);
+
int vprintk_default(const char *fmt, va_list args)
{
return vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, fmt, args);
}
EXPORT_SYMBOL_GPL(vprintk_default);
-static asmlinkage int ve_vprintk(int dst, const char *fmt, va_list args)
+static asmlinkage int ve_vprintk(int dst, struct ve_struct *ve,
+ const char *fmt, va_list args)
{
va_list args2;
- int r = 0;
+ int r = 0, ve_provided = (ve != NULL);
WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
+ if (!ve)
+ ve = get_exec_env();
+
va_copy(args2, args);
if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
r = vprintk(fmt, args);
/* Report IRQ usage here - printing to VE requires explicit ve reference */
- if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
- r = ve_vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, fmt, args2);
+ if ((dst & VE_LOG)) {
+ if (!ve_provided && WARN_ON_ONCE(!in_task()))
+ goto done;
+ if (!ve_is_super(ve))
+ r = __ve_vprintk_emit(ve, 0, LOGLEVEL_DEFAULT, NULL, fmt, args2);
+ }
+done:
va_end(args2);
return r;
@@ -2560,39 +2589,62 @@ asmlinkage int ve_printk(int dst, const char *fmt, ...)
int r;
va_start(args, fmt);
- r = ve_vprintk(dst, fmt, args);
+ r = ve_vprintk(dst, NULL, fmt, args);
va_end(args);
return r;
}
EXPORT_SYMBOL(ve_printk);
-static asmlinkage int ve_vprintk_deferred(int dst, const char *fmt, va_list args)
+asmlinkage int __ve_printk(int dst, struct ve_struct *ve, const char *fmt, ...)
+{
+ va_list args;
+ int r;
+
+ va_start(args, fmt);
+ r = ve_vprintk(dst, ve, fmt, args);
+ va_end(args);
+
+ return r;
+}
+EXPORT_SYMBOL(__ve_printk);
+
+static asmlinkage int ve_vprintk_deferred(int dst, struct ve_struct *ve,
+ const char *fmt, va_list args)
{
va_list args2;
- int r = 0;
+ int r = 0, ve_provided = (ve != NULL);
WARN_ON_ONCE(!dst || (dst & ~VE_LOG_BOTH));
+ if (!ve)
+ ve = get_exec_env();
+
va_copy(args2, args);
if (ve_is_super(get_exec_env()) || (dst & VE0_LOG))
r = vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
/* Report IRQ usage here - printing to VE requires explicit ve reference */
- if ((dst & VE_LOG) && !WARN_ON_ONCE(!in_task()) && !ve_is_super(get_exec_env()))
- r = ve_vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args2);
+ if ((dst & VE_LOG)) {
+ if (!ve_provided && WARN_ON_ONCE(!in_task()))
+ goto done;
+ if (!ve_is_super(ve))
+ r = __ve_vprintk_emit(ve, 0, LOGLEVEL_SCHED, NULL, fmt, args2);
+ }
+done:
va_end(args2);
return r;
}
-asmlinkage int ve_printk_deferred(int dst, const char *fmt, ...)
+asmlinkage int ve_printk_deferred(int dst, struct ve_struct *ve,
+ const char *fmt, ...)
{
va_list args;
int r;
va_start(args, fmt);
- r = ve_vprintk_deferred(dst, fmt, args);
+ r = ve_vprintk_deferred(dst, ve, fmt, args);
va_end(args);
return r;
diff --git a/lib/ratelimit.c b/lib/ratelimit.c
index 98409e870eea..98c45bb19e50 100644
--- a/lib/ratelimit.c
+++ b/lib/ratelimit.c
@@ -11,6 +11,7 @@
#include <linux/ratelimit.h>
#include <linux/jiffies.h>
#include <linux/export.h>
+#include <linux/ve_proto.h>
/*
* __ratelimit - rate limiting
@@ -24,7 +25,7 @@
* 0 means callbacks will be suppressed.
* 1 means go ahead and do it.
*/
-int __ve_ratelimit(int dst, struct ratelimit_state *rs, const char *func)
+int __ve_ratelimit(int dst, struct ve_struct *ve, struct ratelimit_state *rs, const char *func)
{
/* Paired with WRITE_ONCE() in .proc_handler().
* Changing two values seperately could be inconsistent
@@ -55,7 +56,7 @@ int __ve_ratelimit(int dst, struct ratelimit_state *rs, const char *func)
if (m) {
if (!(rs->flags & RATELIMIT_MSG_ON_RELEASE)) {
- ve_printk_deferred(dst, KERN_WARNING
+ ve_printk_deferred(dst, ve, KERN_WARNING
"%s: %d callbacks suppressed\n", func, m);
}
}
@@ -77,6 +78,6 @@ EXPORT_SYMBOL(__ve_ratelimit);
int ___ratelimit(struct ratelimit_state *rs, const char *func)
{
- return __ve_ratelimit(VE0_LOG, rs, func);
+ return __ve_ratelimit(VE0_LOG, get_ve0(), rs, func);
}
EXPORT_SYMBOL(___ratelimit);
diff --git a/net/core/utils.c b/net/core/utils.c
index cffc3bc44baa..bf4e53205a8e 100644
--- a/net/core/utils.c
+++ b/net/core/utils.c
@@ -41,9 +41,9 @@ int net_ratelimit(void)
}
EXPORT_SYMBOL(net_ratelimit);
-int ve_net_ratelimit(int dst)
+int ve_net_ratelimit(int dst, struct ve_struct *ve)
{
- return ve_ratelimit(dst, &net_ratelimit_state);
+ return ve_ratelimit(dst, ve, &net_ratelimit_state);
}
EXPORT_SYMBOL(ve_net_ratelimit);
diff --git a/net/netfilter/nf_conntrack_core.c b/net/netfilter/nf_conntrack_core.c
index f078136edddd..8900a41b48b6 100644
--- a/net/netfilter/nf_conntrack_core.c
+++ b/net/netfilter/nf_conntrack_core.c
@@ -1701,7 +1701,7 @@ __nf_conntrack_alloc(struct net *net,
if (!conntrack_gc_work.early_drop)
conntrack_gc_work.early_drop = true;
atomic_dec(&cnet->count);
- net_veboth_ratelimited(KERN_WARNING "VE%s: "
+ net_veboth_ratelimited(net->owner_ve, KERN_WARNING "VE%s: "
"nf_conntrack table full in netns %u, dropping packet\n",
ve_name(net->owner_ve), net->ns.inum);
return ERR_PTR(-ENOMEM);
diff --git a/net/netfilter/nf_conntrack_expect.c b/net/netfilter/nf_conntrack_expect.c
index 4b077b2cb432..23e78957bb79 100644
--- a/net/netfilter/nf_conntrack_expect.c
+++ b/net/netfilter/nf_conntrack_expect.c
@@ -508,7 +508,7 @@ static inline int __nf_ct_expect_check(struct nf_conntrack_expect *expect,
cnet = nf_ct_pernet(net);
if (cnet->expect_count >= cnet->expect_max) {
- net_veboth_ratelimited(KERN_WARNING "VE%s: "
+ net_veboth_ratelimited(net->owner_ve, KERN_WARNING "VE%s: "
"nf_conntrack: expectation table full in netns %u\n",
ve_name(net->owner_ve), net->ns.inum);
ret = -EMFILE;
diff --git a/net/netfilter/xt_TCPMSS.c b/net/netfilter/xt_TCPMSS.c
index 822b3f71570e..0b9ccc8987e2 100644
--- a/net/netfilter/xt_TCPMSS.c
+++ b/net/netfilter/xt_TCPMSS.c
@@ -105,8 +105,8 @@ tcpmss_mangle_packet(struct sk_buff *skb,
unsigned int min_mtu = min(dst_mtu(skb_dst(skb)), in_mtu);
if (min_mtu <= minlen) {
- net_velog_ratelimited("unknown or invalid path-MTU (%u)\n",
- min_mtu);
+ net_velog_ratelimited(net->owner_ve, "unknown or invalid path-MTU (%u)\n",
+ min_mtu);
return -1;
}
newmss = min_mtu - minlen;
--
2.52.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 15%]
* Re: [PATCH 3/3] iotests/migrate-cpr-exec: cover chardevs with logfile= #VSTOR-148304
@ 2026-10-02 16:55 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-02 16:55 UTC (permalink / raw)
To: Denis V. Lunev, svt-core; +Cc: andrey.drobyshev, den
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Low] iotests: socket logfile test leaves stale path=sock1 in exec command line
- [Low] iotests: assertFalse on BAD_LOG parent dir cannot detect an attempted logfile open
- [Low] iotests: _open_files() compares /proc readlink results to non-canonical paths
- [Low] iotests: pre_cpr rewrite drops logappend=on from the console chardev argument
--
commit 8a854fc4d41646d884346abb08e5af9be8f46b16
Author: Denis V. Lunev <den@openvz.org>
iotests/migrate-cpr-exec: cover chardevs with logfile= #VSTOR-148304
This adds four tests to migrate-cpr-exec. They cover a pty console, a
guest agent socket and a second QMP monitor, each with a log, through
cpr-exec with logfile paths that cannot be created. They also cover
chardev-change to a new log for pty and socket, and a failed
chardev-change whose rollback must keep the old log.
> diff --git a/tests/qemu-iotests/tests/migrate-cpr-exec b/tests/qemu-iotests/tests/migrate-cpr-exec
> index 51e6cbcd6593..e8a6c502c840 100755
> --- a/tests/qemu-iotests/tests/migrate-cpr-exec
> +++ b/tests/qemu-iotests/tests/migrate-cpr-exec
[ ... ]
> + def _open_files(self) -> set:
> + fd_dir = f'/proc/{self.vm.get_pid()}/fd'
> + return {os.readlink(os.path.join(fd_dir, fd))
> + for fd in os.listdir(fd_dir)}
[Severity: Low]
Can this comparison fail when TEST_DIR is not canonical?
os.readlink() on /proc/<pid>/fd/N returns the kernel's resolved path. The
expected paths come from _log_path() as os.path.join(iotests.test_dir, name),
and they are not passed through os.path.realpath().
If TEST_DIR is reached through a symlink, this assertion fails even though
the fds were preserved:
self.assertLessEqual({agent_log, mon_log}, self._open_files())
In the same case, this assertion in _test_chardev_change_logfile() passes
vacuously, so it no longer checks that the old log fd was dropped:
self.assertNotIn(log_old, self._open_files())
I did not confirm how iotests normalizes test_dir. Would applying
os.path.realpath() to the expected paths avoid the mismatch?
[ ... ]
> + def pre_cpr():
> + self._serial_write('before\n')
> + for c in (console, agent, mon):
> + self._replace_arg(c, c.replace(c[c.index('logfile='):],
> + f'logfile={self.BAD_LOG}'))
[Severity: Low]
Does this rewrite drop logappend=on from the console chardev argument?
For the console chardev, c[c.index('logfile='):] is
'logfile=/dev/fdset/1,logappend=on'. The whole tail is replaced, so the new
argument becomes:
pty,id=charserial0,logfile=<BAD_LOG>
The agent and mon arguments have logfile= last, so they are unaffected.
The combination of logappend=on with an fd-backed preserved log is
therefore not exercised after the exec. This is the combination the commit
message says libvirt sends. Today qemu_char_open() does not use the
computed flags when cpr_exec_is_incoming() is true, so the test passes. A
later change that consults logappend on the restore path would not be
caught.
Would it be better to replace only the path value and keep ",logappend=on"?
> + args = list(self.vm._qemu_full_args)
> + i = args.index('-add-fd')
> + self.vm._qemu_full_args = tuple(args[:i] + args[i + 2:])
> +
> + def post_cpr():
> + self._serial_write('after\n')
> + self.assertEqual(self._read(console_log), 'before\nafter\n')
> + self.assertLessEqual({agent_log, mon_log}, self._open_files())
> + self.assertFalse(os.path.exists(os.path.dirname(self.BAD_LOG)))
[Severity: Low]
Can this assertion ever fail?
BAD_LOG is <test_dir>/nonexistent/chr.log, and nothing creates the
"nonexistent" directory. qemu_create() called from qemu_char_open() does
not create parent directories. If a regressed QEMU tried to open the path,
it would get ENOENT and the directory would stay absent, so
assertFalse(os.path.exists(os.path.dirname(self.BAD_LOG))) always holds.
The real evidence that the path is not opened is that the new QEMU starts
at all. The docstring ("the new logfile= paths are never opened") and the
commit message suggest this assertion adds coverage that it does not.
Could the assertion be dropped, or replaced with a check that can actually
fail?
[ ... ]
> + def test_chardev_change_logfile_socket(self) -> None:
> + sock1 = iotests.file_path('chr-log-sock1')
> + sock2 = iotests.file_path('chr-log-sock2')
> + self._test_chardev_change_logfile(
> + 'socket',
> + {'addr': {'type': 'unix', 'data': {'path': sock2}},
> + 'server': True, 'wait': False},
> + f'path={sock1},server=on,wait=off,')
[Severity: Low]
This isn't a bug, but the rewritten exec command line for the socket case
looks inconsistent with the live chardev.
In _test_chardev_change_logfile(), change() calls:
self._replace_arg(arg, f'{backend},id=char0,{opts_old}'
f'logfile={self.BAD_LOG}')
For the socket case opts_old is 'path=<sock1>,server=on,wait=off,'. After
chardev-change the live chardev listens on sock2, so the new command line
still names sock1, which is no longer in use.
The existing test_chardev_change_socket_to_socket updates the path to
sock2. The test passes here because chr_socket_cpr_restore_addr_fd() in
chardev/char-socket.c uses the inherited fd and not the path. As written,
the test does not model what a management layer would send. It would also
not catch a regression that used the path instead of the fd.
Should the rewritten argument use sock2 for the path?
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002164853.2130337-1-den@openvz.org?part=3
^ permalink raw reply [relevance 3%]
* Re: [PATCH 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304
2026-10-02 16:48 3% ` [PATCH 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
@ 2026-10-02 16:54 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-02 16:54 UTC (permalink / raw)
To: svt-core, Denis V. Lunev; +Cc: andrey.drobyshev, den
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] chardev: logfd saved in all CPR modes but claimed only on cpr-exec incoming
--
commit 05df918f015920ae0431d59bdf9bb6ff8741e4fc
Author: Denis V. Lunev <den@openvz.org>
chardev: preserve the logfile fd across cpr-exec #VSTOR-148304
This patch saves the chardev log fd in the cpr fd registry under the chardev label. On cpr-exec incoming, it takes the fd from the registry instead of reopening the logfile path. If the source did not save the fd, the chardev runs without a log.
> diff --git a/chardev/char.c b/chardev/char.c
> --- a/chardev/char.c
> +++ b/chardev/char.c
[ ... ]
> @@ -261,9 +262,21 @@ static void qemu_char_open(Chardev *chr, ChardevBackend *backend,
> } else {
> flags |= O_TRUNC;
> }
> - chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
> - if (chr->logfd < 0) {
> - return;
> + if (cpr_exec_is_incoming()) {
> + chr->logfd = cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID);
> + if (chr->logfd < 0) {
> + warn_report("chardev '%s': log file was not preserved, "
> + "continuing without it", chr->label);
> + }
> + } else {
> + chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
> + if (chr->logfd < 0) {
> + return;
> + }
> + if (!cpr_is_incoming()) {
> + cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
> + cpr_save_fd(chr->label, CPR_CHARDEV_LOG_ID, chr->logfd);
> + }
> }
> }
[Severity: Low]
Is the log fd handled consistently across cpr modes here? The fd is saved
to the registry for every start where cpr_is_incoming() is false, but it
is only claimed when cpr_exec_is_incoming() is true.
In cpr-transfer, the target takes the else branch in qemu_char_open():
chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
so it reopens the logfile path itself. The save is then skipped because
cpr_is_incoming() is true. Can this leave the transferred fd unclaimed in
the registry, and also hit the same permission and virtlogd-bypass problem
the commit message describes for cpr-exec?
This follows the exec-only pattern in char-file.c and char-pty.c. I did not
verify whether cpr-transfer sends every registry fd. If it does not, the
leak does not occur, but the path reopen remains.
[ ... ]
> @@ -315,11 +332,14 @@ static void char_finalize(Object *obj)
> if (chr->be) {
> chr->be->chr = NULL;
> }
> - g_free(chr->filename);
> - g_free(chr->label);
> if (chr->logfd != -1) {
> + if (cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID) == chr->logfd) {
> + cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
> + }
> close(chr->logfd);
> }
> + g_free(chr->filename);
> + g_free(chr->label);
> qemu_mutex_destroy(&chr->chr_write_lock);
> }
[Severity: Low]
In the cpr-transfer target, can the registry entry outlive the chardev
here? cpr_find_fd() in char_finalize() returns the transferred fd, which
differs from chr->logfd, so cpr_delete_fd() is not called.
Would the stale registry entry and its open fd then stay until process
exit?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002164853.2130337-1-den@openvz.org?part=2
^ permalink raw reply [relevance 3%]
* [PATCH 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304
@ 2026-10-02 16:48 7% ` Denis V. Lunev
2026-10-02 16:48 3% ` [PATCH 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
2 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-10-02 16:48 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
Give TYPE_CHARDEV an empty cpr_claim_fds() and make the file, pty and
socket backends chain to their parent's. Every chardev class then has
the hook, so qmp_chardev_change() calls it without a NULL check, and
state owned by the base class can be re-claimed there as well.
No functional change.
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
chardev/char-file.c | 3 +++
chardev/char-pty.c | 3 +++
chardev/char-socket.c | 3 +++
chardev/char.c | 13 +++++++------
4 files changed, 16 insertions(+), 6 deletions(-)
diff --git a/chardev/char-file.c b/chardev/char-file.c
index a90f4a77e13..89011f52e3e 100644
--- a/chardev/char-file.c
+++ b/chardev/char-file.c
@@ -124,7 +124,10 @@ out_close:
static void char_file_cpr_claim_fds(Chardev *chr)
{
FDChardev *s = FD_CHARDEV(chr);
+ ChardevClass *pc = CHARDEV_CLASS(
+ object_class_get_parent(object_class_by_name(TYPE_CHARDEV_FILE)));
+ pc->cpr_claim_fds(chr);
if (s->ioc_out) {
cpr_delete_fd(chr->label, 0);
cpr_save_fd(chr->label, 0, QIO_CHANNEL_FILE(s->ioc_out)->fd);
diff --git a/chardev/char-pty.c b/chardev/char-pty.c
index a0090e4c3d1..8058da92bb4 100644
--- a/chardev/char-pty.c
+++ b/chardev/char-pty.c
@@ -403,7 +403,10 @@ static void char_pty_open(Chardev *chr,
static void char_pty_cpr_claim_fds(Chardev *chr)
{
PtyChardev *s = PTY_CHARDEV(chr);
+ ChardevClass *pc = CHARDEV_CLASS(
+ object_class_get_parent(object_class_by_name(TYPE_CHARDEV_PTY)));
+ pc->cpr_claim_fds(chr);
if (s->ioc) {
cpr_delete_fd(chr->label, 0);
cpr_save_fd(chr->label, 0, QIO_CHANNEL_FILE(s->ioc)->fd);
diff --git a/chardev/char-socket.c b/chardev/char-socket.c
index d20aea764f9..c73a3dd0f7a 100644
--- a/chardev/char-socket.c
+++ b/chardev/char-socket.c
@@ -1681,7 +1681,10 @@ char_socket_get_connected(Object *obj, Error **errp)
static void char_socket_cpr_claim_fds(Chardev *chr)
{
SocketChardev *s = SOCKET_CHARDEV(chr);
+ ChardevClass *pc = CHARDEV_CLASS(
+ object_class_get_parent(object_class_by_name(TYPE_CHARDEV_SOCKET)));
+ pc->cpr_claim_fds(chr);
if (s->sioc) {
cpr_delete_fd(chr->label, CPR_CONNSOCK_ID);
cpr_save_fd(chr->label, CPR_CONNSOCK_ID, s->sioc->fd);
diff --git a/chardev/char.c b/chardev/char.c
index 87f40d0137e..b64767f574e 100644
--- a/chardev/char.c
+++ b/chardev/char.c
@@ -295,12 +295,17 @@ static int null_chr_write(Chardev *chr, const uint8_t *buf, int len)
return len;
}
+static void char_cpr_claim_fds(Chardev *chr)
+{
+}
+
static void char_class_init(ObjectClass *oc, const void *data)
{
ChardevClass *cc = CHARDEV_CLASS(oc);
cc->chr_write = null_chr_write;
cc->chr_be_event = chr_be_event;
+ cc->cpr_claim_fds = char_cpr_claim_fds;
}
static void char_finalize(Object *obj)
@@ -1164,9 +1169,7 @@ ChardevReturn *qmp_chardev_change(const char *id, ChardevBackend *backend,
backend, chr->gcontext, handover_yank_instance, errp);
if (!chr_new) {
/* the failed replacement's open() may have displaced our cpr fds */
- if (cc->cpr_claim_fds) {
- cc->cpr_claim_fds(chr);
- }
+ cc->cpr_claim_fds(chr);
return NULL;
}
@@ -1186,9 +1189,7 @@ ChardevReturn *qmp_chardev_change(const char *id, ChardevBackend *backend,
qemu_chr_be_event(chr, CHR_EVENT_OPENED);
}
/* chr_new's open() may have displaced our cpr fds; re-claim them */
- if (cc->cpr_claim_fds) {
- cc->cpr_claim_fds(chr);
- }
+ cc->cpr_claim_fds(chr);
object_unref(OBJECT(chr_new));
return NULL;
}
--
2.53.0
^ permalink raw reply [relevance 7%]
* [PATCH 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304
2026-10-02 16:48 7% ` [PATCH 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304 Denis V. Lunev
@ 2026-10-02 16:48 3% ` Denis V. Lunev
2026-10-02 16:54 3% ` no-reply
2 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-10-02 16:48 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
A chardev with logfile= opens the log in qemu_char_open() on every
start, including the start of the new QEMU after cpr-exec. The backend
fds are taken from the cpr fd registry there, the log fd is not, so the
new process reopens the path itself.
libvirt normally opens the log through virtlogd and passes a pipe in an
fdset. The qemu-update command line cannot carry that fdset, so it
names the log file path. The exec'd QEMU, running as the qemu user,
then fails to create a log it has no write access to, e.g. a nova
console.log on node-local storage:
qemu-kvm: -chardev pty,id=charserial0,logfile=.../console.log,
logappend=on: Could not create '.../console.log': Permission denied
and the VM is destroyed. Where the open succeeds, the new QEMU writes
the file directly, behind virtlogd's back.
Save the log fd in the cpr registry under the chardev label and take it
from there on cpr-exec incoming, as the backends do. The logfile path
is not opened at all then. If the source did not save it, run without
a log rather than reopen the path. A failed chardev-change re-claims
the log through the base class cpr_claim_fds(), since the replacement's
log has displaced the entry.
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
chardev/char.c | 30 +++++++++++++++++++++++++-----
include/migration/cpr.h | 1 +
2 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/chardev/char.c b/chardev/char.c
index b64767f574e..c037d2e7730 100644
--- a/chardev/char.c
+++ b/chardev/char.c
@@ -40,6 +40,7 @@
#include "qemu/id.h"
#include "qemu/coroutine.h"
#include "qemu/yank.h"
+#include "migration/cpr.h"
#include "chardev-internal.h"
@@ -261,9 +262,21 @@ static void qemu_char_open(Chardev *chr, ChardevBackend *backend,
} else {
flags |= O_TRUNC;
}
- chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
- if (chr->logfd < 0) {
- return;
+ if (cpr_exec_is_incoming()) {
+ chr->logfd = cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID);
+ if (chr->logfd < 0) {
+ warn_report("chardev '%s': log file was not preserved, "
+ "continuing without it", chr->label);
+ }
+ } else {
+ chr->logfd = qemu_create(common->logfile, flags, 0666, errp);
+ if (chr->logfd < 0) {
+ return;
+ }
+ if (!cpr_is_incoming()) {
+ cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
+ cpr_save_fd(chr->label, CPR_CHARDEV_LOG_ID, chr->logfd);
+ }
}
}
@@ -297,6 +310,10 @@ static int null_chr_write(Chardev *chr, const uint8_t *buf, int len)
static void char_cpr_claim_fds(Chardev *chr)
{
+ if (chr->logfd >= 0) {
+ cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
+ cpr_save_fd(chr->label, CPR_CHARDEV_LOG_ID, chr->logfd);
+ }
}
static void char_class_init(ObjectClass *oc, const void *data)
@@ -315,11 +332,14 @@ static void char_finalize(Object *obj)
if (chr->be) {
chr->be->chr = NULL;
}
- g_free(chr->filename);
- g_free(chr->label);
if (chr->logfd != -1) {
+ if (cpr_find_fd(chr->label, CPR_CHARDEV_LOG_ID) == chr->logfd) {
+ cpr_delete_fd(chr->label, CPR_CHARDEV_LOG_ID);
+ }
close(chr->logfd);
}
+ g_free(chr->filename);
+ g_free(chr->label);
qemu_mutex_destroy(&chr->chr_write_lock);
}
diff --git a/include/migration/cpr.h b/include/migration/cpr.h
index d74ff1ea602..8fb79eb0a28 100644
--- a/include/migration/cpr.h
+++ b/include/migration/cpr.h
@@ -15,6 +15,7 @@
#define CPR_LISTENSOCK_ID 0
#define CPR_CONNSOCK_ID 1
+#define CPR_CHARDEV_LOG_ID 2
#define QEMU_CPR_FILE_MAGIC 0x51435052
#define QEMU_CPR_FILE_VERSION 0x00000001
--
2.53.0
^ permalink raw reply [relevance 3%]
* Re: [PATCH hci-8.0 1/1] hw/scsi: avoid deadlock upon TMF request cancelling with VirtIO #VSTOR-148264
2026-10-02 10:58 4% [PATCH hci-8.0 1/1] hw/scsi: avoid deadlock upon TMF request cancelling with VirtIO #VSTOR-148264 Denis V. Lunev
@ 2026-10-02 11:14 0% ` Andrey Drobyshev
0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-10-02 11:14 UTC (permalink / raw)
To: Denis V. Lunev, svt-core; +Cc: den
Ack
Applied to hci-8.0
On 10/2/26 1:58 PM, Denis V. Lunev wrote:
> From: Fiona Ebner <f.ebner@proxmox.com>
>
> When scsi_req_dequeue() is reached via
> scsi_req_cancel_async()
> virtio_scsi_tmf_cancel_req()
> virtio_scsi_do_tmf_aio_context(),
> there is a deadlock when trying to acquire the SCSI device's requests
> lock, because it was already acquired in
> virtio_scsi_do_tmf_aio_context().
>
> In particular, the issue happens with a FreeBSD guest (13, 14, 15,
> maybe more), when it cancels SCSI requests, because of timeout.
>
> This is a regression caused by commit da6eebb33b ("virtio-scsi:
> perform TMFs in appropriate AioContexts") and the introduction of the
> requests_lock earlier.
>
> To fix the issue, only cancel the requests after releasing the
> requests_lock. For this, the SCSI device's requests are iterated while
> holding the requests_lock and the requests to be cancelled are
> collected in a list. Then, the collected requests are cancelled
> one by one while not holding the requests_lock. This is safe, because
> only requests from the current AioContext are collected and acted
> upon.
>
> Originally reported by Proxmox VE users:
> https://bugzilla.proxmox.com/show_bug.cgi?id=6810
> https://forum.proxmox.com/threads/173914/
>
> Fixes: da6eebb33b ("virtio-scsi: perform TMFs in appropriate AioContexts")
> Suggested-by: Stefan Hajnoczi <stefanha@redhat.com>
> Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
> Message-id: 20251017094518.328905-1-f.ebner@proxmox.com
> [Changed g_list_append() to g_list_prepend() to avoid traversing the
> list each time.
> --Stefan]
> Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
> (cherry picked from commit 6910f04aa646f63a0257f77201ad8ea15992b816)
> Signed-off-by: Michael Tokarev <mjt@tls.msk.ru>
> (cherry picked from commit dfd597681092ba72c2c050cd0aa1d80a87822804)
> ---
> hw/scsi/virtio-scsi.c | 14 +++++++++++++-
> 1 file changed, 13 insertions(+), 1 deletion(-)
>
> diff --git a/hw/scsi/virtio-scsi.c b/hw/scsi/virtio-scsi.c
> index 34ae14f7bf9..3b635053b56 100644
> --- a/hw/scsi/virtio-scsi.c
> +++ b/hw/scsi/virtio-scsi.c
> @@ -343,6 +343,7 @@ static void virtio_scsi_do_tmf_aio_context(void *opaque)
> SCSIDevice *d = virtio_scsi_device_get(s, tmf->req.tmf.lun);
> SCSIRequest *r;
> bool match_tag;
> + g_autoptr(GList) reqs = NULL;
>
> if (!d) {
> tmf->resp.tmf.response = VIRTIO_SCSI_S_BAD_TARGET;
> @@ -378,10 +379,21 @@ static void virtio_scsi_do_tmf_aio_context(void *opaque)
> if (match_tag && cmd_req->req.cmd.tag != tmf->req.tmf.tag) {
> continue;
> }
> - virtio_scsi_tmf_cancel_req(tmf, r);
> + /*
> + * Cannot cancel directly, because scsi_req_dequeue() would deadlock
> + * when attempting to acquire the request_lock a second time. Taking
> + * a reference here is paired with an unref after cancelling below.
> + */
> + scsi_req_ref(r);
> + reqs = g_list_prepend(reqs, r);
> }
> }
>
> + for (GList *elem = g_list_first(reqs); elem; elem = g_list_next(elem)) {
> + virtio_scsi_tmf_cancel_req(tmf, elem->data);
> + scsi_req_unref(elem->data);
> + }
> +
> /* Incremented by virtio_scsi_do_tmf() */
> virtio_scsi_tmf_dec_remaining(tmf);
>
^ permalink raw reply [relevance 0%]
* [PATCH hci-8.0 1/1] hw/scsi: avoid deadlock upon TMF request cancelling with VirtIO #VSTOR-148264
@ 2026-10-02 10:58 4% Denis V. Lunev
2026-10-02 11:14 0% ` Andrey Drobyshev
0 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-10-02 10:58 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Fiona Ebner <f.ebner@proxmox.com>
When scsi_req_dequeue() is reached via
scsi_req_cancel_async()
virtio_scsi_tmf_cancel_req()
virtio_scsi_do_tmf_aio_context(),
there is a deadlock when trying to acquire the SCSI device's requests
lock, because it was already acquired in
virtio_scsi_do_tmf_aio_context().
In particular, the issue happens with a FreeBSD guest (13, 14, 15,
maybe more), when it cancels SCSI requests, because of timeout.
This is a regression caused by commit da6eebb33b ("virtio-scsi:
perform TMFs in appropriate AioContexts") and the introduction of the
requests_lock earlier.
To fix the issue, only cancel the requests after releasing the
requests_lock. For this, the SCSI device's requests are iterated while
holding the requests_lock and the requests to be cancelled are
collected in a list. Then, the collected requests are cancelled
one by one while not holding the requests_lock. This is safe, because
only requests from the current AioContext are collected and acted
upon.
Originally reported by Proxmox VE users:
https://bugzilla.proxmox.com/show_bug.cgi?id=6810
https://forum.proxmox.com/threads/173914/
Fixes: da6eebb33b ("virtio-scsi: perform TMFs in appropriate AioContexts")
Suggested-by: Stefan Hajnoczi <stefanha@redhat.com>
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
Message-id: 20251017094518.328905-1-f.ebner@proxmox.com
[Changed g_list_append() to g_list_prepend() to avoid traversing the
list each time.
--Stefan]
Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
(cherry picked from commit 6910f04aa646f63a0257f77201ad8ea15992b816)
Signed-off-by: Michael Tokarev <mjt@tls.msk.ru>
(cherry picked from commit dfd597681092ba72c2c050cd0aa1d80a87822804)
---
hw/scsi/virtio-scsi.c | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/hw/scsi/virtio-scsi.c b/hw/scsi/virtio-scsi.c
index 34ae14f7bf9..3b635053b56 100644
--- a/hw/scsi/virtio-scsi.c
+++ b/hw/scsi/virtio-scsi.c
@@ -343,6 +343,7 @@ static void virtio_scsi_do_tmf_aio_context(void *opaque)
SCSIDevice *d = virtio_scsi_device_get(s, tmf->req.tmf.lun);
SCSIRequest *r;
bool match_tag;
+ g_autoptr(GList) reqs = NULL;
if (!d) {
tmf->resp.tmf.response = VIRTIO_SCSI_S_BAD_TARGET;
@@ -378,10 +379,21 @@ static void virtio_scsi_do_tmf_aio_context(void *opaque)
if (match_tag && cmd_req->req.cmd.tag != tmf->req.tmf.tag) {
continue;
}
- virtio_scsi_tmf_cancel_req(tmf, r);
+ /*
+ * Cannot cancel directly, because scsi_req_dequeue() would deadlock
+ * when attempting to acquire the request_lock a second time. Taking
+ * a reference here is paired with an unref after cancelling below.
+ */
+ scsi_req_ref(r);
+ reqs = g_list_prepend(reqs, r);
}
}
+ for (GList *elem = g_list_first(reqs); elem; elem = g_list_next(elem)) {
+ virtio_scsi_tmf_cancel_req(tmf, elem->data);
+ scsi_req_unref(elem->data);
+ }
+
/* Incremented by virtio_scsi_do_tmf() */
virtio_scsi_tmf_dec_remaining(tmf);
--
2.53.0
^ permalink raw reply [relevance 4%]
* Re: [Devel] [PATCH hci-8.1 v6 2/2] selftests: ve: introduce vtty pair teardown stress test
2026-10-02 4:43 6% ` [Devel] [PATCH hci-8.1 v6 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
@ 2026-10-02 4:54 4% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-02 4:54 UTC (permalink / raw)
To: Eva Kurchatova, khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] selftests/ve: kmsg_check() ignores EPIPE ring overrun and can falsely PASS
- [Low] selftests/ve: kmsg_check() fails on any unrelated kernel WARNING/Oops/BUG
- [Low] selftests/ve: veid uniqueness check is a TOCTOU, so the stress may hit a live container
--
commit e116830e8cd9eabf2049859ae877c3c8c485e837
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
selftests: ve: introduce vtty pair teardown stress test
This adds a selftest that opens and closes a vtty pair from both the
master end (vzctl ioctl) and the slave end (inside a VE) for a
configurable time. It then scans /dev/kmsg for vtty count mismatches and
generic kernel warnings, to catch the use-after-free fixed in c5028cd61a69.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-144771
> diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
> new file mode 100644
> index 000000000000..8be6955d1dbc
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_vtty_stress.c
[ ... ]
> +static int kmsg_check(int fd)
> +{
> + char buf[8192];
> + int found = 0;
> + ssize_t r;
> +
> + if (fd < 0)
> + return 0;
> +
> + for (;;) {
> + r = read(fd, buf, sizeof(buf) - 1);
> + if (r < 0) {
> + /* Ring overrun, continue with the oldest existing record */
> + if (errno == EPIPE)
> + continue;
> + break;
> + }
[Severity: Medium]
Can this test report PASS even though the kernel logged the bug it is
meant to detect?
kmsg_check() is called only once, after the whole run, so /dev/kmsg is
drained after up to 30 seconds of logging. If the log ring wraps during
that time, the next read() returns EPIPE, and the branch below silently
resumes from the oldest surviving record:
if (errno == EPIPE)
continue;
Records emitted early in the run, such as a "bad slave count" message or
a WARNING from a double release_tty(), would be dropped without being
counted. Nothing tells the user that records were lost.
Should the EPIPE case at least print a message that records were lost, or
make the test fail or skip?
> + if (r == 0)
> + break;
> + buf[r] = '\0';
> + /* Known symptoms of a buggy kernel */
> + if (strstr(buf, "vtty") && (strstr(buf, "tty->count") ||
> + strstr(buf, "bad slave count"))) {
> + ksft_print_msg("vtty mismatched count log: %s", buf);
> + found++;
> + }
> + if (strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") ||
> + strstr(buf, ";BUG: ") || strstr(buf, "soft lockup - ") ||
> + strstr(buf, "general protection fault")) {
[Severity: Low]
Can an unrelated kernel message fail this test?
kmsg_open() seeks to the end of /dev/kmsg, and kmsg_check() then reads
every record logged until the end of the run. Any record from another
subsystem that contains "WARNING: ", "Oops: ", ";BUG: ", "soft lockup - "
or "general protection fault" is counted in found.
main() then calls ksft_exit_fail_msg("kernel complained %d time(s)\n", ...).
Only the "tty->count" and "bad slave count" checks are restricted to vtty.
On a busy host, wouldn't this make the result depend on unrelated activity?
Would it be possible to filter by something related to the vtty or tty
code, such as the call trace?
> + ksft_print_msg("kernel log: %s", buf);
> + found++;
> + }
> + }
> + return found;
> +}
[ ... ]
> + ctid = make_ve(cg, CTID_MIN);
> + if (ctid < 0)
> + ksft_exit_skip("cannot make a VE cgroup\n");
> +
> + /* The vtty map is keyed by veid, and ve.veid takes a taken one */
> + if (veid_in_use(ctl, ctid)) {
> + destroy_ve(cg, ctid);
> + ksft_exit_skip("veid %d is in use by a running container\n",
> + ctid);
> + }
> +
> + /* The ve.veid must match between master / slave */
> + snprintf(path, sizeof(path), "%d/ve.veid", ctid);
> + snprintf(val, sizeof(val), "%d", ctid);
> + if (write_file_at(cg, path, val) < 0) {
[Severity: Low]
Is the veid uniqueness check in main() racy?
veid_in_use() queries only running VEs, and ve.veid is written afterwards.
ve_id_write() in kernel/ve/ve.c carries "/* FIXME: check veid is uniqul */"
and does not reserve the id. The test's own VE enters the idr only later,
after the namespace switch.
If a container with the same veid is started between veid_in_use() and the
write of ve.veid, would the test not detect it? It could then drive that
container's vtty pair 0 from both ends.
The commit message says "Before writing ve.veid the test asks the kernel
... whether the veid is taken, and skips when it is." This describes the
check as a full safeguard, but it only narrows the window. Could the
commit message or a code comment say that the check is best-effort?
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261002044431.1632888-1-eva.kurchatova@virtuozzo.com?part=2
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* [Devel] [PATCH hci-8.1 v6 2/2] selftests: ve: introduce vtty pair teardown stress test
2026-10-02 4:43 11% ` [Devel] [PATCH hci-8.1 v6 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
@ 2026-10-02 4:43 6% ` Eva Kurchatova
2026-10-02 4:54 4% ` no-reply
1 sibling, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-10-02 4:43 UTC (permalink / raw)
To: khorenko; +Cc: devel
Either end of a vtty pair creates it, and the last close tears it down.
Race condition in older VZKernel vtty implementation allowed both sides
to decide they were the final one and call release_tty() twice, and a
master opening again while a teardown was in flight used to hand out a
reference to a tty_struct about to be freed. Either way, this caused
use-after-free. That issue was fixed in c5028cd61a69:
"ve/vtty: fix use-after-free on concurrent tty close and reopen"
Hammer both ends for ownership correctness & thread safety verification.
The original reproducer needed a few tens of minutes across a dozen
containers, where vzctl interaction with vtty subsystem was a small part
of the overall runtime, therefore it did not fire on every run. This test
opens and closes nothing but the pair, from both ends and without pause,
so the default stays in the tens of seconds and under the timeout of the
suite, and VE_VTTY_SECS or argv[1] allows to control this.
ve_id_write() does not check that a veid is unique and the vtty map is
keyed by veid, so a clash would hand the test a running container's
pair. Before writing ve.veid the test asks the kernel, through the
vzctl ioctl, whether the veid is taken, and skips when it is. It also
skips where /tmp is mounted nodev: mknod() succeeds there and every
open of the slave node fails.
https://virtuozzo.atlassian.net/browse/VSTOR-144771
Feature: ve selftest
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/ve/.gitignore | 1 +
tools/testing/selftests/ve/Makefile | 3 +-
tools/testing/selftests/ve/ve_vtty_stress.c | 343 ++++++++++++++++++++
3 files changed, 346 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/ve/ve_vtty_stress.c
diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index 3df4d05888dc..d8eb5c346c6e 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,3 +1,4 @@
ve_ns_owner_test
ve_perms_test
ve_mount_accounting_test
+ve_vtty_stress
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index c6efe7c4b4fb..d287a73c5e97 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -1,9 +1,10 @@
# SPDX-License-Identifier: GPL-2.0
# Makefile for ve selftests.
-CFLAGS += -g -Wall -O2
+CFLAGS += -g -Wall -O2 $(KHDR_INCLUDES)
TEST_GEN_PROGS += ve_ns_owner_test
TEST_GEN_PROGS += ve_perms_test
TEST_GEN_PROGS += ve_mount_accounting_test
+TEST_GEN_PROGS += ve_vtty_stress
include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
new file mode 100644
index 000000000000..8be6955d1dbc
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_vtty_stress.c
@@ -0,0 +1,343 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_vtty_stress - open and close a vtty pair from both ends at once.
+ *
+ * Either end of a vtty pair creates it, and the last close tears it down.
+ *
+ * Hammer both ends. The master comes from the vzctl ioctl, which does
+ * not look the VE up. The slave is reachable only from inside the VE, as
+ * TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
+ * vttys index minor - 1, so the test builds a VE with cgroup2, sets
+ * ve.veid to the cgroup it made, and runs the slave side there.
+ *
+ * A warning does not stop the host, so the run reads the kernel log too.
+ * One read() of /dev/kmsg is one record, so the test matches vtty driver
+ * splats, as well as generic kernel warning/oops/kasan reports.
+ *
+ * Usage: ve_vtty_stress [seconds] (default 30, or $VE_VTTY_SECS)
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <stdbool.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <sys/statvfs.h>
+#include <linux/major.h>
+#include <linux/vzcalluser.h>
+#include <sys/sysmacros.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "ve_selftest.h"
+#include "../kselftest.h"
+
+#define VTTY_IDX 0 /* The pair both ends work on */
+#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
+#define DEFAULT_SECS 30
+
+/*
+ * Expected open errno codes: the pair may be closing, or gone already,
+ * when an open lands on it.
+ * Anything else is an environment issue or a bug.
+ */
+static bool expected_errno(int err)
+{
+ return err == EBUSY || err == EIO || err == ENXIO;
+}
+
+static int open_master(int ctl_fd, unsigned int veid)
+{
+ struct vzctl_ve_configure c = {
+ .veid = veid,
+ .key = VE_CONFIGURE_OPEN_TTY,
+ .val = VTTY_IDX,
+ };
+
+ return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
+}
+
+/* Any key but the tty one makes ve_configure() look a running VE up */
+static bool veid_in_use(int ctl_fd, unsigned int veid)
+{
+ struct vzctl_ve_configure c = {
+ .veid = veid,
+ .key = VE_CONFIGURE_OS_RELEASE,
+ };
+
+ return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c) < 0 && errno == ENOKEY;
+}
+
+/* The slave end, inside the VE. Returns the errno that stopped it, or 0. */
+static int hammer_slave(const char *node, time_t deadline,
+ unsigned long *opened)
+{
+ int err = 0;
+
+ while (time(NULL) < deadline) {
+ int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
+
+ if (fd >= 0) {
+ (*opened)++;
+ close(fd);
+ } else if (!expected_errno(errno)) {
+ err = errno;
+ break;
+ }
+ }
+
+ return err;
+}
+
+/* The kernel log, since a warning fails nothing by itself. */
+static int kmsg_open(void)
+{
+ int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+
+ if (fd >= 0)
+ lseek(fd, 0, SEEK_END);
+ return fd;
+}
+
+/*
+ * Check for kernel log warnings, one read() per record, hence
+ * the function name is not checked.
+ */
+static int kmsg_check(int fd)
+{
+ char buf[8192];
+ int found = 0;
+ ssize_t r;
+
+ if (fd < 0)
+ return 0;
+
+ for (;;) {
+ r = read(fd, buf, sizeof(buf) - 1);
+ if (r < 0) {
+ /* Ring overrun, continue with the oldest existing record */
+ if (errno == EPIPE)
+ continue;
+ break;
+ }
+ if (r == 0)
+ break;
+ buf[r] = '\0';
+ /* Known symptoms of a buggy kernel */
+ if (strstr(buf, "vtty") && (strstr(buf, "tty->count") ||
+ strstr(buf, "bad slave count"))) {
+ ksft_print_msg("vtty mismatched count log: %s", buf);
+ found++;
+ }
+ if (strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") ||
+ strstr(buf, ";BUG: ") || strstr(buf, "soft lockup - ") ||
+ strstr(buf, "general protection fault")) {
+ ksft_print_msg("kernel log: %s", buf);
+ found++;
+ }
+ }
+ return found;
+}
+
+int main(int argc, char **argv)
+{
+ unsigned long master_opened = 0, slave_opened = 0;
+ int cg, ctid, ctl, kmsg, ready[2], count[2], status;
+ int master_err = 0, slave_err;
+ const char *env = getenv("VE_VTTY_SECS");
+ char path[PATH_MAX], val[16], c;
+ char dir[] = "/tmp/ve_vtty_XXXXXX";
+ char node[sizeof(dir) + 6];
+ struct namespace_ctx ns;
+ struct statvfs st;
+ time_t deadline;
+ long secs;
+ pid_t pid;
+
+ ksft_print_header();
+ secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
+ if (secs <= 0)
+ secs = DEFAULT_SECS;
+
+ ctl = open("/dev/vzctl", O_RDWR);
+ if (ctl < 0)
+ ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
+ "(vzdev, vzmon) are needed\n",
+ strerror(errno));
+
+ cg = mount_cg2_fd();
+ if (cg < 0)
+ ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
+ if (write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS) < 0)
+ ksft_exit_skip("cannot enable the ve controller: %s\n",
+ strerror(errno));
+
+ ctid = make_ve(cg, CTID_MIN);
+ if (ctid < 0)
+ ksft_exit_skip("cannot make a VE cgroup\n");
+
+ /* The vtty map is keyed by veid, and ve.veid takes a taken one */
+ if (veid_in_use(ctl, ctid)) {
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("veid %d is in use by a running container\n",
+ ctid);
+ }
+
+ /* The ve.veid must match between master / slave */
+ snprintf(path, sizeof(path), "%d/ve.veid", ctid);
+ snprintf(val, sizeof(val), "%d", ctid);
+ if (write_file_at(cg, path, val) < 0) {
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
+ }
+
+ /* Do not report a failure where the vtty pair cannot be had at all */
+ status = open_master(ctl, ctid);
+ if (status < 0 && errno != EBUSY) {
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("no vtty master: %s\n", strerror(errno));
+ }
+ if (status >= 0)
+ close(status);
+
+ /*
+ * Use temp directory for the node the slave end opens.
+ * Cleaned up by the master side instead of relying on the slave.
+ */
+ if (!mkdtemp(dir)) {
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("mkdtemp: %s\n", strerror(errno));
+ }
+ /* A nodev mount takes the mknod() and refuses every open */
+ if (statvfs(dir, &st) == 0 && (st.f_flag & ST_NODEV)) {
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("%s is on a nodev filesystem\n", dir);
+ }
+ snprintf(node, sizeof(node), "%s/slave", dir);
+ if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0) {
+ status = errno;
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("mknod: %s\n", strerror(status));
+ }
+
+ ksft_set_plan(1);
+ kmsg = kmsg_open();
+ if (kmsg < 0)
+ ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
+ "be noticed\n",
+ strerror(errno));
+
+ if (pipe(ready) || pipe(count)) {
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("pipe: %s\n", strerror(errno));
+ }
+
+ /* The VE of the cgroup, which the slave side enters */
+ if (prepare_namespaces(&ns, CLONE_NEWVE, cg, ctid) < 0) {
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("cannot prepare the VE namespace\n");
+ }
+
+ deadline = time(NULL) + secs;
+ ksft_print_msg("VE %d, pair %d, %ld seconds\n", ctid, VTTY_IDX, secs);
+
+ pid = fork();
+ if (pid == 0) {
+ unsigned long n = 0;
+ int err;
+
+ close(ready[0]);
+ close(count[0]);
+ /*
+ * Hammering on after the run has given up would hold
+ * the cgroup open against destroy_ve().
+ */
+ prctl(PR_SET_PDEATHSIG, SIGKILL);
+ if (enter_cgroup(cg, ctid) < 0 ||
+ enter_namespaces(&ns, CLONE_NEWVE) < 0)
+ _exit(errno);
+
+ c = 'r';
+ if (write(ready[1], &c, 1) != 1)
+ _exit(errno);
+ err = hammer_slave(node, deadline, &n);
+ /* Send the count either way: it says how far it got. */
+ if (write(count[1], &n, sizeof(n)) != sizeof(n))
+ _exit(errno);
+ _exit(err);
+ } else if (pid < 0) {
+ close_namespace_fds(&ns);
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("fork: %s\n", strerror(errno));
+ }
+ close(ready[1]);
+ close(count[1]);
+
+ if (read(ready[0], &c, 1) != 1) {
+ waitpid(pid, &status, 0);
+ close_namespace_fds(&ns);
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("the VE side never came up: %s\n",
+ strerror(WIFEXITED(status) ?
+ WEXITSTATUS(status) : EINTR));
+ }
+
+ while (time(NULL) < deadline) {
+ int fd = open_master(ctl, ctid);
+
+ if (fd >= 0) {
+ master_opened++;
+ close(fd);
+ } else if (!expected_errno(errno)) {
+ master_err = errno;
+ break;
+ }
+ }
+
+ if (master_err) {
+ /* The other end runs to its own deadline, stop it */
+ kill(pid, SIGKILL);
+ waitpid(pid, &status, 0);
+ slave_err = 0;
+ } else {
+ if (read(count[0], &slave_opened, sizeof(slave_opened)) !=
+ sizeof(slave_opened))
+ slave_opened = 0;
+ waitpid(pid, &status, 0);
+ slave_err = WIFEXITED(status) ? WEXITSTATUS(status) : EINTR;
+ }
+
+ ksft_print_msg("%lu master opens, %lu slave opens\n", master_opened,
+ slave_opened);
+
+ status = kmsg_check(kmsg);
+ close_namespace_fds(&ns);
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+
+ if (master_err)
+ ksft_exit_fail_msg("the master end stopped: %s\n",
+ strerror(master_err));
+ if (slave_err)
+ ksft_exit_fail_msg("the slave end stopped: %s\n",
+ strerror(slave_err));
+ if (!master_opened)
+ ksft_exit_fail_msg("no master was ever opened\n");
+ if (!slave_opened)
+ ksft_exit_fail_msg("no slave was ever opened\n");
+ if (status)
+ ksft_exit_fail_msg("kernel complained %d time(s)\n", status);
+
+ ksft_test_result_pass("vtty pair closed from both ends\n");
+ ksft_finished();
+}
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 6%]
* [Devel] [PATCH hci-8.1 v6 1/2] selftests: ve: grant the directories the files sit in
@ 2026-10-02 4:43 11% ` Eva Kurchatova
2026-10-02 4:43 6% ` [Devel] [PATCH hci-8.1 v6 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-10-02 4:43 UTC (permalink / raw)
To: khorenko; +Cc: devel
The sysfs variant names nested leaves, kernel/vmcoreinfo and
kernel/notes, while the proc one names files at the top of the
filesystem. Both enforcement tests grant the leaves and nothing else,
so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
cannot traverse, and every open answers ENOENT.
Grant x on each directory along the way, taken from the path the
variant already gives.
Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")
https://virtuozzo.atlassian.net/browse/VSTOR-144955
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/ve/ve_perms_test.c | 34 ++++++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 25ffbd42c380..8096b1fa3b43 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -114,9 +114,11 @@ static int ve_open_rel(int cgv2_fd, int ctid, const char *prefix,
static int set_entry(int cgv2_fd, const char *path, const char *entry,
const char *mask)
{
- char line[256];
+ char line[PATH_MAX];
- snprintf(line, sizeof(line), "%s %s", entry, mask);
+ if (snprintf(line, sizeof(line), "%s %s", entry, mask) >=
+ (int)sizeof(line))
+ return -1;
return write_file_at(cgv2_fd, path, line);
}
@@ -343,6 +345,32 @@ TEST_F(ve_perms, write_parser)
EXPECT_LT(write_file_at(self->cgv2_fd, path, file), 0);
}
+/* A leaf is reachable only once every directory above it can be traversed */
+static int allow_path(int cgv2_fd, const char *path, const char *file)
+{
+ char dir[PATH_MAX];
+ const char *sep;
+
+ for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
+ if (snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file),
+ file) >= (int)sizeof(dir))
+ return -1;
+ if (set_entry(cgv2_fd, path, dir, "x") < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
+{
+ int i;
+
+ for (i = 0; i < NR_FILES; i++)
+ if (allow_path(cgv2_fd, path, files[i]) < 0)
+ return -1;
+ return 0;
+}
+
/*
* Enforcement, allowed side: from inside a VE, granting r lets each file be
* read, and granting x on the directory lets it be traversed (the lookup then
@@ -358,6 +386,7 @@ TEST_F(ve_perms, enforce_allows)
ve_file(path, sizeof(path), self->ctid_a, variant->fs);
+ ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
for (i = 0; i < NR_FILES; i++)
EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
@@ -400,6 +429,7 @@ TEST_F(ve_perms, enforce_denies)
O_RDONLY), 0);
/* Granted w only: visible but not readable. */
+ ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
for (i = 0; i < NR_FILES; i++)
EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 11%]
* Re: [Devel] [PATCH hci-8.1 v5 2/2] selftests: ve: introduce vtty pair teardown stress test
2026-10-01 3:24 6% ` [Devel] [PATCH hci-8.1 v5 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
@ 2026-10-01 3:34 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-10-01 3:34 UTC (permalink / raw)
To: Eva Kurchatova, khorenko; +Cc: devel
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Low] selftests/ve: vtty stress test can hijack a live container's vtty pair via veid clash
- [Low] selftests/ve: kmsg_check() fails the test on any unrelated kernel warning
- [Low] selftests/ve: slave node on possibly-nodev /tmp gives hard failure instead of skip
--
commit 58d6472da1964666674d9430e2db654718c0e4a4
Author: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
selftests: ve: introduce vtty pair teardown stress test
This commit adds ve_vtty_stress, a selftest that opens and closes a vtty
pair from the master and slave ends at the same time. It checks the
kernel log for vtty count mismatches and for generic warnings, oopses and
lockups. The test is wired into the ve selftests Makefile and .gitignore.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-144771
> diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
> new file mode 100644
> index 000000000000..56355bb8dbe4
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_vtty_stress.c
[ ... ]
> +/* The slave end, inside the VE. Returns the errno that stopped it, or 0. */
> +static int hammer_slave(const char *node, time_t deadline,
> + unsigned long *opened)
> +{
> + int err = 0;
> +
> + while (time(NULL) < deadline) {
> + int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
> +
> + if (fd >= 0) {
> + (*opened)++;
> + close(fd);
> + } else if (!expected_errno(errno)) {
> + err = errno;
> + break;
> + }
> + }
> +
> + return err;
> +}
[ ... ]
> +static int kmsg_check(int fd)
> +{
[ ... ]
> + if (strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") ||
> + strstr(buf, ";BUG: ") || strstr(buf, "soft lockup - ") ||
> + strstr(buf, "general protection fault")) {
> + ksft_print_msg("kernel log: %s", buf);
> + found++;
> + }
> + }
> + return found;
> +}
[Severity: Low]
Can an unrelated kernel warning fail this test?
kmsg_open() seeks to the end of /dev/kmsg, and kmsg_check() then scans
every record logged anywhere in the kernel during the run. Only the
"tty->count" and "bad slave count" match is restricted to vtty.
The second condition is global:
strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") || ...
Any warning from another subsystem (a lockdep splat or a driver WARN, for
example) increments found. main() then reaches:
ksft_exit_fail_msg("kernel complained %d time(s)\n", status);
On a busy node this gives a failure that has nothing to do with vtty.
Would it make sense to limit these matches to records that mention vtty,
or to the test's own tasks?
> +int main(int argc, char **argv)
> +{
[ ... ]
> + ctid = make_ve(cg, CTID_MIN);
> + if (ctid < 0)
> + ksft_exit_skip("cannot make a VE cgroup\n");
> +
> + /* The ve.veid must match between master / slave */
> + snprintf(path, sizeof(path), "%d/ve.veid", ctid);
> + snprintf(val, sizeof(val), "%d", ctid);
> + if (write_file_at(cg, path, val) < 0) {
> + destroy_ve(cg, ctid);
> + ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
> + }
[Severity: Low]
Can this test pick up a live container's vtty pair?
make_ve() picks the first cgroup directory name in the 108..199 range
that does not exist. That is a check on cgroup names, not on veids.
The chosen number is then written to ve.veid. ve_id_write() in
kernel/ve/ve.c has this comment and does no uniqueness check:
/* FIXME: check veid is uniqul */
The vtty map is keyed by veid. A running container with the same veid
but a different cgroup name, such as a UUID-named cgroup, would
therefore be reached by open_master(). The test could then take over or
tear down that container's console pair and report spurious failures.
The commit message acknowledges this and says the range makes it
unlikely. Is there a way to check that the veid is unused before
writing it to ve.veid, or to skip the test when other containers are
running?
[ ... ]
> + snprintf(node, sizeof(node), "%s/slave", dir);
> + if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0) {
> + status = errno;
> + rmdir(dir);
> + destroy_ve(cg, ctid);
> + ksft_exit_fail_msg("mknod: %s\n", strerror(status));
> + }
[Severity: Low]
Can a nodev /tmp turn this into a failure instead of a skip?
The node is created with mknod() under a mkdtemp() directory in /tmp.
On a host where /tmp is mounted nodev (systemd tmp.mount with
nosuid,nodev, for example), mknod() succeeds. The node is never
probe-opened before the fork.
In the child, hammer_slave() then gets EACCES from open(). Since
expected_errno() accepts only EBUSY, EIO and ENXIO, hammer_slave()
returns at once, and main() ends with:
ksft_exit_fail_msg("the slave end stopped: %s\n", ...);
The test skips for the other environment problems, such as a missing
master. Should the node be probe-opened before the fork, with a skip on
EACCES, or be created on a filesystem that allows device nodes?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20261001032531.1307025-1-eva.kurchatova@virtuozzo.com?part=2
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 5%]
* [Devel] [PATCH hci-8.1 v5 1/2] selftests: ve: grant the directories the files sit in
@ 2026-10-01 3:24 11% ` Eva Kurchatova
2026-10-01 3:24 6% ` [Devel] [PATCH hci-8.1 v5 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-10-01 3:24 UTC (permalink / raw)
To: khorenko; +Cc: devel
The sysfs variant names nested leaves, kernel/vmcoreinfo and
kernel/notes, while the proc one names files at the top of the
filesystem. Both enforcement tests grant the leaves and nothing else,
so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
cannot traverse, and every open answers ENOENT.
Grant x on each directory along the way, taken from the path the
variant already gives.
Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")
https://virtuozzo.atlassian.net/browse/VSTOR-144955
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/ve/ve_perms_test.c | 34 ++++++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 25ffbd42c380..8096b1fa3b43 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -114,9 +114,11 @@ static int ve_open_rel(int cgv2_fd, int ctid, const char *prefix,
static int set_entry(int cgv2_fd, const char *path, const char *entry,
const char *mask)
{
- char line[256];
+ char line[PATH_MAX];
- snprintf(line, sizeof(line), "%s %s", entry, mask);
+ if (snprintf(line, sizeof(line), "%s %s", entry, mask) >=
+ (int)sizeof(line))
+ return -1;
return write_file_at(cgv2_fd, path, line);
}
@@ -343,6 +345,32 @@ TEST_F(ve_perms, write_parser)
EXPECT_LT(write_file_at(self->cgv2_fd, path, file), 0);
}
+/* A leaf is reachable only once every directory above it can be traversed */
+static int allow_path(int cgv2_fd, const char *path, const char *file)
+{
+ char dir[PATH_MAX];
+ const char *sep;
+
+ for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
+ if (snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file),
+ file) >= (int)sizeof(dir))
+ return -1;
+ if (set_entry(cgv2_fd, path, dir, "x") < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
+{
+ int i;
+
+ for (i = 0; i < NR_FILES; i++)
+ if (allow_path(cgv2_fd, path, files[i]) < 0)
+ return -1;
+ return 0;
+}
+
/*
* Enforcement, allowed side: from inside a VE, granting r lets each file be
* read, and granting x on the directory lets it be traversed (the lookup then
@@ -358,6 +386,7 @@ TEST_F(ve_perms, enforce_allows)
ve_file(path, sizeof(path), self->ctid_a, variant->fs);
+ ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
for (i = 0; i < NR_FILES; i++)
EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
@@ -400,6 +429,7 @@ TEST_F(ve_perms, enforce_denies)
O_RDONLY), 0);
/* Granted w only: visible but not readable. */
+ ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
for (i = 0; i < NR_FILES; i++)
EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 11%]
* [Devel] [PATCH hci-8.1 v5 2/2] selftests: ve: introduce vtty pair teardown stress test
2026-10-01 3:24 11% ` [Devel] [PATCH hci-8.1 v5 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
@ 2026-10-01 3:24 6% ` Eva Kurchatova
2026-10-01 3:34 5% ` no-reply
1 sibling, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-10-01 3:24 UTC (permalink / raw)
To: khorenko; +Cc: devel
Either end of a vtty pair creates it, and the last close tears it down.
Race condition in older VZKernel vtty implementation allowed both sides
to decide they were the final one and call release_tty() twice, and a
master opening again while a teardown was in flight used to hand out a
reference to a tty_struct about to be freed. Either way, this caused
use-after-free. That issue was fixed in c5028cd61a69:
"ve/vtty: fix use-after-free on concurrent tty close and reopen"
Hammer both ends for ownership correctness & thread safety verification.
The original reproducer needed a few tens of minutes across a dozen
containers, where vzctl interaction with vtty subsystem was a small part
of the overall runtime, therefore it did not fire on every run. This test
opens and closes nothing but the pair, from both ends and without pause,
so the default stays in the tens of seconds and under the timeout of the
suite, and VE_VTTY_SECS or argv[1] allows to control this.
The test assumes the veid it picks is not in use by a running container:
ve_id_write() does not check that a veid is unique and the vtty map is
keyed by veid, so a clash would hand the test someone else's pair. The
108..200 range make_ve() uses, makes that unlikely on a test node.
https://virtuozzo.atlassian.net/browse/VSTOR-144771
Feature: ve selftest
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/ve/.gitignore | 1 +
tools/testing/selftests/ve/Makefile | 3 +-
tools/testing/selftests/ve/ve_vtty_stress.c | 317 ++++++++++++++++++++
3 files changed, 320 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/ve/ve_vtty_stress.c
diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index 3df4d05888dc..d8eb5c346c6e 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,3 +1,4 @@
ve_ns_owner_test
ve_perms_test
ve_mount_accounting_test
+ve_vtty_stress
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index c6efe7c4b4fb..d287a73c5e97 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -1,9 +1,10 @@
# SPDX-License-Identifier: GPL-2.0
# Makefile for ve selftests.
-CFLAGS += -g -Wall -O2
+CFLAGS += -g -Wall -O2 $(KHDR_INCLUDES)
TEST_GEN_PROGS += ve_ns_owner_test
TEST_GEN_PROGS += ve_perms_test
TEST_GEN_PROGS += ve_mount_accounting_test
+TEST_GEN_PROGS += ve_vtty_stress
include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
new file mode 100644
index 000000000000..56355bb8dbe4
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_vtty_stress.c
@@ -0,0 +1,317 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_vtty_stress - open and close a vtty pair from both ends at once.
+ *
+ * Either end of a vtty pair creates it, and the last close tears it down.
+ *
+ * Hammer both ends. The master comes from the vzctl ioctl, which does
+ * not look the VE up. The slave is reachable only from inside the VE, as
+ * TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
+ * vttys index minor - 1, so the test builds a VE with cgroup2, sets
+ * ve.veid to the cgroup it made, and runs the slave side there.
+ *
+ * A warning does not stop the host, so the run reads the kernel log too.
+ * One read() of /dev/kmsg is one record, so the test matches vtty driver
+ * splats, as well as generic kernel warning/oops/kasan reports.
+ *
+ * Usage: ve_vtty_stress [seconds] (default 30, or $VE_VTTY_SECS)
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <stdbool.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <linux/major.h>
+#include <linux/vzcalluser.h>
+#include <sys/sysmacros.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "ve_selftest.h"
+#include "../kselftest.h"
+
+#define VTTY_IDX 0 /* The pair both ends work on */
+#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
+#define DEFAULT_SECS 30
+
+/*
+ * Expected open errno codes: the pair may be closing, or gone already,
+ * when an open lands on it.
+ * Anything else is an environment issue or a bug.
+ */
+static bool expected_errno(int err)
+{
+ return err == EBUSY || err == EIO || err == ENXIO;
+}
+
+static int open_master(int ctl_fd, unsigned int veid)
+{
+ struct vzctl_ve_configure c = {
+ .veid = veid,
+ .key = VE_CONFIGURE_OPEN_TTY,
+ .val = VTTY_IDX,
+ };
+
+ return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
+}
+
+/* The slave end, inside the VE. Returns the errno that stopped it, or 0. */
+static int hammer_slave(const char *node, time_t deadline,
+ unsigned long *opened)
+{
+ int err = 0;
+
+ while (time(NULL) < deadline) {
+ int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
+
+ if (fd >= 0) {
+ (*opened)++;
+ close(fd);
+ } else if (!expected_errno(errno)) {
+ err = errno;
+ break;
+ }
+ }
+
+ return err;
+}
+
+/* The kernel log, since a warning fails nothing by itself. */
+static int kmsg_open(void)
+{
+ int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+
+ if (fd >= 0)
+ lseek(fd, 0, SEEK_END);
+ return fd;
+}
+
+/*
+ * Check for kernel log warnings, one read() per record, hence
+ * the function name is not checked.
+ */
+static int kmsg_check(int fd)
+{
+ char buf[8192];
+ int found = 0;
+ ssize_t r;
+
+ if (fd < 0)
+ return 0;
+
+ for (;;) {
+ r = read(fd, buf, sizeof(buf) - 1);
+ if (r < 0) {
+ /* Ring overrun, continue with the oldest existing record */
+ if (errno == EPIPE)
+ continue;
+ break;
+ }
+ if (r == 0)
+ break;
+ buf[r] = '\0';
+ /* Known symptoms of a buggy kernel */
+ if (strstr(buf, "vtty") && (strstr(buf, "tty->count") ||
+ strstr(buf, "bad slave count"))) {
+ ksft_print_msg("vtty mismatched count log: %s", buf);
+ found++;
+ }
+ if (strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") ||
+ strstr(buf, ";BUG: ") || strstr(buf, "soft lockup - ") ||
+ strstr(buf, "general protection fault")) {
+ ksft_print_msg("kernel log: %s", buf);
+ found++;
+ }
+ }
+ return found;
+}
+
+int main(int argc, char **argv)
+{
+ unsigned long master_opened = 0, slave_opened = 0;
+ int cg, ctid, ctl, kmsg, ready[2], count[2], status;
+ int master_err = 0, slave_err;
+ const char *env = getenv("VE_VTTY_SECS");
+ char path[PATH_MAX], val[16], c;
+ char dir[] = "/tmp/ve_vtty_XXXXXX";
+ char node[sizeof(dir) + 6];
+ struct namespace_ctx ns;
+ time_t deadline;
+ long secs;
+ pid_t pid;
+
+ ksft_print_header();
+ secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
+ if (secs <= 0)
+ secs = DEFAULT_SECS;
+
+ ctl = open("/dev/vzctl", O_RDWR);
+ if (ctl < 0)
+ ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
+ "(vzdev, vzmon) are needed\n",
+ strerror(errno));
+
+ cg = mount_cg2_fd();
+ if (cg < 0)
+ ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
+ if (write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS) < 0)
+ ksft_exit_skip("cannot enable the ve controller: %s\n",
+ strerror(errno));
+
+ ctid = make_ve(cg, CTID_MIN);
+ if (ctid < 0)
+ ksft_exit_skip("cannot make a VE cgroup\n");
+
+ /* The ve.veid must match between master / slave */
+ snprintf(path, sizeof(path), "%d/ve.veid", ctid);
+ snprintf(val, sizeof(val), "%d", ctid);
+ if (write_file_at(cg, path, val) < 0) {
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
+ }
+
+ /* Do not report a failure where the vtty pair cannot be had at all */
+ status = open_master(ctl, ctid);
+ if (status < 0 && errno != EBUSY) {
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("no vtty master: %s\n", strerror(errno));
+ }
+ if (status >= 0)
+ close(status);
+
+ /*
+ * Use temp directory for the node the slave end opens.
+ * Cleaned up by the master side instead of relying on the slave.
+ */
+ if (!mkdtemp(dir)) {
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("mkdtemp: %s\n", strerror(errno));
+ }
+ snprintf(node, sizeof(node), "%s/slave", dir);
+ if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0) {
+ status = errno;
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("mknod: %s\n", strerror(status));
+ }
+
+ ksft_set_plan(1);
+ kmsg = kmsg_open();
+ if (kmsg < 0)
+ ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
+ "be noticed\n",
+ strerror(errno));
+
+ if (pipe(ready) || pipe(count)) {
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("pipe: %s\n", strerror(errno));
+ }
+
+ /* The VE of the cgroup, which the slave side enters */
+ if (prepare_namespaces(&ns, CLONE_NEWVE, cg, ctid) < 0) {
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("cannot prepare the VE namespace\n");
+ }
+
+ deadline = time(NULL) + secs;
+ ksft_print_msg("VE %d, pair %d, %ld seconds\n", ctid, VTTY_IDX, secs);
+
+ pid = fork();
+ if (pid == 0) {
+ unsigned long n = 0;
+ int err;
+
+ close(ready[0]);
+ close(count[0]);
+ /*
+ * Hammering on after the run has given up would hold
+ * the cgroup open against destroy_ve().
+ */
+ prctl(PR_SET_PDEATHSIG, SIGKILL);
+ if (enter_cgroup(cg, ctid) < 0 ||
+ enter_namespaces(&ns, CLONE_NEWVE) < 0)
+ _exit(errno);
+
+ c = 'r';
+ if (write(ready[1], &c, 1) != 1)
+ _exit(errno);
+ err = hammer_slave(node, deadline, &n);
+ /* Send the count either way: it says how far it got. */
+ if (write(count[1], &n, sizeof(n)) != sizeof(n))
+ _exit(errno);
+ _exit(err);
+ } else if (pid < 0) {
+ close_namespace_fds(&ns);
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("fork: %s\n", strerror(errno));
+ }
+ close(ready[1]);
+ close(count[1]);
+
+ if (read(ready[0], &c, 1) != 1) {
+ waitpid(pid, &status, 0);
+ close_namespace_fds(&ns);
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("the VE side never came up: %s\n",
+ strerror(WIFEXITED(status) ?
+ WEXITSTATUS(status) : EINTR));
+ }
+
+ while (time(NULL) < deadline) {
+ int fd = open_master(ctl, ctid);
+
+ if (fd >= 0) {
+ master_opened++;
+ close(fd);
+ } else if (!expected_errno(errno)) {
+ master_err = errno;
+ break;
+ }
+ }
+
+ if (master_err) {
+ /* The other end runs to its own deadline, stop it */
+ kill(pid, SIGKILL);
+ waitpid(pid, &status, 0);
+ slave_err = 0;
+ } else {
+ if (read(count[0], &slave_opened, sizeof(slave_opened)) !=
+ sizeof(slave_opened))
+ slave_opened = 0;
+ waitpid(pid, &status, 0);
+ slave_err = WIFEXITED(status) ? WEXITSTATUS(status) : EINTR;
+ }
+
+ ksft_print_msg("%lu master opens, %lu slave opens\n", master_opened,
+ slave_opened);
+
+ status = kmsg_check(kmsg);
+ close_namespace_fds(&ns);
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+
+ if (master_err)
+ ksft_exit_fail_msg("the master end stopped: %s\n",
+ strerror(master_err));
+ if (slave_err)
+ ksft_exit_fail_msg("the slave end stopped: %s\n",
+ strerror(slave_err));
+ if (!master_opened)
+ ksft_exit_fail_msg("no master was ever opened\n");
+ if (!slave_opened)
+ ksft_exit_fail_msg("no slave was ever opened\n");
+ if (status)
+ ksft_exit_fail_msg("kernel complained %d time(s)\n", status);
+
+ ksft_test_result_pass("vtty pair closed from both ends\n");
+ ksft_finished();
+}
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 6%]
* Re: [Devel] [PATCH hci-8.1 2/3] fs/fuse kio: refactor transmit path in pcs_sockio_send()
2026-09-30 7:42 3% ` no-reply
@ 2026-09-30 16:29 0% ` Konstantin Khorenko
0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-30 16:29 UTC (permalink / raw)
To: Liu Kui, Alexey Kuznetsov; +Cc: devel, azaitsev
i have commited this patch, but this review looks valid, is you agree,
please consider sending an incremental patch like
diff --git a/fs/fuse/kio/pcs/pcs_sock_io.c b/fs/fuse/kio/pcs/pcs_sock_io.c
index 2da6d51c898b0..3a2228088540a 100644
--- a/fs/fuse/kio/pcs/pcs_sock_io.c
+++ b/fs/fuse/kio/pcs/pcs_sock_io.c
@@ -307,6 +307,12 @@ static int do_sockio_sendmsg(struct pcs_sockio *sio, struct pcs_msg *msg)
if (!iov_iter_count(it)) {
/* Current iter is exhausted, init new one */
msg->get_iter(msg, sio->write_offset, it, WRITE);
+
+ /* An empty refill would spin this loop forever */
+ if (WARN_ON(!iov_iter_count(it))) {
+ clear_and_wake_up_bit(PCS_MSG_BUSY, &msg->flags);
+ return -PCS_ERR_NET_ABORT;
+ }
}
BUG_ON(iov_iter_count(it) > left);
--
Best regards,
Konstantin Khorenko,
Virtuozzo Linux Kernel Team
On 9/30/26 09:42, no-reply@sashiko.virtuozzo.com wrote:
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> - [Medium] fs/fuse kio: do_sockio_sendmsg() can spin forever on an empty get_iter() refill
> - [Low] fs/fuse kio: iov_iter_sendpages_ok() under-checks pages of unaligned multi-page bvecs
> --
>
> commit 2b6fc40ff00b50732f7cf6408214e55943ba4135
> Author: Liu Kui <kui.liu@virtuozzo.com>
>
> fs/fuse kio: refactor transmit path in pcs_sockio_send()
>
> This patch replaces do_send_one_seg() with do_sockio_sendmsg(). The new
> function submits the whole iov_iter in one sock_sendmsg() call, and uses
> MSG_SPLICE_PAGES when all pages qualify. It also clears PCS_MSG_BUSY in
> the transport whenever IO on the msg ends.
>
> Link: https://virtuozzo.atlassian.net/browse/VSTOR-142167
>
>> diff --git a/fs/fuse/kio/pcs/pcs_sock_io.c b/fs/fuse/kio/pcs/pcs_sock_io.c
>> --- a/fs/fuse/kio/pcs/pcs_sock_io.c
>> +++ b/fs/fuse/kio/pcs/pcs_sock_io.c
> [ ... ]
>
>> +static bool iov_iter_sendpages_ok(struct iov_iter *it)
>> +{
>> + const struct bio_vec *bvec = it->bvec;
>> +
>> + if (!iov_iter_is_bvec(it))
>> + return false;
>> +
>> + while (bvec < (it->bvec + it->nr_segs)) {
>> + if (!sendpages_ok(bvec->bv_page, bvec->bv_len, bvec->bv_offset))
>> + return false;
>> + bvec++;
>> + }
>> +
>> + return true;
>> +}
> [Severity: Low]
> Can iov_iter_sendpages_ok() skip the last page of a multi-page bvec that
> has an unaligned bv_offset?
>
> sendpages_ok() starts at page + (offset >> PAGE_SHIFT) and steps
> PAGE_SIZE for len bytes. For example, with bv_offset=2048 and
> bv_len=4096 the range covers two pages, but only one is checked with
> sendpage_ok().
>
> The bvec builder in pcs_krpc.c (umem branch) sets bv_offset to a
> non-page-aligned value and bv_len up to the contiguous length, so such
> bvecs look possible:
>
> bvec->bv_offset = offset;
> ...
> bvec->bv_len = len < contig ? len : contig;
>
> If that happens, MSG_SPLICE_PAGES could be set in do_sockio_sendmsg()
> while the unchecked page is a slab page or has a zero refcount. That
> page would then be spliced into the socket by reference instead of being
> copied. Is the page check meant to cover the pages that the byte range
> actually touches?
>
>> +static int do_sockio_sendmsg(struct pcs_sockio *sio, struct pcs_msg *msg)
>> +{
> [ ... ]
>
>> + while (sio->write_offset < msg->size) {
>> + size_t left = msg->size - sio->write_offset;
>> + struct msghdr sockmsg = {.msg_flags = flags};
>> +
>> + TRACE("sio(%px) offset:%d msg:%px left:%zu, it->len:%zu\n", sio, sio->write_offset, msg, left, iov_iter_count(it));
>> +
>> + if (!iov_iter_count(it)) {
>> + /* Current iter is exhausted, init new one */
>> + msg->get_iter(msg, sio->write_offset, it, WRITE);
>> + }
>> +
>> + BUG_ON(iov_iter_count(it) > left);
> [ ... ]
>
>> + while (iov_iter_count(it)) {
> [Severity: Medium]
> Can do_sockio_sendmsg() spin forever here if msg->get_iter() returns an
> iterator with a zero count while sio->write_offset < msg->size?
>
> In that case the BUG_ON(iov_iter_count(it) > left) check passes, the
> inner while (iov_iter_count(it)) loop is skipped, and sock_sendmsg() is
> never called. WARN_ON(msg_data_left(&sockmsg)) sees zero and does not
> fire. write_offset does not change, so the outer loop repeats with the
> same state and no exit, which would be a soft lockup.
>
> The old do_send_one_seg() returned 0 for an empty segment, and
> pcs_sockio_send() turned that into a WARN_ON and sio_abort(). I did not
> find a get_iter() callback that returns an empty iterator for an
> in-range offset (krpc_msg_get_data() and pcs_get_iter_inline() look
> fine), so this looks latent. Would it make sense to add a check that
> fails the message when get_iter() returns an empty iterator?
>
>> + int ret;
> [ ... ]
>
> -- Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930-sockio-refactor-8-1-v1-0-806348b9eb26@virtuozzo.com?part=2
>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [PATCH hci-8.0 9/9] block/block-copy: coalesce write-zeroes tasks #VSTOR-134085
2026-09-30 11:17 26% ` [PATCH hci-8.0 9/9] block/block-copy: coalesce write-zeroes tasks #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:34 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-30 11:34 UTC (permalink / raw)
To: Denis V. Lunev, svt-core; +Cc: andrey.drobyshev, den
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] block-copy: widened write-zeroes task blocks overlapping writers and rate limit for ~2 GiB
- [Low] block-copy: write_zeroes_fail trace reports advanced offset in chunked fallback
--
commit f1b3fcf0ac0c96a51feac391dbd24c727baced51
Author: Denis V. Lunev <den@openvz.org>
block/block-copy: coalesce write-zeroes tasks #VSTOR-134085
This patch lets a write-zeroes task in block-copy grow to about
BDRV_REQUEST_MAX_BYTES instead of being capped at block_copy_chunk_size().
It adds block_copy_widen_zero_area() and a new block_copy_write_zeroes().
The new function first asks the target for BDRV_REQ_NO_FALLBACK and falls
back to chunked writes on -ENOTSUP. The patch also caps the chunk size in
qemu-iotests 056.
> diff --git a/block/block-copy.c b/block/block-copy.c
> --- a/block/block-copy.c
> +++ b/block/block-copy.c
[ ... ]
> +static int64_t block_copy_widen_zero_area(BlockCopyState *s,
> + BlockCopyCallState *call_state,
> + int64_t offset, int64_t search_end,
> + int64_t bytes)
> +{
> + int64_t aligned = QEMU_ALIGN_DOWN(BDRV_REQUEST_MAX_BYTES,
> + s->cluster_size);
> + int64_t zero_chunk = MIN_NON_ZERO(MAX(aligned, s->cluster_size),
> + call_state->max_chunk);
[Severity: Medium]
Can one widened task now hold its BlockReq for too long?
With this limit, block_copy_task_create() can register a single BlockReq of
up to about 2 GiB through reqlist_init_req().
A copy-before-write guest write that overlaps the range waits in
reqlist_wait_one() until one bdrv_co_pwrite_zeroes() of that whole size
completes.
The job rate limit only charges a task after it has run. Pause and cancel
also take effect only between tasks, so they wait for the whole request too.
The only mitigation left is the existing max-chunk option. The commit
message admits this effect for test 056, but the limit is changed for all
users.
Is there a reason not to bound the task size by something smaller by
default?
[ ... ]
> +static int coroutine_fn GRAPH_RDLOCK
> +block_copy_write_zeroes(BlockCopyState *s, int64_t offset, int64_t bytes,
> + bool *error_is_read)
> +{
> + BdrvRequestFlags flags = s->write_flags & ~BDRV_REQ_WRITE_COMPRESSED;
> + int64_t chunk;
> + int ret = 0;
> +
> + if (qatomic_read(&s->zero_widen)) {
> + ret = bdrv_co_pwrite_zeroes(s->target, offset, bytes,
> + flags | BDRV_REQ_NO_FALLBACK);
> + if (ret != -ENOTSUP) {
> + goto out;
> + }
[Severity: Medium]
Is this a problem for slow targets?
The first request covers the whole widened range in one call. If the
target refuses the fast path, for example file-posix on a failing
fallocate, the range is then rewritten in chunks by the while loop below.
Overlapping writers stay blocked on the task's BlockReq during both
passes. NO_FALLBACK is also only optimistic, as the commit message says,
so slow zeroing paths such as a block device or a network filesystem can
still be reached.
[ ... ]
> + offset += n;
> + bytes -= n;
> + }
> +
> +out:
> + if (ret < 0) {
> + trace_block_copy_write_zeroes_fail(s, offset, ret);
[Severity: Low]
This isn't a bug, but the offset passed to trace_block_copy_write_zeroes_fail()
differs between paths.
In the chunked fallback loop, offset is advanced by n after each
successful chunk. If a later chunk fails, the trace reports the advanced
offset. If the NO_FALLBACK request fails, it reports the task start.
The old code always reported the task offset. Would it be better to report
the same value on both paths, for example by using a separate variable for
the loop position?
[ ... ]
> diff --git a/tests/qemu-iotests/056 b/tests/qemu-iotests/056
> --- a/tests/qemu-iotests/056
> +++ b/tests/qemu-iotests/056
[ ... ]
> @@ -117,7 +117,8 @@ class TestCopyBeforeWrite(iotests.QMPTestCase):
> self.vm.pause_drive("drive0")
> self.vm.cmd('drive-backup', device='drive0',
> sync='full', target=target_img,
> - format="file", speed=1)
> + format="file", speed=1,
> + x_perf={'max-chunk': 65536})
[Severity: Medium]
The test has to cap the chunk size by hand to keep the job rate-limited.
Does this show that the throttling behavior changed for real users, with
the workaround applied only in the test?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930111739.3866427-1-den@openvz.org?part=9
^ permalink raw reply [relevance 3%]
* Re: [PATCH hci-8.0 8/9] block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085
2026-09-30 11:17 25% ` [PATCH hci-8.0 8/9] block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:33 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-30 11:33 UTC (permalink / raw)
To: Denis V. Lunev, svt-core; +Cc: den, andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] block: block_copy_calculate_zero_bitmap() can return count 0 forever, hanging the scan
- [Low] block/backup: commit message says full/bitmap but only sync=bitmap uses the new scan
--
commit 252a85c484652c4a95f8a77f49b08e8150b81585
Author: Denis V. Lunev <den@openvz.org>
block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085
This commit adds block_copy_calculate_zero_bitmap() and folds the
up-front source scan into a new backup_scan_source(). The scan now
pre-fills zero_bitmap for sync=bitmap as well as sync=top. sync=full
stays on block_copy_reset_unallocated(), and sync=none keeps the
per-task query.
> block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085
This isn't a code bug, but the subject line and body say that both FULL
and BITMAP get the new up-front scan. Two quoted portions of the commit
message read this way:
"Add block_copy_calculate_zero_bitmap(), driven one query at a time
from backup_scan_source() ..."
"FULL and BITMAP know their complete copy_bitmap before copying
starts"
In the code, sync=full still takes the block_copy_reset_unallocated()
path, and only sync=bitmap calls block_copy_calculate_zero_bitmap(). The
trailing paragraph of the message says so itself: "sync=full already
runs block_copy_reset_unallocated() ... so it stays on that path and only
sync=bitmap moves to block_copy_calculate_zero_bitmap()".
That leaves the message contradicting itself. It also carries the
downstream "hci-8.0:" tag. Could the subject and body be reworded to
match the code?
[Severity: Low]
> diff --git a/block/backup.c b/block/backup.c
> --- a/block/backup.c
> +++ b/block/backup.c
[ ... ]
> +static int coroutine_fn backup_scan_source(BackupBlockJob *s)
> {
[ ... ]
> + for (offset = 0; offset < s->len; offset += count) {
> + if (job_is_cancelled(job)) {
> + return -ECANCELED;
> + }
> +
> + job_pause_point(job);
[ ... ]
> + bdrv_graph_co_rdlock();
> + if (skip) {
> + ret = block_copy_reset_unallocated(s->bcs, offset, &count);
> + } else {
> + block_copy_calculate_zero_bitmap(s->bcs, offset, &cached_end,
> + &count);
> + }
> + bdrv_graph_co_rdunlock();
> diff --git a/block/block-copy.c b/block/block-copy.c
> --- a/block/block-copy.c
> +++ b/block/block-copy.c
[ ... ]
> + if (*cached_end < dirty_end) {
> + int64_t clusters;
> + int64_t query_offset = MAX(dirty_offset, *cached_end);
> + int ret = block_copy_is_cluster_allocated(s, query_offset, &clusters);
> +
> + if (ret >= 0) {
> + *cached_end = query_offset + clusters * s->cluster_size;
> + } else {
> + /* Best-effort: leave this area unmarked; it copies as data. */
> + *cached_end = dirty_end;
> + }
> + }
> +
> + /* 0 if the query above didn't yet reach dirty_end: try again next call. */
> + *count = *cached_end >= dirty_end ? dirty_end - offset : 0;
> +}
Can block_copy_calculate_zero_bitmap() return *count == 0 forever for
sync=bitmap?
block_copy_is_cluster_allocated() sets *pnum = DIV_ROUND_UP(total_count,
cluster_size). That is 0 when bdrv_co_block_status_above() returns
count == 0, for example at or beyond the source EOF. The function still
returns ret >= 0 in that case, since it has an explicit "count == 0"
branch.
If that happens, the ret >= 0 branch above sets
*cached_end = query_offset + 0 * s->cluster_size;
which is still below dirty_end. *count then stays 0.
In backup_scan_source(), "offset += count" does not advance, so the next
iteration repeats the identical query. job_pause_point() and
job_is_cancelled() do not yield unless the job is paused or cancelled.
That would leave the scan spinning without yielding in the job's
AioContext.
I could not confirm that s->len can exceed the real source length, for
example if the source shrinks during the job, so this may be hard to
reach.
The same "offset += count" shape already exists in
block_copy_reset_unallocated() for top/full. However, the sync=bitmap
path through block_copy_calculate_zero_bitmap() is new with this patch.
The later patch "block/block-copy: coalesce write-zeroes tasks" does not
touch this scan.
Would it make sense to force progress, for example by treating
clusters == 0 like the error path (cached_end = dirty_end), or by
clamping clusters to at least 1?
[Severity: Medium]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930111739.3866427-1-den@openvz.org?part=8
^ permalink raw reply [relevance 5%]
* Re: [PATCH hci-8.0 4/9] iotests/124: cover backup of zero clusters and holes #VSTOR-134085
@ 2026-09-30 11:31 5% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-30 11:31 UTC (permalink / raw)
To: svt-core, Denis V. Lunev; +Cc: andrey.drobyshev, den
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] iotests: 124 TAIL_ZERO tests expect behaviour that only appears in later patches
--
commit 147936f45e8ec1c124ea1a5b3dbf035c9a679b89
Author: Denis V. Lunev <den@openvz.org>
iotests/124: cover backup of zero clusters and holes #VSTOR-134085
This commit adds iotest 124 cases that back up sources mixing data,
write-zero and holes with sync=full, sync=top and sync=bitmap. It checks
the target layout with qemu-img map. It covers boundary, large-zero,
tail-cluster and mixed-cluster cases.
> diff --git a/tests/qemu-iotests/124 b/tests/qemu-iotests/124
> --- a/tests/qemu-iotests/124
> +++ b/tests/qemu-iotests/124
[ ... ]
> +# A zero run past the old block_copy_chunk_size() 16M cap.
> +LARGE_ZERO = Case(48 * MB, [
> + (0, MB, 'data'),
> + (4 * MB, 32 * MB, 'zero'),
> + (40 * MB, MB, 'data'),
> +])
[Severity: Medium]
The comment about the old block_copy_chunk_size() 16M cap refers to a
chunk-cap removal that only lands in the later patch "block/block-copy:
coalesce write-zeroes tasks". Is this comment accurate at this commit?
> +# Image size not a multiple of the cluster size: a partial tail cluster.
> +TAIL_SIZE = 4 * MB + 4096
> +TAIL_CLUSTER = (TAIL_SIZE // CLUSTER) * CLUSTER
[ ... ]
> +# The partial tail cluster falls out of zero_bitmap, so it copies as data.
> +TAIL_ZERO = Case(TAIL_SIZE, [
> + (0, MB, 'data'),
> + (2 * MB, TAIL_SIZE - 2 * MB, 'zero'),
> +], [
> + (0, MB, 'data'),
> + (2 * MB, TAIL_CLUSTER - 2 * MB, 'zero'),
> + (TAIL_CLUSTER, TAIL_SIZE - TAIL_CLUSTER, 'data'),
> +])
[Severity: Medium]
Can test_tail_zero and test_tail_zero_bitmap pass at this commit? The
expected 'data' extent at TAIL_CLUSTER appears to hold only after later
patches in the series.
block/block-copy.c has no zero_bitmap at this point, and git grep finds it
only in block/mirror.c. The zero_bitmap is added by the later patch
"block/block-copy: track known-zero source clusters", and
block_copy_mark_zero_prefix() there marks only
QEMU_ALIGN_DOWN(zero_count, cluster_size). The tail cluster then falls
outside the bitmap and is copied as data.
Here is what seems to happen at this commit. TAIL_ZERO writes zeroes from
2M to EOF on a 4M+4096 source. qcow2_co_pwrite_zeroes() skips tail handling
when offset+bytes equals the image end, so the whole partial last cluster
becomes a zero cluster on the source.
block_copy_block_status() then reports ZERO for the run and rounds it up to
the cluster size, because offset+num == s->len. After that,
block_copy_dirty_clusters() selects the write-zeroes method:
block/block-copy.c:block_copy_dirty_clusters() {
...
if (ret & BDRV_BLOCK_ZERO) {
task->method = COPY_WRITE_ZEROES;
}
...
}
block_copy_do_copy() calls bdrv_co_pwrite_zeroes() with nbytes clipped to
s->len. The request ends at the target EOF, so qcow2 again skips tail
handling.
The target would therefore report a zero extent up to EOF, while
assert_map() expects a 4096-byte 'data' extent at TAIL_CLUSTER. The
assert_map() comparison should fail at this commit and pass only at the end
of the series. That breaks bisectability.
The later patches that make the expectation true are "block/block-copy:
track known-zero source clusters" and "block/backup: pre-fill zero_bitmap
for full/bitmap". This was found by static reading and the tests were not
run.
Also, the commit message says the tests pin how backup treats zero clusters
and holes. Is that accurate for the TAIL_ZERO and LARGE_ZERO cases, which
depend on behaviour added later in the series?
Could these cases, or at least their comments and expectations, move into
the patches that introduce the behaviour?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930111739.3866427-1-den@openvz.org?part=4
^ permalink raw reply [relevance 5%]
* Re: [PATCH hci-8.0 7/9] block/block-copy: track known-zero source clusters #VSTOR-134085
2026-09-30 11:17 33% ` [PATCH hci-8.0 7/9] block/block-copy: track known-zero source clusters #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:30 6% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-30 11:30 UTC (permalink / raw)
To: svt-core, Denis V. Lunev; +Cc: den, andrey.drobyshev
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Low] block-copy: ctz32() on 64-bit cluster_size gives wrong zero_bitmap granularity
- [Low] block-copy: unaligned EOF tail cluster never marked zero in zero_bitmap
- [Low] block-copy: skip_unallocated doc stale; early return relies on implicit complete-scan invariant
--
commit 00cb83b3deb7d878b4202ae80e855cacc98ffc19
Author: Denis V. Lunev <den@openvz.org>
block/block-copy: track known-zero source clusters #VSTOR-134085
This patch caches the source's zero-ness in a new zero_bitmap, filled by
the block_copy_reset_unallocated() scan. The bitmap is published through
zero_bitmap_valid once the scan is done. After that,
block_copy_task_create() picks the copy method from the bitmap and ends
each task where the answer changes.
> diff --git a/block/block-copy.c b/block/block-copy.c
> --- a/block/block-copy.c
> +++ b/block/block-copy.c
[ ... ]
> @@ -688,50 +722,79 @@ int block_copy_block_status(BlockCopyState *s, int64_t offset, int64_t bytes,
> return ret;
> }
>
> +/* Only the scan allocates, and it runs before zero_bitmap_valid. */
> +static HBitmap *block_copy_zero_bitmap(BlockCopyState *s)
> +{
> + if (!s->zero_bitmap) {
> + s->zero_bitmap = hbitmap_alloc(s->len, ctz32(s->cluster_size));
[Severity: Low]
Is ctz32() the right helper here? s->cluster_size is an int64_t, and
block_copy_state_new() accepts any power-of-two min_cluster_size up to
INT64_MAX.
For a cluster_size of 2^33 or more, the low 32 bits are zero. In that
case ctz32() returns 32, so hbitmap_alloc() gets granularity 32. The
zero_bitmap then describes different cluster boundaries than copy_bitmap.
Those boundaries would feed hbitmap_get() and hbitmap_next_zero() in
block_copy_task_create(). Would ctz64() be more appropriate? This only
matters for a very large x-perf min-cluster-size, so it may be a niche
case.
> + }
> +
> + return s->zero_bitmap;
> +}
> +
> +static void block_copy_mark_zero_prefix(BlockCopyState *s, int64_t offset,
> + int64_t zero_count)
> +{
> + int64_t zero_bytes = QEMU_ALIGN_DOWN(zero_count, s->cluster_size);
> +
> + if (zero_bytes > 0) {
> + hbitmap_set(block_copy_zero_bitmap(s), offset, zero_bytes);
> + }
> +}
[Severity: Low]
When s->len is not a multiple of cluster_size and the tail reads as
zero, QEMU_ALIGN_DOWN(zero_count, s->cluster_size) drops the tail
cluster. Is that intended?
The old per-task path went through block_copy_block_status(). It aligned
the final chunk up to a full cluster and could return BDRV_BLOCK_ZERO for
it.
Once zero_bitmap_valid is set, block_copy_set_task_method() returns early
and never re-queries. The tail is then read and written as data instead of
using COPY_WRITE_ZEROES. This is only a missed optimization of one
cluster-sized copy per job.
[ ... ]
> @@ -791,6 +854,12 @@ block_copy_set_task_method(BlockCopyState *s, BlockCopyTask *task)
> int ret;
> int64_t status_bytes;
>
> + /* block_copy_task_create() already decided, from zero_bitmap. */
> + if (qatomic_load_acquire(&s->zero_bitmap_valid)) {
> + return true;
> + }
[Severity: Low]
This isn't a bug, but the early return skips the skip_unallocated check
and the block_copy_task_shrink() call. That is correct only because
backup_run() sets the flag after the full scan. The scan has already
cleared every unallocated cluster from copy_bitmap.
The comment on BlockCopyState.skip_unallocated still says:
* In this case, block_copy() will query the source's allocation status,
* skip unallocated regions, clear them in the copy_bitmap, and invoke
* block_copy_reset_unallocated() every time it does.
That comment was not updated for the zero_bitmap short-circuit.
Should the complete-scan requirement be documented on
block_copy_set_zero_bitmap_valid(), or asserted? Otherwise a later caller
could publish the flag without a full scan. Unallocated clusters would
then be copied silently.
> +
> + /* CBW filter could call this early. */
> ret = block_copy_block_status(s, task->req.offset, task->req.bytes,
> &status_bytes);
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930111739.3866427-1-den@openvz.org?part=7
^ permalink raw reply [relevance 6%]
* Re: [PATCH hci-8.0 3/9] block/commit: batch block-status queries #VSTOR-134085
2026-09-30 11:17 18% ` [PATCH hci-8.0 3/9] block/commit: batch block-status queries #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:21 3% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-30 11:21 UTC (permalink / raw)
To: svt-core, Denis V. Lunev; +Cc: andrey.drobyshev, den
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] block/commit: unallocated zero span is still capped at COMMIT_ZERO_CHUNK despite comment
--
commit 1c010ab0f99a89e08cf4922150238581339cd7aa
Author: Denis V. Lunev <den@openvz.org>
block/commit: batch block-status queries #VSTOR-134085
This patch changes commit_iteration() to query block status for the whole
remainder of the image and cache the answer in a CommitStatus owned by
commit_run(). Copying of allocated data stays bounded by
COMMIT_BUFFER_SIZE. Zero spans are bounded by the new COMMIT_ZERO_CHUNK.
> diff --git a/block/commit.c b/block/commit.c
> index a2e807288b23..80b89e6269d8 100644
> --- a/block/commit.c
> +++ b/block/commit.c
[ ... ]
> @@ -129,26 +144,46 @@ static void commit_clean(Job *job)
[ ... ]
> + ret = st->ret;
> + bytes = st->end - offset;
>
> + /* An unallocated span costs no I/O, so it is crossed in one step. */
> + if (ret & BDRV_BLOCK_ZERO) {
> + bytes = MIN(bytes, COMMIT_ZERO_CHUNK);
> + } else if (ret & BDRV_BLOCK_ALLOCATED) {
> + bytes = MIN(bytes, COMMIT_BUFFER_SIZE);
> }
[Severity: Low]
This isn't a bug, but the comment and the commit message don't match what
the code does. The comment says "An unallocated span costs no I/O, so it is
crossed in one step", and the commit message says "an unallocated span is
crossed in one step".
Can a span that reports BDRV_BLOCK_ZERO without BDRV_BLOCK_ALLOCATED still
be capped at COMMIT_ZERO_CHUNK here?
bdrv_co_do_block_status() in block/io.c returns BDRV_BLOCK_ZERO without
BDRV_BLOCK_ALLOCATED in two cases:
- A supports_backing layer that has no cow_bs.
- BDRV_WANT_PRECISE mode with offset at or beyond the backing file size.
This can happen when top is larger than the backing of base_overlay.
For these spans, commit_iteration() takes the first branch:
if (ret & BDRV_BLOCK_ZERO) {
bytes = MIN(bytes, COMMIT_ZERO_CHUNK);
The later "if (ret & BDRV_BLOCK_ALLOCATED)" test is false, so no I/O is
done, but the span is still split into 256M steps. Each extra step goes
through block_job_ratelimit_sleep(), the job_is_cancelled() check and
job_progress_update().
Only the fall-through case, where neither flag is set, is crossed in one
step. Should the cap apply only when both BDRV_BLOCK_ZERO and
BDRV_BLOCK_ALLOCATED are set, or should the comment and commit message be
adjusted to match the current behaviour?
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930111739.3866427-1-den@openvz.org?part=3
^ permalink raw reply [relevance 3%]
* [PATCH hci-8.0 9/9] block/block-copy: coalesce write-zeroes tasks #VSTOR-134085
` (7 preceding siblings ...)
2026-09-30 11:17 25% ` [PATCH hci-8.0 8/9] block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:17 26% ` Denis V. Lunev
2026-09-30 11:34 3% ` no-reply
8 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
Task size was capped to block_copy_chunk_size() regardless of method,
which sizes a task by the copy buffer. A write-zeroes task carries no
buffer, so that cap only splits one long known-zero run into a crowd of
small COPY_WRITE_ZEROES tasks, each with its own request against the
target.
block_copy_task_create() already decides from zero_bitmap how far a task
may run, so add block_copy_widen_zero_area() to that decision: for a
write-zeroes task, re-search the dirty area under BDRV_REQUEST_MAX_BYTES
instead of the buffer chunk size, and let the existing clamp cut it back
to where the zero run ends. That bound is INT_MAX rounded down to a
sector, so nothing has aligned it to cluster_size and it is aligned down
at the use site. Widening is safe: an overlapping caller waits on the
task's BlockReq via reqlist_wait_one() rather than observing it
mid-flight.
A request that large is only cheap where the target zeroes by metadata.
BDRV_REQ_NO_FALLBACK in supported_zero_flags rules out the targets which
cannot, qcow2 v2 and iscsi among them, but it is optimistic for the rest:
file-posix advertises it at open and only learns from a failing
fallocate. So a write-zeroes request asks for it while the target is
believed to oblige, which costs nothing when it does. A refusal ends the
widening, and the range is written in buffer-sized chunks, as is any
task widened before the refusal came. -ENOTSUP says the fast path is
unavailable, not that the target wrote nothing, so the range is
rewritten whole: zeroes over zeroes change nothing.
hci-8.0: 6b1a4754713 made the 056 copy-before-write source read as
zeroes. One widened write-zeroes task now covers all of it, and the
rate limit only charges a task after it ran, so speed=1 no longer
keeps the job alive until the test cancels it. Cap the task with
x-perf max-chunk there.
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
block/block-copy.c | 94 ++++++++++++++++++++++++++++++++++++++----
tests/qemu-iotests/056 | 3 +-
2 files changed, 89 insertions(+), 8 deletions(-)
diff --git a/block/block-copy.c b/block/block-copy.c
index a35fba45183..082257da7be 100644
--- a/block/block-copy.c
+++ b/block/block-copy.c
@@ -161,6 +161,8 @@ typedef struct BlockCopyState {
/* State fields that use a thread-safe API */
BdrvDirtyBitmap *copy_bitmap;
bool top_mode;
+ /* Whether a write-zeroes task may widen; see block_copy_write_zeroes(). */
+ bool zero_widen; /* atomic */
/* Clusters reading as zero; allocated on demand, frozen once valid. */
HBitmap *zero_bitmap;
/* Published only once the scan is done. */
@@ -189,6 +191,34 @@ static int64_t block_copy_chunk_size(BlockCopyState *s)
}
}
+/*
+ * A write-zeroes task carries no buffer, so it may cover far more than
+ * block_copy_chunk_size(). Return how far it may run; the caller clamps it
+ * to where the zero run ends.
+ */
+static int64_t block_copy_widen_zero_area(BlockCopyState *s,
+ BlockCopyCallState *call_state,
+ int64_t offset, int64_t search_end,
+ int64_t bytes)
+{
+ int64_t aligned = QEMU_ALIGN_DOWN(BDRV_REQUEST_MAX_BYTES,
+ s->cluster_size);
+ int64_t zero_chunk = MIN_NON_ZERO(MAX(aligned, s->cluster_size),
+ call_state->max_chunk);
+ int64_t wide_offset, wide_bytes;
+
+ if (!bdrv_dirty_bitmap_next_dirty_area(s->copy_bitmap, offset, search_end,
+ zero_chunk, &wide_offset,
+ &wide_bytes)) {
+ return bytes;
+ }
+
+ /* @offset is dirty, so the search cannot have moved past it. */
+ assert(wide_offset == offset);
+
+ return wide_bytes;
+}
+
/*
* Search for the first dirty area in offset/bytes range and create task at
* the beginning of it.
@@ -200,6 +230,7 @@ block_copy_task_create(BlockCopyState *s, BlockCopyCallState *call_state,
BlockCopyTask *task;
BlockCopyMethod method;
int64_t max_chunk;
+ int64_t search_end = offset + bytes;
QEMU_LOCK_GUARD(&s->lock);
max_chunk = MIN_NON_ZERO(block_copy_chunk_size(s), call_state->max_chunk);
@@ -221,6 +252,10 @@ block_copy_task_create(BlockCopyState *s, BlockCopyCallState *call_state,
if (hbitmap_get(s->zero_bitmap, offset)) {
method = COPY_WRITE_ZEROES;
+ if (qatomic_read(&s->zero_widen)) {
+ bytes = block_copy_widen_zero_area(s, call_state, offset,
+ search_end, bytes);
+ }
boundary = hbitmap_next_zero(s->zero_bitmap, offset, bytes);
} else {
boundary = hbitmap_next_dirty(s->zero_bitmap, offset, bytes);
@@ -481,6 +516,7 @@ BlockCopyState *block_copy_state_new(BdrvChild *source, BdrvChild *target,
.max_transfer = QEMU_ALIGN_DOWN(
block_copy_max_transfer(source, target),
cluster_size),
+ .zero_widen = target->bs->supported_zero_flags & BDRV_REQ_NO_FALLBACK,
};
s->discard_source = discard_source;
@@ -537,6 +573,56 @@ static coroutine_fn int block_copy_task_run(AioTaskPool *pool,
return 0;
}
+/*
+ * Widening a write-zeroes request only pays off where the target zeroes by
+ * metadata, so while the target is believed to oblige, a request asks to fail
+ * instead of falling back to writing the zeroes out. That costs nothing when
+ * it does oblige, and a refusal ends the widening for the rest of the run.
+ */
+static int coroutine_fn GRAPH_RDLOCK
+block_copy_write_zeroes(BlockCopyState *s, int64_t offset, int64_t bytes,
+ bool *error_is_read)
+{
+ BdrvRequestFlags flags = s->write_flags & ~BDRV_REQ_WRITE_COMPRESSED;
+ int64_t chunk;
+ int ret = 0;
+
+ if (qatomic_read(&s->zero_widen)) {
+ ret = bdrv_co_pwrite_zeroes(s->target, offset, bytes,
+ flags | BDRV_REQ_NO_FALLBACK);
+ if (ret != -ENOTSUP) {
+ goto out;
+ }
+
+ /* Redoing the range is safe: zeroes over zeroes change nothing. */
+ qatomic_set(&s->zero_widen, false);
+ }
+
+ WITH_QEMU_LOCK_GUARD(&s->lock) {
+ chunk = block_copy_chunk_size(s);
+ }
+
+ while (bytes) {
+ int64_t n = MIN(bytes, chunk);
+
+ ret = bdrv_co_pwrite_zeroes(s->target, offset, n, flags);
+ if (ret < 0) {
+ break;
+ }
+
+ offset += n;
+ bytes -= n;
+ }
+
+out:
+ if (ret < 0) {
+ trace_block_copy_write_zeroes_fail(s, offset, ret);
+ *error_is_read = false;
+ }
+
+ return ret;
+}
+
/*
* block_copy_do_copy
*
@@ -568,13 +654,7 @@ block_copy_do_copy(BlockCopyState *s, int64_t offset, int64_t bytes,
switch (*method) {
case COPY_WRITE_ZEROES:
- ret = bdrv_co_pwrite_zeroes(s->target, offset, nbytes, s->write_flags &
- ~BDRV_REQ_WRITE_COMPRESSED);
- if (ret < 0) {
- trace_block_copy_write_zeroes_fail(s, offset, ret);
- *error_is_read = false;
- }
- return ret;
+ return block_copy_write_zeroes(s, offset, nbytes, error_is_read);
case COPY_RANGE_SMALL:
case COPY_RANGE_FULL:
diff --git a/tests/qemu-iotests/056 b/tests/qemu-iotests/056
index e006d1296d9..72ea28365ea 100755
--- a/tests/qemu-iotests/056
+++ b/tests/qemu-iotests/056
@@ -117,7 +117,8 @@ class TestCopyBeforeWrite(iotests.QMPTestCase):
self.vm.pause_drive("drive0")
self.vm.cmd('drive-backup', device='drive0',
sync='full', target=target_img,
- format="file", speed=1)
+ format="file", speed=1,
+ x_perf={'max-chunk': 65536})
self.vm.cmd('block-job-pause', device="drive0")
# Speed is low enough that this must be an uncopied range, which will
# trigger the before write notifier
--
2.53.0
^ permalink raw reply [relevance 26%]
* [PATCH hci-8.0 8/9] block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085
` (6 preceding siblings ...)
2026-09-30 11:17 33% ` [PATCH hci-8.0 7/9] block/block-copy: track known-zero source clusters #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:17 25% ` Denis V. Lunev
2026-09-30 11:33 5% ` no-reply
2026-09-30 11:17 26% ` [PATCH hci-8.0 9/9] block/block-copy: coalesce write-zeroes tasks #VSTOR-134085 Denis V. Lunev
8 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
Only sync=top runs an up-front scan, so the previous patch did nothing
for the other modes. FULL and BITMAP know their complete copy_bitmap
before copying starts, and the same copy-before-write guarantee holds:
a cluster still dirty when the copy loop reaches it has not been written
since the scan.
Add block_copy_calculate_zero_bitmap(), driven one query at a time from
backup_scan_source() so the graph rdlock and the pause point are taken
per query, and query each confirmed span once however many dirty areas
it covers. The scan only fills zero_bitmap and never clears copy_bitmap,
so sync=full still writes zeroes for clusters the source never
allocated; it just stops re-querying to find that out. Unlike the
per-task queries it replaces the scan is serial, so a sparsely dirtied
job against a slow source may spend more time here than it saves. The
sync=top loop had the same shape, so both fold into
backup_scan_source().
sync=none keeps the per-task query. Its bitmap says "anything may be
copied", not "this will be copied", and it only ever copies what the
guest writes during the fleecing window, so scanning the whole image at
attach is the wrong trade.
hci-8.0: sync=full already runs block_copy_reset_unallocated() since
6b1a4754713, so it stays on that path and only sync=bitmap moves to
block_copy_calculate_zero_bitmap(). Unallocated clusters are not
written for sync=full here.
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
block/backup.c | 95 +++++++++++++++++++++++---------------
block/block-copy.c | 41 ++++++++++++++++
include/block/block-copy.h | 4 ++
3 files changed, 104 insertions(+), 36 deletions(-)
diff --git a/block/backup.c b/block/backup.c
index 03ad1ad350b..eef4512b8bb 100644
--- a/block/backup.c
+++ b/block/backup.c
@@ -250,60 +250,83 @@ static void backup_init_bcs_bitmap(BackupBlockJob *job)
job_progress_set_remaining(&job->common.job, estimate);
}
-static int coroutine_fn backup_run(Job *job, Error **errp)
+/*
+ * Walk the source before copying starts: fill zero_bitmap and, for sync=top
+ * and sync=full, drop the clusters that are not allocated.
+ */
+static int coroutine_fn backup_scan_source(BackupBlockJob *s)
{
- BackupBlockJob *s = container_of(job, BackupBlockJob, common.job);
- int ret;
-
- backup_init_bcs_bitmap(s);
-
- if (s->sync_mode == MIRROR_SYNC_MODE_TOP ||
- s->sync_mode == MIRROR_SYNC_MODE_FULL)
- {
- int64_t offset = 0;
- int64_t count;
+ Job *job = &s->common.job;
+ bool skip = s->sync_mode == MIRROR_SYNC_MODE_TOP ||
+ s->sync_mode == MIRROR_SYNC_MODE_FULL;
+ int64_t offset, cached_end = 0, count;
+ int ret = 0;
- for (offset = 0; offset < s->len; ) {
- if (job_is_cancelled(job)) {
- return -ECANCELED;
- }
+ for (offset = 0; offset < s->len; offset += count) {
+ if (job_is_cancelled(job)) {
+ return -ECANCELED;
+ }
- job_pause_point(job);
+ job_pause_point(job);
- if (job_is_cancelled(job)) {
- return -ECANCELED;
- }
+ if (job_is_cancelled(job)) {
+ return -ECANCELED;
+ }
- /* rdlock protects the subsequent call to bdrv_is_allocated() */
- bdrv_graph_co_rdlock();
+ /* rdlock protects the block-status queries below */
+ bdrv_graph_co_rdlock();
+ if (skip) {
ret = block_copy_reset_unallocated(s->bcs, offset, &count);
- bdrv_graph_co_rdunlock();
- if (ret < 0) {
- return ret;
- }
+ } else {
+ block_copy_calculate_zero_bitmap(s->bcs, offset, &cached_end,
+ &count);
+ }
+ bdrv_graph_co_rdunlock();
- offset += count;
+ if (ret < 0) {
+ return ret;
}
- block_copy_set_zero_bitmap_valid(s->bcs);
}
- if (s->sync_mode == MIRROR_SYNC_MODE_NONE) {
+ block_copy_set_zero_bitmap_valid(s->bcs);
+
+ return 0;
+}
+
+static int coroutine_fn backup_run(Job *job, Error **errp)
+{
+ BackupBlockJob *s = container_of(job, BackupBlockJob, common.job);
+ int ret;
+
+ backup_init_bcs_bitmap(s);
+
+ switch (s->sync_mode) {
+ case MIRROR_SYNC_MODE_TOP:
+ case MIRROR_SYNC_MODE_FULL:
+ case MIRROR_SYNC_MODE_BITMAP:
+ ret = backup_scan_source(s);
+ if (ret < 0) {
+ return ret;
+ }
+ break;
+
+ case MIRROR_SYNC_MODE_NONE:
/*
* All bits are set in bcs bitmap to allow any cluster to be copied.
- * This does not actually require them to be copied.
+ * This does not actually require them to be copied. Yield until the
+ * job is cancelled and let the before_write notify callback service
+ * CoW requests.
*/
while (!job_is_cancelled(job)) {
- /*
- * Yield until the job is cancelled. We just let our before_write
- * notify callback service CoW requests.
- */
job_yield(job);
}
- } else {
- return backup_loop(s);
+ return 0;
+
+ default:
+ g_assert_not_reached();
}
- return 0;
+ return backup_loop(s);
}
static void coroutine_fn backup_pause(Job *job)
diff --git a/block/block-copy.c b/block/block-copy.c
index 27a5d1a41b2..a35fba45183 100644
--- a/block/block-copy.c
+++ b/block/block-copy.c
@@ -843,6 +843,47 @@ int64_t coroutine_fn block_copy_reset_unallocated(BlockCopyState *s,
return ret;
}
+/*
+ * One scan step, like block_copy_reset_unallocated(); @cached_end
+ * tracks progress across calls, @count how far @offset should move.
+ */
+void coroutine_fn GRAPH_RDLOCK
+block_copy_calculate_zero_bitmap(BlockCopyState *s, int64_t offset,
+ int64_t *cached_end, int64_t *count)
+{
+ int64_t dirty_offset, dirty_bytes, dirty_end;
+ bool found;
+
+ /* CBW mutates copy_bitmap from its own AioContext meanwhile. */
+ WITH_QEMU_LOCK_GUARD(&s->lock) {
+ found = bdrv_dirty_bitmap_next_dirty_area(s->copy_bitmap, offset,
+ s->len, INT64_MAX,
+ &dirty_offset, &dirty_bytes);
+ }
+
+ if (!found) {
+ *count = s->len - offset;
+ return;
+ }
+ dirty_end = dirty_offset + dirty_bytes;
+
+ if (*cached_end < dirty_end) {
+ int64_t clusters;
+ int64_t query_offset = MAX(dirty_offset, *cached_end);
+ int ret = block_copy_is_cluster_allocated(s, query_offset, &clusters);
+
+ if (ret >= 0) {
+ *cached_end = query_offset + clusters * s->cluster_size;
+ } else {
+ /* Best-effort: leave this area unmarked; it copies as data. */
+ *cached_end = dirty_end;
+ }
+ }
+
+ /* 0 if the query above didn't yet reach dirty_end: try again next call. */
+ *count = *cached_end >= dirty_end ? dirty_end - offset : 0;
+}
+
/*
* Decide how @task is copied: COPY_WRITE_ZEROES if it reads as zero. May
* shrink @task. Returns false if @task is to be skipped (already ended,
diff --git a/include/block/block-copy.h b/include/block/block-copy.h
index 57e13f82f78..40dd5432735 100644
--- a/include/block/block-copy.h
+++ b/include/block/block-copy.h
@@ -43,6 +43,10 @@ void block_copy_reset(BlockCopyState *s, int64_t offset, int64_t bytes);
int64_t coroutine_fn GRAPH_RDLOCK
block_copy_reset_unallocated(BlockCopyState *s, int64_t offset, int64_t *count);
+void coroutine_fn GRAPH_RDLOCK
+block_copy_calculate_zero_bitmap(BlockCopyState *s, int64_t offset,
+ int64_t *cached_end, int64_t *count);
+
int coroutine_fn block_copy(BlockCopyState *s, int64_t offset, int64_t bytes,
bool ignore_ratelimit, uint64_t timeout_ns,
BlockCopyAsyncCallbackFunc cb,
--
2.53.0
^ permalink raw reply [relevance 25%]
* [PATCH hci-8.0 2/9] iotests/040: cover large and fragmented commit runs #VSTOR-134085
2026-09-30 11:17 21% ` [PATCH hci-8.0 1/9] block/commit: pass BDRV_WANT_PRECISE to block-status #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:17 3% ` Denis V. Lunev
2026-09-30 11:17 18% ` [PATCH hci-8.0 3/9] block/commit: batch block-status queries #VSTOR-134085 Denis V. Lunev
` (6 subsequent siblings)
8 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
040 never commits runs long enough to span more than one block-status
query, so the answer that commit_iteration() is about to start caching
goes untested.
Add two cases on a base <- mid <- active chain, committing mid so the
job takes the regular commit path rather than active commit, and compare
base against a snapshot of mid taken before the commit. One case mixes
multi-megabyte data, zero and hole runs, the other fragments them down
to single clusters with every transition off the 512K boundary.
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
tests/qemu-iotests/040 | 102 ++++++++++++++++++++++++++++++++++++-
tests/qemu-iotests/040.out | 4 +-
2 files changed, 103 insertions(+), 3 deletions(-)
diff --git a/tests/qemu-iotests/040 b/tests/qemu-iotests/040
index 5c18e413ec9..e01c9385bae 100755
--- a/tests/qemu-iotests/040
+++ b/tests/qemu-iotests/040
@@ -25,13 +25,14 @@
import time
import os
import iotests
-from iotests import qemu_img, qemu_io
+from iotests import qemu_img, qemu_img_create, qemu_io, compare_images
import struct
import errno
backing_img = os.path.join(iotests.test_dir, 'backing.img')
mid_img = os.path.join(iotests.test_dir, 'mid.img')
test_img = os.path.join(iotests.test_dir, 'test.img')
+reference_img = os.path.join(iotests.test_dir, 'reference.img')
class ImageCommitTestCase(iotests.QMPTestCase):
'''Abstract base class for image commit test cases'''
@@ -951,6 +952,105 @@ class TestCommitWithOverriddenBacking(iotests.QMPTestCase):
self.vm.qmp('block-job-complete', device='commit')
self.vm.event_wait('BLOCK_JOB_COMPLETED')
+class TestCommitLargeRuns(iotests.QMPTestCase):
+ """Commit runs long enough to cross commit_iteration()'s cached span."""
+
+ MB = 1024 * 1024
+ CLUSTER = 64 * 1024
+
+ # Runs several COMMIT_BUFFER_SIZE (512K) chunks long, of every kind.
+ SIZE = 32 * MB
+ LAYOUT = [
+ (0, 4 * MB, 'data'),
+ (4 * MB, 6 * MB, 'hole'),
+ (10 * MB, 4 * MB, 'data'),
+ (14 * MB, 6 * MB, 'zero'),
+ (20 * MB, 4 * MB, 'data'),
+ (24 * MB, 8 * MB, 'hole'),
+ ]
+
+ # The same, with every transition off the 512K boundary.
+ SIZE_FRAGMENTED = 384 * CLUSTER # 24M
+ LAYOUT_FRAGMENTED = [
+ (0, 45 * CLUSTER, 'data'),
+ (45 * CLUSTER, 55 * CLUSTER, 'hole'),
+ (100 * CLUSTER, CLUSTER, 'data'),
+ (101 * CLUSTER, 49 * CLUSTER, 'zero'),
+ (150 * CLUSTER, 80 * CLUSTER, 'data'),
+ (230 * CLUSTER, CLUSTER, 'hole'),
+ (231 * CLUSTER, 69 * CLUSTER, 'data'),
+ (300 * CLUSTER, 83 * CLUSTER, 'zero'),
+ (383 * CLUSTER, CLUSTER, 'hole'),
+ ]
+
+ def setUp(self):
+ self.vm = iotests.VM()
+ self.vm.launch()
+
+ def tearDown(self):
+ self.vm.shutdown()
+ for img in (backing_img, mid_img, test_img, reference_img):
+ if os.path.exists(img):
+ os.remove(img)
+
+ def build_images(self, layout, size):
+ # A pattern of its own in base, so a misplaced cluster shows up.
+ qemu_img_create('-f', iotests.imgfmt, backing_img, str(size))
+ qemu_io('-c', f'write -P 0x11 0 {size}', backing_img)
+
+ qemu_img_create('-f', iotests.imgfmt, '-b', backing_img, '-F',
+ iotests.imgfmt, mid_img, str(size))
+ for offset, length, kind in layout:
+ if kind == 'data':
+ qemu_io('-c', f'write -P 0x22 {offset} {length}', mid_img)
+ elif kind == 'zero':
+ qemu_io('-c', f'write -z {offset} {length}', mid_img)
+
+ # What base must equal once mid is committed into it.
+ qemu_img('convert', '-f', iotests.imgfmt, '-O', iotests.imgfmt,
+ mid_img, reference_img)
+
+ # An empty layer above mid, so top_node=mid is not the active one.
+ qemu_img_create('-f', iotests.imgfmt, '-b', mid_img, '-F',
+ iotests.imgfmt, test_img, str(size))
+
+ self.vm.cmd('blockdev-add', {
+ 'node-name': 'base',
+ 'driver': iotests.imgfmt,
+ 'file': {'driver': 'file', 'filename': backing_img},
+ })
+ self.vm.cmd('blockdev-add', {
+ 'node-name': 'mid',
+ 'driver': iotests.imgfmt,
+ 'file': {'driver': 'file', 'filename': mid_img},
+ 'backing': 'base',
+ })
+ self.vm.cmd('blockdev-add', {
+ 'node-name': 'active',
+ 'driver': iotests.imgfmt,
+ 'file': {'driver': 'file', 'filename': test_img},
+ 'backing': 'mid',
+ })
+
+ def commit_and_verify(self):
+ self.vm.cmd('block-commit', job_id='commit0', device='active',
+ top_node='mid', base_node='base')
+ self.wait_until_completed(drive='commit0')
+
+ self.vm.cmd('blockdev-del', node_name='active')
+ self.vm.cmd('blockdev-del', node_name='mid')
+ self.vm.cmd('blockdev-del', node_name='base')
+ self.assertTrue(compare_images(reference_img, backing_img))
+
+ def test_commit_large_runs(self):
+ self.build_images(self.LAYOUT, self.SIZE)
+ self.commit_and_verify()
+
+ def test_commit_fragmented_runs(self):
+ self.build_images(self.LAYOUT_FRAGMENTED, self.SIZE_FRAGMENTED)
+ self.commit_and_verify()
+
+
if __name__ == '__main__':
iotests.main(supported_fmts=['qcow2', 'qed'],
supported_protocols=['file'])
diff --git a/tests/qemu-iotests/040.out b/tests/qemu-iotests/040.out
index 1bb1dc5f0ea..d2e2a2d98f6 100644
--- a/tests/qemu-iotests/040.out
+++ b/tests/qemu-iotests/040.out
@@ -1,5 +1,5 @@
-.................................................................
+...................................................................
----------------------------------------------------------------------
-Ran 65 tests
+Ran 67 tests
OK
--
2.53.0
^ permalink raw reply [relevance 3%]
* [PATCH hci-8.0 7/9] block/block-copy: track known-zero source clusters #VSTOR-134085
` (5 preceding siblings ...)
2026-09-30 11:17 23% ` [PATCH hci-8.0 6/9] block/block-copy: extract block_copy_set_task_method() #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:17 33% ` Denis V. Lunev
2026-09-30 11:30 6% ` no-reply
2026-09-30 11:17 25% ` [PATCH hci-8.0 8/9] block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085 Denis V. Lunev
2026-09-30 11:17 26% ` [PATCH hci-8.0 9/9] block/block-copy: coalesce write-zeroes tasks #VSTOR-134085 Denis V. Lunev
8 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
block_copy_reset_unallocated()'s up-front scan already queries the
source, and the same query reports zero-ness. The copy loop re-queries
per task and, having no answer up front, can only size a task by the
copy buffer, so a mostly-zero image is cut into thousands of pieces
that each turn out to read as zero.
Cache the answer in zero_bitmap, published by zero_bitmap_valid once
the scan is done. block_copy_task_create() then picks the method from
the bitmap and ends the task where the answer changes, so a zero task
never reaches into data and a copy task never swallows a zero run. The
per-task query stays as the fallback until the flag is set, since CBW
intercepts guest writes while the scan is still running.
The scan and the query it replaces must resolve BDRV_BLOCK_ZERO against
the same part of the chain, so the scan moves from bdrv_co_is_allocated()
to bdrv_co_block_status_above() and the base selection is factored out
into block_copy_status_base().
zero_bitmap is a plain HBitmap, not a BdrvDirtyBitmap: an internal cache
has no business in query-named-block-nodes. One writer and readers gated
by the flag need no mutex, but the publish needs ordering, hence
store-release and load-acquire. It is allocated on demand, so sync=none
and a standalone copy-before-write filter do not pay for it.
hci-8.0: 6b1a4754713 ("backup: skip copying unallocated for full
mode") scans for sync=full too and leaves skip_unallocated set after
the scan, choosing the base from top_mode instead. So
block_copy_status_base() keys on top_mode, and replaces the base
block_copy_is_cluster_allocated() computed on its own.
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
block/backup.c | 1 +
block/block-copy.c | 117 ++++++++++++++++++++++++++++++-------
include/block/block-copy.h | 1 +
3 files changed, 98 insertions(+), 21 deletions(-)
diff --git a/block/backup.c b/block/backup.c
index f7d8421812a..03ad1ad350b 100644
--- a/block/backup.c
+++ b/block/backup.c
@@ -284,6 +284,7 @@ static int coroutine_fn backup_run(Job *job, Error **errp)
offset += count;
}
+ block_copy_set_zero_bitmap_valid(s->bcs);
}
if (s->sync_mode == MIRROR_SYNC_MODE_NONE) {
diff --git a/block/block-copy.c b/block/block-copy.c
index e210688b1df..27a5d1a41b2 100644
--- a/block/block-copy.c
+++ b/block/block-copy.c
@@ -20,6 +20,8 @@
#include "block/block_int-io.h"
#include "block/dirty-bitmap.h"
#include "block/reqlist.h"
+#include "qemu/hbitmap.h"
+#include "qemu/host-utils.h"
#include "system/block-backend.h"
#include "qemu/units.h"
#include "qemu/co-shared-resource.h"
@@ -159,7 +161,10 @@ typedef struct BlockCopyState {
/* State fields that use a thread-safe API */
BdrvDirtyBitmap *copy_bitmap;
bool top_mode;
-
+ /* Clusters reading as zero; allocated on demand, frozen once valid. */
+ HBitmap *zero_bitmap;
+ /* Published only once the scan is done. */
+ bool zero_bitmap_valid; /* atomic, store-release/load-acquire */
ProgressMeter *progress;
SharedResource *mem;
RateLimit rate_limit;
@@ -193,6 +198,7 @@ block_copy_task_create(BlockCopyState *s, BlockCopyCallState *call_state,
int64_t offset, int64_t bytes)
{
BlockCopyTask *task;
+ BlockCopyMethod method;
int64_t max_chunk;
QEMU_LOCK_GUARD(&s->lock);
@@ -204,6 +210,27 @@ block_copy_task_create(BlockCopyState *s, BlockCopyCallState *call_state,
return NULL;
}
+ method = s->method;
+
+ /*
+ * The scan already knows how this range reads: pick the method here and
+ * stop the task where the answer changes.
+ */
+ if (qatomic_load_acquire(&s->zero_bitmap_valid)) {
+ int64_t boundary;
+
+ if (hbitmap_get(s->zero_bitmap, offset)) {
+ method = COPY_WRITE_ZEROES;
+ boundary = hbitmap_next_zero(s->zero_bitmap, offset, bytes);
+ } else {
+ boundary = hbitmap_next_dirty(s->zero_bitmap, offset, bytes);
+ }
+
+ if (boundary >= 0) {
+ bytes = boundary - offset;
+ }
+ }
+
assert(QEMU_IS_ALIGNED(offset, s->cluster_size));
bytes = QEMU_ALIGN_UP(bytes, s->cluster_size);
@@ -218,7 +245,7 @@ block_copy_task_create(BlockCopyState *s, BlockCopyCallState *call_state,
.task.func = block_copy_task_entry,
.s = s,
.call_state = call_state,
- .method = s->method,
+ .method = method,
};
reqlist_init_req(&s->reqs, &task->req, offset, bytes);
@@ -274,6 +301,9 @@ void block_copy_state_free(BlockCopyState *s)
ratelimit_destroy(&s->rate_limit);
bdrv_release_dirty_bitmap(s->copy_bitmap);
+ if (s->zero_bitmap) {
+ hbitmap_free(s->zero_bitmap);
+ }
shres_destroy(s->mem);
g_free(s);
}
@@ -655,20 +685,24 @@ static coroutine_fn int block_copy_task_entry(AioTask *task)
return ret;
}
+/* The scan and the per-task query must resolve BDRV_BLOCK_ZERO alike. */
+static GRAPH_RDLOCK BlockDriverState *block_copy_status_base(BlockCopyState *s)
+{
+ if (qatomic_read(&s->top_mode)) {
+ return bdrv_backing_chain_next(s->source->bs);
+ }
+
+ return NULL;
+}
+
static coroutine_fn GRAPH_RDLOCK
int block_copy_block_status(BlockCopyState *s, int64_t offset, int64_t bytes,
int64_t *pnum)
{
int64_t num;
- BlockDriverState *base;
+ BlockDriverState *base = block_copy_status_base(s);
int ret;
- if (qatomic_read(&s->top_mode)) {
- base = bdrv_backing_chain_next(s->source->bs);
- } else {
- base = NULL;
- }
-
ret = bdrv_co_block_status_above(s->source->bs, base, offset, bytes, &num,
NULL, NULL);
if (ret < 0 || num < s->cluster_size) {
@@ -688,50 +722,79 @@ int block_copy_block_status(BlockCopyState *s, int64_t offset, int64_t bytes,
return ret;
}
+/* Only the scan allocates, and it runs before zero_bitmap_valid. */
+static HBitmap *block_copy_zero_bitmap(BlockCopyState *s)
+{
+ if (!s->zero_bitmap) {
+ s->zero_bitmap = hbitmap_alloc(s->len, ctz32(s->cluster_size));
+ }
+
+ return s->zero_bitmap;
+}
+
+static void block_copy_mark_zero_prefix(BlockCopyState *s, int64_t offset,
+ int64_t zero_count)
+{
+ int64_t zero_bytes = QEMU_ALIGN_DOWN(zero_count, s->cluster_size);
+
+ if (zero_bytes > 0) {
+ hbitmap_set(block_copy_zero_bitmap(s), offset, zero_bytes);
+ }
+}
+
/*
* Check if the cluster starting at offset is allocated or not.
* return via pnum the number of contiguous clusters sharing this allocation.
+ * Also marks the zero prefix of the range in zero_bitmap.
*/
static int coroutine_fn GRAPH_RDLOCK
block_copy_is_cluster_allocated(BlockCopyState *s, int64_t offset,
int64_t *pnum)
{
BlockDriverState *bs = s->source->bs;
- BlockDriverState *base;
+ BlockDriverState *base = block_copy_status_base(s);
+ int64_t orig_offset = offset;
int64_t count, total_count = 0;
+ int64_t zero_count = 0;
+ bool zero_broken = false;
int64_t bytes = s->len - offset;
int ret;
- if (s->top_mode && s->source->bs->backing) {
- base = s->source->bs->backing->bs;
- } else {
- base = NULL;
- }
-
assert(QEMU_IS_ALIGNED(offset, s->cluster_size));
while (true) {
/* protected in backup_run() */
- ret = bdrv_co_is_allocated_above(bs, base, false, offset, bytes,
- &count);
+ ret = bdrv_co_block_status_above(bs, base, offset, bytes, &count,
+ NULL, NULL);
if (ret < 0) {
return ret;
}
+ if (!zero_broken) {
+ if (ret & BDRV_BLOCK_ZERO) {
+ zero_count += count;
+ } else {
+ zero_broken = true;
+ }
+ }
+
total_count += count;
- if (ret || count == 0) {
+ if ((ret & BDRV_BLOCK_ALLOCATED) || count == 0) {
/*
- * ret: partial segment(s) are considered allocated.
+ * BDRV_BLOCK_ALLOCATED: partial segment(s) are considered
+ * allocated.
* otherwise: unallocated tail is treated as an entire segment.
*/
*pnum = DIV_ROUND_UP(total_count, s->cluster_size);
- return ret;
+ block_copy_mark_zero_prefix(s, orig_offset, zero_count);
+ return !!(ret & BDRV_BLOCK_ALLOCATED);
}
/* Unallocated segment(s) with uncertain following segment(s) */
if (total_count >= s->cluster_size) {
*pnum = total_count / s->cluster_size;
+ block_copy_mark_zero_prefix(s, orig_offset, zero_count);
return 0;
}
@@ -791,6 +854,12 @@ block_copy_set_task_method(BlockCopyState *s, BlockCopyTask *task)
int ret;
int64_t status_bytes;
+ /* block_copy_task_create() already decided, from zero_bitmap. */
+ if (qatomic_load_acquire(&s->zero_bitmap_valid)) {
+ return true;
+ }
+
+ /* CBW filter could call this early. */
ret = block_copy_block_status(s, task->req.offset, task->req.bytes,
&status_bytes);
assert(ret >= 0); /* never fail */
@@ -1122,6 +1191,12 @@ void block_copy_set_options(BlockCopyState *s, bool skip_unallocated,
qatomic_set(&s->top_mode, top_mode);
}
+void block_copy_set_zero_bitmap_valid(BlockCopyState *s)
+{
+ block_copy_zero_bitmap(s);
+ qatomic_store_release(&s->zero_bitmap_valid, true);
+}
+
void block_copy_set_speed(BlockCopyState *s, uint64_t speed)
{
ratelimit_set_speed(&s->rate_limit, speed, BLOCK_COPY_SLICE_TIME);
diff --git a/include/block/block-copy.h b/include/block/block-copy.h
index d9304d1cf56..57e13f82f78 100644
--- a/include/block/block-copy.h
+++ b/include/block/block-copy.h
@@ -102,5 +102,6 @@ int64_t block_copy_cluster_size(BlockCopyState *s);
void block_copy_set_options(BlockCopyState *s, bool skip_unallocated,
bool top_mode);
void block_copy_set_global_error(BlockCopyState *s, int error);
+void block_copy_set_zero_bitmap_valid(BlockCopyState *s);
#endif /* BLOCK_COPY_H */
--
2.53.0
^ permalink raw reply [relevance 33%]
* [PATCH hci-8.0 5/9] block/block-copy: don't reserve memory for zero tasks #VSTOR-134085
` (3 preceding siblings ...)
@ 2026-09-30 11:17 25% ` Denis V. Lunev
2026-09-30 11:17 23% ` [PATCH hci-8.0 6/9] block/block-copy: extract block_copy_set_task_method() #VSTOR-134085 Denis V. Lunev
` (3 subsequent siblings)
8 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
block_copy_dirty_clusters() charges task->req.bytes to the shared memory
pool for every task, but a COPY_WRITE_ZEROES task allocates no bounce
buffer, so there is nothing to account for. Add
block_copy_task_shres_bytes() and use it at all three call sites.
Zero tasks lose the throttling the pool gave them incidentally and are
bounded only by BLOCK_COPY_MAX_WORKERS (64).
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
block/block-copy.c | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/block/block-copy.c b/block/block-copy.c
index e68d3d8b141..aecbe0c16ab 100644
--- a/block/block-copy.c
+++ b/block/block-copy.c
@@ -470,6 +470,11 @@ void block_copy_set_progress_meter(BlockCopyState *s, ProgressMeter *pm)
s->progress = pm;
}
+static uint64_t block_copy_task_shres_bytes(BlockCopyTask *task)
+{
+ return task->method == COPY_WRITE_ZEROES ? 0 : task->req.bytes;
+}
+
/*
* Takes ownership of @task
*
@@ -491,7 +496,7 @@ static coroutine_fn int block_copy_task_run(AioTaskPool *pool,
aio_task_pool_wait_slot(pool);
if (aio_task_pool_status(pool) < 0) {
- co_put_to_shres(task->s->mem, task->req.bytes);
+ co_put_to_shres(task->s->mem, block_copy_task_shres_bytes(task));
block_copy_task_end(task, -ECANCELED);
g_free(task);
return -ECANCELED;
@@ -636,7 +641,7 @@ static coroutine_fn int block_copy_task_entry(AioTask *task)
progress_work_done(s->progress, t->req.bytes);
}
}
- co_put_to_shres(s->mem, t->req.bytes);
+ co_put_to_shres(s->mem, block_copy_task_shres_bytes(t));
block_copy_task_end(t, ret);
if (s->discard_source && ret == 0) {
@@ -855,7 +860,7 @@ block_copy_dirty_clusters(BlockCopyCallState *call_state)
trace_block_copy_process(s, task->req.offset);
- co_get_from_shres(s->mem, task->req.bytes);
+ co_get_from_shres(s->mem, block_copy_task_shres_bytes(task));
offset = task_end(task);
bytes = end - offset;
--
2.53.0
^ permalink raw reply [relevance 25%]
* [PATCH hci-8.0 6/9] block/block-copy: extract block_copy_set_task_method() #VSTOR-134085
` (4 preceding siblings ...)
2026-09-30 11:17 25% ` [PATCH hci-8.0 5/9] block/block-copy: don't reserve memory for zero tasks #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:17 23% ` Denis V. Lunev
2026-09-30 11:17 33% ` [PATCH hci-8.0 7/9] block/block-copy: track known-zero source clusters #VSTOR-134085 Denis V. Lunev
` (2 subsequent siblings)
8 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
Move the per-task block-status query and its skip_unallocated and
BDRV_BLOCK_ZERO handling out of block_copy_dirty_clusters() into
block_copy_set_task_method(). No behavior change. The next patch
changes that decision, and having it in one place keeps that change to
just the new logic.
The block-status result no longer lands in the caller's @ret, so drop
the note saying @ret may be positive at the out: label.
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
block/block-copy.c | 46 ++++++++++++++++++++++++++++++----------------
1 file changed, 30 insertions(+), 16 deletions(-)
diff --git a/block/block-copy.c b/block/block-copy.c
index aecbe0c16ab..e210688b1df 100644
--- a/block/block-copy.c
+++ b/block/block-copy.c
@@ -780,6 +780,35 @@ int64_t coroutine_fn block_copy_reset_unallocated(BlockCopyState *s,
return ret;
}
+/*
+ * Decide how @task is copied: COPY_WRITE_ZEROES if it reads as zero. May
+ * shrink @task. Returns false if @task is to be skipped (already ended,
+ * not freed).
+ */
+static bool coroutine_fn GRAPH_RDLOCK
+block_copy_set_task_method(BlockCopyState *s, BlockCopyTask *task)
+{
+ int ret;
+ int64_t status_bytes;
+
+ ret = block_copy_block_status(s, task->req.offset, task->req.bytes,
+ &status_bytes);
+ assert(ret >= 0); /* never fail */
+ if (status_bytes < task->req.bytes) {
+ block_copy_task_shrink(task, status_bytes);
+ }
+ if (qatomic_read(&s->skip_unallocated) && !(ret & BDRV_BLOCK_ALLOCATED)) {
+ block_copy_task_end(task, 0);
+ trace_block_copy_skip_range(s, task->req.offset, task->req.bytes);
+ return false;
+ }
+ if (ret & BDRV_BLOCK_ZERO) {
+ task->method = COPY_WRITE_ZEROES;
+ }
+
+ return true;
+}
+
/*
* block_copy_dirty_clusters
*
@@ -812,7 +841,6 @@ block_copy_dirty_clusters(BlockCopyCallState *call_state)
while (bytes && aio_task_pool_status(aio) == 0 &&
!qatomic_read(&call_state->cancelled)) {
BlockCopyTask *task;
- int64_t status_bytes;
task = block_copy_task_create(s, call_state, offset, bytes);
if (!task) {
@@ -826,24 +854,12 @@ block_copy_dirty_clusters(BlockCopyCallState *call_state)
found_dirty = true;
- ret = block_copy_block_status(s, task->req.offset, task->req.bytes,
- &status_bytes);
- assert(ret >= 0); /* never fail */
- if (status_bytes < task->req.bytes) {
- block_copy_task_shrink(task, status_bytes);
- }
- if (qatomic_read(&s->skip_unallocated) &&
- !(ret & BDRV_BLOCK_ALLOCATED)) {
- block_copy_task_end(task, 0);
- trace_block_copy_skip_range(s, task->req.offset, task->req.bytes);
+ if (!block_copy_set_task_method(s, task)) {
offset = task_end(task);
bytes = end - offset;
g_free(task);
continue;
}
- if (ret & BDRV_BLOCK_ZERO) {
- task->method = COPY_WRITE_ZEROES;
- }
if (!call_state->ignore_ratelimit) {
uint64_t ns = ratelimit_calculate_delay(&s->rate_limit, 0);
@@ -884,8 +900,6 @@ out:
* block_copy_task_run. If it fails, it means some task already failed
* for real reason, let's return first failure.
* Still, assert that we don't rewrite failure by success.
- *
- * Note: ret may be positive here because of block-status result.
*/
assert(ret >= 0 || aio_task_pool_status(aio) < 0);
ret = aio_task_pool_status(aio);
--
2.53.0
^ permalink raw reply [relevance 23%]
* [PATCH hci-8.0 1/9] block/commit: pass BDRV_WANT_PRECISE to block-status #VSTOR-134085
@ 2026-09-30 11:17 21% ` Denis V. Lunev
2026-09-30 11:17 3% ` [PATCH hci-8.0 2/9] iotests/040: cover large and fragmented commit runs #VSTOR-134085 Denis V. Lunev
` (7 subsequent siblings)
8 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
c33159dec790 replaced the want_zero bool with a bitmask of BDRV_WANT_*
flags. block/commit.c was missed and still passes "true", which is
BDRV_BLOCK_DATA, not BDRV_WANT_PRECISE. Convert it like the other
callers.
Fixes: c33159dec790 ("block: Expand block status mode from bool to flags")
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
CC: Eric Blake <eblake@redhat.com>
---
block/commit.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/block/commit.c b/block/commit.c
index 0d9e1a16d7a..a2e807288b2 100644
--- a/block/commit.c
+++ b/block/commit.c
@@ -139,8 +139,8 @@ static int commit_iteration(CommitBlockJob *s, int64_t offset,
/* Copy if allocated above the base */
WITH_GRAPH_RDLOCK_GUARD() {
ret = bdrv_co_common_block_status_above(blk_bs(s->top),
- s->base_overlay, true, true, offset, COMMIT_BUFFER_SIZE,
- &bytes, NULL, NULL, NULL);
+ s->base_overlay, true, BDRV_WANT_PRECISE, offset,
+ COMMIT_BUFFER_SIZE, &bytes, NULL, NULL, NULL);
}
trace_commit_one_iteration(s, offset, bytes, ret);
--
2.53.0
^ permalink raw reply [relevance 21%]
* [PATCH hci-8.0 3/9] block/commit: batch block-status queries #VSTOR-134085
2026-09-30 11:17 21% ` [PATCH hci-8.0 1/9] block/commit: pass BDRV_WANT_PRECISE to block-status #VSTOR-134085 Denis V. Lunev
2026-09-30 11:17 3% ` [PATCH hci-8.0 2/9] iotests/040: cover large and fragmented commit runs #VSTOR-134085 Denis V. Lunev
@ 2026-09-30 11:17 18% ` Denis V. Lunev
2026-09-30 11:21 3% ` no-reply
` (5 subsequent siblings)
8 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-09-30 11:17 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
commit_iteration() asks for block status COMMIT_BUFFER_SIZE (512K) at a
time, so a long run above base pays one query per 512K for an answer
the whole run shares.
Query the remainder of the image instead and keep the answer in a
CommitStatus owned by commit_run(). No device or export writes top
above base_overlay while the job runs: they ask for
BLK_PERM_CONSISTENT_READ along with BLK_PERM_WRITE, and the job
withholds CONSISTENT_READ there, having to share WRITE or it would
block its own writes to base through the backing chain. A job target
asks for WRITE alone and is refused by the op blocker instead. From
filtered_base downwards CONSISTENT_READ is shared again, and there only
the job writes.
Copying is still bounded by the read buffer. Zeroes need no buffer, so
COMMIT_ZERO_CHUNK bounds them, keeping a cancel from waiting on a huge
write-zeroes, and an unallocated span is crossed in one step.
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
CC: John Snow <jsnow@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
block/commit.c | 59 ++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 48 insertions(+), 11 deletions(-)
diff --git a/block/commit.c b/block/commit.c
index a2e807288b2..80b89e6269d 100644
--- a/block/commit.c
+++ b/block/commit.c
@@ -31,8 +31,23 @@ enum {
* contiguous regions of the image is efficient.
*/
COMMIT_BUFFER_SIZE = 512 * 1024, /* in bytes */
+
+ /*
+ * Zeroes need no buffer, so they are bounded by this instead. It stays
+ * well under BDRV_REQUEST_MAX_BYTES and keeps a cancel from waiting on
+ * a multi-gigabyte write.
+ */
+ COMMIT_ZERO_CHUNK = 256 * 1024 * 1024, /* in bytes */
};
+/* Last block-status answer, covering [offset, end). Empty when equal. */
+typedef struct CommitStatus {
+ int64_t len;
+ int64_t offset;
+ int64_t end;
+ int ret;
+} CommitStatus;
+
typedef struct CommitBlockJob {
BlockJob common;
BlockDriverState *commit_top_bs;
@@ -129,26 +144,46 @@ static void commit_clean(Job *job)
}
static int commit_iteration(CommitBlockJob *s, int64_t offset,
- int64_t *requested_bytes, void *buf)
+ int64_t *requested_bytes, void *buf,
+ CommitStatus *st)
{
BlockErrorAction action;
- int64_t bytes = *requested_bytes;
+ int64_t bytes;
int ret = 0;
bool error_in_source = true;
- /* Copy if allocated above the base */
- WITH_GRAPH_RDLOCK_GUARD() {
- ret = bdrv_co_common_block_status_above(blk_bs(s->top),
- s->base_overlay, true, BDRV_WANT_PRECISE, offset,
- COMMIT_BUFFER_SIZE, &bytes, NULL, NULL, NULL);
+ assert(offset >= st->offset);
+
+ if (offset >= st->end) {
+ /* Copy if allocated above the base */
+ WITH_GRAPH_RDLOCK_GUARD() {
+ ret = bdrv_co_common_block_status_above(blk_bs(s->top),
+ s->base_overlay, true, BDRV_WANT_PRECISE, offset,
+ st->len - offset, &bytes, NULL, NULL, NULL);
+ }
+
+ if (ret < 0) {
+ trace_commit_one_iteration(s, offset, 0, ret);
+ goto fail;
+ }
+
+ st->offset = offset;
+ st->end = offset + bytes;
+ st->ret = ret;
}
- trace_commit_one_iteration(s, offset, bytes, ret);
+ ret = st->ret;
+ bytes = st->end - offset;
- if (ret < 0) {
- goto fail;
+ /* An unallocated span costs no I/O, so it is crossed in one step. */
+ if (ret & BDRV_BLOCK_ZERO) {
+ bytes = MIN(bytes, COMMIT_ZERO_CHUNK);
+ } else if (ret & BDRV_BLOCK_ALLOCATED) {
+ bytes = MIN(bytes, COMMIT_BUFFER_SIZE);
}
+ trace_commit_one_iteration(s, offset, bytes, ret);
+
if (ret & BDRV_BLOCK_ALLOCATED) {
if (ret & BDRV_BLOCK_ZERO) {
/*
@@ -214,6 +249,7 @@ static int coroutine_fn commit_run(Job *job, Error **errp)
int64_t n = 0; /* bytes */
QEMU_AUTO_VFREE void *buf = NULL;
int64_t len, base_len;
+ CommitStatus st = { 0 };
len = blk_co_getlength(s->top);
if (len < 0) {
@@ -234,6 +270,7 @@ static int coroutine_fn commit_run(Job *job, Error **errp)
}
buf = blk_blockalign(s->top, COMMIT_BUFFER_SIZE);
+ st.len = len;
for (offset = 0; offset < len; offset += n) {
/* Note that even when no rate limit is applied we need to yield
@@ -244,7 +281,7 @@ static int coroutine_fn commit_run(Job *job, Error **errp)
break;
}
- ret = commit_iteration(s, offset, &n, buf);
+ ret = commit_iteration(s, offset, &n, buf, &st);
if (ret < 0) {
return ret;
--
2.53.0
^ permalink raw reply [relevance 18%]
* Re: [Devel] [PATCH hci-8.1 2/3] fs/fuse kio: refactor transmit path in pcs_sockio_send()
@ 2026-09-30 7:42 3% ` no-reply
2026-09-30 16:29 0% ` Konstantin Khorenko
0 siblings, 1 reply; 200+ results
From: no-reply @ 2026-09-30 7:42 UTC (permalink / raw)
To: Liu Kui, Konstantin Khorenko; +Cc: devel, azaitsev
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] fs/fuse kio: do_sockio_sendmsg() can spin forever on an empty get_iter() refill
- [Low] fs/fuse kio: iov_iter_sendpages_ok() under-checks pages of unaligned multi-page bvecs
--
commit 2b6fc40ff00b50732f7cf6408214e55943ba4135
Author: Liu Kui <kui.liu@virtuozzo.com>
fs/fuse kio: refactor transmit path in pcs_sockio_send()
This patch replaces do_send_one_seg() with do_sockio_sendmsg(). The new
function submits the whole iov_iter in one sock_sendmsg() call, and uses
MSG_SPLICE_PAGES when all pages qualify. It also clears PCS_MSG_BUSY in
the transport whenever IO on the msg ends.
Link: https://virtuozzo.atlassian.net/browse/VSTOR-142167
> diff --git a/fs/fuse/kio/pcs/pcs_sock_io.c b/fs/fuse/kio/pcs/pcs_sock_io.c
> --- a/fs/fuse/kio/pcs/pcs_sock_io.c
> +++ b/fs/fuse/kio/pcs/pcs_sock_io.c
[ ... ]
> +static bool iov_iter_sendpages_ok(struct iov_iter *it)
> +{
> + const struct bio_vec *bvec = it->bvec;
> +
> + if (!iov_iter_is_bvec(it))
> + return false;
> +
> + while (bvec < (it->bvec + it->nr_segs)) {
> + if (!sendpages_ok(bvec->bv_page, bvec->bv_len, bvec->bv_offset))
> + return false;
> + bvec++;
> + }
> +
> + return true;
> +}
[Severity: Low]
Can iov_iter_sendpages_ok() skip the last page of a multi-page bvec that
has an unaligned bv_offset?
sendpages_ok() starts at page + (offset >> PAGE_SHIFT) and steps
PAGE_SIZE for len bytes. For example, with bv_offset=2048 and
bv_len=4096 the range covers two pages, but only one is checked with
sendpage_ok().
The bvec builder in pcs_krpc.c (umem branch) sets bv_offset to a
non-page-aligned value and bv_len up to the contiguous length, so such
bvecs look possible:
bvec->bv_offset = offset;
...
bvec->bv_len = len < contig ? len : contig;
If that happens, MSG_SPLICE_PAGES could be set in do_sockio_sendmsg()
while the unchecked page is a slab page or has a zero refcount. That
page would then be spliced into the socket by reference instead of being
copied. Is the page check meant to cover the pages that the byte range
actually touches?
> +static int do_sockio_sendmsg(struct pcs_sockio *sio, struct pcs_msg *msg)
> +{
[ ... ]
> + while (sio->write_offset < msg->size) {
> + size_t left = msg->size - sio->write_offset;
> + struct msghdr sockmsg = {.msg_flags = flags};
> +
> + TRACE("sio(%px) offset:%d msg:%px left:%zu, it->len:%zu\n", sio, sio->write_offset, msg, left, iov_iter_count(it));
> +
> + if (!iov_iter_count(it)) {
> + /* Current iter is exhausted, init new one */
> + msg->get_iter(msg, sio->write_offset, it, WRITE);
> + }
> +
> + BUG_ON(iov_iter_count(it) > left);
[ ... ]
> + while (iov_iter_count(it)) {
[Severity: Medium]
Can do_sockio_sendmsg() spin forever here if msg->get_iter() returns an
iterator with a zero count while sio->write_offset < msg->size?
In that case the BUG_ON(iov_iter_count(it) > left) check passes, the
inner while (iov_iter_count(it)) loop is skipped, and sock_sendmsg() is
never called. WARN_ON(msg_data_left(&sockmsg)) sees zero and does not
fire. write_offset does not change, so the outer loop repeats with the
same state and no exit, which would be a soft lockup.
The old do_send_one_seg() returned 0 for an empty segment, and
pcs_sockio_send() turned that into a WARN_ON and sio_abort(). I did not
find a get_iter() callback that returns an empty iterator for an
in-range offset (krpc_msg_get_data() and pcs_get_iter_inline() look
fine), so this looks latent. Would it make sense to add a check that
fails the message when get_iter() returns an empty iterator?
> + int ret;
[ ... ]
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930-sockio-refactor-8-1-v1-0-806348b9eb26@virtuozzo.com?part=2
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH hci-8.1 3/3] fs/fuse: use wait_var_event to wait for fuse file references
2026-09-30 5:44 2% ` [Devel] [PATCH hci-8.1 3/3] fs/fuse: use wait_var_event to wait for fuse file references Liu Kui
@ 2026-09-30 5:51 0% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-30 5:51 UTC (permalink / raw)
To: Konstantin Khorenko, Liu Kui; +Cc: devel, azaitsev
> On close_wait mounts, fuse_release_close_wait() needs to wait for all
> asynchronous references to the fuse file to be dropped before sending
> FUSE_RELEASE. This wait on fi->page_waitq was inherited from the past
> implementation and is no longer appropriate as the wait condition
> evolved:
>
> - fi->page_waitq is meant to serve page-under-writeback waiters and
> fuse_set_nowrite()'s writectr waiters; now it also has to serve the
> ff reference waiter, so every wakeup can wake unrelated waiters.
>
> - the wake is decoupled from the decrement, so every path dropping an
> ff reference must remember to wake fi->page_waitq, and because of
> this we grew two variants of the stock fuse_file_put():
> __fuse_file_put() and fuse_release_ff(). This is bug prone, and a
> misuse can leave the releaser blocked forever.
>
> Instead of waiting on fi->page_waitq, a simple wait_var_event() on
> &ff->count serves the purpose best: the matching wake_up_var() uses
> the address purely as a hash key and never dereferences it, so the
> wakeup can be folded into fuse_file_put() itself. With that, both
> __fuse_file_put() and fuse_release_ff() become redundant and are
> removed. This cleanup also makes the code less divergent from
> mainline.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-147878
>
> Signed-off-by: Liu Kui <kui.liu@virtuozzo.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.vzint.dev/#/patchset/20260930-close-wait-rework-v2-v1-0-f8062178544a@virtuozzo.com?part=3
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* [Devel] [PATCH hci-8.1 3/3] fs/fuse: use wait_var_event to wait for fuse file references
@ 2026-09-30 5:44 2% ` Liu Kui
2026-09-30 5:51 0% ` no-reply
0 siblings, 1 reply; 200+ results
From: Liu Kui @ 2026-09-30 5:44 UTC (permalink / raw)
To: Konstantin Khorenko; +Cc: azaitsev, devel, Liu Kui
On close_wait mounts, fuse_release_close_wait() needs to wait for all
asynchronous references to the fuse file to be dropped before sending
FUSE_RELEASE. This wait on fi->page_waitq was inherited from the past
implementation and is no longer appropriate as the wait condition
evolved:
- fi->page_waitq is meant to serve page-under-writeback waiters and
fuse_set_nowrite()'s writectr waiters; now it also has to serve the
ff reference waiter, so every wakeup can wake unrelated waiters.
- the wake is decoupled from the decrement, so every path dropping an
ff reference must remember to wake fi->page_waitq, and because of
this we grew two variants of the stock fuse_file_put():
__fuse_file_put() and fuse_release_ff(). This is bug prone, and a
misuse can leave the releaser blocked forever.
Instead of waiting on fi->page_waitq, a simple wait_var_event() on
&ff->count serves the purpose best: the matching wake_up_var() uses
the address purely as a hash key and never dereferences it, so the
wakeup can be folded into fuse_file_put() itself. With that, both
__fuse_file_put() and fuse_release_ff() become redundant and are
removed. This cleanup also makes the code less divergent from
mainline.
https://virtuozzo.atlassian.net/browse/VSTOR-147878
Signed-off-by: Liu Kui <kui.liu@virtuozzo.com>
---
fs/fuse/file.c | 69 +++++++++++++++++++++-----------------
fs/fuse/fuse_i.h | 2 +-
fs/fuse/kio/pcs/pcs_fuse_kdirect.c | 3 +-
3 files changed, 42 insertions(+), 32 deletions(-)
diff --git a/fs/fuse/file.c b/fs/fuse/file.c
index d7c9ea8a7c45..74c8d925e2fd 100644
--- a/fs/fuse/file.c
+++ b/fs/fuse/file.c
@@ -24,6 +24,7 @@
#include <linux/fiemap.h>
#include <linux/jhash.h>
#include <linux/file.h>
+#include <linux/wait_bit.h>
unsigned int hash_seed_r;
module_param(hash_seed_r, uint, 0644);
@@ -140,12 +141,23 @@ static void fuse_release_end(struct fuse_mount *fm, struct fuse_args *args,
kfree(ra);
}
-static void fuse_file_put(struct fuse_file *ff, bool sync)
+void fuse_file_put(struct fuse_file *ff, bool sync)
{
+ /* read before the drop: ff may not be touched after a non-final one */
+ bool close_wait = ff->fm->fc->close_wait;
+
if (refcount_dec_and_test(&ff->count)) {
struct fuse_release_args *ra = &ff->args->release_args;
struct fuse_args *args = (ra ? &ra->args : NULL);
+ /*
+ * On close_wait the final put must come from a synchronous
+ * caller: fuse_release() waits for ff->count == 1 while
+ * holding its own reference, so an asynchronous completion
+ * can never drop the last one.
+ */
+ WARN_ON(!sync && close_wait);
+
if (ra && ra->inode)
fuse_file_io_release(ff, ra->inode);
@@ -165,14 +177,25 @@ static void fuse_file_put(struct fuse_file *ff, bool sync)
fuse_release_end(ff->fm, args, -ENOTCONN);
}
kfree(ff);
+ return;
}
-}
-static void __fuse_file_put(struct fuse_file *ff)
-{
- if (refcount_dec_and_test(&ff->count))
- BUG();
+ /*
+ * ff may already be freed by a racing final put -- don't touch it
+ * past this point.
+ */
+ if (close_wait) {
+ /*
+ * Pairs with wait_var_event() in fuse_release_close_wait():
+ * the wake_up_var() contract requires a full barrier between
+ * the counter update and the wakeup, and refcount_dec()
+ * alone is only release-ordered.
+ */
+ smp_mb__after_atomic();
+ wake_up_var(&ff->count);
+ }
}
+EXPORT_SYMBOL_GPL(fuse_file_put);
struct fuse_file *fuse_file_open(struct fuse_mount *fm, u64 nodeid,
unsigned int open_flags, bool isdir)
@@ -557,9 +580,10 @@ static void fuse_release_close_wait(struct inode *inode, struct file *file)
/*
* Wait until we are the last one holding a reference, so that all
* in-flight requests on this file have finished and FUSE_RELEASE
- * is sent by the releaser itself.
+ * is sent by the releaser itself. Woken by the wake_up_var() in
+ * fuse_file_put().
*/
- wait_event(fi->page_waitq, refcount_read(&ff->count) == 1);
+ wait_var_event(&ff->count, refcount_read(&ff->count) == 1);
inode_lock(inode);
fuse_sync_writes(inode);
@@ -1252,22 +1276,6 @@ static int fuse_read_folio(struct file *file, struct folio *folio)
return err;
}
-void fuse_release_ff(struct inode *inode, struct fuse_file *ff)
-{
- if (ff) {
- if (ff->fm->fc->close_wait) {
- struct fuse_inode *fi = get_fuse_inode(inode);
- spin_lock(&fi->lock);
- __fuse_file_put(ff);
- wake_up(&fi->page_waitq);
- spin_unlock(&fi->lock);
- } else {
- fuse_file_put(ff, false);
- }
- }
-}
-EXPORT_SYMBOL_GPL(fuse_release_ff);
-
void fuse_revoke_readpages(struct fuse_file *ff)
{
struct fuse_io_args *ia;
@@ -1330,7 +1338,7 @@ static void fuse_readpages_end(struct fuse_mount *fm, struct fuse_args *args,
fuse_invalidate_atime(inode);
if (ff)
- fuse_release_ff(inode, ff);
+ fuse_file_put(ff, false);
fuse_io_free(ia);
}
@@ -2189,7 +2197,7 @@ static void fuse_writepage_free(struct fuse_writepage_args *wpa)
for (i = 0; i < ap->num_pages; i++)
__free_page(ap->pages[i]);
- fuse_release_ff(wpa->inode, wpa->ia.ff);
+ fuse_file_put(wpa->ia.ff, false);
kfree(ap->pages);
kfree(wpa);
@@ -2572,7 +2580,7 @@ static int fuse_writepage_locked(struct folio *folio)
return 0;
err_writepage_args:
- fuse_release_ff(inode, ff);
+ fuse_file_put(ff, false);
err_nofile:
folio_put(tmp_folio);
err:
@@ -2859,7 +2867,8 @@ static int fuse_writepages(struct address_space *mapping,
WARN_ON(!data.wpa->ia.ap.num_pages);
fuse_writepages_send(&data);
}
- fuse_release_ff(inode, data.ff);
+ if (data.ff)
+ fuse_file_put(data.ff, false);
kfree(data.orig_pages);
out:
@@ -3929,7 +3938,7 @@ static int fuse_request_fiemap(struct inode *inode, u32 cur_max,
ap.pages = fuse_pages_alloc(fc->max_pages, GFP_KERNEL, &ap.descs);
if (!ap.pages) {
- fuse_release_ff(inode, ff);
+ fuse_file_put(ff, false);
return -ENOMEM;
}
@@ -4021,7 +4030,7 @@ static int fuse_request_fiemap(struct inode *inode, u32 cur_max,
ap.pages[allocated] = NULL;
}
kfree(ap.pages);
- fuse_release_ff(inode, ff);
+ fuse_file_put(ff, false);
return err;
}
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index f6da643b9c86..e94d40458af3 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1673,7 +1673,7 @@ int fuse_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo,
struct fuse_file *__fuse_write_file_get(struct fuse_inode *fi);
struct fuse_file *fuse_file_get(struct fuse_file *ff);
-void fuse_release_ff(struct inode *inode, struct fuse_file *ff);
+void fuse_file_put(struct fuse_file *ff, bool sync);
struct fuse_req *fuse_dev_find_request(unsigned int fd, u64 unique);
void fuse_revoke_readpages(struct fuse_file *ff);
diff --git a/fs/fuse/kio/pcs/pcs_fuse_kdirect.c b/fs/fuse/kio/pcs/pcs_fuse_kdirect.c
index efbab75654c8..5064198a7b4c 100644
--- a/fs/fuse/kio/pcs/pcs_fuse_kdirect.c
+++ b/fs/fuse/kio/pcs/pcs_fuse_kdirect.c
@@ -728,7 +728,8 @@ static int submit_size_grow(struct inode *inode, unsigned long long size)
args.out_args[0].value = &outarg;
err = fuse_simple_request(fm, &args);
- fuse_release_ff(inode, ff);
+ if (ff)
+ fuse_file_put(ff, false);
return err;
--
Git-155)
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 2%]
* Re: [Devel] [PATCH hci-8.1] ms/selftests: net: reduce txtimestamp deschedule flakes
2026-09-29 12:48 6% [Devel] [PATCH hci-8.1] ms/selftests: net: reduce txtimestamp deschedule flakes Eva Kurchatova
@ 2026-09-29 12:52 0% ` no-reply
0 siblings, 0 replies; 200+ results
From: no-reply @ 2026-09-29 12:52 UTC (permalink / raw)
To: Eva Kurchatova, khorenko; +Cc: devel
> From: Willem de Bruijn <willemb@google.com>
>
> This test occasionally fails due to exceeding timing bounds, as
> run in continuous testing on netdev.bots:
>
> https://netdev.bots.linux.dev/contest.html?test=txtimestamp-sh
>
> A common pattern is a single elevated delay between USR and SND.
>
> # 8.36 [+0.00] test SND
> # 8.36 [+0.00] USR: 1767864384 s 240994 us (seq=0, len=0)
> # 8.44 [+0.08] ERROR: 18461 us expected between 10000 and 18000
> # 8.44 [+0.00] SND: 1767864384 s 259455 us (seq=42, len=10) (USR +18460 us)
> # 8.52 [+0.07] SND: 1767864384 s 339523 us (seq=42, len=10) (USR +10005 us)
> # 8.52 [+0.00] USR: 1767864384 s 409580 us (seq=0, len=0)
> [ ... ]
> (cherry picked from commit c65182ef9df6bb96fd85b56a2bcdd18d64c4d3b5)
>
> https://virtuozzo.atlassian.net/browse/VSTOR-147735
> Feature: fix selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260929124822.770587-1-eva.kurchatova@virtuozzo.com?part=1
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* [Devel] [PATCH hci-8.1] ms/selftests: net: reduce txtimestamp deschedule flakes
@ 2026-09-29 12:48 6% Eva Kurchatova
2026-09-29 12:52 0% ` no-reply
0 siblings, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-09-29 12:48 UTC (permalink / raw)
To: khorenko; +Cc: devel
From: Willem de Bruijn <willemb@google.com>
This test occasionally fails due to exceeding timing bounds, as
run in continuous testing on netdev.bots:
https://netdev.bots.linux.dev/contest.html?test=txtimestamp-sh
A common pattern is a single elevated delay between USR and SND.
# 8.36 [+0.00] test SND
# 8.36 [+0.00] USR: 1767864384 s 240994 us (seq=0, len=0)
# 8.44 [+0.08] ERROR: 18461 us expected between 10000 and 18000
# 8.44 [+0.00] SND: 1767864384 s 259455 us (seq=42, len=10) (USR +18460 us)
# 8.52 [+0.07] SND: 1767864384 s 339523 us (seq=42, len=10) (USR +10005 us)
# 8.52 [+0.00] USR: 1767864384 s 409580 us (seq=0, len=0)
# 8.60 [+0.08] SND: 1767864384 s 419586 us (seq=42, len=10) (USR +10005 us)
# 8.60 [+0.00] USR: 1767864384 s 489645 us (seq=0, len=0)
# 8.68 [+0.08] SND: 1767864384 s 499651 us (seq=42, len=10) (USR +10005 us)
# 8.68 [+0.00] USR-SND: count=4, avg=12119 us, min=10005 us, max=18460 us
(Note that other delays are nowhere near the large 8ms tolerance.)
One hypothesis is that the task is descheduled between taking the USR
timestamp and sending the packet. Possibly in printing.
Delay taking the timestamp closer to sendmsg, and delay printing until
after sendmsg.
With this change, failure rate is significantly lower in current runs.
Link: https://lore.kernel.org/netdev/20260107110521.1aab55e9@kernel.org/
Suggested-by: Jakub Kicinski <kuba@kernel.org>
Signed-off-by: Willem de Bruijn <willemb@google.com>
Link: https://patch.msgid.link/20260112163355.3510150-1-willemdebruijn.kernel@gmail.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
(cherry picked from commit c65182ef9df6bb96fd85b56a2bcdd18d64c4d3b5)
https://virtuozzo.atlassian.net/browse/VSTOR-147735
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/net/txtimestamp.c | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/tools/testing/selftests/net/txtimestamp.c b/tools/testing/selftests/net/txtimestamp.c
index dae91eb97d69..f13615654970 100644
--- a/tools/testing/selftests/net/txtimestamp.c
+++ b/tools/testing/selftests/net/txtimestamp.c
@@ -206,12 +206,10 @@ static void __print_timestamp(const char *name, struct timespec *cur,
fprintf(stderr, "\n");
}
-static void print_timestamp_usr(void)
+static void record_timestamp_usr(void)
{
if (clock_gettime(CLOCK_REALTIME, &ts_usr))
error(1, errno, "clock_gettime");
-
- __print_timestamp(" USR", &ts_usr, 0, 0);
}
static void print_timestamp(struct scm_timestamping *tss, int tstype,
@@ -599,8 +597,6 @@ static void do_test(int family, unsigned int report_opt)
fill_header_udp(buf + off, family == PF_INET);
}
- print_timestamp_usr();
-
iov.iov_base = buf;
iov.iov_len = total_len;
@@ -655,10 +651,14 @@ static void do_test(int family, unsigned int report_opt)
}
+ record_timestamp_usr();
+
val = sendmsg(fd, &msg, 0);
if (val != total_len)
error(1, errno, "send");
+ __print_timestamp(" USR", &ts_usr, 0, 0);
+
/* wait for all errors to be queued, else ACKs arrive OOO */
if (cfg_sleep_usec)
usleep(cfg_sleep_usec);
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 6%]
* [Devel] [PATCH hci-8.1 v2 3/3] selftests: drv-net: read the channel count over netlink
@ 2026-09-28 19:37 4% ` Eva Kurchatova
0 siblings, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-28 19:37 UTC (permalink / raw)
To: khorenko; +Cc: devel
napi_threaded.py asks ethtool for the channel count as JSON, which
ethtool 6.11 cannot print for -l:
ethtool: bad command line argument(s)
JSON output not available for this subcommand
not ok 1 napi_threaded.napi_init
Read the count over ethtool netlink instead, so the test does not
depend on the ethtool version installed.
https://virtuozzo.atlassian.net/browse/VSTOR-139651
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/drivers/net/napi_threaded.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/drivers/net/napi_threaded.py b/tools/testing/selftests/drivers/net/napi_threaded.py
index ed66efa481b0..33cc3f8752d3 100755
--- a/tools/testing/selftests/drivers/net/napi_threaded.py
+++ b/tools/testing/selftests/drivers/net/napi_threaded.py
@@ -7,7 +7,7 @@ Test napi threaded states.
from lib.py import ksft_run, ksft_exit
from lib.py import ksft_eq, ksft_ne, ksft_ge
-from lib.py import NetDrvEnv, NetdevFamily
+from lib.py import EthtoolFamily, NetDrvEnv, NetdevFamily
from lib.py import cmd, defer, ethtool
@@ -28,7 +28,9 @@ def _set_threaded_state(cfg, threaded) -> None:
def _setup_deferred_cleanup(cfg) -> None:
- combined = ethtool(f"-l {cfg.ifname}", json=True)[0].get("combined", 0)
+ # Not every ethtool prints -l as JSON, ask the kernel
+ chan = EthtoolFamily().channels_get({'header': {'dev-index': cfg.ifindex}})
+ combined = chan.get('combined-count', 0)
ksft_ge(combined, 2)
defer(ethtool, f"-L {cfg.ifname} combined {combined}")
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* Re: [QEMU HCI-7.4 PATCH] migration: set .vm_old_state in migration_stop_vm() #VSTOR-147066
2026-09-28 13:23 30% [QEMU HCI-7.4 PATCH] migration: set .vm_old_state in migration_stop_vm() #VSTOR-147066 Andrey Drobyshev
@ 2026-09-28 16:51 0% ` Denis V. Lunev
0 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-28 16:51 UTC (permalink / raw)
To: Andrey Drobyshev, svt-core; +Cc: den
On 9/28/26 15:23, Andrey Drobyshev wrote:
> Currently, when a postcopy migration starts, the .vm_old_state isn't
> being recorded. Thus it remains to be set to -1. That might result into
> a VM crash in case migration fails:
>
> migration_iteration_finish(s)
> runstate_set(s->vm_old_state=-1)
> assert(new_state < RUN_STATE__MAX)
>
> To avoid that, let's just move saving the previous state directly into
> the migration_stop_vm() helper. Add 2nd function argument to
> migration_stop_vm() to achieve that.
>
> This code is mimicking what's already done in hci-8.0 and master.
>
> Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
> ---
> migration/migration.c | 21 ++++++++++-----------
> migration/migration.h | 2 +-
> 2 files changed, 11 insertions(+), 12 deletions(-)
>
> diff --git a/migration/migration.c b/migration/migration.c
> index 00924b4a2a7..8f33181e00a 100644
> --- a/migration/migration.c
> +++ b/migration/migration.c
> @@ -157,9 +157,14 @@ static gint page_request_addr_cmp(gconstpointer ap, gconstpointer bp)
> return (a > b) - (a < b);
> }
>
> -int migration_stop_vm(RunState state)
> +int migration_stop_vm(MigrationState *s, RunState state)
> {
> - int ret = vm_stop_force_state(state);
> + int ret;
> +
> + s->vm_old_state = runstate_get();
> + global_state_store();
> +
> + ret = vm_stop_force_state(state);
>
> trace_vmstate_downtime_checkpoint("src-vm-stopped");
>
> @@ -2460,8 +2465,7 @@ static int postcopy_start(MigrationState *ms, Error **errp)
> migration_downtime_start(ms);
>
> qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
> - global_state_store();
> - ret = migration_stop_vm(RUN_STATE_FINISH_MIGRATE);
> + ret = migration_stop_vm(ms, RUN_STATE_FINISH_MIGRATE);
> if (ret < 0) {
> goto fail;
> }
> @@ -2660,10 +2664,7 @@ static int migration_completion_precopy(MigrationState *s,
> migration_downtime_start(s);
> qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
>
> - s->vm_old_state = runstate_get();
> - global_state_store();
> -
> - ret = migration_stop_vm(RUN_STATE_FINISH_MIGRATE);
> + ret = migration_stop_vm(s, RUN_STATE_FINISH_MIGRATE);
> trace_migration_completion_vm_stop(ret);
> if (ret < 0) {
> goto out_unlock;
> @@ -3520,11 +3521,9 @@ static void *bg_migration_thread(void *opaque)
> * transition in vm_stop_force_state() we need to wakeup it up.
> */
> qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
> - s->vm_old_state = runstate_get();
>
> - global_state_store();
> /* Forcibly stop VM before saving state of vCPUs and devices */
> - if (migration_stop_vm(RUN_STATE_PAUSED)) {
> + if (migration_stop_vm(s, RUN_STATE_PAUSED)) {
> goto fail;
> }
> /*
> diff --git a/migration/migration.h b/migration/migration.h
> index f2704a0e3bd..fc33ee465ac 100644
> --- a/migration/migration.h
> +++ b/migration/migration.h
> @@ -549,6 +549,6 @@ int migration_rp_wait(MigrationState *s);
> */
> void migration_rp_kick(MigrationState *s);
>
> -int migration_stop_vm(RunState state);
> +int migration_stop_vm(MigrationState *s, RunState state);
>
> #endif
Why it isn't needed for this ticket. In this incident the destination
crashed first, and the source then aborted on runstate_set(-1). With your
destination fix, bitmap-only post-copy migration completes, so the
source failure path is never reached. The source bug now fires only when a
post-copy migration fails for some other reason, such as the
destination dying or a stream error during completion. Your two commits
apply cleanly
to origin/hci-7.4 without Andrey's patch.
Why his patch alone is risky. Once vm_old_state is RUNNING,
migration_iteration_finish() on 7.4 handles any post-copy failure the
same way:
bdrv_activate_all(), then vm_start() on the source. That also happens
after the device package has been sent and the destination has run the
guest.
- In this incident the destination ran the guest for about 4 seconds,
writing to shared NFS.
- If the destination is dead, its image locks are gone, so activation
succeeds.
- The source then resumes with RAM that is older than the disk. That
risks silent guest filesystem corruption.
- Today's abort loses the VM but leaves the disk crash-consistent,
which is arguably the safer failure.
Upstream says the same in 26f65c01edc (v10.2): once the destination
has started, "dest QEMU's RAM is the only version that is consistent with
current status of the shared storage", so the source must not start
the VM. That commit only covers the case where the destination still
holds the
locks.
Why not take upstream:
- 9867d4ddd04 is the commit that fixed vm_old_state upstream. It
conflicts on 7.4: it depends on the notifier rework series, about 8
commits that
touch spice, virtio-net and vfio. It also changes CPR-reboot
behaviour by stopping the VM at migration start. That's too much for a
hotfix, and
it brings the same restart-after-destination-ran behaviour.
- 7b842fe354c (POSTCOPY_DEVICE, v10.2) is the real upstream answer. It
lets the source resume only while the destination is still loading device
state and hasn't started the guest. It is too big a backport to qemu
8.2.
^ permalink raw reply [relevance 0%]
* [Devel] [PATCH hci-8.1 2/2] selftests/ve: check that host can go over container mount limit
@ 2026-09-28 15:24 5% ` Pavel Tikhomirov
0 siblings, 0 replies; 200+ results
From: Pavel Tikhomirov @ 2026-09-28 15:24 UTC (permalink / raw)
To: Konstantin Khorenko, Vladimir Riabchun, Vasileios Almpanis; +Cc: devel
Like pseudosuper_allows_overuse, check that a bind mount done from ve0
in the container mount namespace succeeds when the container has no
mounts left, and is charged to the container.
https://virtuozzo.atlassian.net/browse/VSTOR-147661
Feature: per-ve failcounters
Signed-off-by: Pavel Tikhomirov <ptikhomirov@virtuozzo.com>
---
.../selftests/ve/ve_mount_accounting_test.c | 37 ++++++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/tools/testing/selftests/ve/ve_mount_accounting_test.c b/tools/testing/selftests/ve/ve_mount_accounting_test.c
index e2a882137e11d..8bb13e9615427 100644
--- a/tools/testing/selftests/ve/ve_mount_accounting_test.c
+++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
@@ -539,6 +539,43 @@ TEST_F(ve_mnt_acc, failcount)
ASSERT_TRUE(strstr(failcount_str, "mnt: 1\n") != NULL);
}
+static int _create_bind_mount(void *id_ptr)
+{
+ char path[PATH_MAX];
+ int id = *(int *)id_ptr;
+
+ snprintf(path, sizeof(path), TMP_DIR "%d", id);
+
+ if (mkdir(path, 0755) < 0) {
+ fprintf(stderr, "Failed to create directory %s: %s\n", path, strerror(errno));
+ return -1;
+ }
+ if (!mount(path, path, NULL, MS_BIND, NULL))
+ return 0;
+ fprintf(stderr, "Failed to bind mount %s: %s\n", path, strerror(errno));
+
+ rmdir(path);
+ return -1;
+}
+
+/* Test that host can go over the limit in container mount namespace. */
+TEST_F(ve_mnt_acc, host_allows_overuse)
+{
+ char path_max_nr[64];
+ int id = 0;
+
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt), 0);
+
+ /* Bind mount from ve0, charged to the container's mount namespace owner */
+ ASSERT_EQ(run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
+ CLONE_NEWNS, _create_bind_mount, &id), 0);
+ ASSERT_EQ(wait_on_mnt_avail(self, -1), 0);
+
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+}
+
int main(int argc, char **argv)
{
need_avail = mmap(NULL, sizeof(int),
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 5%]
* [Devel] [PATCH hci-8.1 v4 2/2] selftests: ve: introduce vtty pair teardown stress test
2026-09-28 15:16 11% ` [Devel] [PATCH hci-8.1 v4 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
@ 2026-09-28 15:16 6% ` Eva Kurchatova
1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-28 15:16 UTC (permalink / raw)
To: khorenko; +Cc: devel
Either end of a vtty pair creates it, and the last close tears it down.
Race condition in older VZKernel vtty implementation allowed both sides
to decide they were the final one and call release_tty() twice, and a
master opening again while a teardown was in flight used to hand out a
reference to a tty_struct about to be freed. Either way, this caused
use-after-free. That issue was fixed in c5028cd61a69:
"ve/vtty: fix use-after-free on concurrent tty close and reopen"
Hammer both ends for ownership correctness & thread safety verification.
The original reproducer needed a few tens of minutes across a dozen
containers, where vzctl interaction with vtty subsystem was a small part
of the overall runtime, therefore it did not fire on every run. This test
opens and closes nothing but the pair, from both ends and without pause,
so the default stays in the tens of seconds and under the timeout of the
suite, and VE_VTTY_SECS or argv[1] allows to control this.
The test assumes the veid it picks is not in use by a running container:
ve_id_write() does not check that a veid is unique and the vtty map is
keyed by veid, so a clash would hand the test someone else's pair. The
108..200 range make_ve() uses, makes that unlikely on a test node.
https://virtuozzo.atlassian.net/browse/VSTOR-144771
Feature: ve selftest
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/ve/.gitignore | 1 +
tools/testing/selftests/ve/Makefile | 1 +
tools/testing/selftests/ve/ve_vtty_stress.c | 314 ++++++++++++++++++++
3 files changed, 316 insertions(+)
create mode 100644 tools/testing/selftests/ve/ve_vtty_stress.c
diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index afa4c568c2c9..9d8d86093827 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,2 +1,3 @@
ve_ns_owner_test
ve_perms_test
+ve_vtty_stress
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index ec40cbc7b3a1..6cd5823c4d93 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
TEST_GEN_PROGS += ve_ns_owner_test
TEST_GEN_PROGS += ve_perms_test
+TEST_GEN_PROGS += ve_vtty_stress
include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
new file mode 100644
index 000000000000..85cb6fdb3fcc
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_vtty_stress.c
@@ -0,0 +1,314 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_vtty_stress - open and close a vtty pair from both ends at once.
+ *
+ * Either end of a vtty pair creates it, and the last close tears it down.
+ *
+ * Hammer both ends. The master comes from the vzctl ioctl, which does
+ * not look the VE up. The slave is reachable only from inside the VE, as
+ * TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
+ * vttys index minor - 1, so the test builds a VE with cgroup2, sets
+ * ve.veid to the cgroup it made, and runs the slave side there.
+ *
+ * A warning does not stop the host, so the run reads the kernel log too.
+ * One read() of /dev/kmsg is one record, so the test matches vtty driver
+ * splats, as well as generic kernel warning/oops/kasan reports.
+ *
+ * Usage: ve_vtty_stress [seconds] (default 30, or $VE_VTTY_SECS)
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <stdbool.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <linux/major.h>
+#include <sys/sysmacros.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "ve_selftest.h"
+#include "../kselftest.h"
+
+/* The ioctl argument; the trailing data[] the uapi struct has is unused. */
+struct vzctl_ve_configure {
+ unsigned int veid, key, val, size;
+};
+
+#define VZCTL_VE_CONFIGURE _IOW('.', 15, struct vzctl_ve_configure)
+#define VE_CONFIGURE_OPEN_TTY 5
+
+#define VTTY_IDX 0 /* The pair both ends work on */
+#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
+#define DEFAULT_SECS 30
+
+/*
+ * Expected open errno codes: the pair may be closing, or gone already,
+ * when an open lands on it.
+ * Anything else is an environment issue or a bug.
+ */
+static bool expected_errno(int err)
+{
+ return err == EBUSY || err == EIO || err == ENXIO;
+}
+
+static int open_master(int ctl_fd, unsigned int veid)
+{
+ struct vzctl_ve_configure c = {
+ .veid = veid,
+ .key = VE_CONFIGURE_OPEN_TTY,
+ .val = VTTY_IDX,
+ };
+
+ return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
+}
+
+/* The slave end, inside the VE. Returns the errno that stopped it, or 0. */
+static int hammer_slave(const char *node, time_t deadline,
+ unsigned long *opened)
+{
+ int err = 0;
+
+ while (time(NULL) < deadline) {
+ int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
+
+ if (fd >= 0) {
+ (*opened)++;
+ close(fd);
+ } else if (!expected_errno(errno)) {
+ err = errno;
+ break;
+ }
+ }
+
+ return err;
+}
+
+/* The kernel log, since a warning fails nothing by itself. */
+static int kmsg_open(void)
+{
+ int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+
+ if (fd >= 0)
+ lseek(fd, 0, SEEK_END);
+ return fd;
+}
+
+/*
+ * Check for kernel log warnings, one read() per record, hence
+ * the function name is not checked.
+ */
+static int kmsg_check(int fd)
+{
+ char buf[8192];
+ int found = 0;
+ ssize_t r;
+
+ if (fd < 0)
+ return 0;
+
+ for (;;) {
+ r = read(fd, buf, sizeof(buf) - 1);
+ if (r < 0) {
+ /* Ring overrun, continue with the oldest existing record */
+ if (errno == EPIPE)
+ continue;
+ break;
+ }
+ if (r == 0)
+ break;
+ buf[r] = '\0';
+ /* Known symptoms of a buggy kernel */
+ if (strstr(buf, "vtty") && (strstr(buf, "tty->count") ||
+ strstr(buf, "bad slave count"))) {
+ ksft_print_msg("vtty mismatched count log: %s", buf);
+ found++;
+ }
+ if (strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") ||
+ strstr(buf, ";BUG: ") || strstr(buf, "soft lockup - ") ||
+ strstr(buf, "general protection fault")) {
+ ksft_print_msg("kernel log: %s", buf);
+ found++;
+ }
+ }
+ return found;
+}
+
+int main(int argc, char **argv)
+{
+ unsigned long master_opened = 0, slave_opened = 0;
+ int cg, ctid, ctl, kmsg, ready[2], count[2], status;
+ int master_err = 0, slave_err;
+ const char *env = getenv("VE_VTTY_SECS");
+ char path[PATH_MAX], val[16], c;
+ char dir[] = "/tmp/ve_vtty_XXXXXX";
+ char node[sizeof(dir) + 6];
+ time_t deadline;
+ long secs;
+ pid_t pid;
+
+ ksft_print_header();
+ secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
+ if (secs <= 0)
+ secs = DEFAULT_SECS;
+
+ ctl = open("/dev/vzctl", O_RDWR);
+ if (ctl < 0)
+ ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
+ "(vzdev, vzmon) are needed\n",
+ strerror(errno));
+
+ cg = mount_cg2_fd();
+ if (cg < 0)
+ ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
+ write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS);
+
+ ctid = make_ve(cg, CTID_MIN);
+ if (ctid < 0)
+ ksft_exit_skip("cannot make a VE cgroup\n");
+
+ /* The ve.veid must match between master / slave */
+ snprintf(path, sizeof(path), "%d/ve.veid", ctid);
+ snprintf(val, sizeof(val), "%d", ctid);
+ if (write_file_at(cg, path, val) < 0) {
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
+ }
+
+ /* Do not report a failure where the vtty pair cannot be had at all */
+ status = open_master(ctl, ctid);
+ if (status < 0 && errno != EBUSY) {
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("no vtty master: %s\n", strerror(errno));
+ }
+ if (status >= 0)
+ close(status);
+
+ /*
+ * Use temp directory for the node the slave end opens.
+ * Cleaned up by the master side instead of relying on the slave.
+ */
+ if (!mkdtemp(dir)) {
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("mkdtemp: %s\n", strerror(errno));
+ }
+ snprintf(node, sizeof(node), "%s/slave", dir);
+ if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0) {
+ status = errno;
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("mknod: %s\n", strerror(status));
+ }
+
+ ksft_set_plan(1);
+ kmsg = kmsg_open();
+ if (kmsg < 0)
+ ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
+ "be noticed\n",
+ strerror(errno));
+
+ if (pipe(ready) || pipe(count)) {
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("pipe: %s\n", strerror(errno));
+ }
+
+ deadline = time(NULL) + secs;
+ ksft_print_msg("VE %d, pair %d, %ld seconds\n", ctid, VTTY_IDX, secs);
+
+ pid = fork();
+ if (pid == 0) {
+ close(ready[0]);
+ close(count[0]);
+ if (enter_cgroup(cg, ctid) < 0)
+ _exit(errno ? errno : 1);
+ /* CLONE_NEWVE puts the task in the VE of the cgroup */
+ if (unshare(CLONE_NEWVE | CLONE_NEWPID | CLONE_NEWNS) < 0)
+ _exit(errno ? errno : 1);
+ if (fork() == 0) {
+ unsigned long n = 0;
+ int err;
+
+ /*
+ * Hammering on after the run has given up would hold
+ * the cgroup open against destroy_ve().
+ */
+ prctl(PR_SET_PDEATHSIG, SIGKILL);
+
+ c = 'r';
+ if (write(ready[1], &c, 1) != 1)
+ _exit(errno);
+ err = hammer_slave(node, deadline, &n);
+ /* Send the count either way: it says how far it got. */
+ if (write(count[1], &n, sizeof(n)) != sizeof(n))
+ _exit(errno);
+ _exit(err);
+ }
+ wait(&status);
+ _exit(WIFEXITED(status) ? WEXITSTATUS(status) : 1);
+ } else if (pid < 0) {
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("fork: %s\n", strerror(errno));
+ }
+ close(ready[1]);
+ close(count[1]);
+
+ if (read(ready[0], &c, 1) != 1) {
+ waitpid(pid, &status, 0);
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("the VE side never came up\n");
+ }
+
+ while (time(NULL) < deadline) {
+ int fd = open_master(ctl, ctid);
+
+ if (fd >= 0) {
+ master_opened++;
+ close(fd);
+ } else if (!expected_errno(errno)) {
+ master_err = errno;
+ break;
+ }
+ }
+
+ if (master_err) {
+ /* The other end runs to its own deadline, stop it */
+ kill(pid, SIGKILL);
+ waitpid(pid, &status, 0);
+ slave_err = 0;
+ } else {
+ if (read(count[0], &slave_opened, sizeof(slave_opened)) !=
+ sizeof(slave_opened))
+ slave_opened = 0;
+ waitpid(pid, &status, 0);
+ slave_err = WIFEXITED(status) ? WEXITSTATUS(status) : EINTR;
+ }
+
+ ksft_print_msg("%lu master opens, %lu slave opens\n", master_opened,
+ slave_opened);
+
+ status = kmsg_check(kmsg);
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+
+ if (master_err)
+ ksft_exit_fail_msg("the master end stopped: %s\n",
+ strerror(master_err));
+ if (slave_err)
+ ksft_exit_fail_msg("the slave end stopped: %s\n",
+ strerror(slave_err));
+ if (!master_opened)
+ ksft_exit_fail_msg("no master was ever opened\n");
+ if (!slave_opened)
+ ksft_exit_fail_msg("no slave was ever opened\n");
+ if (status)
+ ksft_exit_fail_msg("kernel complained %d time(s)\n", status);
+
+ ksft_test_result_pass("vtty pair closed from both ends\n");
+ ksft_finished();
+}
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 6%]
* [Devel] [PATCH hci-8.1 v4 1/2] selftests: ve: grant the directories the files sit in
@ 2026-09-28 15:16 11% ` Eva Kurchatova
2026-09-28 15:16 6% ` [Devel] [PATCH hci-8.1 v4 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-28 15:16 UTC (permalink / raw)
To: khorenko; +Cc: devel
The sysfs variant names nested leaves, kernel/vmcoreinfo and
kernel/notes, while the proc one names files at the top of the
filesystem. Both enforcement tests grant the leaves and nothing else,
so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
cannot traverse, and every open answers ENOENT:
not ok 14 ve_perms.sysfs.enforce_allows
not ok 15 ve_perms.sysfs.enforce_denies
Grant x on each directory along the way, taken from the path the
variant already gives.
Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")
https://virtuozzo.atlassian.net/browse/VSTOR-144955
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/ve/ve_perms_test.c | 34 ++++++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 25ffbd42c380..8096b1fa3b43 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -114,9 +114,11 @@ static int ve_open_rel(int cgv2_fd, int ctid, const char *prefix,
static int set_entry(int cgv2_fd, const char *path, const char *entry,
const char *mask)
{
- char line[256];
+ char line[PATH_MAX];
- snprintf(line, sizeof(line), "%s %s", entry, mask);
+ if (snprintf(line, sizeof(line), "%s %s", entry, mask) >=
+ (int)sizeof(line))
+ return -1;
return write_file_at(cgv2_fd, path, line);
}
@@ -343,6 +345,32 @@ TEST_F(ve_perms, write_parser)
EXPECT_LT(write_file_at(self->cgv2_fd, path, file), 0);
}
+/* A leaf is reachable only once every directory above it can be traversed */
+static int allow_path(int cgv2_fd, const char *path, const char *file)
+{
+ char dir[PATH_MAX];
+ const char *sep;
+
+ for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
+ if (snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file),
+ file) >= (int)sizeof(dir))
+ return -1;
+ if (set_entry(cgv2_fd, path, dir, "x") < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
+{
+ int i;
+
+ for (i = 0; i < NR_FILES; i++)
+ if (allow_path(cgv2_fd, path, files[i]) < 0)
+ return -1;
+ return 0;
+}
+
/*
* Enforcement, allowed side: from inside a VE, granting r lets each file be
* read, and granting x on the directory lets it be traversed (the lookup then
@@ -358,6 +386,7 @@ TEST_F(ve_perms, enforce_allows)
ve_file(path, sizeof(path), self->ctid_a, variant->fs);
+ ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
for (i = 0; i < NR_FILES; i++)
EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
@@ -400,6 +429,7 @@ TEST_F(ve_perms, enforce_denies)
O_RDONLY), 0);
/* Granted w only: visible but not readable. */
+ ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
for (i = 0; i < NR_FILES; i++)
EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 11%]
* [QEMU HCI-7.4 PATCH] migration: set .vm_old_state in migration_stop_vm() #VSTOR-147066
@ 2026-09-28 13:23 30% Andrey Drobyshev
2026-09-28 16:51 0% ` Denis V. Lunev
0 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-09-28 13:23 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
Currently, when a postcopy migration starts, the .vm_old_state isn't
being recorded. Thus it remains to be set to -1. That might result into
a VM crash in case migration fails:
migration_iteration_finish(s)
runstate_set(s->vm_old_state=-1)
assert(new_state < RUN_STATE__MAX)
To avoid that, let's just move saving the previous state directly into
the migration_stop_vm() helper. Add 2nd function argument to
migration_stop_vm() to achieve that.
This code is mimicking what's already done in hci-8.0 and master.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
migration/migration.c | 21 ++++++++++-----------
migration/migration.h | 2 +-
2 files changed, 11 insertions(+), 12 deletions(-)
diff --git a/migration/migration.c b/migration/migration.c
index 00924b4a2a7..8f33181e00a 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -157,9 +157,14 @@ static gint page_request_addr_cmp(gconstpointer ap, gconstpointer bp)
return (a > b) - (a < b);
}
-int migration_stop_vm(RunState state)
+int migration_stop_vm(MigrationState *s, RunState state)
{
- int ret = vm_stop_force_state(state);
+ int ret;
+
+ s->vm_old_state = runstate_get();
+ global_state_store();
+
+ ret = vm_stop_force_state(state);
trace_vmstate_downtime_checkpoint("src-vm-stopped");
@@ -2460,8 +2465,7 @@ static int postcopy_start(MigrationState *ms, Error **errp)
migration_downtime_start(ms);
qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
- global_state_store();
- ret = migration_stop_vm(RUN_STATE_FINISH_MIGRATE);
+ ret = migration_stop_vm(ms, RUN_STATE_FINISH_MIGRATE);
if (ret < 0) {
goto fail;
}
@@ -2660,10 +2664,7 @@ static int migration_completion_precopy(MigrationState *s,
migration_downtime_start(s);
qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
- s->vm_old_state = runstate_get();
- global_state_store();
-
- ret = migration_stop_vm(RUN_STATE_FINISH_MIGRATE);
+ ret = migration_stop_vm(s, RUN_STATE_FINISH_MIGRATE);
trace_migration_completion_vm_stop(ret);
if (ret < 0) {
goto out_unlock;
@@ -3520,11 +3521,9 @@ static void *bg_migration_thread(void *opaque)
* transition in vm_stop_force_state() we need to wakeup it up.
*/
qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL);
- s->vm_old_state = runstate_get();
- global_state_store();
/* Forcibly stop VM before saving state of vCPUs and devices */
- if (migration_stop_vm(RUN_STATE_PAUSED)) {
+ if (migration_stop_vm(s, RUN_STATE_PAUSED)) {
goto fail;
}
/*
diff --git a/migration/migration.h b/migration/migration.h
index f2704a0e3bd..fc33ee465ac 100644
--- a/migration/migration.h
+++ b/migration/migration.h
@@ -549,6 +549,6 @@ int migration_rp_wait(MigrationState *s);
*/
void migration_rp_kick(MigrationState *s);
-int migration_stop_vm(RunState state);
+int migration_stop_vm(MigrationState *s, RunState state);
#endif
--
2.47.1
^ permalink raw reply [relevance 30%]
* [Devel] [PATCH hci-8.1 v10 10/13] ve: add netif failcounter
` (2 preceding siblings ...)
2026-09-25 16:53 4% ` [Devel] [PATCH hci-8.1 v10 09/13] ve: add mnt failcounter Konstantin Khorenko
@ 2026-09-25 16:53 6% ` Konstantin Khorenko
2026-09-25 16:53 6% ` [Devel] [PATCH hci-8.1 v10 13/13] selftests/ve: add mount accounting selftest Konstantin Khorenko
4 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:53 UTC (permalink / raw)
To: Vasileios Almpanis, Pavel Tikhomirov,
cover.1789491989.git.vladimir.riabchun, Konstantin Khorenko,
devel
From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Count hits of the per-VE network interface limit in ve.failcount.
The per-site "increase ve::netif_max_nr" hints are replaced by the
unified message from ve_failcount_inc.
https://virtuozzo.atlassian.net/browse/VSTOR-135520
Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
include/linux/ve.h | 1 +
kernel/ve/ve.c | 3 +++
net/core/dev.c | 10 ++--------
3 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/include/linux/ve.h b/include/linux/ve.h
index 0395e37208849..5d94470692f72 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -84,6 +84,7 @@ struct ve_struct {
int netns_max_nr;
atomic_t netif_avail_nr;
+ atomic_t netif_failcount;
int netif_max_nr;
atomic_t bpf_prog_avail_nr;
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index 4c3acda6b135e..8505c1dae4a18 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -103,6 +103,7 @@ struct ve_struct ve0 = {
.netns_failcount = ATOMIC_INIT(0),
.netns_max_nr = INT_MAX,
.netif_avail_nr = ATOMIC_INIT(INT_MAX),
+ .netif_failcount = ATOMIC_INIT(0),
.netif_max_nr = INT_MAX,
.bpf_prog_avail_nr = ATOMIC_INIT(INT_MAX),
.bpf_prog_max_nr = INT_MAX,
@@ -798,6 +799,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
ve->netns_max_nr = NETNS_MAX_NR_DEFAULT;
atomic_set(&ve->netif_avail_nr, NETIF_MAX_NR_DEFAULT);
+ atomic_set(&ve->netif_failcount, 0);
ve->netif_max_nr = NETIF_MAX_NR_DEFAULT;
atomic_set(&ve->bpf_prog_avail_nr, BPF_PROG_MAX_NR_DEFAULT);
@@ -1090,6 +1092,7 @@ static const struct ve_failcount_entry {
#define VE_FC_ENTRY(name) { #name, offsetof(struct ve_struct, name##_failcount) }
VE_FC_ENTRY(netns),
VE_FC_ENTRY(mnt),
+ VE_FC_ENTRY(netif),
{}
};
diff --git a/net/core/dev.c b/net/core/dev.c
index c7dddb200489f..7632ad1fd0151 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -10997,10 +10997,7 @@ int register_netdevice(struct net_device *dev)
ret = -ENOMEM;
if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
- ve_pr_warn_ratelimited(VE_LOG_BOTH,
- "CT%s: hits max number of network devices, "
- "increase ve::netif_max_nr parameter\n",
- ve_name(net->owner_ve));
+ ve_failcount_inc(net->owner_ve, netif);
return ret;
}
@@ -12211,10 +12208,7 @@ int __dev_change_net_namespace(struct net_device *dev, struct net *net,
err = -ENOMEM;
if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
- ve_pr_warn_ratelimited(VE_LOG_BOTH,
- "CT%s: hits max number of network devices, "
- "increase ve::netif_max_nr parameter\n",
- ve_name(net->owner_ve));
+ ve_failcount_inc(net->owner_ve, netif);
goto out;
}
atomic_inc(&dev_net(dev)->owner_ve->netif_avail_nr);
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 6%]
* [Devel] [PATCH hci-8.1 v10 13/13] selftests/ve: add mount accounting selftest
` (3 preceding siblings ...)
2026-09-25 16:53 6% ` [Devel] [PATCH hci-8.1 v10 10/13] ve: add netif failcounter Konstantin Khorenko
@ 2026-09-25 16:53 6% ` Konstantin Khorenko
4 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:53 UTC (permalink / raw)
To: Vasileios Almpanis, Pavel Tikhomirov,
cover.1789491989.git.vladimir.riabchun, Konstantin Khorenko,
devel
From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
There are 6 test cases, covered in the new test:
1. Simple mount accouting correctness, just mount/umount.
2. Verification of correct limit hits and changes, including
negative values.
3. Partial mounts test, when mount limit is hit in the middle
of creation.
4. Test that enabled pseudosuper allows overuse.
5. Test that pseudosuper doesn't affect mount accounting.
6. Failcount feature verification.
https://virtuozzo.atlassian.net/browse/VSTOR-135520
Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/ve/.gitignore | 1 +
tools/testing/selftests/ve/Makefile | 1 +
.../selftests/ve/ve_mount_accounting_test.c | 554 +++++++++++++++++++++
tools/testing/selftests/ve/ve_selftest.h | 21 +
4 files changed, 577 insertions(+)
diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index afa4c568c2c9b..3df4d05888dce 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,2 +1,3 @@
ve_ns_owner_test
ve_perms_test
+ve_mount_accounting_test
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index ec40cbc7b3a1b..c6efe7c4b4fba 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
TEST_GEN_PROGS += ve_ns_owner_test
TEST_GEN_PROGS += ve_perms_test
+TEST_GEN_PROGS += ve_mount_accounting_test
include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_mount_accounting_test.c b/tools/testing/selftests/ve/ve_mount_accounting_test.c
new file mode 100644
index 0000000000000..e2a882137e11d
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
@@ -0,0 +1,554 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_mount_accounting selftests
+ *
+ * Tests to check the correctness of mount accounting.
+ */
+#define _GNU_SOURCE
+#include <asm/unistd.h>
+#include <linux/sched.h>
+#include <linux/limits.h>
+#include <sys/wait.h>
+#include <sys/syscall.h>
+#include <sys/stat.h>
+#include <sys/mount.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <time.h>
+
+#include "../kselftest_harness.h"
+#include "ve_selftest.h"
+
+#define TMP_DIR "/ve-mnt-tmp/"
+#define VE_MOUNTS_MAX 128
+
+/*
+ * These values take a while to calculate, the first test will
+ * do this and store the values for those who come after.
+ */
+int *need_avail;
+
+FIXTURE(ve_mnt_acc)
+{
+ int cgv2_fd;
+ int ctid;
+ struct namespace_ctx ns_ctx;
+ int orig_have_mnt;
+};
+
+/*
+ * free_vfsmnt is called via call_rcu, so we need to ensure
+ * that all pending call_rcu tasks are finished.
+ * However, one barrier is not enough:
+ * mntput_no_expire
+ * init_task_work(&mnt->mnt_rcu, __cleanup_mnt);
+ * task_work_add(task, &mnt->mnt_rcu, TWA_RESUME)
+ * OR
+ * schedule_delayed_work(&delayed_mntput_work, 1)
+ * # Both later call cleanup_mnt
+ * cleanup_mnt
+ * call_rcu(&mnt->mnt_rcu, delayed_free_vfsmnt);
+ *
+ * We can't enforce workqueue flush from userspace, so instead
+ * read several times until the value is the one we expect.
+ */
+static int wait_on_mnt_avail(FIXTURE_DATA(ve_mnt_acc) * self, int expected)
+{
+#define NSEC_PER_SEC (1000ULL * 1000 * 1000)
+#define WAIT_TIMEOUT_NS (4096ULL * 1000 * 1000)
+ char path_avail[64];
+ int mnt_avail_nr = 0;
+ unsigned long long delay_ns = 1000000;
+ struct timespec delay;
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+ while (delay_ns <= WAIT_TIMEOUT_NS) {
+ if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr)) {
+ fprintf(stderr, "Failed to read from %s\n", path_avail);
+ return 1;
+ }
+ if (mnt_avail_nr == expected)
+ return 0;
+
+ do_rcu_barrier();
+ delay.tv_sec = delay_ns / NSEC_PER_SEC;
+ delay.tv_nsec = delay_ns % NSEC_PER_SEC;
+ if (nanosleep(&delay, NULL)) {
+ fprintf(stderr, "nanosleep failed: %s\n", strerror(errno));
+ return 1;
+ }
+ delay_ns *= 2;
+ }
+ fprintf(stderr, "Didn't get expected value at %s\n", path_avail);
+ fprintf(stderr, "Expected %d, last read %d\n", expected, mnt_avail_nr);
+ return -1;
+}
+
+static int set_pseudosuper(FIXTURE_DATA(ve_mnt_acc) * self, int value)
+{
+ char path[64];
+
+ snprintf(path, sizeof(path), "%d/ve.pseudosuper", self->ctid);
+ return write_u64_at(self->cgv2_fd, path, value);
+}
+
+static int _create_mount(void *id_ptr)
+{
+ char path[PATH_MAX];
+ int id = *(int *)id_ptr, ret;
+
+ snprintf(path, sizeof(path), TMP_DIR "%d", id);
+
+ if (mkdir(path, 0755) < 0) {
+ fprintf(stderr, "Failed to create directory %s: %s\n", path, strerror(errno));
+ return -1;
+ }
+ ret = mount("tmpfs", path, "tmpfs", 0, "size=1M");
+ if (!ret)
+ return 0;
+ fprintf(stderr, "Failed to mount tmpfs to %s: %s\n", path, strerror(errno));
+
+ rmdir(path);
+ return -1;
+}
+
+/* Create a mount and wait until mount counter is valid */
+static int create_mount(FIXTURE_DATA(ve_mnt_acc) * self, int id)
+{
+ int ret, avail_before;
+ char path[64];
+
+ snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+
+ if (read_s32_at(self->cgv2_fd, path, &avail_before)) {
+ fprintf(stderr, "Failed to read mnt_avail_nr in create_mount\n");
+ return 1;
+ }
+
+ ret = run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
+ CLONE_NEWVE | CLONE_NEWNS, _create_mount, &id);
+
+ if (ret)
+ wait_on_mnt_avail(self, avail_before);
+ else
+ ret = wait_on_mnt_avail(self, avail_before - 1);
+ return ret;
+}
+
+static int _destroy_mount(void *id_ptr)
+{
+ char path[PATH_MAX];
+ struct stat st;
+ int id = *(int *)id_ptr;
+
+ snprintf(path, sizeof(path), TMP_DIR "%d", id);
+
+ if (stat(path, &st))
+ return 1;
+ if (umount(path)) {
+ fprintf(stderr, "failed to umount directory %s: %s\n", path, strerror(errno));
+ return -1;
+ }
+ if (rmdir(path)) {
+ fprintf(stderr, "failed to remove directory %s: %s\n", path, strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+/* Do umount and ensure that mnt_avail_nr is valid on exit(+1) */
+static int destroy_mount(FIXTURE_DATA(ve_mnt_acc) * self, int id)
+{
+ int ret, avail_before;
+ char path[64];
+
+ snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+
+ if (read_s32_at(self->cgv2_fd, path, &avail_before)) {
+ fprintf(stderr, "Failed to read mnt_avail_nr in create_mount\n");
+ return 1;
+ }
+
+ ret = run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
+ CLONE_NEWVE | CLONE_NEWNS, _destroy_mount, &id);
+
+ /* Cost must be exactly 1 */
+ if (!ret)
+ ret = wait_on_mnt_avail(self, avail_before + 1);
+
+ return ret;
+}
+
+#define MAX_MNT_ID 32
+
+static int get_free_mnt_id(void)
+{
+ int i;
+ struct stat st;
+ char path[PATH_MAX];
+
+ for (i = 0; i < MAX_MNT_ID; i++) {
+ snprintf(path, sizeof(path), TMP_DIR "%d", i);
+ if (stat(path, &st))
+ return i;
+ }
+ return -1;
+}
+
+/* Returns 0 if mount failed OR mount cost is not 1 */
+static int try_mount(FIXTURE_DATA(ve_mnt_acc) * self)
+{
+ int mnt_id = get_free_mnt_id();
+
+ if (mnt_id < 0 || create_mount(self, mnt_id))
+ return 0;
+
+ return destroy_mount(self, mnt_id) == 0;
+}
+
+/*
+ * Current kernel code first creates many mounts with ve_owner != ve0,
+ * so this VE needs more mounts than would be really consumed after commit_tree.
+ */
+static int get_required_avail_mnts(FIXTURE_DATA(ve_mnt_acc) * self)
+{
+ int l = 0, r = VE_MOUNTS_MAX + 1, mid;
+ char path_max_nr[64];
+
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+ while (r - l > 1) {
+ mid = (l + r) / 2;
+ if (write_u64_at(self->cgv2_fd, path_max_nr, mid + self->orig_have_mnt))
+ return -1;
+
+ if (try_mount(self))
+ r = mid;
+ else
+ l = mid;
+ }
+ if (r == VE_MOUNTS_MAX + 1) {
+ fprintf(stderr, "%s: %d avail mounts is not enough\n", __func__, r);
+ return -1;
+ }
+ if (write_u64_at(self->cgv2_fd, path_max_nr, VE_MOUNTS_MAX))
+ return -1;
+ return r;
+}
+
+/* Expect mount success and return new avail value */
+static int mount_and_get_avail(struct __test_metadata *_metadata,
+ FIXTURE_DATA(ve_mnt_acc) * self, int mnt_id)
+{
+ char path_avail[64];
+ int mnt_avail_nr;
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+ ASSERT_EQ(create_mount(self, mnt_id), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ return mnt_avail_nr;
+}
+
+/* Expect mount failure and ensure intact avail number */
+static int check_mount_fails(struct __test_metadata *_metadata,
+ FIXTURE_DATA(ve_mnt_acc) * self,
+ int mnt_id, int avail_count)
+{
+ char path_avail[64];
+ int mnt_avail_nr;
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+ if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr))
+ return 1;
+ if (mnt_avail_nr != avail_count) {
+ fprintf(stderr, "%s: mnt_avail(%d) != avail_count(%d) before mount\n",
+ __func__, mnt_avail_nr, avail_count);
+ return -1;
+ }
+
+ if (create_mount(self, mnt_id) == 0) {
+ fprintf(stderr, "%s: successful mount\n", __func__);
+ return -1;
+ }
+
+ if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr))
+ return 1;
+ if (mnt_avail_nr != avail_count) {
+ fprintf(stderr, "%s: mnt_avail(%d) != avail_count(%d) after mount\n",
+ __func__, mnt_avail_nr, avail_count);
+ return -1;
+ }
+ return 0;
+}
+
+FIXTURE_SETUP(ve_mnt_acc)
+{
+ int initial_mnt_avail_nr;
+ char path[64];
+
+ self->cgv2_fd = mount_cg2_fd();
+ ASSERT_GE(self->cgv2_fd, 0);
+ mkdir(TMP_DIR, 0755);
+
+ ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
+ VE_CONTROLLERS), 0);
+
+ self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
+ ASSERT_GE(self->ctid, 0);
+
+ snprintf(path, sizeof(path), "%d/ve.mnt_max_nr", self->ctid);
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path, VE_MOUNTS_MAX), 0);
+
+ /*
+ * The new ve cgroup has not been entered by anything yet, so its
+ * mnt_avail_nr counter should be VE_MOUNTS_MAX.
+ */
+ snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
+ ASSERT_EQ(initial_mnt_avail_nr, VE_MOUNTS_MAX);
+
+ ASSERT_EQ(prepare_namespaces(&self->ns_ctx, CLONE_NEWNS | CLONE_NEWVE,
+ self->cgv2_fd, self->ctid), 0);
+
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
+ self->orig_have_mnt = VE_MOUNTS_MAX - initial_mnt_avail_nr;
+
+ /* Calculate once - it is time-consuming */
+ if (*need_avail == -1)
+ *need_avail = get_required_avail_mnts(self);
+
+ ASSERT_GE(*need_avail, 1);
+ /* try_mount ensures cost is 1 */
+ ASSERT_EQ(try_mount(self), 1);
+};
+
+FIXTURE_TEARDOWN(ve_mnt_acc)
+{
+ for (int i = 0; i < MAX_MNT_ID; i++)
+ _destroy_mount((void *)&i);
+
+ close_namespace_fds(&self->ns_ctx);
+ destroy_ve(self->cgv2_fd, self->ctid);
+ close(self->cgv2_fd);
+ rmdir(TMP_DIR);
+}
+
+/* Simple test to check mount/umount accounting correctness */
+TEST_F(ve_mnt_acc, mount_umount)
+{
+ int original_mnt_avail, mnt_avail_nr;
+ char path[64];
+
+ snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &original_mnt_avail), 0);
+
+ ASSERT_LT(mount_and_get_avail(_metadata, self, 0),
+ original_mnt_avail);
+
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, original_mnt_avail);
+}
+
+/* Test mount limit hits */
+TEST_F(ve_mnt_acc, hit_limits)
+{
+ int mnt_avail_nr;
+ char path_avail[64], path_max_nr[64];
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+ /* Step 1: reduce number of available mounts to *need_avail */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+ self->orig_have_mnt + 1 * (*need_avail)), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, 1 * *need_avail);
+
+ /* Step 2: do one mount, no mounts should be available */
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), *need_avail - 1);
+
+ /* Step 3: check that one more mount fails */
+ ASSERT_EQ(check_mount_fails(_metadata, self, 1, (*need_avail) - 1), 0);
+
+ /* Step 4: increase by 1 and win now */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+ self->orig_have_mnt + 1 + *need_avail), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, *need_avail);
+
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 1), *need_avail - 1);
+
+ /* Step 5: reduce mnt_max_nr so we have more mounts than allowed */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+ self->orig_have_mnt + 1), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, -1);
+
+ /* Step 6: try to do mount when avail < 0, ensure number is intact */
+ ASSERT_EQ(check_mount_fails(_metadata, self, 2, -1), 0);
+
+ /* Step 7: remove one mount, check avail value update, mount should fail */
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+ ASSERT_EQ(check_mount_fails(_metadata, self, 2, 0), 0);
+
+ /* Step 8: remove one more mount and check that new mount succeeds */
+ ASSERT_EQ(destroy_mount(self, 1), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, 1);
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + *need_avail), 0);
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 2), *need_avail - 1);
+
+ ASSERT_EQ(destroy_mount(self, 2), 0);
+}
+
+/*
+ * Mount propagation makes one mount cost more.
+ * This test checks that if we run out of mounts in the middle of creating
+ * a new one, everything is restored smoothly and nothing leaks.
+ */
+TEST_F(ve_mnt_acc, partial_mounts)
+{
+ char path_avail[64], path_max_nr[64];
+ int i;
+
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+ if (*need_avail == 1)
+ SKIP(return, "mount cost is 1, no partial mounts possible");
+
+ for (i = 0; i < *need_avail; i++) {
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + i), 0);
+ ASSERT_EQ(check_mount_fails(_metadata, self, 0, i), 0);
+ }
+}
+
+/* Test that pseudosuper allows negative avail with correct accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_allows_overuse)
+{
+ char path_max_nr[64];
+
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + *need_avail), 0);
+ ASSERT_EQ(set_pseudosuper(self, 1), 0);
+
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), *need_avail - 1);
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 1), *need_avail - 2);
+
+ /* Go below zero */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt), 0);
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 2), -3);
+
+ ASSERT_EQ(set_pseudosuper(self, 0), 0);
+
+ /* Below zero should fail */
+ ASSERT_EQ(check_mount_fails(_metadata, self, 3, -3), 0);
+ /* Now need 2 more to add a new mount */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+ self->orig_have_mnt + 3 + *need_avail - 2), 0);
+ ASSERT_EQ(check_mount_fails(_metadata, self, 3, *need_avail - 2), 0);
+
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+ ASSERT_EQ(check_mount_fails(_metadata, self, 3, *need_avail - 1), 0);
+ ASSERT_EQ(destroy_mount(self, 2), 0);
+
+ ASSERT_EQ(try_mount(self), 1);
+}
+
+/* Test that pseudosuper doesn't disable accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_continues_accounting)
+{
+ int orig_mnt_avail, mnt_avail_nr;
+ char path_avail[64];
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &orig_mnt_avail), 0);
+
+ ASSERT_EQ(set_pseudosuper(self, 0), 0);
+
+ /* mnt 0 - mounted without pseudosuper, umounted with it. */
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), orig_mnt_avail - 1);
+ ASSERT_EQ(set_pseudosuper(self, 1), 0);
+
+ /* Cost is the same when mount/umount happen under pseudosuper. */
+ ASSERT_EQ(try_mount(self), 1);
+
+ /* mnt 1 - mounted with pseudosuper, umounted without it. */
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 1),
+ orig_mnt_avail - 2);
+
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, orig_mnt_avail - 1);
+
+ ASSERT_EQ(set_pseudosuper(self, 0), 0);
+
+ ASSERT_EQ(destroy_mount(self, 1), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, orig_mnt_avail);
+}
+
+/* Test failcount feature */
+TEST_F(ve_mnt_acc, failcount)
+{
+ char path_fc[64], failcount_str[512], path_max_nr[64];
+
+ snprintf(path_fc, sizeof(path_fc), "%d/ve.failcount", self->ctid);
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
+
+ /* Check successful mount doesn't affect failcount */
+ mount_and_get_avail(_metadata, self, 0);
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+
+ /* Check failcount update when mount fails */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, 0), 0);
+ ASSERT_LT(create_mount(self, 1), 0);
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 1\n") != NULL);
+
+ /* Check failcount flush */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_fc, 0), 0);
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
+
+ /* Check failcount update when mount fails again */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, 0), 0);
+ ASSERT_LT(create_mount(self, 1), 0);
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 1\n") != NULL);
+}
+
+int main(int argc, char **argv)
+{
+ need_avail = mmap(NULL, sizeof(int),
+ PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+
+ if (need_avail == MAP_FAILED) {
+ fprintf(stderr, "Failed to mmap shared region: %s\n", strerror(errno));
+ return 1;
+ }
+ *need_avail = -1;
+
+ return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index 7b45f91c1d561..1fc3f8dcac180 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -416,4 +416,25 @@ static inline int is_mounted(const char *path)
return errno == EXDEV ? 1 : -1;
}
+static inline int do_rcu_barrier(void)
+{
+ int fd;
+
+ errno = 0;
+ fd = open("/sys/module/rcutree/parameters/do_rcu_barrier", O_WRONLY);
+
+ if (fd < 0) {
+ fprintf(stderr, "Failed to open do_rcu_barrier: %s\n", strerror(errno));
+ return -1;
+ }
+ errno = 0;
+ if (write(fd, "1", 2) != 2) {
+ fprintf(stderr, "Failed to issue rcu_barrier: %s\n", strerror(errno));
+ close(fd);
+ return -1;
+ }
+ close(fd);
+ return 0;
+}
+
#endif /* __SELFTESTS_VE_VE_SELFTEST_H */
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 6%]
* [Devel] [PATCH hci-8.1 v10 09/13] ve: add mnt failcounter
2026-09-25 16:53 3% ` [Devel] [PATCH hci-8.1 v10 02/13] ve/fs: count available mounts instead of used ones Konstantin Khorenko
2026-09-25 16:53 4% ` [Devel] [PATCH hci-8.1 v10 07/13] ve: introduce per-VE failcounter infrastructure Konstantin Khorenko
@ 2026-09-25 16:53 4% ` Konstantin Khorenko
2026-09-25 16:53 6% ` [Devel] [PATCH hci-8.1 v10 10/13] ve: add netif failcounter Konstantin Khorenko
2026-09-25 16:53 6% ` [Devel] [PATCH hci-8.1 v10 13/13] selftests/ve: add mount accounting selftest Konstantin Khorenko
4 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:53 UTC (permalink / raw)
To: Vasileios Almpanis, Pavel Tikhomirov,
cover.1789491989.git.vladimir.riabchun, Konstantin Khorenko,
devel
From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Count hits of the per-VE mount limit in ve.failcount.
The old warning in alloc_vfsmnt is dropped, otherwise every failed
mount would produce two ratelimited messages. The one printed by
ve_failcount_inc also names the counter to look at and goes to the
container log as well.
https://virtuozzo.atlassian.net/browse/VSTOR-135520
Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
fs/namespace.c | 8 +++-----
include/linux/ve.h | 1 +
kernel/ve/ve.c | 3 +++
3 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/fs/namespace.c b/fs/namespace.c
index 1d6e66cd71745..cf77c3fb98407 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -329,12 +329,8 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
if (!ve)
ve = get_exec_env();
- if (!ve_try_reserve_mount(ve)) {
- pr_warn_ratelimited(
- "CT#%s reached the limit on mounts.\n",
- ve_name(ve));
+ if (!ve_try_reserve_mount(ve))
return NULL;
- }
mnt = kmem_cache_zalloc(mnt_cache, GFP_KERNEL);
if (mnt) {
@@ -3380,6 +3376,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
if (ret)
get_ve(ve);
+ else
+ ve_failcount_inc(ve, mnt);
return ret;
}
diff --git a/include/linux/ve.h b/include/linux/ve.h
index 969390bb511e7..0395e37208849 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -103,6 +103,7 @@ struct ve_struct {
* other containers.
*/
atomic_t mnt_avail_nr; /* number of available VE mounts */
+ atomic_t mnt_failcount;
int mnt_max_nr;
#ifdef CONFIG_COREDUMP
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index 56936ef4cc92b..4c3acda6b135e 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -118,6 +118,7 @@ struct ve_struct ve0 = {
.nd_neigh_nr = ATOMIC_INIT(0),
.mnt_avail_nr = ATOMIC_INIT(INT_MAX),
.mnt_max_nr = INT_MAX,
+ .mnt_failcount = ATOMIC_INIT(0),
#ifdef CONFIG_AIO
.aio_nr_lock = __SPIN_LOCK_UNLOCKED(ve0.aio_nr_lock),
.aio_nr = 0,
@@ -829,6 +830,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
atomic_set(&ve->arp_neigh_nr, 0);
atomic_set(&ve->nd_neigh_nr, 0);
ve->mnt_max_nr = MNT_MAX_NR_DEFAULT;
+ atomic_set(&ve->mnt_failcount, 0);
atomic_set(&ve->mnt_avail_nr, MNT_MAX_NR_DEFAULT);
#ifdef CONFIG_COREDUMP
@@ -1087,6 +1089,7 @@ static const struct ve_failcount_entry {
} ve_failcounts[] = {
#define VE_FC_ENTRY(name) { #name, offsetof(struct ve_struct, name##_failcount) }
VE_FC_ENTRY(netns),
+ VE_FC_ENTRY(mnt),
{}
};
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* [Devel] [PATCH hci-8.1 v10 07/13] ve: introduce per-VE failcounter infrastructure
2026-09-25 16:53 3% ` [Devel] [PATCH hci-8.1 v10 02/13] ve/fs: count available mounts instead of used ones Konstantin Khorenko
@ 2026-09-25 16:53 4% ` Konstantin Khorenko
2026-09-25 16:53 4% ` [Devel] [PATCH hci-8.1 v10 09/13] ve: add mnt failcounter Konstantin Khorenko
` (2 subsequent siblings)
4 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:53 UTC (permalink / raw)
To: Vasileios Almpanis, Pavel Tikhomirov,
cover.1789491989.git.vladimir.riabchun, Konstantin Khorenko,
devel
From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
It may be useful to have a history of resource limit hits for every VE,
this may simplify debugging and provide some information about the
resources usage.
This information is provided by ve.failcount file, any write to it
resets all failcounts.
To add a new failcounter we need to create a new atomic_t field
name_failcount in ve structure and add a new VE_FC_ENTRY in
ve_failcounts array. The counters themselves come in the follow-up
patches, one per resource.
The neighbour table overflow will not be counted for now: neigh_alloc
can run in softirq context, where ve log must not be used, and
ve_failcount_inc writes to it. The neigh_tbl counter will be added
together with the ve_printk softirq fix (VSTOR-146839).
https://virtuozzo.atlassian.net/browse/VSTOR-135520
Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
include/linux/ve.h | 10 ++++++++++
kernel/ve/ve.c | 45 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 55 insertions(+)
diff --git a/include/linux/ve.h b/include/linux/ve.h
index fabc608e31f51..546060eddc765 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -151,6 +151,16 @@ extern int nr_ve;
#define BPF_PROG_MAX_NR_DEFAULT 256 /* number of loaded BPF progs per-VE */
#define MNT_MAX_NR_DEFAULT 4096 /* number of mounts per-VE */
+#define ve_failcount_inc(ve, name) \
+do { \
+ struct ve_struct *__ve = (ve); \
+ \
+ atomic_inc(&__ve->name##_failcount); \
+ ve_pr_warn_ratelimited(VE_LOG_BOTH, "CT%s: hits the " #name \
+ " limit, see the '" #name "' counter in ve.failcount " \
+ "of the container's ve cgroup\n", ve_name(__ve)); \
+} while (0)
+
#ifdef CONFIG_VE
static inline void ve_set_state(struct ve_struct *ve, int new_state)
{
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index c4e5c000bc427..f915f86e7471e 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -1079,6 +1079,45 @@ VE_RESOURCE(mnt);
VE_RESOURCE(netif);
VE_RESOURCE(bpf_prog);
+static const struct ve_failcount_entry {
+ const char *name;
+ size_t offset;
+} ve_failcounts[] = {
+#define VE_FC_ENTRY(name) { #name, offsetof(struct ve_struct, name##_failcount) }
+ {}
+};
+
+static int ve_failcount_read(struct seq_file *sf, void *v)
+{
+ struct ve_struct *ve = css_to_ve(seq_css(sf));
+ const struct ve_failcount_entry *entry;
+ atomic_t *fc;
+
+ for (entry = ve_failcounts; entry->name; entry++) {
+ fc = (void *)ve + entry->offset;
+ seq_printf(sf, "%s: %d\n", entry->name, atomic_read(fc));
+ }
+ return 0;
+}
+
+static ssize_t ve_failcount_write(struct kernfs_open_file *of, char *buf,
+ size_t nbytes, loff_t off)
+{
+ struct ve_struct *ve = css_to_ve(of_css(of));
+ const struct ve_failcount_entry *entry;
+ atomic_t *fc;
+
+ if (!ve_is_super(get_exec_env()) && !ve->is_pseudosuper)
+ return -EPERM;
+
+ for (entry = ve_failcounts; entry->name; entry++) {
+ fc = (void *)ve + entry->offset;
+ atomic_set(fc, 0);
+ }
+
+ return nbytes;
+}
+
static int ve_os_release_read(struct seq_file *sf, void *v)
{
struct cgroup_subsys_state *css = seq_css(sf);
@@ -1618,6 +1657,12 @@ static struct cftype ve_cftypes[] = {
.flags = CFTYPE_NOT_ON_ROOT,
.write_u64 = ve_rpc_kill_write,
},
+ {
+ .name = "failcount",
+ .flags = CFTYPE_NOT_ON_ROOT,
+ .seq_show = ve_failcount_read,
+ .write = ve_failcount_write,
+ },
{ }
};
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* [Devel] [PATCH hci-8.1 v10 02/13] ve/fs: count available mounts instead of used ones
@ 2026-09-25 16:53 3% ` Konstantin Khorenko
2026-09-25 16:53 4% ` [Devel] [PATCH hci-8.1 v10 07/13] ve: introduce per-VE failcounter infrastructure Konstantin Khorenko
` (3 subsequent siblings)
4 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:53 UTC (permalink / raw)
To: Vasileios Almpanis, Pavel Tikhomirov,
cover.1789491989.git.vladimir.riabchun, Konstantin Khorenko,
devel
From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Previous approach with current mounts counter had an issue:
there was a gap between ve_mount_allowed check and ve_mount_nr_inc,
which could allow CT to have more mounts than expected.
Fix this by tracking the number of available mounts instead
of current ones. This also makes resources accounting
more consistent - we are using ***_avail_nr approach more.
One more issue with inconsistent ve value is fixed:
ve_mount_allowed always used ve from get_exec_env, but
ve_mount_nr_inc operated with owner_ve.
Now actual ve value is calculated in the beginning of alloc_vfsmnt.
To avoid incorrect accounting when is_pseudosuper is changed,
update avail_nr count without > 0 check if VE is ve0 or pseudosuper.
This also simplifies ve_mount_put, since increment is
now unconditional.
https://virtuozzo.atlassian.net/browse/VSTOR-135520
Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Reviewed-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
fs/namespace.c | 78 ++++++++++++++++++++++++++++++++----------------------
include/linux/ve.h | 2 +-
kernel/ve/ve.c | 12 ++++-----
3 files changed, 54 insertions(+), 38 deletions(-)
diff --git a/fs/namespace.c b/fs/namespace.c
index 4adc1db84b4c0..1d6e66cd71745 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -317,19 +317,22 @@ int mnt_get_count(struct mount *mnt)
#endif
}
-static inline int ve_mount_allowed(void);
-static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve);
-static inline void ve_mount_nr_dec(struct mount *mnt);
+static inline int ve_try_reserve_mount(struct ve_struct *ve);
+static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve);
static inline void ve_mount_reown(struct mount *mnt, struct ve_struct *ve);
static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
{
struct mount *mnt;
+ struct ve_struct *ve = owner_ve;
- if (!ve_mount_allowed()) {
+ if (!ve)
+ ve = get_exec_env();
+
+ if (!ve_try_reserve_mount(ve)) {
pr_warn_ratelimited(
"CT#%s reached the limit on mounts.\n",
- ve_name(get_exec_env()));
+ ve_name(ve));
return NULL;
}
@@ -337,6 +340,14 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
if (mnt) {
int err;
+#ifdef CONFIG_VE
+ /*
+ * Got ve reference in ve_try_reserve_mount, set mnt ve data
+ * here, so in case of error ve_mount_put sees correct info.
+ */
+ mnt->ve_owner = ve;
+#endif
+
err = mnt_alloc_id(mnt);
if (err)
goto out_free_cache;
@@ -371,7 +382,8 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
INIT_LIST_HEAD(&mnt->mnt_umounting);
INIT_HLIST_HEAD(&mnt->mnt_stuck_children);
mnt->mnt.mnt_idmap = &nop_mnt_idmap;
- ve_mount_nr_inc(mnt, owner_ve);
+ } else {
+ ve_mount_put(mnt, ve);
}
return mnt;
@@ -382,6 +394,8 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
out_free_id:
mnt_free_id(mnt);
out_free_cache:
+ /* Got ve reference in ve_try_reserve_mount */
+ ve_mount_put(mnt, ve);
kmem_cache_free(mnt_cache, mnt);
return NULL;
}
@@ -751,7 +765,9 @@ int sb_prepare_remount_readonly(struct super_block *sb)
static void free_vfsmnt(struct mount *mnt)
{
mnt_idmap_put(mnt_idmap(&mnt->mnt));
- ve_mount_nr_dec(mnt);
+#ifdef CONFIG_VE
+ ve_mount_put(mnt, mnt->ve_owner);
+#endif
kfree_const(mnt->mnt_devname);
#ifdef CONFIG_SMP
free_percpu(mnt->mnt_pcp);
@@ -3214,7 +3230,7 @@ int ve_devmnt_process(struct ve_struct *ve, dev_t dev, void **data_pp, int remou
if (devmnt->dev == dev) {
err = ve_devmnt_check(data, devmnt->allowed_options);
/*
- * In case of @is_pseudouser set, ie restore procedure,
+ * In case of @is_pseudosuper set, ie restore procedure,
* we don't check for allowed options filtering, since
* restore mode is special.
*/
@@ -3353,30 +3369,30 @@ int ve_devmnt_verify(struct ve_struct *ve, dev_t dev, char *opts, bool new_mount
return err;
}
-static inline int ve_mount_allowed(void)
-{
- struct ve_struct *ve = get_exec_env();
-
- return ve_is_super(ve) || ve->is_pseudosuper ||
- atomic_read(&ve->mnt_nr) < (int)sysctl_ve_mount_nr;
-}
-
-static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve)
+static inline int ve_try_reserve_mount(struct ve_struct *ve)
{
- if (!ve)
- ve = get_exec_env();
+ int ret = ve_is_super(ve) || ve->is_pseudosuper;
+ /* Ignore limits in ve0 and pseudosuper cases, but still count. */
+ if (ret)
+ atomic_dec(&ve->mnt_avail_nr);
+ else
+ ret = atomic_dec_if_positive(&ve->mnt_avail_nr) >= 0;
- mnt->ve_owner = get_ve(ve);
- atomic_inc(&ve->mnt_nr);
+ if (ret)
+ get_ve(ve);
+ return ret;
}
-static inline void ve_mount_nr_dec(struct mount *mnt)
+static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve)
{
- struct ve_struct *ve = mnt->ve_owner;
-
- atomic_dec(&ve->mnt_nr);
+ /*
+ * ve argument is needed to reuse this function in alloc_vfsmnt error path.
+ * Other users should pass mnt->ve_owner value.
+ */
+ atomic_inc(&ve->mnt_avail_nr);
put_ve(ve);
- mnt->ve_owner = NULL;
+ if (mnt)
+ mnt->ve_owner = NULL;
}
/*
@@ -3397,9 +3413,9 @@ static inline void ve_mount_reown(struct mount *mnt, struct ve_struct *ve)
if (old == ve)
return;
- atomic_inc(&ve->mnt_nr);
+ atomic_dec(&ve->mnt_avail_nr);
WRITE_ONCE(mnt->ve_owner, get_ve(ve));
- atomic_dec(&old->mnt_nr);
+ atomic_inc(&old->mnt_avail_nr);
put_ve(old);
}
@@ -3422,10 +3438,10 @@ bool is_sb_ve_accessible(struct ve_struct *ve, struct super_block *sb)
#else /* CONFIG_VE */
-static inline int ve_mount_allowed(void) { return 1; }
-static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve) { }
-static inline void ve_mount_nr_dec(struct mount *mnt) { }
+static inline int ve_try_reserve_mount(struct ve_struct *ve) { return 1; }
+static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve) { }
static inline void ve_mount_reown(struct mount *mnt, struct ve_struct *ve) { }
+
#endif /* CONFIG_VE */
/*
diff --git a/include/linux/ve.h b/include/linux/ve.h
index f946abe4d8596..65c058c0f34d4 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -96,7 +96,7 @@ struct ve_struct {
atomic_t nd_neigh_nr;
unsigned long meminfo_val;
- atomic_t mnt_nr; /* number of present VE mounts */
+ atomic_t mnt_avail_nr; /* number of available VE mounts */
#ifdef CONFIG_COREDUMP
char core_pattern[CORENAME_MAX_SIZE];
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index e3ae2e5d9a86c..bed8a58af0853 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -82,7 +82,7 @@ struct ve_struct ve0 = {
.arp_neigh_nr = ATOMIC_INIT(0),
.nd_neigh_nr = ATOMIC_INIT(0),
- .mnt_nr = ATOMIC_INIT(0),
+ .mnt_avail_nr = ATOMIC_INIT(INT_MAX),
.meminfo_val = VE_MEMINFO_SYSTEM,
.umh_running_helpers = ATOMIC_INIT(0),
.umh_helpers_waitq = __WAIT_QUEUE_HEAD_INITIALIZER(ve0.umh_helpers_waitq),
@@ -787,7 +787,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
atomic_set(&ve->arp_neigh_nr, 0);
atomic_set(&ve->nd_neigh_nr, 0);
- atomic_set(&ve->mnt_nr, 0);
+ atomic_set(&ve->mnt_avail_nr, sysctl_ve_mount_nr);
#ifdef CONFIG_COREDUMP
strcpy(ve->core_pattern, "core");
@@ -1063,9 +1063,9 @@ static u64 ve_netns_avail_nr_read(struct cgroup_subsys_state *css, struct cftype
return atomic_read(&css_to_ve(css)->netns_avail_nr);
}
-static u64 ve_mnt_nr_read(struct cgroup_subsys_state *css, struct cftype *cft)
+static s64 ve_mnt_avail_nr_read(struct cgroup_subsys_state *css, struct cftype *cft)
{
- return atomic_read(&css_to_ve(css)->mnt_nr);
+ return atomic_read(&css_to_ve(css)->mnt_avail_nr);
}
static u64 ve_netif_max_nr_read(struct cgroup_subsys_state *css, struct cftype *cft)
@@ -1627,8 +1627,8 @@ static struct cftype ve_cftypes[] = {
.read_u64 = ve_netns_avail_nr_read,
},
{
- .name = "mnt_nr",
- .read_u64 = ve_mnt_nr_read,
+ .name = "mnt_avail_nr",
+ .read_s64 = ve_mnt_avail_nr_read,
},
{
.name = "netif_max_nr",
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH VZ10 v9 6/8] ve: Introduce per-VE failcount
2026-09-15 17:22 9% ` [Devel] [PATCH VZ10 v9 6/8] ve: Introduce per-VE failcount Vladimir Riabchun
2026-09-18 14:42 0% ` Vladimir Riabchun
2026-09-25 16:02 0% ` Konstantin Khorenko
@ 2026-09-25 16:08 4% ` Konstantin Khorenko
2 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:08 UTC (permalink / raw)
To: Vladimir Riabchun, devel
On 9/15/26 19:22, Vladimir Riabchun wrote:
> It may be useful to have a history of resource limit hits for every VE,
> this may simplify debugging and provide some information about the
> resources usage.
>
> This information is provided by ve.failcount file, any write to it
> resets all failcounts.
>
> To add a new failcounter we need to create a new atomic_t field
> name_failcount in ve structure and add a new VE_FC_ENTRY in
> ve_failcounts array.
>
> One change, unrelated to failcounts: aio fields are now initialized
> in ve0.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-135520
>
> Feature: per-ve failcounters
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> ---
> fs/aio.c | 1 +
> fs/namespace.c | 2 ++
> include/linux/ve.h | 16 +++++++++
> kernel/bpf/syscall.c | 1 +
> kernel/ve/ve.c | 70 ++++++++++++++++++++++++++++++++++++++++
> net/core/dev.c | 10 ++----
> net/core/neighbour.c | 3 +-
> net/core/net_namespace.c | 4 ++-
> 8 files changed, 96 insertions(+), 11 deletions(-)
>
> diff --git a/fs/aio.c b/fs/aio.c
> index cb63416af135..4ae06bcd47f2 100644
> --- a/fs/aio.c
> +++ b/fs/aio.c
> @@ -814,6 +814,7 @@ static struct kioctx *ioctx_alloc(unsigned nr_events)
> spin_lock(&ve->aio_nr_lock);
> if (ve->aio_nr + ctx->max_reqs > ve->aio_max_nr ||
> ve->aio_nr + ctx->max_reqs < ve->aio_nr) {
> + ve_failcount_inc(ve, aio);
> spin_unlock(&ve->aio_nr_lock);
> err = -EAGAIN;
> goto err_ctx;
> diff --git a/fs/namespace.c b/fs/namespace.c
> index 1d6e66cd7174..ea01664ff5f2 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -3380,6 +3380,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
>
> if (ret)
> get_ve(ve);
> + else
> + ve_failcount_inc(ve, mnt);
> return ret;
> }
Now every failed mount produces two ratelimited warnings in the host
log: the pre-existing one in alloc_vfsmnt()
pr_warn_ratelimited("CT#%s reached the limit on mounts.\n", ...);
and the one printed by ve_failcount_inc(). They are ratelimited
independently, so the log gets both messages for the same event.
Please drop the alloc_vfsmnt() one in this patch: the
ve_failcount_inc() message carries strictly more information (it also
names the counter to look at) and additionally goes to the container
log.
>
> diff --git a/include/linux/ve.h b/include/linux/ve.h
> index fabc608e31f5..065129b42768 100644
> --- a/include/linux/ve.h
> +++ b/include/linux/ve.h
> @@ -80,12 +80,15 @@ struct ve_struct {
> struct kmapset_key proc_perms_key;
>
> atomic_t netns_avail_nr;
> + atomic_t netns_failcount;
> int netns_max_nr;
>
> atomic_t netif_avail_nr;
> + atomic_t netif_failcount;
> int netif_max_nr;
>
> atomic_t bpf_prog_avail_nr;
> + atomic_t bpf_prog_failcount;
> int bpf_prog_max_nr;
>
> atomic64_t _uevent_seqnum;
> @@ -94,6 +97,7 @@ struct ve_struct {
>
> atomic_t arp_neigh_nr;
> atomic_t nd_neigh_nr;
> + atomic_t neigh_tbl_failcount;
> unsigned long meminfo_val;
>
> /*
> @@ -102,6 +106,7 @@ struct ve_struct {
> * other containers.
> */
> atomic_t mnt_avail_nr; /* number of available VE mounts */
> + atomic_t mnt_failcount;
> int mnt_max_nr;
>
> #ifdef CONFIG_COREDUMP
> @@ -129,6 +134,7 @@ struct ve_struct {
> spinlock_t aio_nr_lock;
> unsigned long aio_nr;
> unsigned long aio_max_nr;
> + atomic_t aio_failcount;
> #endif
> struct vfsmount *devtmpfs_mnt;
> };
> @@ -151,6 +157,16 @@ extern int nr_ve;
> #define BPF_PROG_MAX_NR_DEFAULT 256 /* number of loaded BPF progs per-VE */
> #define MNT_MAX_NR_DEFAULT 4096 /* number of mounts per-VE */
>
> +#define ve_failcount_inc(ve, name) \
> +do { \
> + struct ve_struct *__ve = (ve); \
> + \
> + atomic_inc(&__ve->name##_failcount); \
> + ve_pr_warn_ratelimited(VE_LOG_BOTH, "CT%s: hits the " #name \
> + " limit, see the '" #name "' counter in ve.failcount " \
> + "of the container's ve cgroup\n", ve_name(__ve)); \
> +} while (0)
> +
> #ifdef CONFIG_VE
> static inline void ve_set_state(struct ve_struct *ve, int new_state)
> {
> diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
> index c94d4240e3d3..8498bc9df7ea 100644
> --- a/kernel/bpf/syscall.c
> +++ b/kernel/bpf/syscall.c
> @@ -2891,6 +2891,7 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
> if (!bpf_cap && type == BPF_PROG_TYPE_CGROUP_DEVICE) {
> load_ve = get_exec_env();
> if (atomic_dec_if_positive(&load_ve->bpf_prog_avail_nr) < 0) {
> + ve_failcount_inc(load_ve, bpf_prog);
> load_ve = NULL;
> err = -ENOSPC;
> goto put_token;
> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
> index 5ebdb4b7baa7..9261565208e6 100644
> --- a/kernel/ve/ve.c
> +++ b/kernel/ve/ve.c
> @@ -100,10 +100,13 @@ struct ve_struct ve0 = {
> .features = -1,
> .sched_lat_ve.cur = &ve0_lat_stats,
> .netns_avail_nr = ATOMIC_INIT(INT_MAX),
> + .netns_failcount = ATOMIC_INIT(0),
> .netns_max_nr = INT_MAX,
> .netif_avail_nr = ATOMIC_INIT(INT_MAX),
> + .netif_failcount = ATOMIC_INIT(0),
> .netif_max_nr = INT_MAX,
> .bpf_prog_avail_nr = ATOMIC_INIT(INT_MAX),
> + .bpf_prog_failcount = ATOMIC_INIT(0),
> .bpf_prog_max_nr = INT_MAX,
> .fsync_enable = FSYNC_FILTERED,
> ._randomize_va_space =
> @@ -115,8 +118,16 @@ struct ve_struct ve0 = {
>
> .arp_neigh_nr = ATOMIC_INIT(0),
> .nd_neigh_nr = ATOMIC_INIT(0),
> + .neigh_tbl_failcount = ATOMIC_INIT(0),
> .mnt_avail_nr = ATOMIC_INIT(INT_MAX),
> .mnt_max_nr = INT_MAX,
> + .mnt_failcount = ATOMIC_INIT(0),
> +#ifdef CONFIG_AIO
> + .aio_nr_lock = __SPIN_LOCK_UNLOCKED(aio_nr_lock),
> + .aio_nr = 0,
> + .aio_max_nr = AIO_MAX_NR_DEFAULT,
> + .aio_failcount = ATOMIC_INIT(0),
> +#endif
> .meminfo_val = VE_MEMINFO_SYSTEM,
> #ifdef CONFIG_X86
> .umh_running_helpers = ATOMIC_INIT(0),
> @@ -787,12 +798,15 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
> ve->fsync_enable = FSYNC_FILTERED;
>
> atomic_set(&ve->netns_avail_nr, NETNS_MAX_NR_DEFAULT);
> + atomic_set(&ve->netns_failcount, 0);
> ve->netns_max_nr = NETNS_MAX_NR_DEFAULT;
>
> atomic_set(&ve->netif_avail_nr, NETIF_MAX_NR_DEFAULT);
> + atomic_set(&ve->netif_failcount, 0);
> ve->netif_max_nr = NETIF_MAX_NR_DEFAULT;
>
> atomic_set(&ve->bpf_prog_avail_nr, BPF_PROG_MAX_NR_DEFAULT);
> + atomic_set(&ve->bpf_prog_failcount, 0);
> ve->bpf_prog_max_nr = BPF_PROG_MAX_NR_DEFAULT;
>
> err = ve_log_init(ve);
> @@ -821,7 +835,9 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
>
> atomic_set(&ve->arp_neigh_nr, 0);
> atomic_set(&ve->nd_neigh_nr, 0);
> + atomic_set(&ve->neigh_tbl_failcount, 0);
> ve->mnt_max_nr = MNT_MAX_NR_DEFAULT;
> + atomic_set(&ve->mnt_failcount, 0);
> atomic_set(&ve->mnt_avail_nr, MNT_MAX_NR_DEFAULT);
>
> #ifdef CONFIG_COREDUMP
> @@ -834,6 +850,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
> spin_lock_init(&ve->aio_nr_lock);
> ve->aio_nr = 0;
> ve->aio_max_nr = AIO_MAX_NR_DEFAULT;
> + atomic_set(&ve->aio_failcount, 0);
> #endif
>
> return &ve->css;
> @@ -1074,6 +1091,53 @@ VE_RESOURCE(mnt);
> VE_RESOURCE(netif);
> VE_RESOURCE(bpf_prog);
>
> +static const struct ve_failcount_entry {
> + const char *name;
> + size_t offset;
> +} ve_failcounts[] = {
> +#define VE_FC_ENTRY(name) { #name, offsetof(struct ve_struct, name##_failcount) }
> + VE_FC_ENTRY(netns),
> + VE_FC_ENTRY(mnt),
> + VE_FC_ENTRY(netif),
> + VE_FC_ENTRY(bpf_prog),
> + VE_FC_ENTRY(neigh_tbl),
> +#ifdef CONFIG_AIO
> + VE_FC_ENTRY(aio),
> +#endif
> + {}
> +};
> +
> +static int ve_failcount_read(struct seq_file *sf, void *v)
> +{
> + struct ve_struct *ve = css_to_ve(seq_css(sf));
> + const struct ve_failcount_entry *entry;
> + atomic_t *fc;
> +
> + for (entry = ve_failcounts; entry->name; entry++) {
> + fc = (void *)ve + entry->offset;
> + seq_printf(sf, "%s: %d\n", entry->name, atomic_read(fc));
> + }
> + return 0;
> +}
> +
> +static ssize_t ve_failcount_write(struct kernfs_open_file *of, char *buf,
> + size_t nbytes, loff_t off)
> +{
> + struct ve_struct *ve = css_to_ve(of_css(of));
> + const struct ve_failcount_entry *entry;
> + atomic_t *fc;
> +
> + if (!ve_is_super(get_exec_env()) && !ve->is_pseudosuper)
> + return -EPERM;
> +
> + for (entry = ve_failcounts; entry->name; entry++) {
> + fc = (void *)ve + entry->offset;
> + atomic_set(fc, 0);
> + }
> +
> + return nbytes;
> +}
> +
> static int ve_os_release_read(struct seq_file *sf, void *v)
> {
> struct cgroup_subsys_state *css = seq_css(sf);
> @@ -1613,6 +1677,12 @@ static struct cftype ve_cftypes[] = {
> .flags = CFTYPE_NOT_ON_ROOT,
> .write_u64 = ve_rpc_kill_write,
> },
> + {
> + .name = "failcount",
> + .flags = CFTYPE_NOT_ON_ROOT,
> + .seq_show = ve_failcount_read,
> + .write = ve_failcount_write,
> + },
> { }
> };
>
> diff --git a/net/core/dev.c b/net/core/dev.c
> index c7dddb200489..7632ad1fd015 100644
> --- a/net/core/dev.c
> +++ b/net/core/dev.c
> @@ -10997,10 +10997,7 @@ int register_netdevice(struct net_device *dev)
>
> ret = -ENOMEM;
> if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
> - ve_pr_warn_ratelimited(VE_LOG_BOTH,
> - "CT%s: hits max number of network devices, "
> - "increase ve::netif_max_nr parameter\n",
> - ve_name(net->owner_ve));
> + ve_failcount_inc(net->owner_ve, netif);
> return ret;
> }
>
> @@ -12211,10 +12208,7 @@ int __dev_change_net_namespace(struct net_device *dev, struct net *net,
>
> err = -ENOMEM;
> if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
> - ve_pr_warn_ratelimited(VE_LOG_BOTH,
> - "CT%s: hits max number of network devices, "
> - "increase ve::netif_max_nr parameter\n",
> - ve_name(net->owner_ve));
> + ve_failcount_inc(net->owner_ve, netif);
> goto out;
> }
> atomic_inc(&dev_net(dev)->owner_ve->netif_avail_nr);
> diff --git a/net/core/neighbour.c b/net/core/neighbour.c
> index f90deb17fb25..c7d5e6df50c2 100644
> --- a/net/core/neighbour.c
> +++ b/net/core/neighbour.c
> @@ -520,8 +520,7 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
> (glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
> time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
> if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
> - net_info_ratelimited("%s: neighbor table overflow!\n",
> - tbl->id);
> + ve_failcount_inc(ve, neigh_tbl);
> NEIGH_CACHE_STAT_INC(tbl, table_fulls);
> goto out_entries;
> }
> diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c
> index b3d54cad984a..328e1c96b6f4 100644
> --- a/net/core/net_namespace.c
> +++ b/net/core/net_namespace.c
> @@ -486,8 +486,10 @@ void net_drop_ns(void *p)
> #ifdef CONFIG_VE
> static int dec_netns_avail(struct ve_struct *ve)
> {
> - if (atomic_dec_if_positive(&ve->netns_avail_nr) < 0)
> + if (atomic_dec_if_positive(&ve->netns_avail_nr) < 0) {
> + ve_failcount_inc(ve, netns);
> return -ENOSPC;
> + }
> return 0;
> }
>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* Re: [Devel] [PATCH VZ10 v9 8/8] selftests/ve: Add mount accounting selftest
2026-09-15 17:22 6% ` [Devel] [PATCH VZ10 v9 8/8] selftests/ve: Add mount accounting selftest Vladimir Riabchun
2026-09-18 15:06 0% ` Vladimir Riabchun
@ 2026-09-25 16:04 0% ` Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:04 UTC (permalink / raw)
To: Vladimir Riabchun, devel
On 9/15/26 19:22, Vladimir Riabchun wrote:
> There are 6 test cases, covered in the new test:
> 1. Simple mount accouting correctness, just mount/umount.
> 2. Verification of correct limit hits and changes, including
> negative values.
> 3. Partial mounts test, when mount limit is hit in the middle
> of creation.
> 4. Test that enabled pseudosuper allows overuse.
> 5. Test that pseudosuper doesn't affect mount accounting.
> 6. Failcount feature verification.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-135520
>
> Feature: per-ve failcounters
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> ---
> tools/testing/selftests/ve/.gitignore | 1 +
> tools/testing/selftests/ve/Makefile | 1 +
> .../selftests/ve/ve_mount_accounting_test.c | 550 ++++++++++++++++++
> tools/testing/selftests/ve/ve_selftest.h | 21 +
> 4 files changed, 573 insertions(+)
> create mode 100644 tools/testing/selftests/ve/ve_mount_accounting_test.c
>
<skipped>
> + * We can't enforce workqueue flush from userspace, so instead
> + * read several times until the value is the one we expect.
> + */
> +static int wait_on_mnt_avail(FIXTURE_DATA(ve_mnt_acc) * self, int expected)
> +{
> +#define WAIT_TIMEOUT_NS (4096UL * 1000 * 1000)
> + char path_avail[64];
> + int mnt_avail_nr = 0;
> + struct timespec delay = { .tv_nsec = 1000000 };
> +
> + snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
> +
> + while (delay.tv_nsec <= WAIT_TIMEOUT_NS) {
> + if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr)) {
> + fprintf(stderr, "Failed to read from %s\n", path_avail);
> + return 1;
> + }
> + if (mnt_avail_nr == expected)
> + return 0;
> +
> + do_rcu_barrier();
> + if (nanosleep(&delay, NULL)) {
> + fprintf(stderr, "nanosleep failed: %s\n", strerror(errno));
> + return 1;
> + }
> + delay.tv_nsec *= 2;
nanosleep() requires tv_nsec < 1e9 and fails with EINVAL otherwise.
The doubling goes 1ms, 2ms, ..., 512ms, and the next step is 1024ms,
which no longer fits tv_nsec. So the loop never gets anywhere near
WAIT_TIMEOUT_NS: after ~1s of total sleeping the next iteration bails
out with "nanosleep failed: Invalid argument" and returns 1, instead
of the intended ~8s of waiting and the timeout report with the
expected/read values.
This defeats the purpose of the helper: on a loaded machine, where
the counter takes a while to settle, the test fails early with a
misleading error - exactly the case this function was reworked for
in v9.
Please keep the delay in a plain nanoseconds variable and normalize
it before sleeping:
delay.tv_sec = ns / 1000000000;
delay.tv_nsec = ns % 1000000000;
As a side note, tv_nsec is long, so 2048ms would overflow it on a
32-bit build; the normalization fixes that too.
<skipped>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH VZ10 v9 6/8] ve: Introduce per-VE failcount
2026-09-15 17:22 9% ` [Devel] [PATCH VZ10 v9 6/8] ve: Introduce per-VE failcount Vladimir Riabchun
2026-09-18 14:42 0% ` Vladimir Riabchun
@ 2026-09-25 16:02 0% ` Konstantin Khorenko
2026-09-25 16:08 4% ` Konstantin Khorenko
2 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 16:02 UTC (permalink / raw)
To: Vladimir Riabchun, devel
i will drop neighbor related failcounter from this patch as it increases the amount of printks inside irq
--
Best regards,
Konstantin Khorenko,
Virtuozzo Linux Kernel Team
On 9/15/26 19:22, Vladimir Riabchun wrote:
> It may be useful to have a history of resource limit hits for every VE,
> this may simplify debugging and provide some information about the
> resources usage.
>
> This information is provided by ve.failcount file, any write to it
> resets all failcounts.
>
> To add a new failcounter we need to create a new atomic_t field
> name_failcount in ve structure and add a new VE_FC_ENTRY in
> ve_failcounts array.
>
> One change, unrelated to failcounts: aio fields are now initialized
> in ve0.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-135520
>
> Feature: per-ve failcounters
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> ---
> fs/aio.c | 1 +
> fs/namespace.c | 2 ++
> include/linux/ve.h | 16 +++++++++
> kernel/bpf/syscall.c | 1 +
> kernel/ve/ve.c | 70 ++++++++++++++++++++++++++++++++++++++++
> net/core/dev.c | 10 ++----
> net/core/neighbour.c | 3 +-
> net/core/net_namespace.c | 4 ++-
> 8 files changed, 96 insertions(+), 11 deletions(-)
>
> diff --git a/fs/aio.c b/fs/aio.c
> index cb63416af135..4ae06bcd47f2 100644
> --- a/fs/aio.c
> +++ b/fs/aio.c
> @@ -814,6 +814,7 @@ static struct kioctx *ioctx_alloc(unsigned nr_events)
> spin_lock(&ve->aio_nr_lock);
> if (ve->aio_nr + ctx->max_reqs > ve->aio_max_nr ||
> ve->aio_nr + ctx->max_reqs < ve->aio_nr) {
> + ve_failcount_inc(ve, aio);
> spin_unlock(&ve->aio_nr_lock);
> err = -EAGAIN;
> goto err_ctx;
> diff --git a/fs/namespace.c b/fs/namespace.c
> index 1d6e66cd7174..ea01664ff5f2 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -3380,6 +3380,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
>
> if (ret)
> get_ve(ve);
> + else
> + ve_failcount_inc(ve, mnt);
> return ret;
> }
>
> diff --git a/include/linux/ve.h b/include/linux/ve.h
> index fabc608e31f5..065129b42768 100644
> --- a/include/linux/ve.h
> +++ b/include/linux/ve.h
> @@ -80,12 +80,15 @@ struct ve_struct {
> struct kmapset_key proc_perms_key;
>
> atomic_t netns_avail_nr;
> + atomic_t netns_failcount;
> int netns_max_nr;
>
> atomic_t netif_avail_nr;
> + atomic_t netif_failcount;
> int netif_max_nr;
>
> atomic_t bpf_prog_avail_nr;
> + atomic_t bpf_prog_failcount;
> int bpf_prog_max_nr;
>
> atomic64_t _uevent_seqnum;
> @@ -94,6 +97,7 @@ struct ve_struct {
>
> atomic_t arp_neigh_nr;
> atomic_t nd_neigh_nr;
> + atomic_t neigh_tbl_failcount;
> unsigned long meminfo_val;
>
> /*
> @@ -102,6 +106,7 @@ struct ve_struct {
> * other containers.
> */
> atomic_t mnt_avail_nr; /* number of available VE mounts */
> + atomic_t mnt_failcount;
> int mnt_max_nr;
>
> #ifdef CONFIG_COREDUMP
> @@ -129,6 +134,7 @@ struct ve_struct {
> spinlock_t aio_nr_lock;
> unsigned long aio_nr;
> unsigned long aio_max_nr;
> + atomic_t aio_failcount;
> #endif
> struct vfsmount *devtmpfs_mnt;
> };
> @@ -151,6 +157,16 @@ extern int nr_ve;
> #define BPF_PROG_MAX_NR_DEFAULT 256 /* number of loaded BPF progs per-VE */
> #define MNT_MAX_NR_DEFAULT 4096 /* number of mounts per-VE */
>
> +#define ve_failcount_inc(ve, name) \
> +do { \
> + struct ve_struct *__ve = (ve); \
> + \
> + atomic_inc(&__ve->name##_failcount); \
> + ve_pr_warn_ratelimited(VE_LOG_BOTH, "CT%s: hits the " #name \
> + " limit, see the '" #name "' counter in ve.failcount " \
> + "of the container's ve cgroup\n", ve_name(__ve)); \
> +} while (0)
> +
> #ifdef CONFIG_VE
> static inline void ve_set_state(struct ve_struct *ve, int new_state)
> {
> diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
> index c94d4240e3d3..8498bc9df7ea 100644
> --- a/kernel/bpf/syscall.c
> +++ b/kernel/bpf/syscall.c
> @@ -2891,6 +2891,7 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
> if (!bpf_cap && type == BPF_PROG_TYPE_CGROUP_DEVICE) {
> load_ve = get_exec_env();
> if (atomic_dec_if_positive(&load_ve->bpf_prog_avail_nr) < 0) {
> + ve_failcount_inc(load_ve, bpf_prog);
> load_ve = NULL;
> err = -ENOSPC;
> goto put_token;
> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
> index 5ebdb4b7baa7..9261565208e6 100644
> --- a/kernel/ve/ve.c
> +++ b/kernel/ve/ve.c
> @@ -100,10 +100,13 @@ struct ve_struct ve0 = {
> .features = -1,
> .sched_lat_ve.cur = &ve0_lat_stats,
> .netns_avail_nr = ATOMIC_INIT(INT_MAX),
> + .netns_failcount = ATOMIC_INIT(0),
> .netns_max_nr = INT_MAX,
> .netif_avail_nr = ATOMIC_INIT(INT_MAX),
> + .netif_failcount = ATOMIC_INIT(0),
> .netif_max_nr = INT_MAX,
> .bpf_prog_avail_nr = ATOMIC_INIT(INT_MAX),
> + .bpf_prog_failcount = ATOMIC_INIT(0),
> .bpf_prog_max_nr = INT_MAX,
> .fsync_enable = FSYNC_FILTERED,
> ._randomize_va_space =
> @@ -115,8 +118,16 @@ struct ve_struct ve0 = {
>
> .arp_neigh_nr = ATOMIC_INIT(0),
> .nd_neigh_nr = ATOMIC_INIT(0),
> + .neigh_tbl_failcount = ATOMIC_INIT(0),
> .mnt_avail_nr = ATOMIC_INIT(INT_MAX),
> .mnt_max_nr = INT_MAX,
> + .mnt_failcount = ATOMIC_INIT(0),
> +#ifdef CONFIG_AIO
> + .aio_nr_lock = __SPIN_LOCK_UNLOCKED(aio_nr_lock),
> + .aio_nr = 0,
> + .aio_max_nr = AIO_MAX_NR_DEFAULT,
> + .aio_failcount = ATOMIC_INIT(0),
> +#endif
> .meminfo_val = VE_MEMINFO_SYSTEM,
> #ifdef CONFIG_X86
> .umh_running_helpers = ATOMIC_INIT(0),
> @@ -787,12 +798,15 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
> ve->fsync_enable = FSYNC_FILTERED;
>
> atomic_set(&ve->netns_avail_nr, NETNS_MAX_NR_DEFAULT);
> + atomic_set(&ve->netns_failcount, 0);
> ve->netns_max_nr = NETNS_MAX_NR_DEFAULT;
>
> atomic_set(&ve->netif_avail_nr, NETIF_MAX_NR_DEFAULT);
> + atomic_set(&ve->netif_failcount, 0);
> ve->netif_max_nr = NETIF_MAX_NR_DEFAULT;
>
> atomic_set(&ve->bpf_prog_avail_nr, BPF_PROG_MAX_NR_DEFAULT);
> + atomic_set(&ve->bpf_prog_failcount, 0);
> ve->bpf_prog_max_nr = BPF_PROG_MAX_NR_DEFAULT;
>
> err = ve_log_init(ve);
> @@ -821,7 +835,9 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
>
> atomic_set(&ve->arp_neigh_nr, 0);
> atomic_set(&ve->nd_neigh_nr, 0);
> + atomic_set(&ve->neigh_tbl_failcount, 0);
> ve->mnt_max_nr = MNT_MAX_NR_DEFAULT;
> + atomic_set(&ve->mnt_failcount, 0);
> atomic_set(&ve->mnt_avail_nr, MNT_MAX_NR_DEFAULT);
>
> #ifdef CONFIG_COREDUMP
> @@ -834,6 +850,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
> spin_lock_init(&ve->aio_nr_lock);
> ve->aio_nr = 0;
> ve->aio_max_nr = AIO_MAX_NR_DEFAULT;
> + atomic_set(&ve->aio_failcount, 0);
> #endif
>
> return &ve->css;
> @@ -1074,6 +1091,53 @@ VE_RESOURCE(mnt);
> VE_RESOURCE(netif);
> VE_RESOURCE(bpf_prog);
>
> +static const struct ve_failcount_entry {
> + const char *name;
> + size_t offset;
> +} ve_failcounts[] = {
> +#define VE_FC_ENTRY(name) { #name, offsetof(struct ve_struct, name##_failcount) }
> + VE_FC_ENTRY(netns),
> + VE_FC_ENTRY(mnt),
> + VE_FC_ENTRY(netif),
> + VE_FC_ENTRY(bpf_prog),
> + VE_FC_ENTRY(neigh_tbl),
> +#ifdef CONFIG_AIO
> + VE_FC_ENTRY(aio),
> +#endif
> + {}
> +};
> +
> +static int ve_failcount_read(struct seq_file *sf, void *v)
> +{
> + struct ve_struct *ve = css_to_ve(seq_css(sf));
> + const struct ve_failcount_entry *entry;
> + atomic_t *fc;
> +
> + for (entry = ve_failcounts; entry->name; entry++) {
> + fc = (void *)ve + entry->offset;
> + seq_printf(sf, "%s: %d\n", entry->name, atomic_read(fc));
> + }
> + return 0;
> +}
> +
> +static ssize_t ve_failcount_write(struct kernfs_open_file *of, char *buf,
> + size_t nbytes, loff_t off)
> +{
> + struct ve_struct *ve = css_to_ve(of_css(of));
> + const struct ve_failcount_entry *entry;
> + atomic_t *fc;
> +
> + if (!ve_is_super(get_exec_env()) && !ve->is_pseudosuper)
> + return -EPERM;
> +
> + for (entry = ve_failcounts; entry->name; entry++) {
> + fc = (void *)ve + entry->offset;
> + atomic_set(fc, 0);
> + }
> +
> + return nbytes;
> +}
> +
> static int ve_os_release_read(struct seq_file *sf, void *v)
> {
> struct cgroup_subsys_state *css = seq_css(sf);
> @@ -1613,6 +1677,12 @@ static struct cftype ve_cftypes[] = {
> .flags = CFTYPE_NOT_ON_ROOT,
> .write_u64 = ve_rpc_kill_write,
> },
> + {
> + .name = "failcount",
> + .flags = CFTYPE_NOT_ON_ROOT,
> + .seq_show = ve_failcount_read,
> + .write = ve_failcount_write,
> + },
> { }
> };
>
> diff --git a/net/core/dev.c b/net/core/dev.c
> index c7dddb200489..7632ad1fd015 100644
> --- a/net/core/dev.c
> +++ b/net/core/dev.c
> @@ -10997,10 +10997,7 @@ int register_netdevice(struct net_device *dev)
>
> ret = -ENOMEM;
> if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
> - ve_pr_warn_ratelimited(VE_LOG_BOTH,
> - "CT%s: hits max number of network devices, "
> - "increase ve::netif_max_nr parameter\n",
> - ve_name(net->owner_ve));
> + ve_failcount_inc(net->owner_ve, netif);
> return ret;
> }
>
> @@ -12211,10 +12208,7 @@ int __dev_change_net_namespace(struct net_device *dev, struct net *net,
>
> err = -ENOMEM;
> if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
> - ve_pr_warn_ratelimited(VE_LOG_BOTH,
> - "CT%s: hits max number of network devices, "
> - "increase ve::netif_max_nr parameter\n",
> - ve_name(net->owner_ve));
> + ve_failcount_inc(net->owner_ve, netif);
> goto out;
> }
> atomic_inc(&dev_net(dev)->owner_ve->netif_avail_nr);
> diff --git a/net/core/neighbour.c b/net/core/neighbour.c
> index f90deb17fb25..c7d5e6df50c2 100644
> --- a/net/core/neighbour.c
> +++ b/net/core/neighbour.c
> @@ -520,8 +520,7 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
> (glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
> time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
> if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
> - net_info_ratelimited("%s: neighbor table overflow!\n",
> - tbl->id);
> + ve_failcount_inc(ve, neigh_tbl);
> NEIGH_CACHE_STAT_INC(tbl, table_fulls);
> goto out_entries;
> }
> diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c
> index b3d54cad984a..328e1c96b6f4 100644
> --- a/net/core/net_namespace.c
> +++ b/net/core/net_namespace.c
> @@ -486,8 +486,10 @@ void net_drop_ns(void *p)
> #ifdef CONFIG_VE
> static int dec_netns_avail(struct ve_struct *ve)
> {
> - if (atomic_dec_if_positive(&ve->netns_avail_nr) < 0)
> + if (atomic_dec_if_positive(&ve->netns_avail_nr) < 0) {
> + ve_failcount_inc(ve, netns);
> return -ENOSPC;
> + }
> return 0;
> }
>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* [PATCH QEMU HCI-8.0 v5 11/13] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
` (7 preceding siblings ...)
2026-09-25 14:11 26% ` [PATCH QEMU HCI-8.0 v5 09/13] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 11% ` Andrey Drobyshev
2026-09-25 14:11 20% ` [PATCH QEMU HCI-8.0 v5 13/13] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
On CPR incoming we skip the usb_host_open() rescan, so libusb only sets
up the endpoints from the device's default configuration. If the guest
had enabled an endpoint by selecting a non-zero altsetting (e.g. an
isochronous streaming endpoint), that endpoint is now missing, and the
guest's transfers to it simply stall.
The altsetting itself is restored by vmload now, so let's call
usb_host_ep_update() on the CPR target to rebuild the endpoints out of
it.
The kernel claims are still there on the preserved FD, but libusb's own
handle knows nothing about them, so a later SET_INTERFACE would fail with
LIBUSB_ERROR_NOT_FOUND. Let's re-claim the interfaces on the target as
well to bring libusb's view back in sync - on the preserved FD this is a
no-op at the usbfs level anyway.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 82 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 742844ce426..8078de01260 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -937,7 +937,7 @@ static void usb_host_ep_update(USBHostDevice *s)
alt = udev->altsetting[intf->bInterfaceNumber];
if (alt != 0) {
- assert(alt < conf->interface[i].num_altsetting);
+ assert(alt >= 0 && alt < conf->interface[i].num_altsetting);
intf = &conf->interface[i].altsetting[alt];
}
@@ -1994,6 +1994,75 @@ static void usb_host_post_load_bh(void *opaque)
usb_host_auto_check(NULL);
}
+/*
+ * On the CPR target libusb knows nothing about the interface claims the
+ * preserved fd still holds, so a later SET_INTERFACE would fail with
+ * NOT_FOUND. Re-claim them to bring libusb's view back in sync; on the
+ * preserved fd it's a no-op at the usbfs level anyway.
+ */
+static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
+{
+ USBDevice *udev = USB_DEVICE(s);
+ struct libusb_config_descriptor *conf;
+ int n, rc;
+
+ rc = libusb_get_active_config_descriptor(s->dev, &conf);
+ if (rc != 0) {
+ usb_host_libusb_error("libusb_get_active_config_descriptor", rc);
+ return -1;
+ }
+
+ n = MIN(conf->bNumInterfaces, USB_MAX_INTERFACES);
+ for (int i = 0; i < n; i++) {
+ rc = libusb_claim_interface(s->dh, i);
+ if (rc != 0) {
+ usb_host_libusb_error("libusb_claim_interface", rc);
+ libusb_free_config_descriptor(conf);
+ return -1;
+ }
+ s->ifs[i].claimed = true;
+ }
+
+ udev->ninterfaces = conf->bNumInterfaces;
+ udev->configuration = conf->bConfigurationValue;
+ libusb_free_config_descriptor(conf);
+
+ return 0;
+}
+
+/*
+ * During CPR the altsetting array is included in the migration stream.
+ * Reject a value this device can't honour, so that usb_host_ep_update()
+ * on the target never sees an out of range altsetting. In this case
+ * we fail the incoming migration instead of aborting on it.
+ */
+static int usb_host_cpr_check_altsettings(USBHostDevice *s)
+{
+ USBDevice *udev = USB_DEVICE(s);
+ struct libusb_config_descriptor *conf;
+
+ if (libusb_get_active_config_descriptor(s->dev, &conf) != 0) {
+ error_report("usb-host: cannot read config descriptor on "
+ "CPR incoming");
+ return -1;
+ }
+
+ for (int i = 0; i < conf->bNumInterfaces; i++) {
+ int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
+ int alt = udev->altsetting[ifnum];
+
+ if (alt < 0 || alt >= conf->interface[i].num_altsetting) {
+ error_report("usb-host: migrated altsetting %d out of range for "
+ "interface %d", alt, ifnum);
+ libusb_free_config_descriptor(conf);
+ return -1;
+ }
+ }
+
+ libusb_free_config_descriptor(conf);
+ return 0;
+}
+
static int usb_host_post_load(void *opaque, int version_id)
{
USBHostDevice *dev = opaque;
@@ -2005,6 +2074,18 @@ static int usb_host_post_load(void *opaque, int version_id)
* Skip it for CPR.
*/
if (cpr_is_incoming()) {
+ /*
+ * We kept the device open across CPR, so libusb still needs its
+ * interface claims and endpoints rebuilt here. And we only start
+ * reaping now, once the source has handed the fd off to us.
+ */
+ if (usb_host_cpr_check_altsettings(dev) < 0) {
+ return -1;
+ }
+ if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
+ return -1;
+ }
+ usb_host_ep_update(dev);
usb_host_cpr_restart_events();
return 0;
}
--
2.47.1
^ permalink raw reply [relevance 11%]
* [PATCH QEMU HCI-8.0 v5 13/13] usb-host: make CPR migration blocker conditional #VSTOR-137800
` (8 preceding siblings ...)
2026-09-25 14:11 11% ` [PATCH QEMU HCI-8.0 v5 11/13] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 20% ` Andrey Drobyshev
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
A previous commit added an unconditional blocker for CPR migrations.
That is to refuse CPR gracefully instead of crashing and for the previous
commits to remain bisectable.
Now the restore path is complete: the FD is preserved, in-flight URBs
are drained and replayed, the device is handed off across cpr-transfer,
and the endpoints and isochronous streams are reconstructed on the
target. So let's lift the CPR blocker for the devices which satisfy the
preconditions.
Still, the remaining preconditions for CPR to work are:
* We must open host device FD ourselves, so that libusb owns the FD.
This way we're able to preserve the FD during CPR;
* Device ID must be provided as a unique stable key for CPR FD registry.
Use them to guard the blocker addition. Apart from that, prohibit CPR on
WIN32 platform and on hosts with older libusb versions.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 50 +++++++++++++++++++++++++++++++++++---------------
1 file changed, 35 insertions(+), 15 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 8078de01260..0623d6a7183 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1484,24 +1484,44 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
-#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
- if (s->hostdevice && DEVICE(s)->id) {
- migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
- MIG_MODE_CPR_TRANSFER,
- MIG_MODE_CPR_EXEC, -1);
+#ifdef CONFIG_WIN32
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "WIN32 platform is not supported",
+ DEVICE(s)->id ?: "(anonymous)");
+#elif LIBUSB_API_VERSION < 0x01000107
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "libusb API version 0x%08x is too old",
+ DEVICE(s)->id ?: "(anonymous)", (unsigned) LIBUSB_API_VERSION);
+#else
+ if (!(s->hostdevice && DEVICE(s)->id)) {
+ /*
+ * Host FD must be owned by libusb to support CPR migration. That's
+ * equivalent to hostdevice= property being present. Also device ID
+ * is required as stable key for CPR FD registry.
+ */
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "hostdevice= and a device id are required",
+ DEVICE(s)->id ?: "(anonymous)");
}
#endif
- error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
- DEVICE(s)->id ?: "(anonymous)");
- if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
- MIG_MODE_CPR_TRANSFER,
- MIG_MODE_CPR_EXEC, -1) < 0) {
- qemu_remove_exit_notifier(&s->exit);
- if (s->needs_autoscan) {
- QTAILQ_REMOVE(&hostdevs, s, next);
- }
- usb_host_close(s);
+ if (s->cpr_blocker) {
+ if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1) < 0) {
+ qemu_remove_exit_notifier(&s->exit);
+ if (s->needs_autoscan) {
+ QTAILQ_REMOVE(&hostdevs, s, next);
+ }
+ usb_host_close(s);
+ }
+ } else {
+ migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1);
}
}
--
2.47.1
^ permalink raw reply [relevance 20%]
* [PATCH QEMU HCI-8.0 v5 09/13] usb-host: hand off the device across cpr-transfer #VSTOR-137800
` (6 preceding siblings ...)
2026-09-25 14:11 22% ` [PATCH QEMU HCI-8.0 v5 08/13] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 26% ` Andrey Drobyshev
2026-09-25 14:11 11% ` [PATCH QEMU HCI-8.0 v5 11/13] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 20% ` [PATCH QEMU HCI-8.0 v5 13/13] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
Unlike cpr-exec, cpr-transfer keeps the source QEMU alive: it hands the
usbfs FD to the new QEMU over the migration channel and only exits once
the target is up. While the handoff is in flight both processes hold the
same FD, and only one may reap URB events on it - if both do, one reaps
the other's in-flight completions and wedges an isochronous transfer that
cannot be retried. So reaping is handed off along with the FD:
* The target doesn't reap until it takes over: it stops events right
after opening the FD in .realize() and restarts in .post_load().
* The source stops reaping in .pre_save() and, since it stays alive,
restarts from the MIG_EVENT_PRECOPY_FAILED notifier on failure.
* On success the source's .exit() leaves the device untouched - no
reset, interface release or host-driver rebind - and just closes its
own copy of the FD.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 94 ++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 83 insertions(+), 11 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 6b8c0cf0a82..742844ce426 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -110,6 +110,7 @@ struct USBHostDevice {
NotifierWithReturn cpr_notifier;
GSList *cpr_inflight;
bool cpr_drained;
+ bool cpr_handed_off;
libusb_device *dev;
libusb_device_handle *dh;
struct libusb_device_descriptor ddesc;
@@ -1265,16 +1266,19 @@ static int usb_host_close(USBHostDevice *s)
trace_usb_host_close(s->bus_num, s->addr);
- usb_host_abort_xfers(s);
- usb_host_iso_free_all(s);
+ if (!s->cpr_handed_off) {
+ usb_host_abort_xfers(s);
+ usb_host_iso_free_all(s);
- if (udev->attached) {
- usb_device_detach(udev);
+ if (udev->attached) {
+ usb_device_detach(udev);
+ }
+
+ usb_host_release_interfaces(s);
+ libusb_reset_device(s->dh);
+ usb_host_attach_kernel(s);
}
- usb_host_release_interfaces(s);
- libusb_reset_device(s->dh);
- usb_host_attach_kernel(s);
libusb_close(s->dh);
s->dh = NULL;
s->dev = NULL;
@@ -1309,10 +1313,17 @@ static void usb_host_exit_notifier(struct Notifier *n, void *data)
USBHostDevice *s = container_of(n, USBHostDevice, exit);
if (s->dh) {
- usb_host_abort_xfers(s);
- usb_host_release_interfaces(s);
- libusb_reset_device(s->dh);
- usb_host_attach_kernel(s);
+ /*
+ * Handed to new QEMU across cpr-transfer: do not reset, release
+ * interfaces or rebind the host driver underneath it; just close
+ * our handle.
+ */
+ if (!s->cpr_handed_off) {
+ usb_host_abort_xfers(s);
+ usb_host_release_interfaces(s);
+ libusb_reset_device(s->dh);
+ usb_host_attach_kernel(s);
+ }
libusb_close(s->dh);
}
}
@@ -1336,6 +1347,8 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
}
static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+static void usb_host_cpr_stop_events(void);
+static void usb_host_cpr_restart_events(void);
/*
* A failed CPR migration resumes the source VM with the drained
@@ -1356,6 +1369,11 @@ static int usb_host_cpr_notifier(NotifierWithReturn *notifier,
return 0;
}
+ if (s->cpr_handed_off) {
+ usb_host_cpr_restart_events();
+ s->cpr_handed_off = false;
+ }
+
for (it = s->cpr_inflight; it; it = it->next) {
p = it->data;
/* Replay from the start of the TD */
@@ -1428,6 +1446,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
error_setg(errp, "failed to open host usb device %s", s->hostdevice);
return;
}
+ if (cpr_is_incoming()) {
+ /*
+ * The source still owns the shared fd until it hands off. Don't
+ * reap URBs on it yet, or we would steal the source's in-flight
+ * completions; .post_load() restarts events once we take over.
+ */
+ usb_host_cpr_stop_events();
+ }
} else
#endif
if (s->match.addr && s->match.bus_num &&
@@ -1979,6 +2005,7 @@ static int usb_host_post_load(void *opaque, int version_id)
* Skip it for CPR.
*/
if (cpr_is_incoming()) {
+ usb_host_cpr_restart_events();
return 0;
}
@@ -1991,6 +2018,47 @@ static int usb_host_post_load(void *opaque, int version_id)
return 0;
}
+#ifndef CONFIG_WIN32
+
+/*
+ * cpr-transfer: the usbfs fd is shared with the new QEMU via SCM_RIGHTS.
+ * Stop this (source) process from reaping URBs on it, so the new QEMU can
+ * drive the device without both processes racing on the same fd. Restart
+ * on migration failure, when the source resumes.
+ */
+static void usb_host_cpr_stop_events(void)
+{
+ const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+ libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
+ if (poll) {
+ for (int i = 0; poll[i] != NULL; i++) {
+ usb_host_del_fd(poll[i]->fd, ctx);
+ }
+ free(poll);
+ }
+}
+
+static void usb_host_cpr_restart_events(void)
+{
+ const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+ libusb_set_pollfd_notifiers(ctx, usb_host_add_fd, usb_host_del_fd, ctx);
+ if (poll) {
+ for (int i = 0; poll[i] != NULL; i++) {
+ usb_host_add_fd(poll[i]->fd, poll[i]->events, ctx);
+ }
+ free(poll);
+ }
+}
+
+#else
+
+static void usb_host_cpr_stop_events(void) {}
+static void usb_host_cpr_restart_events(void) {}
+
+#endif
+
static int usb_host_pre_save(void *opaque)
{
USBHostDevice *s = opaque;
@@ -2002,6 +2070,10 @@ static int usb_host_pre_save(void *opaque)
if (usb_host_cpr_drain_xfers(s) < 0) {
return -1;
}
+ if (mode == MIG_MODE_CPR_TRANSFER) {
+ usb_host_cpr_stop_events();
+ s->cpr_handed_off = true;
+ }
}
return 0;
}
--
2.47.1
^ permalink raw reply [relevance 26%]
* [PATCH QEMU HCI-8.0 v5 08/13] usb-host: skip product-string read on CPR incoming #VSTOR-137800
` (5 preceding siblings ...)
2026-09-25 14:11 21% ` [PATCH QEMU HCI-8.0 v5 07/13] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 22% ` Andrey Drobyshev
2026-09-25 14:11 26% ` [PATCH QEMU HCI-8.0 v5 09/13] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
` (2 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
usb_host_open() reads the product string descriptor from the device
with a synchronous control transfer. On the CPR target this runs while
the device is being handed over, and the transfer can block there,
hanging the incoming migration.
The string only fills the cosmetic product_desc, so skip the read when
we're the CPR target and fall back to the synthetic "host:bus.addr"
name.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index a14a3748838..6b8c0cf0a82 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1097,7 +1097,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
udev->speed = speed_map[libusb_speed];
usb_host_speed_compat(s);
- if (s->ddesc.iProduct) {
+ if (s->ddesc.iProduct && !cpr_is_incoming()) {
libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
(unsigned char *)udev->product_desc,
sizeof(udev->product_desc));
--
2.47.1
^ permalink raw reply [relevance 22%]
* [PATCH QEMU HCI-8.0 v5 07/13] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
` (4 preceding siblings ...)
2026-09-25 14:11 25% ` [PATCH QEMU HCI-8.0 v5 05/13] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 21% ` Andrey Drobyshev
2026-09-25 14:11 22% ` [PATCH QEMU HCI-8.0 v5 08/13] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
` (3 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
The previous commit drains the in-flight URBs in .pre_save() and leaves
their packets ASYNC for the target to replay. If migration fails, the
source resumes instead, with those packets still owned by the
controller and nothing left to complete them.
Remember the drained packets in a list, and re-issue them on the
preserved FD from a MIG_EVENT_PRECOPY_FAILED notifier. Control
transfers can't be reconstructed from the packet alone, so complete
those as errors and let the guest driver retry.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 96 +++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 92 insertions(+), 4 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 9e9ee7d305f..a14a3748838 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -107,6 +107,9 @@ struct USBHostDevice {
int hostfd;
Error *cpr_blocker;
+ NotifierWithReturn cpr_notifier;
+ GSList *cpr_inflight;
+ bool cpr_drained;
libusb_device *dev;
libusb_device_handle *dh;
struct libusb_device_descriptor ddesc;
@@ -425,6 +428,23 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
return NULL;
}
+/*
+ * A CANCELLED reap with r->p still set -> the CPR drain cancelled it (every
+ * other path clears r->p first). Don't complete it, let the target replay it.
+ * Returns true if the caller should just free the request.
+ */
+static bool usb_host_req_cpr_replay(USBHostRequest *r,
+ struct libusb_transfer *xfer)
+{
+ if (xfer->status != LIBUSB_TRANSFER_CANCELLED) {
+ return false;
+ }
+ if (r->p->state == USB_PACKET_ASYNC) {
+ r->host->cpr_inflight = g_slist_append(r->host->cpr_inflight, r->p);
+ }
+ return true;
+}
+
static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
{
USBHostRequest *r = xfer->user_data;
@@ -434,7 +454,7 @@ static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
if (r->p == NULL) {
goto out; /* request was canceled */
}
- if (xfer->status == LIBUSB_TRANSFER_CANCELLED) {
+ if (usb_host_req_cpr_replay(r, xfer)) {
goto out; /* CPR drain: the target will replay this packet */
}
@@ -487,7 +507,7 @@ static void LIBUSB_CALL usb_host_req_complete_data(struct libusb_transfer *xfer)
if (r->p == NULL) {
goto out; /* request was canceled */
}
- if (xfer->status == LIBUSB_TRANSFER_CANCELLED) {
+ if (usb_host_req_cpr_replay(r, xfer)) {
goto out; /* CPR drain: the target will replay this packet */
}
@@ -1166,8 +1186,13 @@ static bool usb_host_iso_inflight(USBHostDevice *s)
}
/*
- * Quiesce the device for a CPR switchover. Cancel and reap all URBs,
- * so that no URB submitted by this process can outlive it.
+ * Quiesce the device for a CPR switchover. Cancel and reap all URBs
+ * without completing their packets, so that no URB submitted by this
+ * process can outlive it. CPR target re-executes them in
+ * usb_xhci_post_load() kicking the running endpoints.
+ *
+ * Keep the list of packets so that they can be re-issued on this side
+ * instead if MIG_EVENT_PRECOPY_FAILED fires.
*/
static int usb_host_cpr_drain_xfers(USBHostDevice *s)
{
@@ -1176,6 +1201,8 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
USBHostIsoXfer *xfer;
int limit = 100;
+ s->cpr_drained = true;
+
/*
* Cancel and reap every URB. Completing a reaped URB can make xHCI queue
* the next TD, so re-cancel whatever reappears until the queue stays empty.
@@ -1308,6 +1335,53 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
return ret;
}
+static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+
+/*
+ * A failed CPR migration resumes the source VM with the drained
+ * packets still owned by the host controller as in-flight. Re-issue
+ * them on the preserved fd. Control transfers cannot be reconstructed
+ * from the packet alone; complete them as errors and let the guest
+ * driver retry.
+ */
+static int usb_host_cpr_notifier(NotifierWithReturn *notifier,
+ MigrationEvent *e, Error **errp)
+{
+ USBHostDevice *s = container_of(notifier, USBHostDevice, cpr_notifier);
+ USBDevice *udev = USB_DEVICE(s);
+ GSList *it;
+ USBPacket *p;
+
+ if (e->type != MIG_EVENT_PRECOPY_FAILED || !s->cpr_drained) {
+ return 0;
+ }
+
+ for (it = s->cpr_inflight; it; it = it->next) {
+ p = it->data;
+ /* Replay from the start of the TD */
+ p->actual_length = 0;
+ if (p->ep->nr == 0) {
+ /*
+ * p->ep[0] is control endpoint. Control transfers can't
+ * be reconstructed reliably, so complete them as errors
+ * and let the guest retry.
+ */
+ p->status = USB_RET_IOERROR;
+ usb_generic_async_ctrl_complete(udev, p);
+ } else {
+ usb_host_handle_data(udev, p);
+ if (p->status != USB_RET_ASYNC) {
+ /* Completed synchronously with error */
+ usb_packet_complete(udev, p);
+ }
+ }
+ }
+ g_slist_free(s->cpr_inflight);
+ s->cpr_inflight = NULL;
+ s->cpr_drained = false;
+ return 0;
+}
+
static void usb_host_realize(USBDevice *udev, Error **errp)
{
USBHostDevice *s = USB_HOST_DEVICE(udev);
@@ -1384,6 +1458,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
+#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
+ if (s->hostdevice && DEVICE(s)->id) {
+ migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1);
+ }
+#endif
+
error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
DEVICE(s)->id ?: "(anonymous)");
if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
@@ -1417,6 +1499,9 @@ static void usb_host_unrealize(USBDevice *udev)
}
usb_host_close(s);
migrate_del_blocker(&s->cpr_blocker);
+ migration_remove_notifier(&s->cpr_notifier);
+ g_slist_free(s->cpr_inflight);
+ s->cpr_inflight = NULL;
}
static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1424,6 +1509,9 @@ static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
USBHostDevice *s = USB_HOST_DEVICE(udev);
USBHostRequest *r;
+ /* Packet is being cancelled -> it mustn't be re-issued on failed CPR */
+ s->cpr_inflight = g_slist_remove(s->cpr_inflight, p);
+
if (p->combined) {
usb_combined_packet_cancel(udev, p);
return;
--
2.47.1
^ permalink raw reply [relevance 21%]
* [PATCH QEMU HCI-8.0 v5 05/13] usb-host: drain in-flight URBs across CPR #VSTOR-137800
` (3 preceding siblings ...)
2026-09-25 14:11 25% ` [PATCH QEMU HCI-8.0 v5 04/13] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 25% ` Andrey Drobyshev
2026-09-25 14:11 21% ` [PATCH QEMU HCI-8.0 v5 07/13] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
` (4 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
URBs submitted by the old QEMU must not outlive it across the
switchover: the kernel would complete them into buffers of an address
space which is gone after exec, and the new libusb context would reap
URB pointers it never submitted.
So, in .pre_save() for the CPR modes, cancel and reap every pending
transfer. One that already completed in the kernel is delivered
normally - its data is real and the guest must see it. One still in
flight is suppressed: its packet stays ASYNC and its TD stays on the
transfer ring, so the target re-executes it from the start of the TD
when usb_xhci_post_load() kicks the endpoints. Completing a transfer
can make xHCI queue the next TD, so keep cancelling until the request
list stays empty.
If the device won't release its URBs within the bounded wait, error
the still in-flight packets out -> the resumed guest retries them --
leak the requests, since libusb still owns their transfers, and abort
the migration.
Isochronous URBs are not on the request list but on their own rings,
and freeing them while in-flight would leave them queued on the
preserved FD. Cancel them so they are unlinked and reaped: an iso URB
in flight when the guest stops may never complete on its own. The
target reconstructs the iso stream from the guest's transfer ring.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 158 +++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 135 insertions(+), 23 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 9b70d6deef9..ebad04ec305 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -48,6 +48,7 @@
#include "qapi/error.h"
#include "migration/blocker.h"
#include "migration/cpr.h"
+#include "migration/misc.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -403,7 +404,10 @@ static void usb_host_req_free(USBHostRequest *r)
#ifdef CONFIG_WIN32
request_count--;
#endif
- QTAILQ_REMOVE(&r->host->requests, r, next);
+ /* Skip if a timed-out reap already unlinked it (avoid a double remove) */
+ if (QTAILQ_IN_USE(r, next)) {
+ QTAILQ_REMOVE(&r->host->requests, r, next);
+ }
libusb_free_transfer(r->xfer);
g_free(r->buffer);
g_free(r);
@@ -430,6 +434,9 @@ static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
if (r->p == NULL) {
goto out; /* request was canceled */
}
+ if (xfer->status == LIBUSB_TRANSFER_CANCELLED) {
+ goto out; /* CPR drain: the target will replay this packet */
+ }
r->p->status = status_map[xfer->status];
r->p->actual_length = xfer->actual_length;
@@ -480,6 +487,9 @@ static void LIBUSB_CALL usb_host_req_complete_data(struct libusb_transfer *xfer)
if (r->p == NULL) {
goto out; /* request was canceled */
}
+ if (xfer->status == LIBUSB_TRANSFER_CANCELLED) {
+ goto out; /* CPR drain: the target will replay this packet */
+ }
r->p->status = status_map[xfer->status];
if (r->in && xfer->actual_length) {
@@ -500,22 +510,29 @@ out:
}
}
-static void usb_host_req_abort(USBHostRequest *r)
+/* Fail r's in-flight packet with an error status and detach it from r. */
+static void usb_host_req_complete_err(USBHostRequest *r, int status)
{
- USBHostDevice *s = r->host;
- bool inflight = (r->p && r->p->state == USB_PACKET_ASYNC);
+ USBHostDevice *s = r->host;
- if (inflight) {
- r->p->status = USB_RET_NODEV;
- trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
- r->p->status, r->p->actual_length);
- if (r->p->ep->nr == 0) {
- usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
- } else {
- usb_packet_complete(USB_DEVICE(s), r->p);
- }
- r->p = NULL;
+ if (!r->p || r->p->state != USB_PACKET_ASYNC) {
+ return;
+ }
+ r->p->status = status;
+ trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
+ r->p->status, r->p->actual_length);
+ if (r->p->ep->nr == 0) {
+ usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
+ } else {
+ usb_packet_complete(USB_DEVICE(s), r->p);
+ }
+ r->p = NULL;
+}
+static void usb_host_req_abort(USBHostRequest *r)
+{
+ if (r->p && r->p->state == USB_PACKET_ASYNC) {
+ usb_host_req_complete_err(r, USB_RET_NODEV);
libusb_cancel_transfer(r->xfer);
}
}
@@ -1096,9 +1113,18 @@ fail:
return -1;
}
-static void usb_host_reap_xfers(USBHostDevice *s)
+/* libusb still owns cancelled-but-unreaped transfers: unlink, don't free. */
+static void usb_host_drop_requests(USBHostDevice *s)
{
USBHostRequest *r, *rtmp;
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ QTAILQ_REMOVE(&s->requests, r, next);
+ }
+}
+
+static void usb_host_reap_xfers(USBHostDevice *s)
+{
int limit = 100;
while (QTAILQ_FIRST(&s->requests) != NULL) {
@@ -1108,15 +1134,10 @@ static void usb_host_reap_xfers(USBHostDevice *s)
libusb_handle_events_timeout(ctx, &tv);
if (--limit == 0) {
/*
- * Don't wait forever for libusb calling the complete
- * callback (which will unlink and free the request).
- *
- * Leaking memory here, to make sure libusb will not
- * access memory which we have released already.
+ * Don't wait forever for libusb to call the complete callback
+ * (which unlinks and frees the request).
*/
- QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
- QTAILQ_REMOVE(&s->requests, r, next);
- }
+ usb_host_drop_requests(s);
return;
}
}
@@ -1132,6 +1153,81 @@ static void usb_host_abort_xfers(USBHostDevice *s)
usb_host_reap_xfers(s);
}
+static bool usb_host_iso_inflight(USBHostDevice *s)
+{
+ USBHostIsoRing *ring;
+
+ QTAILQ_FOREACH(ring, &s->isorings, next) {
+ if (!QTAILQ_EMPTY(&ring->inflight)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/*
+ * Quiesce the device for a CPR switchover. Cancel and reap all URBs,
+ * so that no URB submitted by this process can outlive it.
+ */
+static int usb_host_cpr_drain_xfers(USBHostDevice *s)
+{
+ USBHostRequest *r, *rtmp;
+ USBHostIsoRing *ring;
+ USBHostIsoXfer *xfer;
+ int limit = 100;
+
+ /*
+ * Cancel and reap every URB. Completing a reaped URB can make xHCI queue
+ * the next TD, so re-cancel whatever reappears until the queue stays empty.
+ */
+ while (!QTAILQ_EMPTY(&s->requests)) {
+ struct timeval tv = { .tv_usec = 2500 };
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ libusb_cancel_transfer(r->xfer);
+ }
+ libusb_handle_events_timeout(ctx, &tv);
+ if (--limit == 0) {
+ /*
+ * The device won't release these URBs. Error the packets out so
+ * the resumed guest retries them, leak the requests (libusb still
+ * owns their transfers) and abort the migration.
+ */
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ usb_host_req_complete_err(r, USB_RET_IOERROR);
+ }
+ usb_host_drop_requests(s);
+ return -1;
+ }
+ }
+
+ /*
+ * Iso URBs are on the rings, not s->requests. Cancel them: one in
+ * flight when the guest stops may never complete on its own, and none
+ * may be left on the preserved fd. The target replays from the ring.
+ */
+ QTAILQ_FOREACH(ring, &s->isorings, next) {
+ QTAILQ_FOREACH(xfer, &ring->inflight, next) {
+ libusb_cancel_transfer(xfer->xfer);
+ }
+ }
+
+ /*
+ * Cap the reap at 2x a full ring:
+ * iso_urb_count URBs x iso_urb_frames packets each
+ */
+ limit = 2 * s->iso_urb_count * s->iso_urb_frames;
+ while (usb_host_iso_inflight(s)) {
+ struct timeval tv = { .tv_usec = 1000 };
+ libusb_handle_events_timeout(ctx, &tv);
+ if (limit-- == 0) {
+ return -1;
+ }
+ }
+ usb_host_iso_free_all(s);
+ return 0;
+}
+
static int usb_host_close(USBHostDevice *s)
{
USBDevice *udev = USB_DEVICE(s);
@@ -1807,10 +1903,26 @@ static int usb_host_post_load(void *opaque, int version_id)
return 0;
}
+static int usb_host_pre_save(void *opaque)
+{
+ USBHostDevice *s = opaque;
+ MigMode mode = migrate_mode();
+
+ /* URBs submitted by this process must not outlive CPR migration */
+ if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
+ s->dh) {
+ if (usb_host_cpr_drain_xfers(s) < 0) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
static const VMStateDescription vmstate_usb_host = {
.name = "usb-host",
.version_id = 1,
.minimum_version_id = 1,
+ .pre_save = usb_host_pre_save,
.post_load = usb_host_post_load,
.fields = (const VMStateField[]) {
VMSTATE_USB_DEVICE(parent_obj, USBHostDevice),
--
2.47.1
^ permalink raw reply [relevance 25%]
* [PATCH QEMU HCI-8.0 v5 04/13] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800
` (2 preceding siblings ...)
2026-09-25 14:11 26% ` [PATCH QEMU HCI-8.0 v5 03/13] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 25% ` Andrey Drobyshev
2026-09-25 14:11 25% ` [PATCH QEMU HCI-8.0 v5 05/13] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
` (5 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
usb_host_abort_xfers() cancels every pending request and then pumps
libusb events until all of them are reaped, with a bounded wait. Split
that reap-wait loop out into usb_host_reap_xfers(). No functional
change.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b946b0ef350..9b70d6deef9 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1096,15 +1096,11 @@ fail:
return -1;
}
-static void usb_host_abort_xfers(USBHostDevice *s)
+static void usb_host_reap_xfers(USBHostDevice *s)
{
USBHostRequest *r, *rtmp;
int limit = 100;
- QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
- usb_host_req_abort(r);
- }
-
while (QTAILQ_FIRST(&s->requests) != NULL) {
struct timeval tv;
memset(&tv, 0, sizeof(tv));
@@ -1126,6 +1122,16 @@ static void usb_host_abort_xfers(USBHostDevice *s)
}
}
+static void usb_host_abort_xfers(USBHostDevice *s)
+{
+ USBHostRequest *r, *rtmp;
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ usb_host_req_abort(r);
+ }
+ usb_host_reap_xfers(s);
+}
+
static int usb_host_close(USBHostDevice *s)
{
USBDevice *udev = USB_DEVICE(s);
--
2.47.1
^ permalink raw reply [relevance 25%]
* [PATCH QEMU HCI-8.0 v5 01/13] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
@ 2026-09-25 14:11 31% ` Andrey Drobyshev
2026-09-25 14:11 13% ` [PATCH QEMU HCI-8.0 v5 02/13] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
` (8 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
The FD passed into usb_host_open() for a wrapped device is only stored in
s->hostfd after libusb_wrap_sys_device() succeeds, and the failure path
doesn't close it. if wrapping fails, or a later step of the open sequence
fails (e.g. usb_device_attach), the FD is leaked, as usb_host_close() only
runs for fully opened devices, and the realize error path doesn't clean it
up either.
Store the FD in s->hostfd before wrapping and close it on the failure
path.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b74670ae256..83645fbe0ce 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -954,6 +954,11 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
int rc;
Error *local_err = NULL;
+ /* Make sure failure path frees caller-owned FD */
+ if (!dev) {
+ s->hostfd = hostfd;
+ }
+
if (s->bh_postld_pending) {
return -1;
}
@@ -978,7 +983,6 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
if (rc != 0) {
goto fail;
}
- s->hostfd = hostfd;
dev = libusb_get_device(s->dh);
bus_num = libusb_get_bus_number(dev);
addr = libusb_get_device_address(dev);
@@ -1066,6 +1070,10 @@ fail:
s->dh = NULL;
s->dev = NULL;
}
+ if (s->hostfd != -1) {
+ close(s->hostfd);
+ s->hostfd = -1;
+ }
return -1;
}
--
2.47.1
^ permalink raw reply [relevance 31%]
* [PATCH QEMU HCI-8.0 v5 03/13] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800
2026-09-25 14:11 31% ` [PATCH QEMU HCI-8.0 v5 01/13] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 13% ` [PATCH QEMU HCI-8.0 v5 02/13] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 26% ` Andrey Drobyshev
2026-09-25 14:11 25% ` [PATCH QEMU HCI-8.0 v5 04/13] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
` (6 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
In .realize(), save freshly opened host device FD into the CPR registry,
and then reuse it on CPR target. Also, add deletion of that FD from
registry to usb_host_open() / usb_host_close() cleanup paths.
For this to work, we also need to skip the .post-load() code which
closes, detaches the device, and then rescans the host bus to reopen
matching hostdevs. For the CPR-case migration we don't want any of that
as the hostdev FD stays preserved, and the guest shouldn't notice the
switchover.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 33 ++++++++++++++++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 5c3da00d8eb..b946b0ef350 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -47,6 +47,7 @@
#include "qapi/error.h"
#include "migration/blocker.h"
+#include "migration/cpr.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -947,6 +948,21 @@ static void usb_host_ep_update(USBHostDevice *s)
libusb_free_config_descriptor(conf);
}
+static char *usb_host_cpr_fd_name(USBHostDevice *s)
+{
+ /* Prefix the key so it can't clash with another CPR label */
+ return DEVICE(s)->id ? g_strdup_printf("usb-host/%s", DEVICE(s)->id)
+ : NULL;
+}
+
+static void usb_host_cpr_delete_fd(USBHostDevice *s)
+{
+ g_autofree char *name = usb_host_cpr_fd_name(s);
+ if (name) {
+ cpr_delete_fd(name, 0);
+ }
+}
+
static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
{
USBDevice *udev = USB_DEVICE(s);
@@ -1073,6 +1089,7 @@ fail:
s->dev = NULL;
}
if (s->hostfd != -1) {
+ usb_host_cpr_delete_fd(s);
close(s->hostfd);
s->hostfd = -1;
}
@@ -1134,6 +1151,7 @@ static int usb_host_close(USBHostDevice *s)
s->dev = NULL;
if (s->hostfd != -1) {
+ usb_host_cpr_delete_fd(s);
close(s->hostfd);
s->hostfd = -1;
}
@@ -1220,9 +1238,12 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
if (s->hostdevice) {
+ g_autofree char *name = usb_host_cpr_fd_name(s);
int fd;
+
s->needs_autoscan = false;
- fd = qemu_open(s->hostdevice, O_RDWR, errp);
+ fd = name ? cpr_open_fd(s->hostdevice, O_RDWR, name, 0, errp)
+ : qemu_open(s->hostdevice, O_RDWR, errp);
if (fd < 0) {
return;
}
@@ -1761,6 +1782,16 @@ static int usb_host_post_load(void *opaque, int version_id)
{
USBHostDevice *dev = opaque;
+ /*
+ * For CPR migration, device wasn't released/reset, and the guest
+ * is unaware of the switchover. The detach/rescan performed in
+ * usb_host_post_load_bh() only exists for cross-host migration.
+ * Skip it for CPR.
+ */
+ if (cpr_is_incoming()) {
+ return 0;
+ }
+
if (!dev->bh_postld) {
dev->bh_postld = qemu_bh_new_guarded(usb_host_post_load_bh, dev,
&DEVICE(dev)->mem_reentrancy_guard);
--
2.47.1
^ permalink raw reply [relevance 26%]
* [PATCH QEMU HCI-8.0 v5 02/13] usb-host: add migration blocker for CPR modes #VSTOR-137800
2026-09-25 14:11 31% ` [PATCH QEMU HCI-8.0 v5 01/13] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-09-25 14:11 13% ` Andrey Drobyshev
2026-09-25 14:11 26% ` [PATCH QEMU HCI-8.0 v5 03/13] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
` (7 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-25 14:11 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
We're about to add support for CPR migration to usb-host in the upcoming
commits. Right now CPR is crashing with usb-host devices. Let's add
migration blocker upfront, for both cpr-exec and cpr-transfer. The
blocker is added unconditionally, so that we resuse CPR migration instead
of crashing on it. That is for the sake of bisectability of the upcoming
commits. It is going to be lifted (conditioned) in a following patch
once CPR support is added. Also, blocker is added at the end of
usb_host_realize(), so we have to cleanup all the side effects done
earlier in .realize() in case migrate_add_blocker_modes() fails.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 83645fbe0ce..5c3da00d8eb 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -46,6 +46,7 @@
#endif
#include "qapi/error.h"
+#include "migration/blocker.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -103,6 +104,7 @@ struct USBHostDevice {
char port[16];
int hostfd;
+ Error *cpr_blocker;
libusb_device *dev;
libusb_device_handle *dh;
struct libusb_device_descriptor ddesc;
@@ -1258,6 +1260,18 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
+
+ error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+ DEVICE(s)->id ?: "(anonymous)");
+ if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1) < 0) {
+ qemu_remove_exit_notifier(&s->exit);
+ if (s->needs_autoscan) {
+ QTAILQ_REMOVE(&hostdevs, s, next);
+ }
+ usb_host_close(s);
+ }
}
static void usb_host_instance_init(Object *obj)
@@ -1279,6 +1293,7 @@ static void usb_host_unrealize(USBDevice *udev)
QTAILQ_REMOVE(&hostdevs, s, next);
}
usb_host_close(s);
+ migrate_del_blocker(&s->cpr_blocker);
}
static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
--
2.47.1
^ permalink raw reply [relevance 13%]
* [Devel] [PATCH hci-8.1 v3 1/2] selftests: pstore: skip when no backend is registered
@ 2026-09-25 13:18 4% ` Eva Kurchatova
0 siblings, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-25 13:18 UTC (permalink / raw)
To: khorenko; +Cc: devel
An unregistered pstore backend reads back "(null)", and every latter
check fails. This also contradicts the pstore_crash_test documentation.
pstore_crash_test has carried the comment, but never implemented it:
# exit if pstore backend is not registered
Fix by skipping an unregistered backend in common_tests.
https://virtuozzo.atlassian.net/browse/VSTOR-142447
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/pstore/common_tests | 17 ++++++++++++-----
.../selftests/pstore/pstore_post_reboot_tests | 3 ---
2 files changed, 12 insertions(+), 8 deletions(-)
diff --git a/tools/testing/selftests/pstore/common_tests b/tools/testing/selftests/pstore/common_tests
index 4509f0cc9c91..8f140fa572af 100755
--- a/tools/testing/selftests/pstore/common_tests
+++ b/tools/testing/selftests/pstore/common_tests
@@ -55,6 +55,8 @@ operate_files() { # tested value, files, operation
# Parameters
TEST_STRING_PATTERN="Testing pstore: uuid="
+# Kselftest framework requirement - SKIP code is 4.
+ksft_skip=4
UUID=`cat /proc/sys/kernel/random/uuid`
TOP_DIR=`absdir $0`
LOG_DIR=$TOP_DIR/logs/`date +%Y%m%d-%H%M%S`_${UUID}/
@@ -74,10 +76,15 @@ prlog "=== Pstore unit tests (`basename $0`) ==="
prlog "UUID="$UUID
prlog -n "Checking pstore backend is registered ... "
-backend=`cat /sys/module/pstore/parameters/backend`
-show_result $?
+# No file at all where pstore is not loaded, "(null)" where none registered
+backend=`cat /sys/module/pstore/parameters/backend 2>/dev/null`
+if [ -z "$backend" ] || [ "$backend" = "(null)" ]; then
+ prlog "SKIP"
+ prlog -e "\tbackend=${backend:-<none>}"
+ prlog -e "\tcmdline=`cat /proc/cmdline`"
+ prlog "pstore backend is not registered, skipping"
+ exit $ksft_skip
+fi
+prlog "ok"
prlog -e "\tbackend=${backend}"
prlog -e "\tcmdline=`cat /proc/cmdline`"
-if [ $rc -ne 0 ]; then
- exit 1
-fi
diff --git a/tools/testing/selftests/pstore/pstore_post_reboot_tests b/tools/testing/selftests/pstore/pstore_post_reboot_tests
index d6da5e86efbf..2edd48a87dda 100755
--- a/tools/testing/selftests/pstore/pstore_post_reboot_tests
+++ b/tools/testing/selftests/pstore/pstore_post_reboot_tests
@@ -7,9 +7,6 @@
# Written by Hiraku Toyooka <hiraku.toyooka.gu@hitachi.com>
#
-# Kselftest framework requirement - SKIP code is 4.
-ksft_skip=4
-
. ./common_tests
if [ -e $REBOOT_FLAG ]; then
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* [Devel] [PATCH VZ10 v4 3/3] selftests/ve: Add more helpers
2026-09-25 12:47 3% [Devel] [PATCH VZ10 v4 0/3] Improvements for ve selftests Konstantin Khorenko
2026-09-25 12:47 4% ` [Devel] [PATCH VZ10 v4 1/3] ve_namespace: Allow entering non-running VE namespace Konstantin Khorenko
2026-09-25 12:47 6% ` [Devel] [PATCH VZ10 v4 2/3] selftests/ve: Add namespace management helpers Konstantin Khorenko
@ 2026-09-25 12:47 5% ` Konstantin Khorenko
2 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 12:47 UTC (permalink / raw)
To: Vladimir Riabchun, Vasileios Almpanis; +Cc: devel
From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Some more read/write helpers may be useful.
Also, add a helper to execute functions in child process with
switched namespaces and cgroup.
https://virtuozzo.atlassian.net/browse/VSTOR-144504
Feature: ve selftests
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Reviewed-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
Reviewed-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/ve/ve_selftest.h | 84 +++++++++++++++++++++++++++++---
1 file changed, 77 insertions(+), 7 deletions(-)
diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index bc9dc28a8d074..7b45f91c1d561 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -175,6 +175,14 @@ static inline int write_file_at(int dirfd, const char *path, const char *val)
return (ret == (int)len) ? 0 : -1;
}
+static inline int write_u64_at(int dirfd, const char *path, unsigned long long val)
+{
+ char s[32];
+
+ snprintf(s, sizeof(s), "%llu", val);
+ return write_file_at(dirfd, path, s);
+}
+
static inline int read_file_at(int dirfd, const char *path, char *buf,
size_t buflen)
{
@@ -205,14 +213,9 @@ static inline int read_u64_at(int dirfd, const char *path,
unsigned long long *out)
{
char buf[32] = {0}, *end;
- int fd, ret;
-
- fd = openat(dirfd, path, O_RDONLY);
- if (fd < 0)
- return -1;
+ int ret;
- ret = read(fd, buf, sizeof(buf) - 1);
- close(fd);
+ ret = read_file_at(dirfd, path, buf, sizeof(buf));
if (ret <= 0)
return -1;
@@ -223,6 +226,25 @@ static inline int read_u64_at(int dirfd, const char *path,
return 0;
}
+static inline int read_s32_at(int dirfd, const char *path,
+ int *out)
+{
+ char buf[32] = {0}, *end;
+ long tmp;
+ int ret;
+
+ ret = read_file_at(dirfd, path, buf, sizeof(buf));
+ if (ret <= 0)
+ return -1;
+
+ errno = 0;
+ tmp = strtol(buf, &end, 10);
+ if (errno || end == buf || tmp < INT_MIN || tmp > INT_MAX)
+ return -1;
+ *out = tmp;
+ return 0;
+}
+
/* A fresh, private cgroup2 mount, returned as an fd to use with *at() calls. */
static inline int mount_cg2_fd(void)
{
@@ -266,6 +288,54 @@ static inline int enter_cgroup(int cgv2_fd, int ctid)
return ret;
}
+/*
+ * Run function in VE cgroup and given namespaces.
+ *
+ * Namespaces are provided via namespace_ctx and entered based on flags mask.
+ *
+ * Return values:
+ * - 0 if function returns zero
+ * - -1 if function returns negative value
+ * - 1 if function returns positive value
+ * - 2 if setup fails
+ */
+static inline int run_in_ve(int cgv2_fd, int ctid, struct namespace_ctx *ctx,
+ int flags, int (*fn)(void *), void *arg)
+{
+ int status;
+ pid_t pid;
+
+ pid = fork();
+ if (pid < 0) {
+ fprintf(stderr, "%s: fork failed\n", __func__);
+ return 2;
+ }
+ if (pid == 0) {
+ int ret;
+
+ if (enter_cgroup(cgv2_fd, ctid) < 0) {
+ fprintf(stderr, "%s: enter_cgroup failed\n", __func__);
+ _exit(254);
+ }
+ if (enter_namespaces(ctx, flags) < 0)
+ _exit(254);
+
+ ret = fn(arg);
+ if (ret < 0)
+ ret = 1;
+ else if (ret > 0)
+ ret = 255;
+ _exit(ret);
+ }
+ if (waitpid(pid, &status, 0) < 0 || !WIFEXITED(status) || WEXITSTATUS(status) == 254)
+ return 2;
+ if (WEXITSTATUS(status) == 255)
+ return 1;
+ if (WEXITSTATUS(status))
+ return -1;
+ return 0;
+}
+
/*
* Create a fresh VE cgroup at the first free id at or after @from and unhide
* its ve.* control files. Return the new id, or -1.
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 5%]
* [Devel] [PATCH VZ10 v4 1/3] ve_namespace: Allow entering non-running VE namespace
2026-09-25 12:47 3% [Devel] [PATCH VZ10 v4 0/3] Improvements for ve selftests Konstantin Khorenko
@ 2026-09-25 12:47 4% ` Konstantin Khorenko
2026-09-25 12:47 6% ` [Devel] [PATCH VZ10 v4 2/3] selftests/ve: Add namespace management helpers Konstantin Khorenko
2026-09-25 12:47 5% ` [Devel] [PATCH VZ10 v4 3/3] selftests/ve: Add more helpers Konstantin Khorenko
2 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 12:47 UTC (permalink / raw)
To: Vladimir Riabchun, Vasileios Almpanis; +Cc: devel
From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
The restriction doesn't bring a lot of value and makes tests more
complex: to enter a VE namespace, the VE has to be actually started.
In vzctl we have locking to prevent parallel creations.
If someone decides to skip locks and start one CT
in parallel - big mistake, but not ours.
Still, entering a non-running CT is a strange thing to do. Our
selftests do it on purpose, but if some of our userspace does the
same on a production node, that is a bug we want to know about and
fix, so leave a warning in place of the check.
https://virtuozzo.atlassian.net/browse/VSTOR-144504
Feature: ve selftests
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Reviewed-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
Reviewed-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
kernel/ve/ve_namespace.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/kernel/ve/ve_namespace.c b/kernel/ve/ve_namespace.c
index 3dbe0dde2645d..6b12bcd605ea4 100644
--- a/kernel/ve/ve_namespace.c
+++ b/kernel/ve/ve_namespace.c
@@ -227,7 +227,9 @@ static int ve_ns_install(struct nsset *nsset, struct ns_common *new)
return -EPERM;
if (!VE_IS_RUNNING(ve_ns->ve))
- return -EINVAL;
+ pr_warn_once("CT: %s: setns to the VE namespace of a non-running CT (state %d) by %s (%d)\n",
+ ve_name(ve_ns->ve), ve_ns->ve->state,
+ current->comm, task_pid_nr(current));
/*
* Don't need to put_ve_ns(nsset->ve_ns) here,
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* [Devel] [PATCH VZ10 v4 2/3] selftests/ve: Add namespace management helpers
2026-09-25 12:47 3% [Devel] [PATCH VZ10 v4 0/3] Improvements for ve selftests Konstantin Khorenko
2026-09-25 12:47 4% ` [Devel] [PATCH VZ10 v4 1/3] ve_namespace: Allow entering non-running VE namespace Konstantin Khorenko
@ 2026-09-25 12:47 6% ` Konstantin Khorenko
2026-09-25 12:47 5% ` [Devel] [PATCH VZ10 v4 3/3] selftests/ve: Add more helpers Konstantin Khorenko
2 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 12:47 UTC (permalink / raw)
To: Vladimir Riabchun, Vasileios Almpanis; +Cc: devel
From: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
When mount namespace is created in a child, it is destroyed
when the child dies. We may need to keep it for a while to check
how VE handles resources.
Instead of keeping a thread which lives in a new ve+mnt namespaces,
let's store open namespace file descriptors to enter them later
without keeping children alive.
https://virtuozzo.atlassian.net/browse/VSTOR-144504
Feature: ve selftests
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
Reviewed-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
Reviewed-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
tools/testing/selftests/ve/ve_selftest.h | 130 +++++++++++++++++++++++++++++++
1 file changed, 130 insertions(+)
diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index c53bf7900d208..bc9dc28a8d074 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -31,6 +31,136 @@
#define VE_CONTROLLERS \
"+cpuset +cpu +cpuacct +io +memory +hugetlb +pids +rdma +misc +ve"
+struct namespace_ctx {
+ int ns_save_mask; /* Mask of stored CLONE_NEW* namespaces. */
+ /* Namespace file descriptors. Name should match /proc/PID/ns/NAME */
+ int ve_fd;
+ int mnt_fd;
+};
+
+static inline void close_namespace_fds(struct namespace_ctx *ctx)
+{
+ if (ctx->ns_save_mask & CLONE_NEWVE)
+ close(ctx->ve_fd);
+ if (ctx->ns_save_mask & CLONE_NEWNS)
+ close(ctx->mnt_fd);
+
+ ctx->ns_save_mask = 0;
+}
+
+static inline int save_namespace_fds(struct namespace_ctx *ctx, int flags)
+{
+#define OPEN_NS(name, flag) \
+do { \
+ if (flags & (flag)) { \
+ ctx->name##_fd = open("/proc/self/ns/" #name, O_RDONLY); \
+ if (ctx->name##_fd < 0) { \
+ fprintf(stderr, "%s: could not open fd %s: %s\n", \
+ __func__, #name, strerror(errno)); \
+ goto err; \
+ } \
+ ctx->ns_save_mask |= (flag); \
+ } \
+} while (0)
+
+ ctx->ns_save_mask = 0;
+
+ OPEN_NS(ve, CLONE_NEWVE);
+ OPEN_NS(mnt, CLONE_NEWNS);
+
+#undef OPEN_NS
+ return 0;
+
+err:
+ close_namespace_fds(ctx);
+ return -1;
+}
+
+static inline int enter_namespaces(struct namespace_ctx *ctx, int flags)
+{
+#define JOIN_NS(name, flag) \
+do { \
+ if (flags & (flag)) { \
+ if (!(ctx->ns_save_mask & (flag))) { \
+ fprintf(stderr, "%s: could not switch ns %s:" \
+ " no fd in context.\n", __func__, #name); \
+ return -1; \
+ } \
+ if (setns(ctx->name##_fd, (flag))) { \
+ fprintf(stderr, "%s: could not switch ns %s: %s\n", \
+ __func__, #name, strerror(errno)); \
+ return -1; \
+ } \
+ } \
+} while (0)
+
+ JOIN_NS(ve, CLONE_NEWVE);
+ JOIN_NS(mnt, CLONE_NEWNS);
+
+#undef JOIN_NS
+ return 0;
+}
+
+static inline int enter_cgroup(int cgv2_fd, int ctid);
+
+/*
+ * Create new namespaces and store them in ctx.
+ * CLONE_NEWVE is treated specially - before unshare corresponding
+ * CT cgroup is entered, after it root cgroup is entered (not the original one).
+ */
+static inline int prepare_namespaces(struct namespace_ctx *ctx, int flags,
+ int cgv2_fd, int ctid)
+{
+ struct namespace_ctx tmp_ctx;
+ int ret = 0;
+
+ ctx->ns_save_mask = 0;
+
+ if (save_namespace_fds(&tmp_ctx, flags) < 0)
+ return -1;
+
+ if (flags & CLONE_NEWVE) {
+ if (enter_cgroup(cgv2_fd, ctid) < 0) {
+ fprintf(stderr, "%s: failed to enter CT cgroup\n", __func__);
+ ret = -1;
+ goto restore_ns;
+ }
+ }
+
+ if (unshare(flags) < 0) {
+ fprintf(stderr, "%s: failed to unshare, flags %x: %s\n",
+ __func__, flags, strerror(errno));
+ ret = -1;
+ goto restore_cg;
+ }
+
+ if (save_namespace_fds(ctx, flags)) {
+ fprintf(stderr, "%s: failed to save new namespaces, flags %x\n",
+ __func__, flags);
+ ret = -1;
+ }
+
+restore_cg:
+ if (flags & CLONE_NEWVE) {
+ if (enter_cgroup(cgv2_fd, 0) < 0) {
+ fprintf(stderr, "%s: failed to restore root cgroup\n", __func__);
+ ret = -1;
+ }
+ }
+
+restore_ns:
+ if (enter_namespaces(&tmp_ctx, flags) < 0) {
+ fprintf(stderr, "%s: failed to restore old namespaces, flags %x\n",
+ __func__, flags);
+ ret = -1;
+ }
+
+ close_namespace_fds(&tmp_ctx);
+ if (ret < 0)
+ close_namespace_fds(ctx);
+ return ret;
+}
+
static inline int write_file_at(int dirfd, const char *path, const char *val)
{
int fd, ret;
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 6%]
* [Devel] [PATCH VZ10 v4 0/3] Improvements for ve selftests
@ 2026-09-25 12:47 3% Konstantin Khorenko
2026-09-25 12:47 4% ` [Devel] [PATCH VZ10 v4 1/3] ve_namespace: Allow entering non-running VE namespace Konstantin Khorenko
` (2 more replies)
0 siblings, 3 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-25 12:47 UTC (permalink / raw)
To: Vladimir Riabchun, Vasileios Almpanis; +Cc: devel
This change adds helpers for creating and preserving various
namespaces and running functions inside them.
This appeared useful while implementing ve mount accounting
selftests - it required keeping mount namespace alive and
doing different things at different moments of time.
To make setns work with CLONE_NEWVE without starting VE,
the first patch changes the -EINVAL return to a warning message.
https://virtuozzo.atlassian.net/browse/VSTOR-144504
Feature: ve selftests
Changes in v4:
- Moved the ve_namespace patch to the front, before the selftest
helpers that rely on it.
- ve_namespace: the warning follows the "CT: %s:" format of other
ve.c messages and prints the VE state and the offender's comm/pid;
the commit message now explains why the warning is kept.
- namespace management helpers: prepare_namespaces() leaves ctx
empty on any failure (mask zeroed on entry, saved fds closed on
error), so unconditional cleanup in callers is safe; fixed a
comment typo.
- more helpers: dropped a stray blank line in run_in_ve().
- Collected Reviewed-by tags.
- Link to v3: http://lore.virtuozzo.com/20260909-ve-selftest-improv-v3-0-4b41980d9a45@virtuozzo.com
Changes in v3:
- Patch 1: wrap flag -> (flag) in macros, undef them after use.
- Patch 2: Fix comments in run_in_ve function.
- Link to v2: http://lore.virtuozzo.com/20260909-ve-selftest-improv-v2-0-aad0a25d6569@virtuozzo.com
Changes in v2:
- Patch 1: fixed JOIN_NS order in enter_namespaces, added proper
cleanup in prepare_namespaces.
- Patch 2: increased buf size in write_u64_at, added range check in
read_s32_at, separated setup failures in run_in_ve from fn setup
failures.
- Patch 3: Removed kselftests mention.
- Link to v1: http://lore.virtuozzo.com/20260907-ve-selftest-improv-v1-0-a17847abbd48@virtuozzo.com
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
Vladimir Riabchun (3):
ve_namespace: Allow entering non-running VE namespace
selftests/ve: Add namespace management helpers
selftests/ve: Add more helpers
kernel/ve/ve_namespace.c | 4 +-
tools/testing/selftests/ve/ve_selftest.h | 214 ++++++++++++++++++++++++++++++-
2 files changed, 210 insertions(+), 8 deletions(-)
---
base-commit: f7ba712715849bef1a54e1f13fab79b094ccc0df
change-id: 20260907-ve-selftest-improv-2d043d021cae
Best regards,
--
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* [PATCH QEMU HCI-8.0 v4 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800
` (8 preceding siblings ...)
2026-09-24 16:41 11% ` [PATCH QEMU HCI-8.0 v4 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 20% ` Andrey Drobyshev
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev, den
A previous commit added an unconditional blocker for CPR migrations.
That is to refuse CPR gracefully instead of crashing and for the previous
commits to remain bisectable.
Now the restore path is complete: the FD is preserved, in-flight URBs
are drained and replayed, the device is handed off across cpr-transfer,
and the endpoints and isochronous streams are reconstructed on the
target. So let's lift the CPR blocker for the devices which satisfy the
preconditions.
Still, the remaining preconditions for CPR to work are:
* We must open host device FD ourselves, so that libusb owns the FD.
This way we're able to preserve the FD during CPR;
* Device ID must be provided as a unique stable key for CPR FD registry.
Use them to guard the blocker addition. Apart from that, prohibit CPR on
WIN32 platform and on hosts with older libusb versions.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 50 +++++++++++++++++++++++++++++++++++---------------
1 file changed, 35 insertions(+), 15 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index eb349915eee..8582da02567 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1449,24 +1449,44 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
-#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
- if (s->hostdevice && DEVICE(s)->id) {
- migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
- MIG_MODE_CPR_TRANSFER,
- MIG_MODE_CPR_EXEC, -1);
+#ifdef CONFIG_WIN32
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "WIN32 platform is not supported",
+ DEVICE(s)->id ?: "(anonymous)");
+#elif LIBUSB_API_VERSION < 0x01000107
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "libusb API version 0x%08x is too old",
+ DEVICE(s)->id ?: "(anonymous)", (unsigned) LIBUSB_API_VERSION);
+#else
+ if (!(s->hostdevice && DEVICE(s)->id)) {
+ /*
+ * Host FD must be owned by libusb to support CPR migration. That's
+ * equivalent to hostdevice= property being present. Also device ID
+ * is required as stable key for CPR FD registry.
+ */
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "hostdevice= and a device id are required",
+ DEVICE(s)->id ?: "(anonymous)");
}
#endif
- error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
- DEVICE(s)->id ?: "(anonymous)");
- if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
- MIG_MODE_CPR_TRANSFER,
- MIG_MODE_CPR_EXEC, -1) < 0) {
- qemu_remove_exit_notifier(&s->exit);
- if (s->needs_autoscan) {
- QTAILQ_REMOVE(&hostdevs, s, next);
- }
- usb_host_close(s);
+ if (s->cpr_blocker) {
+ if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1) < 0) {
+ qemu_remove_exit_notifier(&s->exit);
+ if (s->needs_autoscan) {
+ QTAILQ_REMOVE(&hostdevs, s, next);
+ }
+ usb_host_close(s);
+ }
+ } else {
+ migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1);
}
}
--
2.47.1
^ permalink raw reply [relevance 20%]
* [PATCH QEMU HCI-8.0 v4 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
` (7 preceding siblings ...)
2026-09-24 16:41 26% ` [PATCH QEMU HCI-8.0 v4 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 11% ` Andrey Drobyshev
2026-09-24 16:41 20% ` [PATCH QEMU HCI-8.0 v4 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev, den
On CPR incoming we skip the usb_host_open() rescan, so libusb only sets
up the endpoints from the device's default configuration. If the guest
had enabled an endpoint by selecting a non-zero altsetting (e.g. an
isochronous streaming endpoint), that endpoint is now missing, and the
guest's transfers to it simply stall.
The altsetting itself is restored by vmload now, so let's call
usb_host_ep_update() on the CPR target to rebuild the endpoints out of
it.
The kernel claims are still there on the preserved FD, but libusb's own
handle knows nothing about them, so a later SET_INTERFACE would fail with
LIBUSB_ERROR_NOT_FOUND. Let's re-claim the interfaces on the target as
well to bring libusb's view back in sync - on the preserved FD this is a
no-op at the usbfs level anyway.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 82 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 57984e72e45..eb349915eee 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -907,7 +907,7 @@ static void usb_host_ep_update(USBHostDevice *s)
alt = udev->altsetting[intf->bInterfaceNumber];
if (alt != 0) {
- assert(alt < conf->interface[i].num_altsetting);
+ assert(alt >= 0 && alt < conf->interface[i].num_altsetting);
intf = &conf->interface[i].altsetting[alt];
}
@@ -1959,6 +1959,75 @@ static void usb_host_post_load_bh(void *opaque)
usb_host_auto_check(NULL);
}
+/*
+ * On the CPR target libusb knows nothing about the interface claims the
+ * preserved fd still holds, so a later SET_INTERFACE would fail with
+ * NOT_FOUND. Re-claim them to bring libusb's view back in sync; on the
+ * preserved fd it's a no-op at the usbfs level anyway.
+ */
+static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
+{
+ USBDevice *udev = USB_DEVICE(s);
+ struct libusb_config_descriptor *conf;
+ int n, rc;
+
+ rc = libusb_get_active_config_descriptor(s->dev, &conf);
+ if (rc != 0) {
+ usb_host_libusb_error("libusb_get_active_config_descriptor", rc);
+ return -1;
+ }
+
+ n = MIN(conf->bNumInterfaces, USB_MAX_INTERFACES);
+ for (int i = 0; i < n; i++) {
+ rc = libusb_claim_interface(s->dh, i);
+ if (rc != 0) {
+ usb_host_libusb_error("libusb_claim_interface", rc);
+ libusb_free_config_descriptor(conf);
+ return -1;
+ }
+ s->ifs[i].claimed = true;
+ }
+
+ udev->ninterfaces = conf->bNumInterfaces;
+ udev->configuration = conf->bConfigurationValue;
+ libusb_free_config_descriptor(conf);
+
+ return 0;
+}
+
+/*
+ * During CPR the altsetting array is included in the migration stream.
+ * Reject a value this device can't honour, so that usb_host_ep_update()
+ * on the target never sees an out of range altsetting. In this case
+ * we fail the incoming migration instead of aborting on it.
+ */
+static int usb_host_cpr_check_altsettings(USBHostDevice *s)
+{
+ USBDevice *udev = USB_DEVICE(s);
+ struct libusb_config_descriptor *conf;
+
+ if (libusb_get_active_config_descriptor(s->dev, &conf) != 0) {
+ error_report("usb-host: cannot read config descriptor on "
+ "CPR incoming");
+ return -1;
+ }
+
+ for (int i = 0; i < conf->bNumInterfaces; i++) {
+ int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
+ int alt = udev->altsetting[ifnum];
+
+ if (alt < 0 || alt >= conf->interface[i].num_altsetting) {
+ error_report("usb-host: migrated altsetting %d out of range for "
+ "interface %d", alt, ifnum);
+ libusb_free_config_descriptor(conf);
+ return -1;
+ }
+ }
+
+ libusb_free_config_descriptor(conf);
+ return 0;
+}
+
static int usb_host_post_load(void *opaque, int version_id)
{
USBHostDevice *dev = opaque;
@@ -1970,6 +2039,18 @@ static int usb_host_post_load(void *opaque, int version_id)
* Skip it for CPR.
*/
if (cpr_is_incoming()) {
+ /*
+ * We kept the device open across CPR, so libusb still needs its
+ * interface claims and endpoints rebuilt here. And we only start
+ * reaping now, once the source has handed the fd off to us.
+ */
+ if (usb_host_cpr_check_altsettings(dev) < 0) {
+ return -1;
+ }
+ if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
+ return -1;
+ }
+ usb_host_ep_update(dev);
usb_host_cpr_restart_events();
return 0;
}
--
2.47.1
^ permalink raw reply [relevance 11%]
* [PATCH QEMU HCI-8.0 v4 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800
` (6 preceding siblings ...)
2026-09-24 16:41 22% ` [PATCH QEMU HCI-8.0 v4 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 26% ` Andrey Drobyshev
2026-09-24 16:41 11% ` [PATCH QEMU HCI-8.0 v4 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 20% ` [PATCH QEMU HCI-8.0 v4 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev, den
Unlike cpr-exec, cpr-transfer keeps the source QEMU alive: it hands the
usbfs FD to the new QEMU over the migration channel and only exits once
the target is up. While the handoff is in flight both processes hold the
same FD, and only one may reap URB events on it - if both do, one reaps
the other's in-flight completions and wedges an isochronous transfer that
cannot be retried. So reaping is handed off along with the FD:
* The target doesn't reap until it takes over: it stops events right
after opening the FD in .realize() and restarts in .post_load().
* The source stops reaping in .pre_save() and, since it stays alive,
restarts from the MIG_EVENT_PRECOPY_FAILED notifier on failure.
* On success the source's .exit() leaves the device untouched - no
reset, interface release or host-driver rebind - and just closes its
own copy of the FD.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 94 ++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 83 insertions(+), 11 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 0108ccba72e..57984e72e45 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -110,6 +110,7 @@ struct USBHostDevice {
NotifierWithReturn cpr_notifier;
GSList *cpr_inflight;
bool cpr_drained;
+ bool cpr_handed_off;
libusb_device *dev;
libusb_device_handle *dh;
struct libusb_device_descriptor ddesc;
@@ -1230,16 +1231,19 @@ static int usb_host_close(USBHostDevice *s)
trace_usb_host_close(s->bus_num, s->addr);
- usb_host_abort_xfers(s);
- usb_host_iso_free_all(s);
+ if (!s->cpr_handed_off) {
+ usb_host_abort_xfers(s);
+ usb_host_iso_free_all(s);
- if (udev->attached) {
- usb_device_detach(udev);
+ if (udev->attached) {
+ usb_device_detach(udev);
+ }
+
+ usb_host_release_interfaces(s);
+ libusb_reset_device(s->dh);
+ usb_host_attach_kernel(s);
}
- usb_host_release_interfaces(s);
- libusb_reset_device(s->dh);
- usb_host_attach_kernel(s);
libusb_close(s->dh);
s->dh = NULL;
s->dev = NULL;
@@ -1274,10 +1278,17 @@ static void usb_host_exit_notifier(struct Notifier *n, void *data)
USBHostDevice *s = container_of(n, USBHostDevice, exit);
if (s->dh) {
- usb_host_abort_xfers(s);
- usb_host_release_interfaces(s);
- libusb_reset_device(s->dh);
- usb_host_attach_kernel(s);
+ /*
+ * Handed to new QEMU across cpr-transfer: do not reset, release
+ * interfaces or rebind the host driver underneath it; just close
+ * our handle.
+ */
+ if (!s->cpr_handed_off) {
+ usb_host_abort_xfers(s);
+ usb_host_release_interfaces(s);
+ libusb_reset_device(s->dh);
+ usb_host_attach_kernel(s);
+ }
libusb_close(s->dh);
}
}
@@ -1301,6 +1312,8 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
}
static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+static void usb_host_cpr_stop_events(void);
+static void usb_host_cpr_restart_events(void);
/*
* A failed CPR migration resumes the source VM with the drained
@@ -1321,6 +1334,11 @@ static int usb_host_cpr_notifier(NotifierWithReturn *notifier,
return 0;
}
+ if (s->cpr_handed_off) {
+ usb_host_cpr_restart_events();
+ s->cpr_handed_off = false;
+ }
+
for (it = s->cpr_inflight; it; it = it->next) {
p = it->data;
/* Replay from the start of the TD */
@@ -1393,6 +1411,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
error_setg(errp, "failed to open host usb device %s", s->hostdevice);
return;
}
+ if (cpr_is_incoming()) {
+ /*
+ * The source still owns the shared fd until it hands off. Don't
+ * reap URBs on it yet, or we would steal the source's in-flight
+ * completions; .post_load() restarts events once we take over.
+ */
+ usb_host_cpr_stop_events();
+ }
} else
#endif
if (s->match.addr && s->match.bus_num &&
@@ -1944,6 +1970,7 @@ static int usb_host_post_load(void *opaque, int version_id)
* Skip it for CPR.
*/
if (cpr_is_incoming()) {
+ usb_host_cpr_restart_events();
return 0;
}
@@ -1956,6 +1983,47 @@ static int usb_host_post_load(void *opaque, int version_id)
return 0;
}
+#ifndef CONFIG_WIN32
+
+/*
+ * cpr-transfer: the usbfs fd is shared with the new QEMU via SCM_RIGHTS.
+ * Stop this (source) process from reaping URBs on it, so the new QEMU can
+ * drive the device without both processes racing on the same fd. Restart
+ * on migration failure, when the source resumes.
+ */
+static void usb_host_cpr_stop_events(void)
+{
+ const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+ libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
+ if (poll) {
+ for (int i = 0; poll[i] != NULL; i++) {
+ usb_host_del_fd(poll[i]->fd, ctx);
+ }
+ free(poll);
+ }
+}
+
+static void usb_host_cpr_restart_events(void)
+{
+ const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+ libusb_set_pollfd_notifiers(ctx, usb_host_add_fd, usb_host_del_fd, ctx);
+ if (poll) {
+ for (int i = 0; poll[i] != NULL; i++) {
+ usb_host_add_fd(poll[i]->fd, poll[i]->events, ctx);
+ }
+ free(poll);
+ }
+}
+
+#else
+
+static void usb_host_cpr_stop_events(void) {}
+static void usb_host_cpr_restart_events(void) {}
+
+#endif
+
static int usb_host_pre_save(void *opaque)
{
USBHostDevice *s = opaque;
@@ -1967,6 +2035,10 @@ static int usb_host_pre_save(void *opaque)
if (usb_host_cpr_drain_xfers(s) < 0) {
return -1;
}
+ if (mode == MIG_MODE_CPR_TRANSFER) {
+ usb_host_cpr_stop_events();
+ s->cpr_handed_off = true;
+ }
}
return 0;
}
--
2.47.1
^ permalink raw reply [relevance 26%]
* [PATCH QEMU HCI-8.0 v4 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800
` (5 preceding siblings ...)
2026-09-24 16:41 21% ` [PATCH QEMU HCI-8.0 v4 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 22% ` Andrey Drobyshev
2026-09-24 16:41 26% ` [PATCH QEMU HCI-8.0 v4 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
` (2 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev, den
usb_host_open() reads the product string descriptor from the device
with a synchronous control transfer. On the CPR target this runs while
the device is being handed over, and the transfer can block there,
hanging the incoming migration.
The string only fills the cosmetic product_desc, so skip the read when
we're the CPR target and fall back to the synthetic "host:bus.addr"
name.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 1e261554609..0108ccba72e 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1067,7 +1067,7 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
udev->speed = speed_map[libusb_speed];
usb_host_speed_compat(s);
- if (s->ddesc.iProduct) {
+ if (s->ddesc.iProduct && !cpr_is_incoming()) {
libusb_get_string_descriptor_ascii(s->dh, s->ddesc.iProduct,
(unsigned char *)udev->product_desc,
sizeof(udev->product_desc));
--
2.47.1
^ permalink raw reply [relevance 22%]
* [PATCH QEMU HCI-8.0 v4 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
` (4 preceding siblings ...)
2026-09-24 16:41 23% ` [PATCH QEMU HCI-8.0 v4 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 21% ` Andrey Drobyshev
2026-09-24 16:41 22% ` [PATCH QEMU HCI-8.0 v4 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
` (3 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev, den
The previous commit drains the in-flight URBs in .pre_save() and leaves
their packets ASYNC for the target to replay. If migration fails, the
source resumes instead, with those packets still owned by the
controller and nothing left to complete them.
Remember the drained packets in a list, and re-issue them on the
preserved FD from a MIG_EVENT_PRECOPY_FAILED notifier. Control
transfers can't be reconstructed from the packet alone, so complete
those as errors and let the guest driver retry.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 76 insertions(+), 2 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 4ea469de2d9..1e261554609 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -107,6 +107,9 @@ struct USBHostDevice {
int hostfd;
Error *cpr_blocker;
+ NotifierWithReturn cpr_notifier;
+ GSList *cpr_inflight;
+ bool cpr_drained;
libusb_device *dev;
libusb_device_handle *dh;
struct libusb_device_descriptor ddesc;
@@ -1150,8 +1153,13 @@ static bool usb_host_iso_inflight(USBHostDevice *s)
}
/*
- * Quiesce the device for a CPR switchover. Cancel and reap all URBs,
- * so that no URB submitted by this process can outlive it.
+ * Quiesce the device for a CPR switchover. Cancel and reap all URBs
+ * without completing their packets, so that no URB submitted by this
+ * process can outlive it. CPR target re-executes them in
+ * usb_xhci_post_load() kicking the running endpoints.
+ *
+ * Keep the list of packets so that they can be re-issued on this side
+ * instead if MIG_EVENT_PRECOPY_FAILED fires.
*/
static int usb_host_cpr_drain_xfers(USBHostDevice *s)
{
@@ -1162,6 +1170,10 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
if (r->p) {
+ if (r->p->state == USB_PACKET_ASYNC) {
+ /* In-flight req, in submission order, for the failure path */
+ s->cpr_inflight = g_slist_append(s->cpr_inflight, r->p);
+ }
/*
* Clear r->p so the reap callback early-outs on it: the
* callback must still run (it frees the request), but it
@@ -1171,6 +1183,7 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
}
libusb_cancel_transfer(r->xfer);
}
+ s->cpr_drained = true;
if (!usb_host_reap_xfers(s)) {
/*
@@ -1287,6 +1300,53 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
return ret;
}
+static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+
+/*
+ * A failed CPR migration resumes the source VM with the drained
+ * packets still owned by the host controller as in-flight. Re-issue
+ * them on the preserved fd. Control transfers cannot be reconstructed
+ * from the packet alone; complete them as errors and let the guest
+ * driver retry.
+ */
+static int usb_host_cpr_notifier(NotifierWithReturn *notifier,
+ MigrationEvent *e, Error **errp)
+{
+ USBHostDevice *s = container_of(notifier, USBHostDevice, cpr_notifier);
+ USBDevice *udev = USB_DEVICE(s);
+ GSList *it;
+ USBPacket *p;
+
+ if (e->type != MIG_EVENT_PRECOPY_FAILED || !s->cpr_drained) {
+ return 0;
+ }
+
+ for (it = s->cpr_inflight; it; it = it->next) {
+ p = it->data;
+ /* Replay from the start of the TD */
+ p->actual_length = 0;
+ if (p->ep->nr == 0) {
+ /*
+ * p->ep[0] is control endpoint. Control transfers can't
+ * be reconstructed reliably, so complete them as errors
+ * and let the guest retry.
+ */
+ p->status = USB_RET_IOERROR;
+ usb_generic_async_ctrl_complete(udev, p);
+ } else {
+ usb_host_handle_data(udev, p);
+ if (p->status != USB_RET_ASYNC) {
+ /* Completed synchronously with error */
+ usb_packet_complete(udev, p);
+ }
+ }
+ }
+ g_slist_free(s->cpr_inflight);
+ s->cpr_inflight = NULL;
+ s->cpr_drained = false;
+ return 0;
+}
+
static void usb_host_realize(USBDevice *udev, Error **errp)
{
USBHostDevice *s = USB_HOST_DEVICE(udev);
@@ -1363,6 +1423,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
+#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
+ if (s->hostdevice && DEVICE(s)->id) {
+ migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1);
+ }
+#endif
+
error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
DEVICE(s)->id ?: "(anonymous)");
if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
@@ -1396,6 +1464,9 @@ static void usb_host_unrealize(USBDevice *udev)
}
usb_host_close(s);
migrate_del_blocker(&s->cpr_blocker);
+ migration_remove_notifier(&s->cpr_notifier);
+ g_slist_free(s->cpr_inflight);
+ s->cpr_inflight = NULL;
}
static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1403,6 +1474,9 @@ static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
USBHostDevice *s = USB_HOST_DEVICE(udev);
USBHostRequest *r;
+ /* Packet is being cancelled -> it mustn't be re-issued on failed CPR */
+ s->cpr_inflight = g_slist_remove(s->cpr_inflight, p);
+
if (p->combined) {
usb_combined_packet_cancel(udev, p);
return;
--
2.47.1
^ permalink raw reply [relevance 21%]
* [PATCH QEMU HCI-8.0 v4 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800
` (3 preceding siblings ...)
2026-09-24 16:41 25% ` [PATCH QEMU HCI-8.0 v4 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 23% ` Andrey Drobyshev
2026-09-24 16:41 21% ` [PATCH QEMU HCI-8.0 v4 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
` (4 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev, den
URBs submitted by the old QEMU must not outlive it across the
switchover: the kernel would complete them into buffers of an address
space which is gone after exec, and the new libusb context would reap
URB pointers it never submitted.
So, in .pre_save() for the CPR modes, cancel and reap every pending
transfer, without completing its packet. The packets stay ASYNC and
their TDs remain on the transfer rings, so the target re-executes them
when usb_xhci_post_load() kicks the endpoints - the same in-flight
replay as on regular live migration. A partially executed transfer is
simply re-run from the start of the TD.
Isochronous URBs are not on the request list but on their own rings,
and freeing them while in-flight would leave them queued on the
preserved FD. Cancel them so they are unlinked and reaped: an iso URB
in flight when the guest stops may never complete on its own. The
target reconstructs the iso stream from the guest's transfer ring.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 94 insertions(+), 3 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 9b70d6deef9..4ea469de2d9 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -48,6 +48,7 @@
#include "qapi/error.h"
#include "migration/blocker.h"
#include "migration/cpr.h"
+#include "migration/misc.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -403,7 +404,10 @@ static void usb_host_req_free(USBHostRequest *r)
#ifdef CONFIG_WIN32
request_count--;
#endif
- QTAILQ_REMOVE(&r->host->requests, r, next);
+ /* Skip if a timed-out reap already unlinked it (avoid a double remove) */
+ if (QTAILQ_IN_USE(r, next)) {
+ QTAILQ_REMOVE(&r->host->requests, r, next);
+ }
libusb_free_transfer(r->xfer);
g_free(r->buffer);
g_free(r);
@@ -1096,7 +1100,7 @@ fail:
return -1;
}
-static void usb_host_reap_xfers(USBHostDevice *s)
+static bool usb_host_reap_xfers(USBHostDevice *s)
{
USBHostRequest *r, *rtmp;
int limit = 100;
@@ -1117,9 +1121,10 @@ static void usb_host_reap_xfers(USBHostDevice *s)
QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
QTAILQ_REMOVE(&s->requests, r, next);
}
- return;
+ return false;
}
}
+ return true;
}
static void usb_host_abort_xfers(USBHostDevice *s)
@@ -1132,6 +1137,76 @@ static void usb_host_abort_xfers(USBHostDevice *s)
usb_host_reap_xfers(s);
}
+static bool usb_host_iso_inflight(USBHostDevice *s)
+{
+ USBHostIsoRing *ring;
+
+ QTAILQ_FOREACH(ring, &s->isorings, next) {
+ if (!QTAILQ_EMPTY(&ring->inflight)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/*
+ * Quiesce the device for a CPR switchover. Cancel and reap all URBs,
+ * so that no URB submitted by this process can outlive it.
+ */
+static int usb_host_cpr_drain_xfers(USBHostDevice *s)
+{
+ USBHostRequest *r, *rtmp;
+ USBHostIsoRing *ring;
+ USBHostIsoXfer *xfer;
+ int limit;
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ if (r->p) {
+ /*
+ * Clear r->p so the reap callback early-outs on it: the
+ * callback must still run (it frees the request), but it
+ * must not complete the packet.
+ */
+ r->p = NULL;
+ }
+ libusb_cancel_transfer(r->xfer);
+ }
+
+ if (!usb_host_reap_xfers(s)) {
+ /*
+ * No URB can be left live on the FD across CPR migration. If
+ * we can't reap them -> abort the migration.
+ */
+ return -1;
+ }
+
+ /*
+ * Iso URBs are on the rings, not s->requests. Cancel them: one in
+ * flight when the guest stops may never complete on its own, and none
+ * may be left on the preserved fd. The target replays from the ring.
+ */
+ QTAILQ_FOREACH(ring, &s->isorings, next) {
+ QTAILQ_FOREACH(xfer, &ring->inflight, next) {
+ libusb_cancel_transfer(xfer->xfer);
+ }
+ }
+
+ /*
+ * Cap the reap at 2x a full ring:
+ * iso_urb_count URBs x iso_urb_frames packets each
+ */
+ limit = 2 * s->iso_urb_count * s->iso_urb_frames;
+ while (usb_host_iso_inflight(s)) {
+ struct timeval tv = { .tv_usec = 1000 };
+ libusb_handle_events_timeout(ctx, &tv);
+ if (limit-- == 0) {
+ return -1;
+ }
+ }
+ usb_host_iso_free_all(s);
+ return 0;
+}
+
static int usb_host_close(USBHostDevice *s)
{
USBDevice *udev = USB_DEVICE(s);
@@ -1807,10 +1882,26 @@ static int usb_host_post_load(void *opaque, int version_id)
return 0;
}
+static int usb_host_pre_save(void *opaque)
+{
+ USBHostDevice *s = opaque;
+ MigMode mode = migrate_mode();
+
+ /* URBs submitted by this process must not outlive CPR migration */
+ if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
+ s->dh) {
+ if (usb_host_cpr_drain_xfers(s) < 0) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
static const VMStateDescription vmstate_usb_host = {
.name = "usb-host",
.version_id = 1,
.minimum_version_id = 1,
+ .pre_save = usb_host_pre_save,
.post_load = usb_host_post_load,
.fields = (const VMStateField[]) {
VMSTATE_USB_DEVICE(parent_obj, USBHostDevice),
--
2.47.1
^ permalink raw reply [relevance 23%]
* [PATCH QEMU HCI-8.0 v4 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800
2026-09-24 16:41 31% ` [PATCH QEMU HCI-8.0 v4 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 13% ` [PATCH QEMU HCI-8.0 v4 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 26% ` Andrey Drobyshev
2026-09-24 16:41 25% ` [PATCH QEMU HCI-8.0 v4 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
` (6 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev, den
In .realize(), save freshly opened host device FD into the CPR registry,
and then reuse it on CPR target. Also, add deletion of that FD from
registry to usb_host_open() / usb_host_close() cleanup paths.
For this to work, we also need to skip the .post-load() code which
closes, detaches the device, and then rescans the host bus to reopen
matching hostdevs. For the CPR-case migration we don't want any of that
as the hostdev FD stays preserved, and the guest shouldn't notice the
switchover.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 33 ++++++++++++++++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 5c3da00d8eb..b946b0ef350 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -47,6 +47,7 @@
#include "qapi/error.h"
#include "migration/blocker.h"
+#include "migration/cpr.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -947,6 +948,21 @@ static void usb_host_ep_update(USBHostDevice *s)
libusb_free_config_descriptor(conf);
}
+static char *usb_host_cpr_fd_name(USBHostDevice *s)
+{
+ /* Prefix the key so it can't clash with another CPR label */
+ return DEVICE(s)->id ? g_strdup_printf("usb-host/%s", DEVICE(s)->id)
+ : NULL;
+}
+
+static void usb_host_cpr_delete_fd(USBHostDevice *s)
+{
+ g_autofree char *name = usb_host_cpr_fd_name(s);
+ if (name) {
+ cpr_delete_fd(name, 0);
+ }
+}
+
static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
{
USBDevice *udev = USB_DEVICE(s);
@@ -1073,6 +1089,7 @@ fail:
s->dev = NULL;
}
if (s->hostfd != -1) {
+ usb_host_cpr_delete_fd(s);
close(s->hostfd);
s->hostfd = -1;
}
@@ -1134,6 +1151,7 @@ static int usb_host_close(USBHostDevice *s)
s->dev = NULL;
if (s->hostfd != -1) {
+ usb_host_cpr_delete_fd(s);
close(s->hostfd);
s->hostfd = -1;
}
@@ -1220,9 +1238,12 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
if (s->hostdevice) {
+ g_autofree char *name = usb_host_cpr_fd_name(s);
int fd;
+
s->needs_autoscan = false;
- fd = qemu_open(s->hostdevice, O_RDWR, errp);
+ fd = name ? cpr_open_fd(s->hostdevice, O_RDWR, name, 0, errp)
+ : qemu_open(s->hostdevice, O_RDWR, errp);
if (fd < 0) {
return;
}
@@ -1761,6 +1782,16 @@ static int usb_host_post_load(void *opaque, int version_id)
{
USBHostDevice *dev = opaque;
+ /*
+ * For CPR migration, device wasn't released/reset, and the guest
+ * is unaware of the switchover. The detach/rescan performed in
+ * usb_host_post_load_bh() only exists for cross-host migration.
+ * Skip it for CPR.
+ */
+ if (cpr_is_incoming()) {
+ return 0;
+ }
+
if (!dev->bh_postld) {
dev->bh_postld = qemu_bh_new_guarded(usb_host_post_load_bh, dev,
&DEVICE(dev)->mem_reentrancy_guard);
--
2.47.1
^ permalink raw reply [relevance 26%]
* [PATCH QEMU HCI-8.0 v4 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800
` (2 preceding siblings ...)
2026-09-24 16:41 26% ` [PATCH QEMU HCI-8.0 v4 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 25% ` Andrey Drobyshev
2026-09-24 16:41 23% ` [PATCH QEMU HCI-8.0 v4 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
` (5 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev, den
usb_host_abort_xfers() cancels every pending request and then pumps
libusb events until all of them are reaped, with a bounded wait.
Split the reap-wait loop into usb_host_reap_xfers(), so that the
following commit can reuse it to reap canceled transfers without going
through the abort path. No functional change.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b946b0ef350..9b70d6deef9 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1096,15 +1096,11 @@ fail:
return -1;
}
-static void usb_host_abort_xfers(USBHostDevice *s)
+static void usb_host_reap_xfers(USBHostDevice *s)
{
USBHostRequest *r, *rtmp;
int limit = 100;
- QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
- usb_host_req_abort(r);
- }
-
while (QTAILQ_FIRST(&s->requests) != NULL) {
struct timeval tv;
memset(&tv, 0, sizeof(tv));
@@ -1126,6 +1122,16 @@ static void usb_host_abort_xfers(USBHostDevice *s)
}
}
+static void usb_host_abort_xfers(USBHostDevice *s)
+{
+ USBHostRequest *r, *rtmp;
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ usb_host_req_abort(r);
+ }
+ usb_host_reap_xfers(s);
+}
+
static int usb_host_close(USBHostDevice *s)
{
USBDevice *udev = USB_DEVICE(s);
--
2.47.1
^ permalink raw reply [relevance 25%]
* [PATCH QEMU HCI-8.0 v4 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
@ 2026-09-24 16:41 31% ` Andrey Drobyshev
2026-09-24 16:41 13% ` [PATCH QEMU HCI-8.0 v4 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
` (8 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev, den
The FD passed into usb_host_open() for a wrapped device is only stored in
s->hostfd after libusb_wrap_sys_device() succeeds, and the failure path
doesn't close it. if wrapping fails, or a later step of the open sequence
fails (e.g. usb_device_attach), the FD is leaked, as usb_host_close() only
runs for fully opened devices, and the realize error path doesn't clean it
up either.
Store the FD in s->hostfd before wrapping and close it on the failure
path.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b74670ae256..83645fbe0ce 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -954,6 +954,11 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
int rc;
Error *local_err = NULL;
+ /* Make sure failure path frees caller-owned FD */
+ if (!dev) {
+ s->hostfd = hostfd;
+ }
+
if (s->bh_postld_pending) {
return -1;
}
@@ -978,7 +983,6 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
if (rc != 0) {
goto fail;
}
- s->hostfd = hostfd;
dev = libusb_get_device(s->dh);
bus_num = libusb_get_bus_number(dev);
addr = libusb_get_device_address(dev);
@@ -1066,6 +1070,10 @@ fail:
s->dh = NULL;
s->dev = NULL;
}
+ if (s->hostfd != -1) {
+ close(s->hostfd);
+ s->hostfd = -1;
+ }
return -1;
}
--
2.47.1
^ permalink raw reply [relevance 31%]
* [PATCH QEMU HCI-8.0 v4 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800
2026-09-24 16:41 31% ` [PATCH QEMU HCI-8.0 v4 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-09-24 16:41 13% ` Andrey Drobyshev
2026-09-24 16:41 26% ` [PATCH QEMU HCI-8.0 v4 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
` (7 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-24 16:41 UTC (permalink / raw)
To: svt-core; +Cc: Andrey Drobyshev, den
We're about to add support for CPR migration to usb-host in the upcoming
commits. Right now CPR is crashing with usb-host devices. Let's add
migration blocker upfront, for both cpr-exec and cpr-transfer. The
blocker is added unconditionally, so that we resuse CPR migration instead
of crashing on it. That is for the sake of bisectability of the upcoming
commits. It is going to be lifted (conditioned) in a following patch
once CPR support is added. Also, blocker is added at the end of
usb_host_realize(), so we have to cleanup all the side effects done
earlier in .realize() in case migrate_add_blocker_modes() fails.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 83645fbe0ce..5c3da00d8eb 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -46,6 +46,7 @@
#endif
#include "qapi/error.h"
+#include "migration/blocker.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -103,6 +104,7 @@ struct USBHostDevice {
char port[16];
int hostfd;
+ Error *cpr_blocker;
libusb_device *dev;
libusb_device_handle *dh;
struct libusb_device_descriptor ddesc;
@@ -1258,6 +1260,18 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
+
+ error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+ DEVICE(s)->id ?: "(anonymous)");
+ if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1) < 0) {
+ qemu_remove_exit_notifier(&s->exit);
+ if (s->needs_autoscan) {
+ QTAILQ_REMOVE(&hostdevs, s, next);
+ }
+ usb_host_close(s);
+ }
}
static void usb_host_instance_init(Object *obj)
@@ -1279,6 +1293,7 @@ static void usb_host_unrealize(USBDevice *udev)
QTAILQ_REMOVE(&hostdevs, s, next);
}
usb_host_close(s);
+ migrate_del_blocker(&s->cpr_blocker);
}
static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
--
2.47.1
^ permalink raw reply [relevance 13%]
* Re: [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in
2026-09-24 15:01 3% [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-09-24 15:01 6% ` [Devel] [PATCH hci-8.1 v3 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
@ 2026-09-24 16:17 0% ` Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-24 16:17 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
changelog v2->v3 ?
+ none of my comments are fixed in both those patches.
NAK for both.
And ask Claude + Fable to review your patches before sending them in the list.
--
Best regards,
Konstantin Khorenko,
Virtuozzo Linux Kernel Team
On 9/24/26 17:01, Eva Kurchatova wrote:
> The sysfs variant names nested leaves, kernel/vmcoreinfo and
> kernel/notes, while the proc one names files at the top of the
> filesystem. Both enforcement tests grant the leaves and nothing else,
> so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
> cannot traverse, and every open answers ENOENT:
>
> not ok 14 ve_perms.sysfs.enforce_allows
> not ok 15 ve_perms.sysfs.enforce_denies
>
> Grant x on each directory along the way, taken from the path the
> variant already gives.
>
> Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")
>
> https://virtuozzo.atlassian.net/browse/VSTOR-144955
>
> Feature: fix vz selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> ---
> tools/testing/selftests/ve/ve_perms_test.c | 33 ++++++++++++++++++++++
> 1 file changed, 33 insertions(+)
>
> diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
> index 25ffbd42c380..2b542dc30139 100644
> --- a/tools/testing/selftests/ve/ve_perms_test.c
> +++ b/tools/testing/selftests/ve/ve_perms_test.c
> @@ -348,6 +348,37 @@ TEST_F(ve_perms, write_parser)
> * read, and granting x on the directory lets it be traversed (the lookup then
> * fails ENOENT on the missing child, not EACCES).
> */
> +/*
> + * A leaf is only reachable once every directory above it can be traversed.
> + * The variants name their files relative to the mount point, so grant x on
> + * each directory along the way; a file at the top of the filesystem has
> + * none and needs nothing.
> + */
> +static int allow_path(int cgv2_fd, const char *path, const char *file)
> +{
> + char dir[240]; /* set_entry() writes "<dir> <mask>" into 256 */
> + const char *sep;
> +
> + for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
> + if (sep - file >= (long)sizeof(dir))
> + return -1;
> + snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file), file);
> + if (set_entry(cgv2_fd, path, dir, "x") < 0)
> + return -1;
> + }
> + return 0;
> +}
> +
> +static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
> +{
> + int i;
> +
> + for (i = 0; i < NR_FILES; i++)
> + if (allow_path(cgv2_fd, path, files[i]) < 0)
> + return -1;
> + return 0;
> +}
> +
> TEST_F(ve_perms, enforce_allows)
> {
> char path[PATH_MAX], absent[128];
> @@ -358,6 +389,7 @@ TEST_F(ve_perms, enforce_allows)
>
> ve_file(path, sizeof(path), self->ctid_a, variant->fs);
>
> + ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
> ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
> for (i = 0; i < NR_FILES; i++)
> EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
> @@ -400,6 +432,7 @@ TEST_F(ve_perms, enforce_denies)
> O_RDONLY), 0);
>
> /* Granted w only: visible but not readable. */
> + ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
> ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
> for (i = 0; i < NR_FILES; i++)
> EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* [Devel] [PATCH hci-8.1 v3 2/2] selftests: ve: introduce vtty pair teardown stress test
2026-09-24 15:01 3% [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
@ 2026-09-24 15:01 6% ` Eva Kurchatova
2026-09-24 16:17 0% ` [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-24 15:01 UTC (permalink / raw)
To: vasileios.almpanis; +Cc: devel
Either end of a vtty pair creates it, and the last close tears it down.
Race condition in older VZKernel vtty implementation allowed both sides
to decide they were the final one and call release_tty() twice, and a
master opening again while a teardown was in flight used to hand out a
reference to a tty_struct about to be freed. Either way, this caused
use-after-free.
Hammer both ends for ownership correctness & thread safety verification.
Sixty seconds by default, enough to bring down a buggy kernel.
https://virtuozzo.atlassian.net/browse/VSTOR-144771
Feature: ve selftest
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/ve/.gitignore | 1 +
tools/testing/selftests/ve/Makefile | 1 +
tools/testing/selftests/ve/settings | 1 +
tools/testing/selftests/ve/ve_vtty_stress.c | 319 ++++++++++++++++++++
4 files changed, 322 insertions(+)
create mode 100644 tools/testing/selftests/ve/settings
create mode 100644 tools/testing/selftests/ve/ve_vtty_stress.c
diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index afa4c568c2c9..9d8d86093827 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,2 +1,3 @@
ve_ns_owner_test
ve_perms_test
+ve_vtty_stress
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index ec40cbc7b3a1..6cd5823c4d93 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
TEST_GEN_PROGS += ve_ns_owner_test
TEST_GEN_PROGS += ve_perms_test
+TEST_GEN_PROGS += ve_vtty_stress
include ../lib.mk
diff --git a/tools/testing/selftests/ve/settings b/tools/testing/selftests/ve/settings
new file mode 100644
index 000000000000..694d70710ff0
--- /dev/null
+++ b/tools/testing/selftests/ve/settings
@@ -0,0 +1 @@
+timeout=300
diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
new file mode 100644
index 000000000000..4de40a45428b
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_vtty_stress.c
@@ -0,0 +1,319 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_vtty_stress - open and close a vtty pair from both ends at once.
+ *
+ * Either end of a vtty pair creates it, and the last close tears it down.
+ *
+ * Race condition in older VZKernel vtty implementation allowed both sides
+ * to decide they were the final one and call release_tty() twice, and a
+ * master opening again while a teardown was in flight used to hand out a
+ * reference to a tty_struct about to be freed. Either way, this caused
+ * use-after-free.
+ *
+ * Hammer both ends. The master comes from the vzctl ioctl, which does
+ * not look the VE up. The slave is reachable only from inside the VE, as
+ * TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
+ * vttys index minor - 1, so the test builds a VE with cgroup2, sets
+ * ve.veid to the cgroup it made, and runs the slave side there.
+ *
+ * A warning does not stop the host, so the run reads the kernel log too.
+ * One read() of /dev/kmsg is one record, so the test matches vtty driver
+ * splats, as well as generic kernel warning/oops/kasan reports.
+ *
+ * Usage: ve_vtty_stress [seconds] (default 60, or $VE_VTTY_SECS)
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <stdbool.h>
+#include <sys/ioctl.h>
+#include <sys/prctl.h>
+#include <linux/major.h>
+#include <sys/sysmacros.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "ve_selftest.h"
+#include "../kselftest.h"
+
+/* The ioctl argument; the trailing data[] the uapi struct has is unused. */
+struct vzctl_ve_configure {
+ unsigned int veid, key, val, size;
+};
+
+#define VZCTL_VE_CONFIGURE _IOW('.', 15, struct vzctl_ve_configure)
+#define VE_CONFIGURE_OPEN_TTY 5
+
+#define VTTY_IDX 0 /* the pair both ends work on */
+#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
+#define DEFAULT_SECS 60
+
+/*
+ * Expected open errno codes: the pair may be closing, or gone already,
+ * when an open lands on it.
+ * Anything else is an environment issue or a bug.
+ */
+static bool expected_errno(int err)
+{
+ return err == EBUSY || err == EIO || err == ENXIO;
+}
+
+static int open_master(int ctl_fd, unsigned int veid)
+{
+ struct vzctl_ve_configure c = {
+ .veid = veid,
+ .key = VE_CONFIGURE_OPEN_TTY,
+ .val = VTTY_IDX,
+ };
+
+ return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
+}
+
+/* The slave end, inside the VE. Returns the errno that stopped it, or 0. */
+static int hammer_slave(const char *node, time_t deadline,
+ unsigned long *opened)
+{
+ int err = 0;
+
+ while (time(NULL) < deadline) {
+ int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
+
+ if (fd >= 0) {
+ (*opened)++;
+ close(fd);
+ } else if (!expected_errno(errno)) {
+ err = errno;
+ break;
+ }
+ }
+
+ return err;
+}
+
+/* The kernel log, since a warning fails nothing by itself. */
+static int kmsg_open(void)
+{
+ int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+
+ if (fd >= 0)
+ lseek(fd, 0, SEEK_END);
+ return fd;
+}
+
+/*
+ * Check for kernel log warnings, one read() per record, hence
+ * the function name is not checked.
+ */
+static int kmsg_check(int fd)
+{
+ char buf[8192];
+ int found = 0;
+ ssize_t r;
+
+ if (fd < 0)
+ return 0;
+
+ for (;;) {
+ r = read(fd, buf, sizeof(buf) - 1);
+ if (r < 0) {
+ /* Ring overrun, continue with the oldest existing record */
+ if (errno == EPIPE)
+ continue;
+ break;
+ }
+ if (r == 0)
+ break;
+ buf[r] = '\0';
+ /* Known symptoms of a buggy kernel */
+ if (strstr(buf, "vtty") && (strstr(buf, "tty->count") ||
+ strstr(buf, "bad slave count"))) {
+ ksft_print_msg("vtty mismatched count log: %s", buf);
+ found++;
+ }
+ if (strstr(buf, "WARNING: ") || strstr(buf, "Oops: ") ||
+ strstr(buf, ";BUG: ") || strstr(buf, "soft lockup - ")) {
+ ksft_print_msg("kernel log: %s", buf);
+ found++;
+ }
+ }
+ return found;
+}
+
+int main(int argc, char **argv)
+{
+ unsigned long master_opened = 0, slave_opened = 0;
+ int cg, ctid, ctl, kmsg, ready[2], count[2], status;
+ int master_err = 0, slave_err;
+ const char *env = getenv("VE_VTTY_SECS");
+ char path[PATH_MAX], val[16], c;
+ char dir[] = "/tmp/ve_vtty_XXXXXX";
+ char node[sizeof(dir) + 6];
+ time_t deadline;
+ long secs;
+ pid_t pid;
+
+ ksft_print_header();
+ secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
+ if (secs <= 0)
+ secs = DEFAULT_SECS;
+
+ ctl = open("/dev/vzctl", O_RDWR);
+ if (ctl < 0)
+ ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
+ "(vzdev, vzmon) are needed\n",
+ strerror(errno));
+
+ cg = mount_cg2_fd();
+ if (cg < 0)
+ ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
+ write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS);
+
+ ctid = make_ve(cg, CTID_MIN);
+ if (ctid < 0)
+ ksft_exit_skip("cannot make a VE cgroup\n");
+
+ /* The ve.veid must match between master / slave */
+ snprintf(path, sizeof(path), "%d/ve.veid", ctid);
+ snprintf(val, sizeof(val), "%d", ctid);
+ if (write_file_at(cg, path, val) < 0) {
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
+ }
+
+ /* Do not report a failure where the vtty pair cannot be had at all */
+ status = open_master(ctl, ctid);
+ if (status < 0 && errno != EBUSY) {
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("no vtty master: %s\n", strerror(errno));
+ }
+ if (status >= 0)
+ close(status);
+
+ /*
+ * Use temp directory for the node the slave end opens.
+ * Cleaned up by the master side instead of relying on the slave.
+ */
+ if (!mkdtemp(dir)) {
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("mkdtemp: %s\n", strerror(errno));
+ }
+ snprintf(node, sizeof(node), "%s/slave", dir);
+ if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0) {
+ status = errno;
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("mknod: %s\n", strerror(status));
+ }
+
+ ksft_set_plan(1);
+ kmsg = kmsg_open();
+ if (kmsg < 0)
+ ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
+ "be noticed\n",
+ strerror(errno));
+
+ if (pipe(ready) || pipe(count)) {
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("pipe: %s\n", strerror(errno));
+ }
+
+ deadline = time(NULL) + secs;
+ ksft_print_msg("VE %d, pair %d, %ld seconds\n", ctid, VTTY_IDX, secs);
+
+ pid = fork();
+ if (pid == 0) {
+ close(ready[0]);
+ close(count[0]);
+ if (enter_cgroup(cg, ctid) < 0)
+ _exit(errno ? errno : 1);
+ /* CLONE_NEWVE puts the task in the VE of the cgroup */
+ if (unshare(CLONE_NEWVE | CLONE_NEWPID | CLONE_NEWNS) < 0)
+ _exit(errno ? errno : 1);
+ if (fork() == 0) {
+ unsigned long n = 0;
+ int err;
+
+ /*
+ * Hammering on after the run has given up would hold
+ * the cgroup open against destroy_ve().
+ */
+ prctl(PR_SET_PDEATHSIG, SIGKILL);
+
+ c = 'r';
+ if (write(ready[1], &c, 1) != 1)
+ _exit(errno);
+ err = hammer_slave(node, deadline, &n);
+ /* Send the count either way: it says how far it got. */
+ if (write(count[1], &n, sizeof(n)) != sizeof(n))
+ _exit(errno);
+ _exit(err);
+ }
+ wait(&status);
+ _exit(WIFEXITED(status) ? WEXITSTATUS(status) : 1);
+ } else if (pid < 0) {
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("fork: %s\n", strerror(errno));
+ }
+ close(ready[1]);
+ close(count[1]);
+
+ if (read(ready[0], &c, 1) != 1) {
+ waitpid(pid, &status, 0);
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("the VE side never came up\n");
+ }
+
+ while (time(NULL) < deadline) {
+ int fd = open_master(ctl, ctid);
+
+ if (fd >= 0) {
+ master_opened++;
+ close(fd);
+ } else if (!expected_errno(errno)) {
+ master_err = errno;
+ break;
+ }
+ }
+
+ if (master_err) {
+ /* The other end runs to its own deadline, stop it */
+ kill(pid, SIGKILL);
+ waitpid(pid, &status, 0);
+ slave_err = 0;
+ } else {
+ if (read(count[0], &slave_opened, sizeof(slave_opened)) !=
+ sizeof(slave_opened))
+ slave_opened = 0;
+ waitpid(pid, &status, 0);
+ slave_err = WIFEXITED(status) ? WEXITSTATUS(status) : EINTR;
+ }
+
+ ksft_print_msg("%lu master opens, %lu slave opens\n", master_opened,
+ slave_opened);
+
+ status = kmsg_check(kmsg);
+ unlink(node);
+ rmdir(dir);
+ destroy_ve(cg, ctid);
+
+ if (master_err)
+ ksft_exit_fail_msg("the master end stopped: %s\n",
+ strerror(master_err));
+ if (slave_err)
+ ksft_exit_fail_msg("the slave end stopped: %s\n",
+ strerror(slave_err));
+ if (!master_opened)
+ ksft_exit_fail_msg("no master was ever opened\n");
+ if (!slave_opened)
+ ksft_exit_fail_msg("no slave was ever opened\n");
+ if (status)
+ ksft_exit_fail_msg("kernel complained %d time(s)\n", status);
+
+ ksft_test_result_pass("vtty pair closed from both ends\n");
+ ksft_finished();
+}
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 6%]
* [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in
@ 2026-09-24 15:01 3% Eva Kurchatova
2026-09-24 15:01 6% ` [Devel] [PATCH hci-8.1 v3 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-09-24 16:17 0% ` [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in Konstantin Khorenko
0 siblings, 2 replies; 200+ results
From: Eva Kurchatova @ 2026-09-24 15:01 UTC (permalink / raw)
To: vasileios.almpanis; +Cc: devel
The sysfs variant names nested leaves, kernel/vmcoreinfo and
kernel/notes, while the proc one names files at the top of the
filesystem. Both enforcement tests grant the leaves and nothing else,
so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
cannot traverse, and every open answers ENOENT:
not ok 14 ve_perms.sysfs.enforce_allows
not ok 15 ve_perms.sysfs.enforce_denies
Grant x on each directory along the way, taken from the path the
variant already gives.
Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")
https://virtuozzo.atlassian.net/browse/VSTOR-144955
Feature: fix vz selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/ve/ve_perms_test.c | 33 ++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 25ffbd42c380..2b542dc30139 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -348,6 +348,37 @@ TEST_F(ve_perms, write_parser)
* read, and granting x on the directory lets it be traversed (the lookup then
* fails ENOENT on the missing child, not EACCES).
*/
+/*
+ * A leaf is only reachable once every directory above it can be traversed.
+ * The variants name their files relative to the mount point, so grant x on
+ * each directory along the way; a file at the top of the filesystem has
+ * none and needs nothing.
+ */
+static int allow_path(int cgv2_fd, const char *path, const char *file)
+{
+ char dir[240]; /* set_entry() writes "<dir> <mask>" into 256 */
+ const char *sep;
+
+ for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
+ if (sep - file >= (long)sizeof(dir))
+ return -1;
+ snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file), file);
+ if (set_entry(cgv2_fd, path, dir, "x") < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
+{
+ int i;
+
+ for (i = 0; i < NR_FILES; i++)
+ if (allow_path(cgv2_fd, path, files[i]) < 0)
+ return -1;
+ return 0;
+}
+
TEST_F(ve_perms, enforce_allows)
{
char path[PATH_MAX], absent[128];
@@ -358,6 +389,7 @@ TEST_F(ve_perms, enforce_allows)
ve_file(path, sizeof(path), self->ctid_a, variant->fs);
+ ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
for (i = 0; i < NR_FILES; i++)
EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
@@ -400,6 +432,7 @@ TEST_F(ve_perms, enforce_denies)
O_RDONLY), 0);
/* Granted w only: visible but not readable. */
+ ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
for (i = 0; i < NR_FILES; i++)
EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH vz10 4/4] selftests: drv-net-hw: skip the tests that need a real interface
@ 2026-09-24 13:23 3% ` Konstantin Khorenko
0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-24 13:23 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
> ethtool, ethtool_extended_state, hw_stats_l3 and hw_stats_l3_gre need a
> device that can set link speed and autonegotiation, report an extended
> link state, or offload L3 statistics. Where none is configured, lib.sh
> creates a veth pair instead, which supports none of that, so every case
> fails on something the kernel correctly refuses.
>
> Upstream answers this by keeping such tests in drivers/net/hw and not
> running them on veth at all: commit 0c499a351777 ("selftests:
> forwarding: Ditch skip_on_veth()") removed the equivalent check once the
> directory was split out. This guard is therefore not for upstream, it
> only stops a run that substitutes veth pairs from reporting failures.
>
> Skip before sourcing lib.sh, because lib.sh makes the veth pair as it is
> sourced. Exiting afterwards leaves the devices behind - vrf_cleanup()
> removes them and a test that skips never gets there - and a later test
> that adds a veth by one of those names then cannot. hw_stats_l3_gre
> asks for six interfaces, so one run left veth0 to veth5 in the initial
> namespace and the netfilter suite failed on it.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-139651
> Feature: fix selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/drivers/net/hw/ethtool.sh b/tools/testing/selftests/drivers/net/hw/ethtool.sh
> index fa6953de6b6dd..b1744758627c2 100755
> --- a/tools/testing/selftests/drivers/net/hw/ethtool.sh
> +++ b/tools/testing/selftests/drivers/net/hw/ethtool.sh
> @@ -11,6 +11,22 @@ ALL_TESTS="
> "
> NUM_NETIFS=2
> lib_dir=$(dirname "$0")
> +
> +# The test needs a device capable of setting link speed and
> +# autonegotiation. Where no real interfaces are configured, lib.sh makes
> +# a veth pair instead, which cannot, and every case then fails on
> +# something the kernel correctly refuses. Check this before sourcing
> +# lib.sh: it creates that pair as it is sourced, so exiting afterwards
> +# leaves the devices behind, and the next test that wants one of those
> +# names cannot create it.
This is the commit message pasted into the test, four times. The
rationale belongs in the commit message only. In the code one line is
enough, something like:
> +
> +: "${NETIF_CREATE:=yes}"
> +: "${NETIF_TYPE:=veth}"
> +if [[ "$NETIF_CREATE" = yes && "$NETIF_TYPE" = veth ]]; then
> + echo "SKIP: needs a real interface, set NETIFS in forwarding.config"
> + exit 4 # ksft_skip, lib.sh is not sourced yet
> +fi
> +
> source "$lib_dir"/../../../net/forwarding/lib.sh
> source ethtool_lib.sh
>
> diff --git a/tools/testing/selftests/drivers/net/hw/ethtool_extended_state.sh b/tools/testing/selftests/drivers/net/hw/ethtool_extended_state.sh
> index a7584448416ea..c6c96117b1f12 100755
> --- a/tools/testing/selftests/drivers/net/hw/ethtool_extended_state.sh
> +++ b/tools/testing/selftests/drivers/net/hw/ethtool_extended_state.sh
> @@ -9,6 +9,22 @@ ALL_TESTS="
>
> NUM_NETIFS=2
> lib_dir=$(dirname "$0")
> +
> +# The test needs a device capable of reporting an extended link state.
> +# Where no real interfaces are configured, lib.sh makes a veth pair
> +# instead, which cannot, and every case then fails on something the
> +# kernel correctly refuses. Check this before sourcing lib.sh: it
> +# creates that pair as it is sourced, so exiting afterwards leaves the
> +# devices behind, and the next test that wants one of those names cannot
> +# create it.
> +
> +: "${NETIF_CREATE:=yes}"
> +: "${NETIF_TYPE:=veth}"
> +if [[ "$NETIF_CREATE" = yes && "$NETIF_TYPE" = veth ]]; then
> + echo "SKIP: needs a real interface, set NETIFS in forwarding.config"
> + exit 4 # ksft_skip, lib.sh is not sourced yet
> +fi
> +
> source "$lib_dir"/../../../net/forwarding/lib.sh
> source ethtool_lib.sh
>
> diff --git a/tools/testing/selftests/drivers/net/hw/hw_stats_l3.sh b/tools/testing/selftests/drivers/net/hw/hw_stats_l3.sh
> index 67fafefc80bea..1776a4954601b 100755
> --- a/tools/testing/selftests/drivers/net/hw/hw_stats_l3.sh
> +++ b/tools/testing/selftests/drivers/net/hw/hw_stats_l3.sh
> @@ -49,6 +49,22 @@ ALL_TESTS="
> "
> NUM_NETIFS=4
> lib_dir=$(dirname "$0")
> +
> +# The test needs a device capable of offloading L3 statistics. Where no
> +# real interfaces are configured, lib.sh makes a veth pair instead,
> +# which cannot, and every case then fails on something the kernel
> +# correctly refuses. Check this before sourcing lib.sh: it creates that
> +# pair as it is sourced, so exiting afterwards leaves the devices
> +# behind, and the next test that wants one of those names cannot create
> +# it.
> +
> +: "${NETIF_CREATE:=yes}"
> +: "${NETIF_TYPE:=veth}"
> +if [[ "$NETIF_CREATE" = yes && "$NETIF_TYPE" = veth ]]; then
> + echo "SKIP: needs a real interface, set NETIFS in forwarding.config"
> + exit 4 # ksft_skip, lib.sh is not sourced yet
> +fi
> +
The check does not see forwarding.config. lib.sh sets these defaults
(lib.sh:54-55), then sources net/forwarding/forwarding.config
(lib.sh:126), and only then creates the interfaces (lib.sh:433). Here
lib.sh has not been sourced yet, so the config file has not been read
and the two variables fall back to yes/veth unless they are exported in
the environment.
So a user who does what the SKIP line tells them to, puts real NETIFS
into forwarding.config and sets NETIF_CREATE=no there, still gets the
skip: on real hardware all four tests never run. Only an exported
NETIF_CREATE=no would get past it, and forwarding.config is not
sourced into the environment anywhere.
Read the same file lib.sh reads before deciding:
forwarding_dir=$lib_dir/../../../net/forwarding
[[ -f $forwarding_dir/forwarding.config ]] && \
source "$forwarding_dir/forwarding.config"
: "${NETIF_CREATE:=yes}"
: "${NETIF_TYPE:=veth}"
Since the same block goes into four files, put it into a small helper
next to ethtool_lib.sh and source that before lib.sh, instead of
copying it.
> source "$lib_dir"/../../../net/forwarding/lib.sh
> source "$lib_dir"/../../../net/forwarding/tc_common.sh
>
Two more notes on the set of tests:
ethtool_rmon.sh in the same directory reads rmon histogram counters via
"ethtool -S --groups rmon" and has no skip of its own, so on veth it
fails the same way. If it fails in your runs too, it belongs in this
patch. ethtool_mm.sh and loopback.sh already skip by themselves.
The reference to 0c499a351777 is correct and the "why not upstream"
paragraph is useful, keep it.
--
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* [PATCH hci-8.0/hci-8.1 1/2] migration/block-dirty-bitmap: fix postcopy abort with bitmaps on several nodes #VSTOR-147066
@ 2026-09-24 12:13 35% ` Denis V. Lunev
0 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-24 12:13 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den, Denis V. Lunev
From: Denis V. Lunev <den@openvz.org>
With dirty bitmaps on more than one node, the source switches nodes
during the postcopy bulk phase and sends the node name again. The
destination reads those chunks in the postcopy listen thread, which does
not hold the BQL, and resolves the name through bdrv_lookup_bs():
qemu-system-x86_64: ../block.c:6400: bdrv_lookup_bs: Assertion
`qemu_in_main_thread()' failed.
The bdrv_find_dirty_bitmap() call right after it needs the BQL or the
dirty bitmap lock as well. Taking the BQL there would deadlock: the
listen thread holds s->lock, and dirty_bitmap_mig_before_vm_start()
takes s->lock with the BQL held.
Every node and bitmap that a non-START chunk refers to was set up by a
START chunk, and those are loaded under the BQL during setup. Record the
node alias there, and resolve all other chunks against s->bitmaps
instead of the block graph.
Bitmaps on a single node never resend the node name after setup, which
is why migrate-bitmaps-postcopy-test does not catch this.
Fixes: f791bf7f93f2 ("assertions for block global state API")
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
migration/block-dirty-bitmap.c | 59 +++++++++++++++++++++++++++++++---
1 file changed, 55 insertions(+), 4 deletions(-)
diff --git a/migration/block-dirty-bitmap.c b/migration/block-dirty-bitmap.c
index 9c8c308773d..d17eec412bb 100644
--- a/migration/block-dirty-bitmap.c
+++ b/migration/block-dirty-bitmap.c
@@ -136,6 +136,7 @@ typedef struct DBMSaveState {
} DBMSaveState;
typedef struct LoadBitmapState {
+ char node_alias[256];
BlockDriverState *bs;
BdrvDirtyBitmap *bitmap;
bool migrated;
@@ -861,6 +862,7 @@ static int dirty_bitmap_load_start(QEMUFile *f, DBMLoadState *s)
}
b = g_new(LoadBitmapState, 1);
+ g_strlcpy(b->node_alias, s->node_alias, sizeof(b->node_alias));
b->bs = s->bs;
b->bitmap = s->bitmap;
b->migrated = false;
@@ -1058,6 +1060,54 @@ static int dirty_bitmap_load_bits(QEMUFile *f, DBMLoadState *s)
return 0;
}
+/*
+ * Only START chunks are guaranteed to be loaded under the BQL; the rest may
+ * come from the postcopy listen thread and must not touch the block graph.
+ */
+static BlockDriverState *dirty_bitmap_load_find_bs(DBMLoadState *s,
+ const char *device,
+ const char *node_name,
+ Error **errp)
+{
+ GSList *item;
+
+ if (s->flags & DIRTY_BITMAP_MIG_FLAG_START) {
+ return bdrv_lookup_bs(device, node_name, errp);
+ }
+
+ for (item = s->bitmaps; item; item = g_slist_next(item)) {
+ LoadBitmapState *b = item->data;
+
+ if (!strcmp(b->node_alias, s->node_alias)) {
+ return b->bs;
+ }
+ }
+
+ error_setg(errp, "Error: no migrated bitmaps on node alias '%s'",
+ s->node_alias);
+ return NULL;
+}
+
+static BdrvDirtyBitmap *dirty_bitmap_load_find_bitmap(DBMLoadState *s)
+{
+ GSList *item;
+
+ if (s->flags & DIRTY_BITMAP_MIG_FLAG_START) {
+ return bdrv_find_dirty_bitmap(s->bs, s->bitmap_name);
+ }
+
+ for (item = s->bitmaps; item; item = g_slist_next(item)) {
+ LoadBitmapState *b = item->data;
+
+ if (b->bs == s->bs &&
+ !strcmp(bdrv_dirty_bitmap_name(b->bitmap), s->bitmap_name)) {
+ return b->bitmap;
+ }
+ }
+
+ return NULL;
+}
+
static int dirty_bitmap_load_header(QEMUFile *f, DBMLoadState *s,
GHashTable *alias_map)
{
@@ -1086,11 +1136,12 @@ static int dirty_bitmap_load_header(QEMUFile *f, DBMLoadState *s,
s->bs = NULL;
} else {
bitmap_alias_map = amin->subtree;
- s->bs = bdrv_lookup_bs(NULL, amin->string, &local_err);
+ s->bs = dirty_bitmap_load_find_bs(s, NULL, amin->string,
+ &local_err);
}
} else {
- s->bs = bdrv_lookup_bs(s->node_alias, s->node_alias,
- &local_err);
+ s->bs = dirty_bitmap_load_find_bs(s, s->node_alias,
+ s->node_alias, &local_err);
}
if (!s->bs) {
error_report_err(local_err);
@@ -1141,7 +1192,7 @@ static int dirty_bitmap_load_header(QEMUFile *f, DBMLoadState *s,
if (!s->cancelled) {
g_strlcpy(s->bitmap_name, bitmap_name, sizeof(s->bitmap_name));
- s->bitmap = bdrv_find_dirty_bitmap(s->bs, s->bitmap_name);
+ s->bitmap = dirty_bitmap_load_find_bitmap(s);
/*
* bitmap may be NULL here, it wouldn't be an error if it is the
--
2.53.0
^ permalink raw reply [relevance 35%]
* Re: [Devel] [PATCH vz10 3/3] selftests: drv-net: read the channel count over netlink
@ 2026-09-24 11:53 0% ` Konstantin Khorenko
0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-24 11:53 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
> napi_threaded needs the number of combined channels of the device and
> asks ethtool to print it as JSON. Not every ethtool prints that one:
> ethtool 6.11 does it for -k, -a, -c, -g and -x, but not for -l, and
> the test ends in the first case:
>
> CmdExitFailure: Command failed: ethtool --json -l eth4
> STDERR: b'ethtool: bad command line argument(s)
> JSON output not available for this subcommand
>
> not ok 1 napi_threaded.napi_init
>
> Ask the kernel for the count over netlink, which is where ethtool reads
> it too, so the test no longer depends on the ethtool version installed.
> Changing the count stays with ethtool -L, which needs no JSON. No other
> test in the group asks ethtool for something it cannot print, so this is
> the only one that needs it.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-139651
> Feature: fix selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/drivers/net/napi_threaded.py b/tools/testing/selftests/drivers/net/napi_threaded.py
> index ed66efa481b0f..73c81a319b49f 100755
> --- a/tools/testing/selftests/drivers/net/napi_threaded.py
> +++ b/tools/testing/selftests/drivers/net/napi_threaded.py
> @@ -7,7 +7,7 @@ Test napi threaded states.
>
> from lib.py import ksft_run, ksft_exit
> from lib.py import ksft_eq, ksft_ne, ksft_ge
> -from lib.py import NetDrvEnv, NetdevFamily
> +from lib.py import EthtoolFamily, NetDrvEnv, NetdevFamily
> from lib.py import cmd, defer, ethtool
>
>
> @@ -28,7 +28,13 @@ from lib.py import cmd, defer, ethtool
>
>
> def _setup_deferred_cleanup(cfg) -> None:
> - combined = ethtool(f"-l {cfg.ifname}", json=True)[0].get("combined", 0)
> + # Not every ethtool prints the channel counts as JSON, the one on
> + # the test machine here does not:
> + # ethtool: bad command line argument(s)
> + # JSON output not available for this subcommand
> + # Ask the kernel for them instead, it is the same number.
The long description with error messages examples should be in the commit
message, not in the code comment. A short comment like
"not every ethtool prints -l as JSON, ask the kernel"
should be enough.
--
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH vz10 v2 1/2] selftests: pstore: skip when no backend is registered
2026-09-10 3:17 4% [Devel] [PATCH vz10 v2 1/2] selftests: pstore: skip when no backend is registered Eva Kurchatova
2026-09-23 18:03 0% ` Konstantin Khorenko
@ 2026-09-23 18:37 0% ` Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-23 18:37 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
> An unregistered pstore backend reads back "(null)", and every latter
latter -> later
> check fails. This also contradicts the pstore_crash_test documentation.
>
> Fix by skipping an unregistered backend in common_tests
>
> https://virtuozzo.atlassian.net/browse/VSTOR-142447
> Feature: fix selftests
>
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/pstore/common_tests b/tools/testing/selftests/pstore/common_tests
> index 4509f0cc9c918..71cd4473f45a5 100755
> --- a/tools/testing/selftests/pstore/common_tests
> +++ b/tools/testing/selftests/pstore/common_tests
> @@ -55,6 +55,8 @@ operate_files() { # tested value, files, operation
>
> # Parameters
> TEST_STRING_PATTERN="Testing pstore: uuid="
> +# Kselftest framework requirement - SKIP code is 4.
> +ksft_skip=4
> UUID=`cat /proc/sys/kernel/random/uuid`
> TOP_DIR=`absdir $0`
> LOG_DIR=$TOP_DIR/logs/`date +%Y%m%d-%H%M%S`_${UUID}/
> @@ -74,8 +76,17 @@ prlog "=== Pstore unit tests (`basename $0`) ==="
> prlog "UUID="$UUID
>
> prlog -n "Checking pstore backend is registered ... "
> -backend=`cat /sys/module/pstore/parameters/backend`
> -show_result $?
> +# A tristate pstore leaves no parameter file at all when it is not built
> +# or not loaded, and an unregistered backend reads back as "(null)".
> +backend=`cat /sys/module/pstore/parameters/backend 2>/dev/null`
> +if [ -z "$backend" ] || [ "$backend" = "(null)" ]; then
> + prlog "SKIP"
> + prlog -e "\tbackend=${backend:-<none>}"
> + prlog -e "\tcmdline=`cat /proc/cmdline`"
> + prlog "pstore backend is not registered, skipping"
> + exit $ksft_skip
> +fi
> +show_result 0
> prlog -e "\tbackend=${backend}"
> prlog -e "\tcmdline=`cat /proc/cmdline`"
> if [ $rc -ne 0 ]; then
After the patch and "show_result 0" $rc here cannot become 0.
-show_result 0
+prlog "ok"
prlog -e "\tbackend=${backend}"
prlog -e "\tcmdline=`cat /proc/cmdline`"
-if [ $rc -ne 0 ]; then
- exit 1
-fi
> diff --git a/tools/testing/selftests/pstore/pstore_post_reboot_tests b/tools/testing/selftests/pstore/pstore_post_reboot_tests
> index d6da5e86efbf8..a5464407f5b21 100755
> --- a/tools/testing/selftests/pstore/pstore_post_reboot_tests
> +++ b/tools/testing/selftests/pstore/pstore_post_reboot_tests
> @@ -7,8 +7,6 @@
> # Written by Hiraku Toyooka <hiraku.toyooka.gu@hitachi.com>
> #
>
> -# Kselftest framework requirement - SKIP code is 4.
> -ksft_skip=4
>
After remove, we have 2 empty lines here, drop one.
--
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH vz10 v2 1/2] selftests: pstore: skip when no backend is registered
2026-09-10 3:17 4% [Devel] [PATCH vz10 v2 1/2] selftests: pstore: skip when no backend is registered Eva Kurchatova
@ 2026-09-23 18:03 0% ` Konstantin Khorenko
2026-09-23 18:37 0% ` Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-23 18:03 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
changelog?
--
Best regards,
Konstantin Khorenko,
Virtuozzo Linux Kernel Team
On 9/10/26 05:17, Eva Kurchatova wrote:
> An unregistered pstore backend reads back "(null)", and every latter
> check fails. This also contradicts the pstore_crash_test documentation.
>
> Fix by skipping an unregistered backend in common_tests
>
> https://virtuozzo.atlassian.net/browse/VSTOR-142447
> Feature: fix selftests
>
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> ---
> tools/testing/selftests/pstore/common_tests | 15 +++++++++++++--
> .../selftests/pstore/pstore_post_reboot_tests | 2 --
> 2 files changed, 13 insertions(+), 4 deletions(-)
>
> diff --git a/tools/testing/selftests/pstore/common_tests b/tools/testing/selftests/pstore/common_tests
> index 4509f0cc9c91..71cd4473f45a 100755
> --- a/tools/testing/selftests/pstore/common_tests
> +++ b/tools/testing/selftests/pstore/common_tests
> @@ -55,6 +55,8 @@ operate_files() { # tested value, files, operation
>
> # Parameters
> TEST_STRING_PATTERN="Testing pstore: uuid="
> +# Kselftest framework requirement - SKIP code is 4.
> +ksft_skip=4
> UUID=`cat /proc/sys/kernel/random/uuid`
> TOP_DIR=`absdir $0`
> LOG_DIR=$TOP_DIR/logs/`date +%Y%m%d-%H%M%S`_${UUID}/
> @@ -74,8 +76,17 @@ prlog "=== Pstore unit tests (`basename $0`) ==="
> prlog "UUID="$UUID
>
> prlog -n "Checking pstore backend is registered ... "
> -backend=`cat /sys/module/pstore/parameters/backend`
> -show_result $?
> +# A tristate pstore leaves no parameter file at all when it is not built
> +# or not loaded, and an unregistered backend reads back as "(null)".
> +backend=`cat /sys/module/pstore/parameters/backend 2>/dev/null`
> +if [ -z "$backend" ] || [ "$backend" = "(null)" ]; then
> + prlog "SKIP"
> + prlog -e "\tbackend=${backend:-<none>}"
> + prlog -e "\tcmdline=`cat /proc/cmdline`"
> + prlog "pstore backend is not registered, skipping"
> + exit $ksft_skip
> +fi
> +show_result 0
> prlog -e "\tbackend=${backend}"
> prlog -e "\tcmdline=`cat /proc/cmdline`"
> if [ $rc -ne 0 ]; then
> diff --git a/tools/testing/selftests/pstore/pstore_post_reboot_tests b/tools/testing/selftests/pstore/pstore_post_reboot_tests
> index d6da5e86efbf..a5464407f5b2 100755
> --- a/tools/testing/selftests/pstore/pstore_post_reboot_tests
> +++ b/tools/testing/selftests/pstore/pstore_post_reboot_tests
> @@ -7,8 +7,6 @@
> # Written by Hiraku Toyooka <hiraku.toyooka.gu@hitachi.com>
> #
>
> -# Kselftest framework requirement - SKIP code is 4.
> -ksft_skip=4
>
> . ./common_tests
>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH vz10 v2] selftests: binderfs: skip the stress test without binderfs
2026-09-10 3:10 4% [Devel] [PATCH vz10 v2] selftests: binderfs: skip the stress test without binderfs Eva Kurchatova
@ 2026-09-23 18:03 0% ` Konstantin Khorenko
0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-23 18:03 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
changelog?
--
Best regards,
Konstantin Khorenko,
Virtuozzo Linux Kernel Team
On 9/10/26 05:10, Eva Kurchatova wrote:
> binderfs_stress mounts binderfs and asserts on the first mount, so on
> a kernel without CONFIG_ANDROID_BINDERFS the case fails rather than
> reporting that the filesystem is not there.
>
> Ask /proc/filesystems whether the kernel has it, the way
> supports_filesystem() in the landlock tests and check_resctrlfs_support()
> in the resctrl ones already do, and skip when it does not.
>
> Probing with a mount instead would not work here: the test is meant to
> run unprivileged, and path_mount() rejects an unprivileged caller in
> may_mount() before do_new_mount() ever looks the filesystem type up, so
> the probe would see EPERM whether binderfs is there or not. The child
> would then still reach the real mount inside its userns and fail.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-142449
> Feature: fix selftests
>
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> ---
> .../filesystems/binderfs/binderfs_test.c | 25 +++++++++++++++++++
> 1 file changed, 25 insertions(+)
>
> diff --git a/tools/testing/selftests/filesystems/binderfs/binderfs_test.c b/tools/testing/selftests/filesystems/binderfs/binderfs_test.c
> index 319567f0fae1..fd1f9b6e5d46 100644
> --- a/tools/testing/selftests/filesystems/binderfs/binderfs_test.c
> +++ b/tools/testing/selftests/filesystems/binderfs/binderfs_test.c
> @@ -36,6 +36,28 @@
> fd = -EBADF; \
> }
>
> +static bool binderfs_supported(void)
> +{
> + char line[128];
> + bool ret = false;
> + FILE *f;
> +
> + f = fopen("/proc/filesystems", "r");
> + if (!f)
> + return true; /* Cannot tell, let the test run and report. */
> +
> + while (fgets(line, sizeof(line), f)) {
> + /* binderfs has no backing device, hence the "nodev" prefix. */
> + if (!strcmp(line, "nodev\tbinder\n")) {
> + ret = true;
> + break;
> + }
> + }
> +
> + fclose(f);
> + return ret;
> +}
> +
> static void change_mountns(struct __test_metadata *_metadata)
> {
> int ret;
> @@ -388,6 +410,9 @@ TEST(binderfs_stress)
> char binderfs_mntpt[] = P_tmpdir "/binderfs_XXXXXX",
> device_path[sizeof(P_tmpdir "/binderfs_XXXXXX/") + BINDERFS_MAX_NAME];
>
> + if (!binderfs_supported())
> + SKIP(return, "The Android binderfs filesystem is not available");
> +
> ret = socketpair(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, syncfds);
> ASSERT_EQ(ret, 0) {
> TH_LOG("%s - Failed to create socket pair", strerror(errno));
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH vz10 v2] selftests: pci_endpoint: skip when the test device is absent
2026-09-10 3:21 3% [Devel] [PATCH vz10 v2] selftests: pci_endpoint: skip when the test device is absent Eva Kurchatova
@ 2026-09-23 18:03 0% ` Konstantin Khorenko
0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-23 18:03 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
changelog v1-> v2?
--
Best regards,
Konstantin Khorenko,
Virtuozzo Linux Kernel Team
On 9/10/26 05:21, Eva Kurchatova wrote:
> Every fixture opens /dev/pci-endpoint-test.0 and asserts on the result,
> so on a machine that has no PCI endpoint test device all 17 cases fail
> in fixture setup rather than saying the hardware is not there.
>
> Fix by checking the device once before handing over to the harness.
> Doing it that way returns one SKIP line from the kselftest.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-142446
> Feature: fix selftests
>
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> ---
> .../selftests/pci_endpoint/pci_endpoint_test.c | 18 +++++++++++++++++-
> 1 file changed, 17 insertions(+), 1 deletion(-)
>
> diff --git a/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c b/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
> index da0db0e7c969..a6106b6c52e3 100644
> --- a/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
> +++ b/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
> @@ -257,4 +257,20 @@ TEST_F(pcie_ep_doorbell, DOORBELL_TEST)
> pci_ep_ioctl(PCITEST_DOORBELL, 0);
> EXPECT_FALSE(ret) TH_LOG("Test failed for Doorbell\n");
> }
> -TEST_HARNESS_MAIN
> +
> +static bool test_device_available(void)
> +{
> + return access(test_device, F_OK) == 0;
> +}
> +
> +int main(int argc, char **argv)
> +{
> + /*
> + * Only the plain invocation, which is the one kselftest uses, turns
> + * into a skip, so that -l and -h keep working without the hardware.
> + */
> + if (argc == 1 && !test_device_available())
> + ksft_exit_skip("no PCI endpoint test device %s\n", test_device);
> +
> + return test_harness_run(argc, argv);
> +}
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [PATCH 2/2] UefiCpuPkg/MpInitLib: resend the Start-up IPI when an AP misses it #VSTOR-146639
2026-09-21 15:44 3% ` [PATCH 2/2] UefiCpuPkg/MpInitLib: resend the Start-up IPI when an AP misses it #VSTOR-146639 Denis V. Lunev
@ 2026-09-23 13:30 0% ` Andrey Drobyshev
0 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-23 13:30 UTC (permalink / raw)
To: Denis V. Lunev, svt-core
Ack
Overall LGTM, see some minor nits below
On 9/21/26 6:44 PM, Denis V. Lunev wrote:
> From: Denis V. Lunev <den@openvz.org>
>
> WaitApWakeup spins until the AP clears its wakeup signal, with no
> timeout and no way out. If the AP never consumes the Start-up IPI, the
> BSP spins forever and the boot is dead.
>
> A hypervisor can lose that IPI. Under KVM, INIT is latched while a
> vCPU is in SMM but the Start-up IPI is discarded outright, so an AP
> that takes an SMI just before the BSP broadcasts INIT-SIPI-SIPI is
> left in wait-for-SIPI with nothing left to start it. The window is the
> few instructions between an AP releasing the BSP at the end of the SMM
> rendezvous and its own RSM, which a scheduled vCPU can be preempted
> inside of. KVM holds that delivering the SIPI is software's job, so
> the firmware has to cope.
>
> Resend rather than spin. Address the AP individually so that APs
> already running the procedure are not disturbed, and pass the reset
> vector only on the paths that sent a Start-up IPI in the first place,
> so the SEV-SNP and hand-off paths keep waiting as before. A Start-up
> IPI is ignored unless the target is in wait-for-SIPI, so resending
> costs nothing when the AP is merely slow.
>
> The failure this fixes looks like a guest that never boots: firmware
> starts the boot manager, paints nothing further, and one CPU spins
> while the other APs sit idle.
>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> Cc: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
> ---
> UefiCpuPkg/Library/MpInitLib/MpLib.c | 85 +++++++++++++++++++++++++---
> UefiCpuPkg/Library/MpInitLib/MpLib.h | 2 +
> 2 files changed, 80 insertions(+), 7 deletions(-)
>
> diff --git a/UefiCpuPkg/Library/MpInitLib/MpLib.c b/UefiCpuPkg/Library/MpInitLib/MpLib.c
> index 426d998b9a..87e8f824b6 100644
> --- a/UefiCpuPkg/Library/MpInitLib/MpLib.c
> +++ b/UefiCpuPkg/Library/MpInitLib/MpLib.c
> @@ -942,16 +942,63 @@ DxeApEntryPoint (
> ApWakeupFunction (CpuMpData, ProcessorNumber);
> }
>
> +/**
> + Calculate timeout value and return the current performance counter value.
> +
> + @param[in] TimeoutInMicroseconds The timeout value in microseconds.
> + @param[out] CurrentTime Returns the current value of the performance counter.
> +
> + @return Expected timestamp counter for timeout, or 0 if TimeoutInMicroseconds is 0.
> +**/
> +UINT64
> +CalculateTimeout (
> + IN UINTN TimeoutInMicroseconds,
> + OUT UINT64 *CurrentTime
> + );
> +
> +/**
> + Checks whether timeout expires.
> +
> + @param[in, out] PreviousTime On input, the requested time. On output, the elapsed time.
> + @param[in] TotalTime The total amount of elapsed time.
> + @param[in] Timeout The timeout ticks, 0 means infinity.
> +
> + @retval TRUE A timeout occurred.
> + @retval FALSE No timeout occurred.
> +**/
> +BOOLEAN
> +CheckTimeout (
> + IN OUT UINT64 *PreviousTime,
> + IN UINT64 *TotalTime,
> + IN UINT64 Timeout
> + );
> +
These functions are only used here in this translation unit, yet they
aren't declared static. Should we wither declare them static and move
their definitions above (without the need for forward declarations), or
just declare them in MpLib.h?
> /**
> Wait for AP wakeup and write AP start-up signal till AP is waken up.
>
> @param[in] ApStartupSignalBuffer Pointer to AP wakeup signal
> + @param[in] ApicId Local APIC ID of the AP being woken
> + @param[in] StartupRoutine AP reset vector, or 0 if the AP was not
> + woken by INIT-SIPI-SIPI and no Start-up
> + IPI may be resent
> **/
> VOID
> WaitApWakeup (
> - IN volatile UINT32 *ApStartupSignalBuffer
> + IN volatile UINT32 *ApStartupSignalBuffer,
> + IN UINT32 ApicId,
> + IN UINT32 StartupRoutine
> )
> {
> + UINT64 CurrentTime;
> + UINT64 TotalTime;
> + UINT64 Timeout;
> +
> + Timeout = CalculateTimeout (
> + (StartupRoutine != 0) ? AP_SIPI_RESEND_TIMEOUT_US : 0,
> + &CurrentTime
> + );
> + TotalTime = 0;
> +
> //
> // If AP is waken up, StartupApSignal should be cleared.
> // Otherwise, write StartupApSignal again till AP waken up.
> @@ -962,6 +1009,12 @@ WaitApWakeup (
> WAKEUP_AP_SIGNAL
> ) != 0)
> {
> + if (CheckTimeout (&CurrentTime, &TotalTime, Timeout)) {
> + SendStartupIpi (ApicId, StartupRoutine);
> + Timeout = CalculateTimeout (AP_SIPI_RESEND_TIMEOUT_US, &CurrentTime);
> + TotalTime = 0;
> + }
This re-send of IPI should happen rarely. Wouldn't we wanna know when
it does happen? How about adding some debug info when IPI is re-issued?
Andrey
> +
> CpuPause ();
> }
> }
> @@ -1256,8 +1309,10 @@ WakeUpAP (
> CPU_AP_DATA *CpuData;
> BOOLEAN ResetVectorRequired;
> CPU_INFO_IN_HOB *CpuInfoInHob;
> + UINT32 SipiVector;
>
> CpuMpData->FinishedCount = 0;
> + SipiVector = 0;
> ResetVectorRequired = FALSE;
>
> if (CpuMpData->WakeUpByInitSipiSipi ||
> @@ -1320,13 +1375,14 @@ WakeUpAP (
> if (CanUseSevSnpCreateAP (CpuMpData)) {
> SevSnpCreateAP (CpuMpData, -1);
> } else {
> + SipiVector = (UINT32)ExchangeInfo->BufferStart;
> if ((CpuMpData->InitFlag == ApInitConfig) && FixedPcdGetBool (PcdFirstTimeWakeUpAPsBySipi)) {
> //
> // SIPI can be used for the first time wake up after reset to reduce boot time.
> //
> - SendStartupIpiAllExcludingSelf ((UINT32)ExchangeInfo->BufferStart);
> + SendStartupIpiAllExcludingSelf (SipiVector);
> } else {
> - SendInitSipiSipiAllExcludingSelf ((UINT32)ExchangeInfo->BufferStart);
> + SendInitSipiSipiAllExcludingSelf (SipiVector);
> }
> }
> }
> @@ -1398,10 +1454,15 @@ WakeUpAP (
> //
> // Wait all APs waken up if this is not the 1st broadcast of SIPI
> //
> + CpuInfoInHob = (CPU_INFO_IN_HOB *)(UINTN)CpuMpData->CpuInfoInHob;
> for (Index = 0; Index < CpuMpData->CpuCount; Index++) {
> CpuData = &CpuMpData->CpuData[Index];
> if (Index != CpuMpData->BspNumber) {
> - WaitApWakeup (CpuData->StartupApSignal);
> + WaitApWakeup (
> + CpuData->StartupApSignal,
> + CpuInfoInHob[Index].ApicId,
> + SipiVector
> + );
> }
> }
> }
> @@ -1430,9 +1491,10 @@ WakeUpAP (
> if (CanUseSevSnpCreateAP (CpuMpData)) {
> SevSnpCreateAP (CpuMpData, (INTN)ProcessorNumber);
> } else {
> + SipiVector = (UINT32)ExchangeInfo->BufferStart;
> SendInitSipiSipi (
> CpuInfoInHob[ProcessorNumber].ApicId,
> - (UINT32)ExchangeInfo->BufferStart
> + SipiVector
> );
> }
> }
> @@ -1440,7 +1502,12 @@ WakeUpAP (
> //
> // Wait specified AP waken up
> //
> - WaitApWakeup (CpuData->StartupApSignal);
> + CpuInfoInHob = (CPU_INFO_IN_HOB *)(UINTN)CpuMpData->CpuInfoInHob;
> + WaitApWakeup (
> + CpuData->StartupApSignal,
> + CpuInfoInHob[ProcessorNumber].ApicId,
> + SipiVector
> + );
> }
>
> if (ResetVectorRequired) {
> @@ -2017,7 +2084,11 @@ SwitchApContext (
> {
> for (Index = 0; Index < MpHandOff->CpuCount; Index++) {
> if (MpHandOff->ProcessorIndex + Index != BspNumber) {
> - WaitApWakeup ((UINT32 *)(UINTN)(MpHandOff->Info[Index].StartupSignalAddress));
> + WaitApWakeup (
> + (UINT32 *)(UINTN)(MpHandOff->Info[Index].StartupSignalAddress),
> + 0,
> + 0
> + );
> }
> }
> }
> diff --git a/UefiCpuPkg/Library/MpInitLib/MpLib.h b/UefiCpuPkg/Library/MpInitLib/MpLib.h
> index 5d10516cd8..94bda7f1ea 100644
> --- a/UefiCpuPkg/Library/MpInitLib/MpLib.h
> +++ b/UefiCpuPkg/Library/MpInitLib/MpLib.h
> @@ -44,6 +44,8 @@
> #include "MpHandOff.h"
>
> #define WAKEUP_AP_SIGNAL SIGNATURE_32 ('S', 'T', 'A', 'P')
> +
> +#define AP_SIPI_RESEND_TIMEOUT_US 10000
> //
> // To trigger the start-up signal, BSP writes the specified
> // StartupSignalValue to the StartupSignalAddress of each processor.
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH vz10 v2 2/2] selftests: ve: introduce vtty pair teardown stress test
2026-09-16 12:31 5% ` [Devel] [PATCH vz10 v2 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-09-18 7:11 0% ` Vasileios Almpanis
@ 2026-09-22 17:47 3% ` Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-22 17:47 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
> A vtty pair is created by the first master open and torn down by the
> last close. The close racing the peer's close used to let both sides
> decide they were the final one and call release_tty() twice, and a
> master opening again while a teardown was in flight used to hand out a
> reference to a tty_struct about to be freed.
>
> Hammer both ends. The master comes from the vzctl ioctl, which does
> not look the VE up. The slave is only reachable from inside the VE, as
> TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
> vttys index minor - 1, so the test builds a VE with cgroup2, sets
> ve.veid to the cgroup it made, and runs the slave side there.
>
> One of the symtoms of a buggy kernel is dmesg warning, not just an
> eventual crash, so the test checks that. A crashed node should also
> report test failure in CI, but that's less verbose, hence the check.
Typo: symtoms. The second sentence argues against itself ("should also
report ... hence the check"). One sentence carries the point: a warning
does not stop the node, so the test reads the kernel log.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-144771
>
> Feature: ve selftest
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
> index afa4c568c2c9b..9d8d860938273 100644
> --- a/tools/testing/selftests/ve/.gitignore
> +++ b/tools/testing/selftests/ve/.gitignore
> @@ -1,2 +1,3 @@
> ve_ns_owner_test
> ve_perms_test
> +ve_vtty_stress
> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
> index ec40cbc7b3a1b..6cd5823c4d932 100644
> --- a/tools/testing/selftests/ve/Makefile
> +++ b/tools/testing/selftests/ve/Makefile
> @@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
>
> TEST_GEN_PROGS += ve_ns_owner_test
> TEST_GEN_PROGS += ve_perms_test
> +TEST_GEN_PROGS += ve_vtty_stress
>
> include ../lib.mk
> diff --git a/tools/testing/selftests/ve/settings b/tools/testing/selftests/ve/settings
> new file mode 100644
> index 0000000000000..e2206265f67c7
> --- /dev/null
> +++ b/tools/testing/selftests/ve/settings
> @@ -0,0 +1 @@
> +timeout=900
> diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
> new file mode 100644
> index 0000000000000..57edbdeef0c84
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_vtty_stress.c
> @@ -0,0 +1,250 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * ve_vtty_stress - close a vtty pair from both ends at once.
> + *
> + * A vtty pair is created by the first master open and torn down by the
> + * last close. A close racing the peer's close used to let both sides
> + * decide they were the final one and call release_tty() twice, and a
> + * master opened again while a teardown was still in flight used to hand
> + * out a reference to a tty_struct that was about to be freed. Either
> + * way the next close walked freed memory.
> + *
> + * The master comes from the vzctl ioctl, which does not look the VE up.
> + * The slave is only reachable from inside the VE, as TTY_MAJOR minor
> + * 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to vttys index
> + * minor - 1. So the test makes a VE with cgroup2, hammers the master
> + * from the host and the slave from a task inside it, and fails if the
> + * kernel logs a complaint about the teardown.
> + *
> + * Surviving is not a pass on its own: the first symptom is a warning,
> + * which stops nothing by itself.
This is the commit message pasted into the file. The history of the
double release_tty() and the freed tty_struct belongs in the commit,
where git keeps it next to the change. The header needs what the test
does, how it reaches both ends, and the usage line. Also, "created by the
first master open" is not the whole story: the slave open creates the
pair too, since vtty_lookup() returns NULL for a missing map and
tty_init_dev(vttys_driver) then runs vtty_install(). In this test the
slave wins that race often.
> + *
> + * Usage: ve_vtty_stress [seconds] (default 600, or $VE_VTTY_SECS)
> + */
> +#define _GNU_SOURCE
> +#include <sched.h>
> +#include <stdbool.h>
> +#include <sys/ioctl.h>
> +#include <linux/major.h>
> +#include <sys/sysmacros.h>
> +#include <sys/wait.h>
> +#include <time.h>
> +
> +#include "ve_selftest.h"
> +#include "../kselftest.h"
> +
> +/* The ioctl argument; the trailing data[] the uapi struct has is unused. */
> +struct vzctl_ve_configure {
> + unsigned int veid, key, val, size;
> +};
> +
> +#define VZCTL_VE_CONFIGURE _IOW('.', 15, struct vzctl_ve_configure)
> +#define VE_CONFIGURE_OPEN_TTY 5
> +
> +#define VTTY_IDX 0 /* the pair both ends work on */
> +#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
> +#define DEFAULT_SECS 600
ve_vtty_stress is in TEST_GEN_PROGS, so every "make run_tests" of the ve
directory now takes ten minutes longer, and the settings file lifts the
timeout for the whole directory to make room for that. Nothing in the
commit says how long the original bug took to show up. If it reproduced
in seconds, a default in the tens of seconds keeps the test in the
default run and the long soak stays available through VE_VTTY_SECS or
argv[1]. If it really needs minutes, that deserves a sentence in the
commit message.
> +
> +static int open_master(int ctl_fd, unsigned int veid)
> +{
> + struct vzctl_ve_configure c = {
> + .veid = veid,
> + .key = VE_CONFIGURE_OPEN_TTY,
> + .val = VTTY_IDX,
> + };
> +
> + return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
> +}
> +
> +/*
> + * The slave end, inside the VE. Every error here is one the race is
> + * expected to produce: the pair is gone once the master has closed it.
> + */
> +static int hammer_slave(time_t deadline, unsigned long *opened)
> +{
> + char node[] = "/tmp/ve_vtty_slave";
A fixed name under /tmp is not that good.
A name with the pid in it, or a node under a private directory, could be better.
> +
> + unlink(node);
> + if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0)
> + return -1;
> +
> + while (time(NULL) < deadline) {
> + int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
> +
> + if (fd >= 0) {
> + (*opened)++;
> + close(fd);
> + }
> + }
The comment is not true for EPERM, EACCES, ENOENT or ENODEV: none of
those comes from the race, all of them mean the slave cannot be opened
at all (a /tmp mounted nodev gives EACCES from may_open(), for one). The
loop ignores errno, so a broken setup spins for the full run and only
then fails with "no slave was ever opened". Bail out on the first errno
that is not EBUSY/EIO/ENXIO and report it. The master loop has the same
shape and the same fix.
> + unlink(node);
> + return 0;
> +}
> +
> +/* The kernel log, since a warning fails nothing by itself. */
> +static int kmsg_open(void)
> +{
> + int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
> +
> + if (fd >= 0)
> + lseek(fd, 0, SEEK_END);
> + return fd;
> +}
> +
> +static int kmsg_check(int fd)
> +{
> + static const char *const ours[] = {
> + "tty_release", "release_tty", "check_tty_count",
> + "vtty_open_master", "vtty_shutdown",
> + };
> + char buf[8192];
> + int found = 0;
> + ssize_t r;
> + size_t i;
> +
> + while (fd >= 0 && (r = read(fd, buf, sizeof(buf) - 1)) > 0) {
> + buf[r] = '\0';
> + if (!strstr(buf, "WARNING") && !strstr(buf, "BUG") &&
> + !strstr(buf, "Oops") && !strstr(buf, "general protection"))
> + continue;
> + for (i = 0; i < ARRAY_SIZE(ours); i++)
> + if (strstr(buf, ours[i])) {
> + ksft_print_msg("kernel log: %s", buf);
> + found++;
> + break;
> + }
> + }
> + return found;
> +}
/dev/kmsg returns one record per read(), and the keyword and the
function name have to be in the same record for this to match. They
rarely are:
- "WARNING: CPU: 0 PID: 1 at drivers/tty/pty.c:NNN vtty_map_clear+0x.."
names vtty_map_clear(), the function holding the WARN_ON(). It
matches vtty_shutdown only if the compiler inlined the static helper.
The WARN_ON()s in vtty_map_set() and vtty_lookup() never match.
- "Oops: 0000 [#1] ..." and "general protection fault, ..." carry no
function name at all; that is in the RIP: record that follows.
- "BUG: KASAN: slab-use-after-free in tty_lock+0x.." or a
"list_del corruption" splat name none of the five.
- check_tty_count() prints
"vtty_slave vttys0: tty_release: tty->count(N) != (#fd's(M) + ..."
with no WARNING/BUG/Oops in it, so the first strstr() chain skips the
record before the names are tried, and the entry cannot match.
The test can therefore pass on the kernel it is meant to catch. The node
does nothing else during the run, so any WARNING/BUG/Oops/GPF record is a
failure; drop the function filter, and add the "tty->count(" text as a
match of its own.
One more thing here:
The read loop ends on the first error, and /dev/kmsg returns -EPIPE once
when the reader's position has been overwritten (devkmsg_read(), "our
last seen message is gone, return error and reset"), then continues from
the oldest record. Nothing drains the log during the run, so a buggy
kernel that WARNs on every double teardown for ten minutes overruns the
ring, the first read() after the deadline fails with EPIPE, kmsg_check()
returns 0 and the test passes. Skip EPIPE with a continue, or drain the
fd from the main loop.
> +
> +int main(int argc, char **argv)
> +{
> + unsigned long master_opened = 0, slave_opened = 0;
> + int cg, ctid, ctl, kmsg, ready[2], count[2], status;
> + const char *env = getenv("VE_VTTY_SECS");
> + char path[PATH_MAX], val[16], c;
> + time_t deadline;
> + long secs;
> + pid_t pid;
> +
> + ksft_print_header();
> + secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
> + if (secs <= 0)
> + secs = DEFAULT_SECS;
> +
> + ctl = open("/dev/vzctl", O_RDWR);
> + if (ctl < 0)
> + ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
> + "(vzdev, vzmon) are needed\n",
> + strerror(errno));
> +
> + cg = mount_cg2_fd();
> + if (cg < 0)
> + ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
> + write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS);
> +
> + ctid = make_ve(cg, CTID_MIN);
> + if (ctid < 0)
> + ksft_exit_skip("cannot make a VE cgroup\n");
> +
> + /*
> + * The pair is filed under the veid of the VE, so the two ends only
> + * meet if the cgroup carries the one the master is opened for.
> + */
> + snprintf(path, sizeof(path), "%d/ve.veid", ctid);
> + snprintf(val, sizeof(val), "%d", ctid);
ve_id_write() does not check that a veid is unique (the FIXME in
kernel/ve/ve.c says as much), and the vtty map is a global idr keyed by
veid. make_ve() only guarantees that no cgroup of that name exists in the
hierarchy, not that no running container carries that veid. The
108..200 range makes a clash unlikely on a test node; a sentence in the
commit message saying the test assumes it is enough.
> + if (write_file_at(cg, path, val) < 0) {
> + destroy_ve(cg, ctid);
> + ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
> + }
> +
> + /* Do not report a failure where the vtty pair cannot be had at all. */
> + status = open_master(ctl, ctid);
> + if (status < 0 && errno != EBUSY) {
> + destroy_ve(cg, ctid);
> + ksft_exit_skip("no vtty master: %s\n", strerror(errno));
> + }
> + if (status >= 0)
> + close(status);
> +
> + ksft_set_plan(1);
> + kmsg = kmsg_open();
> + if (kmsg < 0)
> + ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
> + "be noticed\n",
> + strerror(errno));
> +
> + if (pipe(ready) || pipe(count))
> + ksft_exit_fail_msg("pipe: %s\n", strerror(errno));
Every other exit after make_ve() calls destroy_ve() first; this one
leaves the cgroup behind.
--
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH vz10 v2 1/2] selftests: ve: grant the directories the files sit in
2026-09-16 12:31 3% [Devel] [PATCH vz10 v2 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-09-16 12:31 5% ` [Devel] [PATCH vz10 v2 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
@ 2026-09-22 17:47 6% ` Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-22 17:47 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
> The sysfs variant names nested leaves, kernel/vmcoreinfo and
> kernel/notes, while the proc one names files at the top of the
> filesystem. Both enforcement tests grant the leaves and nothing else,
> so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
> cannot traverse, and every open answers ENOENT:
>
> not ok 14 ve_perms.sysfs.enforce_allows
> not ok 15 ve_perms.sysfs.enforce_denies
>
> Grant x on each directory along the way, taken from the path the
> variant already gives. A file at the top of the filesystem has none
> and needs nothing, so the proc variant is unaffected.
>
> The suite then passes 16 of 16, where it passed 14 before.
This line is redundant, please remove.
In general, please try to avoid numbers in commit messages which (numbers) will
change soon - this time it's enough just to add one more test and that's it.
>
> Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")
>
> https://virtuozzo.atlassian.net/browse/VSTOR-144955
>
> Feature: fix vz selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
> index 25ffbd42c3802..2b542dc301394 100644
> --- a/tools/testing/selftests/ve/ve_perms_test.c
> +++ b/tools/testing/selftests/ve/ve_perms_test.c
> @@ -348,6 +348,37 @@ TEST_F(ve_perms, write_parser)
> * read, and granting x on the directory lets it be traversed (the lookup then
> * fails ENOENT on the missing child, not EACCES).
> */
Those helpers were inserted between a test comment and the test itself.
> +/*
> + * A leaf is only reachable once every directory above it can be traversed.
> + * The variants name their files relative to the mount point, so grant x on
> + * each directory along the way; a file at the top of the filesystem has
> + * none and needs nothing.
> + */
> +static int allow_path(int cgv2_fd, const char *path, const char *file)
> +{
> + char dir[240]; /* set_entry() writes "<dir> <mask>" into 256 */
The 240 is tied to the 256 inside set_entry() by a comment only, and
set_entry() itself still truncates silently: a long entry is written to
the cgroup file cut short instead of failing. The file already has the
idiom for this in ve_open_rel(), which checks the snprintf() result
against sizeof(). Apply the same in set_entry() and the caller no longer
needs its own length arithmetic:
static int set_entry(int cgv2_fd, const char *path, const char *entry,
const char *mask)
{
- char line[256];
+ char line[PATH_MAX];
- snprintf(line, sizeof(line), "%s %s", entry, mask);
+ if (snprintf(line, sizeof(line), "%s %s", entry, mask) >=
+ (int)sizeof(line))
+ return -1;
return write_file_at(cgv2_fd, path, line);
}
static int allow_path(int cgv2_fd, const char *path, const char *file)
{
- char dir[240]; /* set_entry() writes "<dir> <mask>" into 256 */
+ char dir[PATH_MAX];
const char *sep;
for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
- if (sep - file >= (long)sizeof(dir))
- return -1;
- snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file), file);
+ if (snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file),
+ file) >= (int)sizeof(dir))
+ return -1;
if (set_entry(cgv2_fd, path, dir, "x") < 0)
return -1;
}
return 0;
}
The set_entry() part fixes a pre-existing function, so it can go in its
own commit if you prefer, but it is small enough to stay here.
--
Konstantin Khorenko <khorenko@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 6%]
* [Devel] [PATCH hci-8.1 v3 5/5] ms/tunnels: Drop stale dst when building an ICMP error for PMTUD
2026-09-22 15:00 2% ` [Devel] [PATCH hci-8.1 v3 2/5] selftests: net: skip what this kernel and iproute2 do not have Konstantin Khorenko
@ 2026-09-22 15:00 10% ` Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-22 15:00 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
From: Ido Schimmel <idosch@nvidia.com>
Bridged UDP tunnels such as VXLAN and GENEVE build an ICMP error packet
around an overlay packet if the packet is going to exceed the underlay
path MTU. The ICMP error packet is then injected back into the Rx path
with the source and destination addresses swapped, so that it will be
delivered to the overlay source.
If the overlay packet was routed to the UDP tunnel or locally generated,
then it is already carrying a valid dst entry and this entry is not
dropped when transforming the packet to an ICMP error packet. This
causes the IP layer to reuse the dst entry, leading to the ICMP error
packet being dropped or routed out of the UDP tunnel interface in case
of forwarding.
Prior to the blamed commit this could not happen, as
skb_tunnel_check_pmtu() did not build ICMP errors for PACKET_HOST
packets. Such packets were instead encapsulated and, unless the DF bit
was set in the outer header, fragmented by the underlay.
Fix this by making sure that the ICMP error packet does not have a valid
dst entry, thereby forcing the IP layer to perform a route lookup.
Adjust the bridged PMTU exception selftests accordingly. When the
local sender in ns_a pings the overlay destination with a deadline
(-w), ping exits on the first socket error before any reply is
received and returns a non-zero exit code. The test therefore only
passed because the ICMP error was never delivered. Use a packet count
(-c) like the ns_c line above it, so that the ICMP error counts
against the packet budget and the exit code depends on whether echo
replies were received. This passes with and without the fix.
Fixes: 8930424777e4 ("tunnels: Accept PACKET_HOST in skb_tunnel_check_pmtu().")
Cc: stable@vger.kernel.org
Reported-by: Laika Price <laikabcprice@gmail.com>
Closes: https://lore.kernel.org/netdev/20260614-master-v3-1-9f5060ba1ed1@gmail.com/
Reported-by: Yaroslav Dudkov <aroslavdudkov622@gmail.com>
Closes: https://lore.kernel.org/netdev/20260901081825.287173-1-aroslavdudkov622@gmail.com/
Reported-by: Charles Bordet <rough.rock3059@datachamp.fr>
Closes: https://lore.kernel.org/netdev/aHVhQLPJIhq-SYPM@eldamar.lan/
Signed-off-by: Ido Schimmel <idosch@nvidia.com>
Tested-by: Yaroslav Dudkov <aroslavdudkov622@gmail.com>
Reviewed-by: David Ahern <dsahern@kernel.org>
Reviewed-by: Stefano Brivio <sbrivio@redhat.com>
Reviewed-by: Guillaume Nault <gnault@redhat.com>
Link: https://patch.msgid.link/20260902190112.4126199-1-idosch@nvidia.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
(cherry picked from commit b58d749633203d92c265317b45fccee555090352)
https://virtuozzo.atlassian.net/browse/VSTOR-139651
Feature: fix ms/net
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
net/ipv4/ip_tunnel_core.c | 6 ++++++
tools/testing/selftests/net/pmtu.sh | 2 +-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/net/ipv4/ip_tunnel_core.c b/net/ipv4/ip_tunnel_core.c
index 8392d304a72eb..7e0f0291b7d56 100644
--- a/net/ipv4/ip_tunnel_core.c
+++ b/net/ipv4/ip_tunnel_core.c
@@ -252,6 +252,9 @@ static int iptunnel_pmtud_build_icmp(struct sk_buff *skb, int mtu)
eth_header(skb, skb->dev, ntohs(eh.h_proto), eh.h_source, eh.h_dest, 0);
skb_reset_mac_header(skb);
+ if (skb_valid_dst(skb))
+ skb_dst_drop(skb);
+
return skb->len;
}
@@ -348,6 +351,9 @@ static int iptunnel_pmtud_build_icmpv6(struct sk_buff *skb, int mtu)
eth_header(skb, skb->dev, ntohs(eh.h_proto), eh.h_source, eh.h_dest, 0);
skb_reset_mac_header(skb);
+ if (skb_valid_dst(skb))
+ skb_dst_drop(skb);
+
return skb->len;
}
diff --git a/tools/testing/selftests/net/pmtu.sh b/tools/testing/selftests/net/pmtu.sh
index 6c651c880fe83..19b9e099b06bd 100755
--- a/tools/testing/selftests/net/pmtu.sh
+++ b/tools/testing/selftests/net/pmtu.sh
@@ -1409,7 +1409,7 @@ test_pmtu_ipvX_over_bridged_vxlanY_or_geneveY_exception() {
mtu "${ns_b}" ${type}_b $((${ll_mtu} + 1000))
run_cmd ${ns_c} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1
- run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -w 1 -s $((${ll_mtu} + 500)) ${dst} || return 1
+ run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1
# Check that exceptions were created
pmtu="$(route_get_dst_pmtu_from_exception "${ns_c}" ${dst})"
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 10%]
* [Devel] [PATCH hci-8.1 v3 2/5] selftests: net: skip what this kernel and iproute2 do not have
@ 2026-09-22 15:00 2% ` Konstantin Khorenko
2026-09-22 15:00 10% ` [Devel] [PATCH hci-8.1 v3 5/5] ms/tunnels: Drop stale dst when building an ICMP error for PMTUD Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-22 15:00 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
From: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
Five cases report a failure where a feature is simply not there.
tls asks for every cipher it knows and fails on the ones the kernel does
not implement, SM4 and ARIA, which answer ENOENT at setsockopt
time. Skip a cipher the kernel refuses that way; the run then reports
632 passed, 264 skipped and none failed.
rtnetlink.sh sets up an erspan tunnel, which this kernel does not have
and which fails with "Unknown device type", and checks a netconf dump
whose exit status iproute2 gets wrong: 6.11 exits 2 on a dump that
worked, 6.17 exits 0. Probe for both and skip when unusable.
rtnetlink.py asks for an IPv4 multicast address dump the kernel answers
with EOPNOTSUPP, and ip_local_port_range asks for protocols it does not
have. Skip those too.
https: //virtuozzo.atlassian.net/browse/VSTOR-139651
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
Reviewed-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
tools/testing/selftests/net/ip_local_port_range.c | 17 ++++++++++---
tools/testing/selftests/net/rtnetlink.py | 11 ++++++--
tools/testing/selftests/net/rtnetlink.sh | 31 +++++++++++++++++++++++
tools/testing/selftests/net/tls.c | 2 ++
4 files changed, 55 insertions(+), 6 deletions(-)
diff --git a/tools/testing/selftests/net/ip_local_port_range.c b/tools/testing/selftests/net/ip_local_port_range.c
index 29451d2244b75..2d7b9c852b2b1 100644
--- a/tools/testing/selftests/net/ip_local_port_range.c
+++ b/tools/testing/selftests/net/ip_local_port_range.c
@@ -118,10 +118,6 @@ static int get_ip_local_port_range(int fd, __u32 *range)
FIXTURE(ip_local_port_range) {};
-FIXTURE_SETUP(ip_local_port_range)
-{
-}
-
FIXTURE_TEARDOWN(ip_local_port_range)
{
}
@@ -180,6 +176,19 @@ FIXTURE_VARIANT_ADD(ip_local_port_range, ip6_mptcp) {
.so_protocol = IPPROTO_MPTCP,
};
+FIXTURE_SETUP(ip_local_port_range)
+{
+ int fd;
+
+ /* Not every protocol under test is built into every kernel. */
+ fd = socket(variant->so_domain, variant->so_type, variant->so_protocol);
+ if (fd < 0 && (errno == EPROTONOSUPPORT || errno == ESOCKTNOSUPPORT ||
+ errno == EAFNOSUPPORT))
+ SKIP(return, "%s", strerror(errno));
+ ASSERT_GE(fd, 0) TH_LOG("socket failed");
+ close(fd);
+}
+
TEST_F(ip_local_port_range, invalid_option_value)
{
__u16 val16;
diff --git a/tools/testing/selftests/net/rtnetlink.py b/tools/testing/selftests/net/rtnetlink.py
index e9ad5e88da97f..e0b44902c602d 100755
--- a/tools/testing/selftests/net/rtnetlink.py
+++ b/tools/testing/selftests/net/rtnetlink.py
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
-from lib.py import ksft_exit, ksft_run, ksft_ge, RtnlAddrFamily
+from lib.py import ksft_exit, ksft_run, ksft_ge, KsftSkipEx
+from lib.py import NlError, RtnlAddrFamily
+import errno
import socket
IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01'
@@ -12,7 +14,12 @@ IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01'
At least the loopback interface should have this address.
"""
- addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True)
+ try:
+ addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True)
+ except NlError as e:
+ if e.nl_msg.error == -errno.EOPNOTSUPP:
+ raise KsftSkipEx("IPv4 multicast address dump is not supported")
+ raise
all_host_multicasts = [
addr for addr in addresses if addr['multicast'] == IPV4_ALL_HOSTS_MULTICAST
diff --git a/tools/testing/selftests/net/rtnetlink.sh b/tools/testing/selftests/net/rtnetlink.sh
index 5f72b447e9400..cc4e94b90002b 100755
--- a/tools/testing/selftests/net/rtnetlink.sh
+++ b/tools/testing/selftests/net/rtnetlink.sh
@@ -142,10 +142,30 @@ kci_del_dummy()
run_cmd ip link del dev "$devdummy"
}
+# Some iproute2 versions exit non-zero from a netconf dump that worked,
+# printing the devconf and then failing anyway. Nothing about the kernel
+# can be learned from the exit status of such an ip, so find out once.
+netconf_exit_status_usable()
+{
+ local out
+
+ out=$(ip -4 netconf show dev lo 2>/dev/null)
+ if [ $? -ne 0 ] && [ -n "$out" ]; then
+ return 1
+ fi
+ return 0
+}
+
kci_test_netconf()
{
dev="$1"
r=$ret
+
+ if ! netconf_exit_status_usable; then
+ end_test "SKIP: ip netconf $dev: iproute2 fails a dump that worked"
+ return $ksft_skip
+ fi
+
run_cmd ip netconf show dev "$dev"
for f in 4 6; do
run_cmd ip -$f netconf show dev "$dev"
@@ -920,6 +940,17 @@ kci_test_erspan()
return $ksft_skip
fi
+ # the kernel can be built without erspan, rtnetlink then has no ops
+ # for the type and says so. A kernel that has it gets past this and
+ # fails on the attributes the probe leaves out.
+ if ip -netns "$testns" link add dev "$DEV_NS" type erspan 2>&1 | \
+ grep -q "Unknown device type"; then
+ end_test "SKIP: erspan: not supported by the kernel"
+ ip netns del "$testns"
+ return $ksft_skip
+ fi
+ ip -netns "$testns" link del dev "$DEV_NS" 2>/dev/null
+
# test native tunnel erspan v1
run_cmd ip -netns "$testns" link add dev "$DEV_NS" type erspan seq \
key 102 local 172.16.1.100 remote 172.16.1.200 \
diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
index c9b0b4337e127..07f2b454d755b 100644
--- a/tools/testing/selftests/net/tls.c
+++ b/tools/testing/selftests/net/tls.c
@@ -413,6 +413,8 @@ FIXTURE_SETUP(tls)
return;
ret = setsockopt(self->fd, SOL_TLS, TLS_TX, &tls12, tls12.len);
+ if (ret < 0 && errno == ENOENT)
+ SKIP(return, "Cipher not built into the kernel");
ASSERT_EQ(ret, 0);
ret = setsockopt(self->cfd, SOL_TLS, TLS_RX, &tls12, tls12.len);
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 2%]
* Re: [Devel] [PATCH vz10 v2 6/6] selftests: net: let the bridged PMTU tests take the ICMP they ask for
2026-09-22 12:02 16% ` [Devel] [PATCH vz10 v2 6/6] selftests: net: let the bridged PMTU tests take the ICMP they ask for Eva Kurchatova
@ 2026-09-22 14:27 0% ` Konstantin Khorenko
0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-22 14:27 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
I am not taking this patch. Instead I will cherry-pick the upstream commit
it is a piece of:
b58d74963320 ("tunnels: Drop stale dst when building an ICMP error for
PMTUD")
Why the test fails here at all
Upstream commit 8930424777e4 ("tunnels: Accept PACKET_HOST in
skb_tunnel_check_pmtu()") made bridged vxlan/geneve build a "Frag needed"
ICMP for locally generated packets too, not only for forwarded ones.
Our tree has it via the RHEL 6.12.0-211.16.1 import (see the blame of
net/ipv4/ip_tunnel_core.c:425). That is the ICMP ns_a's own ping now
receives, and that is what makes ping stop on the -w deadline.
Why the test change alone is not enough
8930424777e4 also introduced a bug. The ICMP error is built around the
original packet, which for a routed or locally generated overlay packet
still carries a valid dst. The error is then re-injected into the Rx
path with that stale dst, so the IP layer skips the route lookup and the
ICMP is either dropped or sent back out of the tunnel instead of being
delivered to the overlay source.
b58d74963320 fixes that: iptunnel_pmtud_build_icmp() and
iptunnel_pmtud_build_icmpv6() drop the dst, so the ICMP gets a proper
route lookup. It is marked Cc: stable and has Fixes: 8930424777e4.
What this patch is
The pmtu.sh hunk here is byte for byte the selftest part of that same
upstream commit. Taking the test half as a vz patch and leaving the
kernel half out would make the test pass while the kernel keeps
misrouting PMTUD errors on bridged tunnels.
So: one ms/ cherry-pick of b58d74963320, kernel fix and test change
together, instead of a vz-only change to the test.
--
Best regards,
Konstantin Khorenko,
Virtuozzo Linux Kernel Team
On 9/22/26 14:02, Eva Kurchatova wrote:
> The bridged tunnel PMTU tests send a packet that is too big to make the
> tunnel answer with "Frag needed", which is how the route exception they
> check gets created. The ping that does it from the namespace holding
> the bridge is given a deadline, and ping(8) says of it:
>
> In this case ping does not stop after count packet are sent, it waits
> either for deadline expire or until count probes are answered or for
> some error notification from network.
>
> so it stops at that very ICMP and reports the loss:
>
> # ping -M want -i 0.1 -w 1 -s 4500 192.168.2.2
> From 192.168.2.2 icmp_seq=2 Frag needed and DF set (mtu = 3950)
> 2 packets transmitted, 0 received, +1 errors, 100% packet loss
>
> TEST: IPv4, bridged vxlan4: PMTU exceptions [FAIL]
>
> The exception is created all the same, and with the right value, the
> test never gets to look at it. Ask for a count of pings instead, as
> the line above this one already does for the other namespace, so the
> error is counted and the run goes on.
>
> All 16 bridged cases pass with this, vxlan and geneve, over IPv4 and
> IPv6.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-139651
> Feature: fix selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
> ---
> tools/testing/selftests/net/pmtu.sh | 7 ++++++-
> 1 file changed, 6 insertions(+), 1 deletion(-)
>
> diff --git a/tools/testing/selftests/net/pmtu.sh b/tools/testing/selftests/net/pmtu.sh
> index 6c651c880fe8..d607429840cf 100755
> --- a/tools/testing/selftests/net/pmtu.sh
> +++ b/tools/testing/selftests/net/pmtu.sh
> @@ -1409,7 +1409,12 @@ test_pmtu_ipvX_over_bridged_vxlanY_or_geneveY_exception() {
> mtu "${ns_b}" ${type}_b $((${ll_mtu} + 1000))
>
> run_cmd ${ns_c} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1
> - run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -w 1 -s $((${ll_mtu} + 500)) ${dst} || return 1
> + # This ping is meant to draw the ICMP that creates the exception,
> + # and ping stops on "some error notification from network" where a
> + # deadline is given, see ping(8), so it would always report the
> + # loss and fail here. Send a count of them instead, as the ping
> + # above does.
> + run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1
>
> # Check that exceptions were created
> pmtu="$(route_get_dst_pmtu_from_exception "${ns_c}" ${dst})"
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* [Devel] [PATCH vz10 v2 6/6] selftests: net: let the bridged PMTU tests take the ICMP they ask for
2026-09-22 12:02 2% ` [Devel] [PATCH vz10 v2 2/6] selftests: net: skip what this kernel and iproute2 do not have Eva Kurchatova
@ 2026-09-22 12:02 16% ` Eva Kurchatova
2026-09-22 14:27 0% ` Konstantin Khorenko
1 sibling, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-09-22 12:02 UTC (permalink / raw)
To: vasileios.almpanis; +Cc: devel
The bridged tunnel PMTU tests send a packet that is too big to make the
tunnel answer with "Frag needed", which is how the route exception they
check gets created. The ping that does it from the namespace holding
the bridge is given a deadline, and ping(8) says of it:
In this case ping does not stop after count packet are sent, it waits
either for deadline expire or until count probes are answered or for
some error notification from network.
so it stops at that very ICMP and reports the loss:
# ping -M want -i 0.1 -w 1 -s 4500 192.168.2.2
From 192.168.2.2 icmp_seq=2 Frag needed and DF set (mtu = 3950)
2 packets transmitted, 0 received, +1 errors, 100% packet loss
TEST: IPv4, bridged vxlan4: PMTU exceptions [FAIL]
The exception is created all the same, and with the right value, the
test never gets to look at it. Ask for a count of pings instead, as
the line above this one already does for the other namespace, so the
error is counted and the run goes on.
All 16 bridged cases pass with this, vxlan and geneve, over IPv4 and
IPv6.
https://virtuozzo.atlassian.net/browse/VSTOR-139651
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/net/pmtu.sh | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/net/pmtu.sh b/tools/testing/selftests/net/pmtu.sh
index 6c651c880fe8..d607429840cf 100755
--- a/tools/testing/selftests/net/pmtu.sh
+++ b/tools/testing/selftests/net/pmtu.sh
@@ -1409,7 +1409,12 @@ test_pmtu_ipvX_over_bridged_vxlanY_or_geneveY_exception() {
mtu "${ns_b}" ${type}_b $((${ll_mtu} + 1000))
run_cmd ${ns_c} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1
- run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -w 1 -s $((${ll_mtu} + 500)) ${dst} || return 1
+ # This ping is meant to draw the ICMP that creates the exception,
+ # and ping stops on "some error notification from network" where a
+ # deadline is given, see ping(8), so it would always report the
+ # loss and fail here. Send a count of them instead, as the ping
+ # above does.
+ run_cmd ${ns_a} ${ping} -q -M want -i 0.1 -c 10 -s $((${ll_mtu} + 500)) ${dst} || return 1
# Check that exceptions were created
pmtu="$(route_get_dst_pmtu_from_exception "${ns_c}" ${dst})"
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 16%]
* [Devel] [PATCH vz10 v2 2/6] selftests: net: skip what this kernel and iproute2 do not have
@ 2026-09-22 12:02 2% ` Eva Kurchatova
2026-09-22 12:02 16% ` [Devel] [PATCH vz10 v2 6/6] selftests: net: let the bridged PMTU tests take the ICMP they ask for Eva Kurchatova
1 sibling, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-22 12:02 UTC (permalink / raw)
To: vasileios.almpanis; +Cc: devel
Five cases report a failure where a feature is simply not there.
tls asks for every cipher it knows and fails on the ones the kernel does
not implement, SM4 and ARIA, which answer ENOENT at setsockopt
time. Skip a cipher the kernel refuses that way; the run then reports
632 passed, 264 skipped and none failed.
rtnetlink.sh sets up an erspan tunnel, which this kernel does not have
and which fails with "Unknown device type", and checks a netconf dump
whose exit status iproute2 gets wrong: 6.11 exits 2 on a dump that
worked, 6.17 exits 0. Probe for both and skip when unusable.
rtnetlink.py asks for an IPv4 multicast address dump the kernel answers
with EOPNOTSUPP, and ip_local_port_range asks for protocols it does not
have. Skip those too.
https://virtuozzo.atlassian.net/browse/VSTOR-139651
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
.../selftests/net/ip_local_port_range.c | 17 +++++++---
tools/testing/selftests/net/rtnetlink.py | 11 +++++--
tools/testing/selftests/net/rtnetlink.sh | 31 +++++++++++++++++++
tools/testing/selftests/net/tls.c | 2 ++
4 files changed, 55 insertions(+), 6 deletions(-)
diff --git a/tools/testing/selftests/net/ip_local_port_range.c b/tools/testing/selftests/net/ip_local_port_range.c
index 29451d2244b7..2d7b9c852b2b 100644
--- a/tools/testing/selftests/net/ip_local_port_range.c
+++ b/tools/testing/selftests/net/ip_local_port_range.c
@@ -118,10 +118,6 @@ static int get_ip_local_port_range(int fd, __u32 *range)
FIXTURE(ip_local_port_range) {};
-FIXTURE_SETUP(ip_local_port_range)
-{
-}
-
FIXTURE_TEARDOWN(ip_local_port_range)
{
}
@@ -180,6 +176,19 @@ FIXTURE_VARIANT_ADD(ip_local_port_range, ip6_mptcp) {
.so_protocol = IPPROTO_MPTCP,
};
+FIXTURE_SETUP(ip_local_port_range)
+{
+ int fd;
+
+ /* Not every protocol under test is built into every kernel. */
+ fd = socket(variant->so_domain, variant->so_type, variant->so_protocol);
+ if (fd < 0 && (errno == EPROTONOSUPPORT || errno == ESOCKTNOSUPPORT ||
+ errno == EAFNOSUPPORT))
+ SKIP(return, "%s", strerror(errno));
+ ASSERT_GE(fd, 0) TH_LOG("socket failed");
+ close(fd);
+}
+
TEST_F(ip_local_port_range, invalid_option_value)
{
__u16 val16;
diff --git a/tools/testing/selftests/net/rtnetlink.py b/tools/testing/selftests/net/rtnetlink.py
index e9ad5e88da97..e0b44902c602 100755
--- a/tools/testing/selftests/net/rtnetlink.py
+++ b/tools/testing/selftests/net/rtnetlink.py
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0
-from lib.py import ksft_exit, ksft_run, ksft_ge, RtnlAddrFamily
+from lib.py import ksft_exit, ksft_run, ksft_ge, KsftSkipEx
+from lib.py import NlError, RtnlAddrFamily
+import errno
import socket
IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01'
@@ -12,7 +14,12 @@ def dump_mcaddr_check(rtnl: RtnlAddrFamily) -> None:
At least the loopback interface should have this address.
"""
- addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True)
+ try:
+ addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True)
+ except NlError as e:
+ if e.nl_msg.error == -errno.EOPNOTSUPP:
+ raise KsftSkipEx("IPv4 multicast address dump is not supported")
+ raise
all_host_multicasts = [
addr for addr in addresses if addr['multicast'] == IPV4_ALL_HOSTS_MULTICAST
diff --git a/tools/testing/selftests/net/rtnetlink.sh b/tools/testing/selftests/net/rtnetlink.sh
index 5f72b447e940..cc4e94b90002 100755
--- a/tools/testing/selftests/net/rtnetlink.sh
+++ b/tools/testing/selftests/net/rtnetlink.sh
@@ -142,10 +142,30 @@ kci_del_dummy()
run_cmd ip link del dev "$devdummy"
}
+# Some iproute2 versions exit non-zero from a netconf dump that worked,
+# printing the devconf and then failing anyway. Nothing about the kernel
+# can be learned from the exit status of such an ip, so find out once.
+netconf_exit_status_usable()
+{
+ local out
+
+ out=$(ip -4 netconf show dev lo 2>/dev/null)
+ if [ $? -ne 0 ] && [ -n "$out" ]; then
+ return 1
+ fi
+ return 0
+}
+
kci_test_netconf()
{
dev="$1"
r=$ret
+
+ if ! netconf_exit_status_usable; then
+ end_test "SKIP: ip netconf $dev: iproute2 fails a dump that worked"
+ return $ksft_skip
+ fi
+
run_cmd ip netconf show dev "$dev"
for f in 4 6; do
run_cmd ip -$f netconf show dev "$dev"
@@ -920,6 +940,17 @@ kci_test_erspan()
return $ksft_skip
fi
+ # the kernel can be built without erspan, rtnetlink then has no ops
+ # for the type and says so. A kernel that has it gets past this and
+ # fails on the attributes the probe leaves out.
+ if ip -netns "$testns" link add dev "$DEV_NS" type erspan 2>&1 | \
+ grep -q "Unknown device type"; then
+ end_test "SKIP: erspan: not supported by the kernel"
+ ip netns del "$testns"
+ return $ksft_skip
+ fi
+ ip -netns "$testns" link del dev "$DEV_NS" 2>/dev/null
+
# test native tunnel erspan v1
run_cmd ip -netns "$testns" link add dev "$DEV_NS" type erspan seq \
key 102 local 172.16.1.100 remote 172.16.1.200 \
diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
index c9b0b4337e12..07f2b454d755 100644
--- a/tools/testing/selftests/net/tls.c
+++ b/tools/testing/selftests/net/tls.c
@@ -413,6 +413,8 @@ FIXTURE_SETUP(tls)
return;
ret = setsockopt(self->fd, SOL_TLS, TLS_TX, &tls12, tls12.len);
+ if (ret < 0 && errno == ENOENT)
+ SKIP(return, "Cipher not built into the kernel");
ASSERT_EQ(ret, 0);
ret = setsockopt(self->cfd, SOL_TLS, TLS_RX, &tls12, tls12.len);
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 2%]
* [Devel] [PATCH vz10 v2 1/3] ms/selftests/posix_timers: Use CLOCK_THREAD_CPUTIME_ID for ITIMER_PROF measurements
@ 2026-09-21 20:46 4% ` Eva Kurchatova
0 siblings, 0 replies; 200+ results
From: Eva Kurchatova @ 2026-09-21 20:46 UTC (permalink / raw)
To: vasileios.almpanis; +Cc: devel
From: John Stultz <jstultz@google.com>
It was reported that the posix_timers test was at times seeing failures
with ITIMER_PROF timers, specifically in cases where the RCU_SOFTIRQ was
taking up significant amounts of time.
Analysis showed that as the time in softirq isn't included in the task
stime + utime accounting used to trigger the SIGPROF so delays from softirq
work could cause it to appear that the signal was incorrectly delayed.
Contributing to this is that the test uses gettimeofday() to measure
itimers, which also means any scheduling delay can also cause failures (as
the task may not be running the entire time).
To fix this, convert all the itimer measurements to use clock_gettime(),
tweaking the logic to use nsecs instead of usecs. Then for ITIMER_PROF
timers, utilize the CLOCK_THREAD_CPUTIME_ID clockid so that it is similarly
measuring the time the task was running.
Signed-off-by: John Stultz <jstultz@google.com>
Signed-off-by: Thomas Gleixner <tglx@kernel.org>
Link: https://patch.msgid.link/20260428173957.1394265-1-jstultz@google.com
The ITIMER_PROF case of it fails on the CI machines, which are two
cpu guests under load:
# Diff too high: 2561992..not ok 3 ITIMER_PROF
2.56 seconds of wall time for the 2 seconds of cpu time the timer
counts, so the task had about four fifths of a cpu.
(cherry picked from commit b00385b8d081ce74f36ea178e04e1b106505fb36)
https://virtuozzo.atlassian.net/browse/VSTOR-143363
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/timers/posix_timers.c | 55 ++++++++++---------
1 file changed, 28 insertions(+), 27 deletions(-)
diff --git a/tools/testing/selftests/timers/posix_timers.c b/tools/testing/selftests/timers/posix_timers.c
index f0eceb0faf34..077a98fb1b1d 100644
--- a/tools/testing/selftests/timers/posix_timers.c
+++ b/tools/testing/selftests/timers/posix_timers.c
@@ -77,19 +77,25 @@ static void sig_handler(int nr)
done = 1;
}
+static inline int64_t calcdiff_ns(struct timespec t1, struct timespec t2)
+{
+ int64_t diff;
+
+ diff = NSEC_PER_SEC * (int64_t)((int) t1.tv_sec - (int) t2.tv_sec);
+ diff += ((int) t1.tv_nsec - (int) t2.tv_nsec);
+ return diff;
+}
+
/*
* Check the expected timer expiration matches the GTOD elapsed delta since
* we armed the timer. Keep a 0.5 sec error margin due to various jitter.
*/
-static int check_diff(struct timeval start, struct timeval end)
+static int check_diff(struct timespec start, struct timespec end)
{
- long long diff;
-
- diff = end.tv_usec - start.tv_usec;
- diff += (end.tv_sec - start.tv_sec) * USEC_PER_SEC;
+ long long diff = calcdiff_ns(end, start);
- if (llabs(diff - DELAY * USEC_PER_SEC) > USEC_PER_SEC / 2) {
- printf("Diff too high: %lld..", diff);
+ if (llabs(diff - DELAY * NSEC_PER_SEC) > NSEC_PER_SEC / 2) {
+ printf("Diff too high: %lld ns..", diff);
return -1;
}
@@ -98,22 +104,25 @@ static int check_diff(struct timeval start, struct timeval end)
static void check_itimer(int which, const char *name)
{
- struct timeval start, end;
+ struct timespec start, end;
struct itimerval val = {
.it_value.tv_sec = DELAY,
};
+ int clock_id = CLOCK_REALTIME;
done = 0;
if (which == ITIMER_VIRTUAL)
signal(SIGVTALRM, sig_handler);
- else if (which == ITIMER_PROF)
+ else if (which == ITIMER_PROF) {
+ clock_id = CLOCK_THREAD_CPUTIME_ID;
signal(SIGPROF, sig_handler);
+ }
else if (which == ITIMER_REAL)
signal(SIGALRM, sig_handler);
- if (gettimeofday(&start, NULL) < 0)
- fatal_error(name, "gettimeofday()");
+ if (clock_gettime(clock_id, &start))
+ fatal_error(name, "clock_gettime()");
if (setitimer(which, &val, NULL) < 0)
fatal_error(name, "setitimer()");
@@ -125,18 +134,19 @@ static void check_itimer(int which, const char *name)
else if (which == ITIMER_REAL)
idle_loop();
- if (gettimeofday(&end, NULL) < 0)
- fatal_error(name, "gettimeofday()");
+ if (clock_gettime(clock_id, &end))
+ fatal_error(name, "clock_gettime()");
ksft_test_result(check_diff(start, end) == 0, "%s\n", name);
}
static void check_timer_create(int which, const char *name)
{
- struct timeval start, end;
+ struct timespec start, end;
struct itimerspec val = {
.it_value.tv_sec = DELAY,
};
+ int clock_id = CLOCK_REALTIME;
timer_t id;
done = 0;
@@ -147,16 +157,16 @@ static void check_timer_create(int which, const char *name)
if (signal(SIGALRM, sig_handler) == SIG_ERR)
fatal_error(name, "signal()");
- if (gettimeofday(&start, NULL) < 0)
- fatal_error(name, "gettimeofday()");
+ if (clock_gettime(clock_id, &start))
+ fatal_error(name, "clock_gettime()");
if (timer_settime(id, 0, &val, NULL) < 0)
fatal_error(name, "timer_settime()");
user_loop();
- if (gettimeofday(&end, NULL) < 0)
- fatal_error(name, "gettimeofday()");
+ if (clock_gettime(clock_id, &end))
+ fatal_error(name, "clock_gettime()");
ksft_test_result(check_diff(start, end) == 0,
"timer_create() per %s\n", name);
@@ -444,15 +454,6 @@ static void check_delete(void)
ksft_test_result(!tsig.signals, "check_delete\n");
}
-static inline int64_t calcdiff_ns(struct timespec t1, struct timespec t2)
-{
- int64_t diff;
-
- diff = NSEC_PER_SEC * (int64_t)((int) t1.tv_sec - (int) t2.tv_sec);
- diff += ((int) t1.tv_nsec - (int) t2.tv_nsec);
- return diff;
-}
-
static void check_sigev_none(int which, const char *name)
{
struct timespec start, now;
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* [PATCH 2/2] UefiCpuPkg/MpInitLib: resend the Start-up IPI when an AP misses it #VSTOR-146639
@ 2026-09-21 15:44 3% ` Denis V. Lunev
2026-09-23 13:30 0% ` Andrey Drobyshev
0 siblings, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-09-21 15:44 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, Denis V. Lunev
From: Denis V. Lunev <den@openvz.org>
WaitApWakeup spins until the AP clears its wakeup signal, with no
timeout and no way out. If the AP never consumes the Start-up IPI, the
BSP spins forever and the boot is dead.
A hypervisor can lose that IPI. Under KVM, INIT is latched while a
vCPU is in SMM but the Start-up IPI is discarded outright, so an AP
that takes an SMI just before the BSP broadcasts INIT-SIPI-SIPI is
left in wait-for-SIPI with nothing left to start it. The window is the
few instructions between an AP releasing the BSP at the end of the SMM
rendezvous and its own RSM, which a scheduled vCPU can be preempted
inside of. KVM holds that delivering the SIPI is software's job, so
the firmware has to cope.
Resend rather than spin. Address the AP individually so that APs
already running the procedure are not disturbed, and pass the reset
vector only on the paths that sent a Start-up IPI in the first place,
so the SEV-SNP and hand-off paths keep waiting as before. A Start-up
IPI is ignored unless the target is in wait-for-SIPI, so resending
costs nothing when the AP is merely slow.
The failure this fixes looks like a guest that never boots: firmware
starts the boot manager, paints nothing further, and one CPU spins
while the other APs sit idle.
Signed-off-by: Denis V. Lunev <den@openvz.org>
Cc: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
UefiCpuPkg/Library/MpInitLib/MpLib.c | 85 +++++++++++++++++++++++++---
UefiCpuPkg/Library/MpInitLib/MpLib.h | 2 +
2 files changed, 80 insertions(+), 7 deletions(-)
diff --git a/UefiCpuPkg/Library/MpInitLib/MpLib.c b/UefiCpuPkg/Library/MpInitLib/MpLib.c
index 426d998b9a..87e8f824b6 100644
--- a/UefiCpuPkg/Library/MpInitLib/MpLib.c
+++ b/UefiCpuPkg/Library/MpInitLib/MpLib.c
@@ -942,16 +942,63 @@ DxeApEntryPoint (
ApWakeupFunction (CpuMpData, ProcessorNumber);
}
+/**
+ Calculate timeout value and return the current performance counter value.
+
+ @param[in] TimeoutInMicroseconds The timeout value in microseconds.
+ @param[out] CurrentTime Returns the current value of the performance counter.
+
+ @return Expected timestamp counter for timeout, or 0 if TimeoutInMicroseconds is 0.
+**/
+UINT64
+CalculateTimeout (
+ IN UINTN TimeoutInMicroseconds,
+ OUT UINT64 *CurrentTime
+ );
+
+/**
+ Checks whether timeout expires.
+
+ @param[in, out] PreviousTime On input, the requested time. On output, the elapsed time.
+ @param[in] TotalTime The total amount of elapsed time.
+ @param[in] Timeout The timeout ticks, 0 means infinity.
+
+ @retval TRUE A timeout occurred.
+ @retval FALSE No timeout occurred.
+**/
+BOOLEAN
+CheckTimeout (
+ IN OUT UINT64 *PreviousTime,
+ IN UINT64 *TotalTime,
+ IN UINT64 Timeout
+ );
+
/**
Wait for AP wakeup and write AP start-up signal till AP is waken up.
@param[in] ApStartupSignalBuffer Pointer to AP wakeup signal
+ @param[in] ApicId Local APIC ID of the AP being woken
+ @param[in] StartupRoutine AP reset vector, or 0 if the AP was not
+ woken by INIT-SIPI-SIPI and no Start-up
+ IPI may be resent
**/
VOID
WaitApWakeup (
- IN volatile UINT32 *ApStartupSignalBuffer
+ IN volatile UINT32 *ApStartupSignalBuffer,
+ IN UINT32 ApicId,
+ IN UINT32 StartupRoutine
)
{
+ UINT64 CurrentTime;
+ UINT64 TotalTime;
+ UINT64 Timeout;
+
+ Timeout = CalculateTimeout (
+ (StartupRoutine != 0) ? AP_SIPI_RESEND_TIMEOUT_US : 0,
+ &CurrentTime
+ );
+ TotalTime = 0;
+
//
// If AP is waken up, StartupApSignal should be cleared.
// Otherwise, write StartupApSignal again till AP waken up.
@@ -962,6 +1009,12 @@ WaitApWakeup (
WAKEUP_AP_SIGNAL
) != 0)
{
+ if (CheckTimeout (&CurrentTime, &TotalTime, Timeout)) {
+ SendStartupIpi (ApicId, StartupRoutine);
+ Timeout = CalculateTimeout (AP_SIPI_RESEND_TIMEOUT_US, &CurrentTime);
+ TotalTime = 0;
+ }
+
CpuPause ();
}
}
@@ -1256,8 +1309,10 @@ WakeUpAP (
CPU_AP_DATA *CpuData;
BOOLEAN ResetVectorRequired;
CPU_INFO_IN_HOB *CpuInfoInHob;
+ UINT32 SipiVector;
CpuMpData->FinishedCount = 0;
+ SipiVector = 0;
ResetVectorRequired = FALSE;
if (CpuMpData->WakeUpByInitSipiSipi ||
@@ -1320,13 +1375,14 @@ WakeUpAP (
if (CanUseSevSnpCreateAP (CpuMpData)) {
SevSnpCreateAP (CpuMpData, -1);
} else {
+ SipiVector = (UINT32)ExchangeInfo->BufferStart;
if ((CpuMpData->InitFlag == ApInitConfig) && FixedPcdGetBool (PcdFirstTimeWakeUpAPsBySipi)) {
//
// SIPI can be used for the first time wake up after reset to reduce boot time.
//
- SendStartupIpiAllExcludingSelf ((UINT32)ExchangeInfo->BufferStart);
+ SendStartupIpiAllExcludingSelf (SipiVector);
} else {
- SendInitSipiSipiAllExcludingSelf ((UINT32)ExchangeInfo->BufferStart);
+ SendInitSipiSipiAllExcludingSelf (SipiVector);
}
}
}
@@ -1398,10 +1454,15 @@ WakeUpAP (
//
// Wait all APs waken up if this is not the 1st broadcast of SIPI
//
+ CpuInfoInHob = (CPU_INFO_IN_HOB *)(UINTN)CpuMpData->CpuInfoInHob;
for (Index = 0; Index < CpuMpData->CpuCount; Index++) {
CpuData = &CpuMpData->CpuData[Index];
if (Index != CpuMpData->BspNumber) {
- WaitApWakeup (CpuData->StartupApSignal);
+ WaitApWakeup (
+ CpuData->StartupApSignal,
+ CpuInfoInHob[Index].ApicId,
+ SipiVector
+ );
}
}
}
@@ -1430,9 +1491,10 @@ WakeUpAP (
if (CanUseSevSnpCreateAP (CpuMpData)) {
SevSnpCreateAP (CpuMpData, (INTN)ProcessorNumber);
} else {
+ SipiVector = (UINT32)ExchangeInfo->BufferStart;
SendInitSipiSipi (
CpuInfoInHob[ProcessorNumber].ApicId,
- (UINT32)ExchangeInfo->BufferStart
+ SipiVector
);
}
}
@@ -1440,7 +1502,12 @@ WakeUpAP (
//
// Wait specified AP waken up
//
- WaitApWakeup (CpuData->StartupApSignal);
+ CpuInfoInHob = (CPU_INFO_IN_HOB *)(UINTN)CpuMpData->CpuInfoInHob;
+ WaitApWakeup (
+ CpuData->StartupApSignal,
+ CpuInfoInHob[ProcessorNumber].ApicId,
+ SipiVector
+ );
}
if (ResetVectorRequired) {
@@ -2017,7 +2084,11 @@ SwitchApContext (
{
for (Index = 0; Index < MpHandOff->CpuCount; Index++) {
if (MpHandOff->ProcessorIndex + Index != BspNumber) {
- WaitApWakeup ((UINT32 *)(UINTN)(MpHandOff->Info[Index].StartupSignalAddress));
+ WaitApWakeup (
+ (UINT32 *)(UINTN)(MpHandOff->Info[Index].StartupSignalAddress),
+ 0,
+ 0
+ );
}
}
}
diff --git a/UefiCpuPkg/Library/MpInitLib/MpLib.h b/UefiCpuPkg/Library/MpInitLib/MpLib.h
index 5d10516cd8..94bda7f1ea 100644
--- a/UefiCpuPkg/Library/MpInitLib/MpLib.h
+++ b/UefiCpuPkg/Library/MpInitLib/MpLib.h
@@ -44,6 +44,8 @@
#include "MpHandOff.h"
#define WAKEUP_AP_SIGNAL SIGNATURE_32 ('S', 'T', 'A', 'P')
+
+#define AP_SIPI_RESEND_TIMEOUT_US 10000
//
// To trigger the start-up signal, BSP writes the specified
// StartupSignalValue to the StartupSignalAddress of each processor.
--
2.53.0
^ permalink raw reply [relevance 3%]
* Re: [Devel] [PATCH vz10 3/7] selftests: net: skip what this kernel and iproute2 do not have
@ 2026-09-21 14:51 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-21 14:51 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
> Five cases report a failure where a feature is simply not there.
>
> tls asks for every cipher it knows and fails on the ones the kernel does
> not implement, SM4 and ARIA, which answer ENOENT at setsockopt
> time. Skip a cipher the kernel refuses that way; the run then reports
> 632 passed, 264 skipped and none failed.
>
> rtnetlink.sh sets up an erspan tunnel, which this kernel does not have
> and which fails with "Unknown device type", and checks a netconf dump
> whose exit status iproute2 gets wrong: 6.11 exits 2 on a dump that
> worked, 6.17 exits 0. Probe for both and skip when unusable.
>
> rtnetlink.py asks for an IPv4 multicast address dump the kernel answers
> with EOPNOTSUPP, and ip_local_port_range asks for protocols it does not
> have. Skip those too.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-139651
> Feature: fix selftests
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/net/ip_local_port_range.c b/tools/testing/selftests/net/ip_local_port_range.c
> index 29451d2244b7..2d7b9c852b2b 100644
> --- a/tools/testing/selftests/net/ip_local_port_range.c
> +++ b/tools/testing/selftests/net/ip_local_port_range.c
> @@ -118,10 +118,6 @@ static int get_ip_local_port_range(int fd, __u32 *range)
>
> FIXTURE(ip_local_port_range) {};
>
> -FIXTURE_SETUP(ip_local_port_range)
> -{
> -}
> -
> FIXTURE_TEARDOWN(ip_local_port_range)
> {
> }
> @@ -180,6 +176,19 @@ FIXTURE_VARIANT_ADD(ip_local_port_range, ip6_mptcp) {
> .so_protocol = IPPROTO_MPTCP,
> };
>
> +FIXTURE_SETUP(ip_local_port_range)
> +{
> + int fd;
> +
> + /* Not every protocol under test is built into every kernel. */
> + fd = socket(variant->so_domain, variant->so_type, variant->so_protocol);
> + if (fd < 0 && (errno == EPROTONOSUPPORT || errno == ESOCKTNOSUPPORT ||
> + errno == EAFNOSUPPORT))
> + SKIP(return, "%s", strerror(errno));
> + ASSERT_GE(fd, 0) TH_LOG("socket failed");
> + close(fd);
> +}
> +
> TEST_F(ip_local_port_range, invalid_option_value)
> {
> __u16 val16;
> diff --git a/tools/testing/selftests/net/rtnetlink.py b/tools/testing/selftests/net/rtnetlink.py
> index baa7b33f313e..425fc7a3941b 100755
> --- a/tools/testing/selftests/net/rtnetlink.py
> +++ b/tools/testing/selftests/net/rtnetlink.py
> @@ -1,7 +1,9 @@
> #! /usr/bin/python3 -sP
> # SPDX-License-Identifier: GPL-2.0
>
> -from lib.py import ksft_exit, ksft_run, ksft_ge, RtnlAddrFamily
> +from lib.py import ksft_exit, ksft_run, ksft_ge, KsftSkipEx
> +from lib.py import NlError, RtnlAddrFamily
> +import errno
> import socket
>
> IPV4_ALL_HOSTS_MULTICAST = b'\xe0\x00\x00\x01'
> @@ -12,7 +14,12 @@ def dump_mcaddr_check(rtnl: RtnlAddrFamily) -> None:
> At least the loopback interface should have this address.
> """
>
> - addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True)
> + try:
> + addresses = rtnl.getmulticast({"ifa-family": socket.AF_INET}, dump=True)
> + except NlError as e:
> + if e.nl_msg.error == -errno.EOPNOTSUPP:
> + raise KsftSkipEx("IPv4 multicast address dump is not supported")
> + raise
>
> all_host_multicasts = [
> addr for addr in addresses if addr['multicast'] == IPV4_ALL_HOSTS_MULTICAST
> diff --git a/tools/testing/selftests/net/rtnetlink.sh b/tools/testing/selftests/net/rtnetlink.sh
> index 5f72b447e940..cc4e94b90002 100755
> --- a/tools/testing/selftests/net/rtnetlink.sh
> +++ b/tools/testing/selftests/net/rtnetlink.sh
> @@ -142,10 +142,30 @@ kci_del_dummy()
> run_cmd ip link del dev "$devdummy"
> }
>
> +# Some iproute2 versions exit non-zero from a netconf dump that worked,
> +# printing the devconf and then failing anyway. Nothing about the kernel
> +# can be learned from the exit status of such an ip, so find out once.
> +netconf_exit_status_usable()
> +{
> + local out
> +
> + out=$(ip -4 netconf show dev lo 2>/dev/null)
> + if [ $? -ne 0 ] && [ -n "$out" ]; then
> + return 1
> + fi
> + return 0
> +}
> +
> kci_test_netconf()
> {
> dev="$1"
> r=$ret
> +
> + if ! netconf_exit_status_usable; then
> + end_test "SKIP: ip netconf $dev: iproute2 fails a dump that worked"
> + return $ksft_skip
> + fi
> +
> run_cmd ip netconf show dev "$dev"
> for f in 4 6; do
> run_cmd ip -$f netconf show dev "$dev"
> @@ -920,6 +940,17 @@ kci_test_erspan()
> return $ksft_skip
> fi
>
> + # the kernel can be built without erspan, rtnetlink then has no ops
> + # for the type and says so. A kernel that has it gets past this and
> + # fails on the attributes the probe leaves out.
> + if ip -netns "$testns" link add dev "$DEV_NS" type erspan 2>&1 | \
> + grep -q "Unknown device type"; then
> + end_test "SKIP: erspan: not supported by the kernel"
> + ip netns del "$testns"
> + return $ksft_skip
> + fi
> + ip -netns "$testns" link del dev "$DEV_NS" 2>/dev/null
> +
> # test native tunnel erspan v1
> run_cmd ip -netns "$testns" link add dev "$DEV_NS" type erspan seq \
> key 102 local 172.16.1.100 remote 172.16.1.200 \
> diff --git a/tools/testing/selftests/net/tls.c b/tools/testing/selftests/net/tls.c
> index c9b0b4337e12..07f2b454d755 100644
> --- a/tools/testing/selftests/net/tls.c
> +++ b/tools/testing/selftests/net/tls.c
> @@ -413,6 +413,8 @@ FIXTURE_SETUP(tls)
> return;
>
> ret = setsockopt(self->fd, SOL_TLS, TLS_TX, &tls12, tls12.len);
> + if (ret < 0 && errno == ENOENT)
> + SKIP(return, "Cipher not built into the kernel");
Just a small question: ENOENT error is not in setsockopt man.
It seems more like ENOPROTOOPT, but if the fix is here, it probably
helped. Could you tell where ENOENT comes from?
Maybe we should test for ENOPROTOOPT as well here?
> ASSERT_EQ(ret, 0);
>
> ret = setsockopt(self->cfd, SOL_TLS, TLS_RX, &tls12, tls12.len);
Reviewed-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
--
Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH VZ10 v9 8/8] selftests/ve: Add mount accounting selftest
2026-09-15 17:22 6% ` [Devel] [PATCH VZ10 v9 8/8] selftests/ve: Add mount accounting selftest Vladimir Riabchun
@ 2026-09-18 15:06 0% ` Vladimir Riabchun
2026-09-25 16:04 0% ` Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-18 15:06 UTC (permalink / raw)
To: devel
Sashiko review:
On 9/15/26 19:22, Vladimir Riabchun wrote:
> There are 6 test cases, covered in the new test:
> 1. Simple mount accouting correctness, just mount/umount.
> 2. Verification of correct limit hits and changes, including
> negative values.
> 3. Partial mounts test, when mount limit is hit in the middle
> of creation.
> 4. Test that enabled pseudosuper allows overuse.
> 5. Test that pseudosuper doesn't affect mount accounting.
> 6. Failcount feature verification.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-135520
>
> Feature: per-ve failcounters
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> ---
> tools/testing/selftests/ve/.gitignore | 1 +
> tools/testing/selftests/ve/Makefile | 1 +
> .../selftests/ve/ve_mount_accounting_test.c | 550 ++++++++++++++++++
> tools/testing/selftests/ve/ve_selftest.h | 21 +
> 4 files changed, 573 insertions(+)
> create mode 100644 tools/testing/selftests/ve/ve_mount_accounting_test.c
>
> diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
> index afa4c568c2c9..3df4d05888dc 100644
> --- a/tools/testing/selftests/ve/.gitignore
> +++ b/tools/testing/selftests/ve/.gitignore
> @@ -1,2 +1,3 @@
> ve_ns_owner_test
> ve_perms_test
> +ve_mount_accounting_test
> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
> index ec40cbc7b3a1..c6efe7c4b4fb 100644
> --- a/tools/testing/selftests/ve/Makefile
> +++ b/tools/testing/selftests/ve/Makefile
> @@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
>
> TEST_GEN_PROGS += ve_ns_owner_test
> TEST_GEN_PROGS += ve_perms_test
> +TEST_GEN_PROGS += ve_mount_accounting_test
>
> include ../lib.mk
> diff --git a/tools/testing/selftests/ve/ve_mount_accounting_test.c b/tools/testing/selftests/ve/ve_mount_accounting_test.c
> new file mode 100644
> index 000000000000..6ba13dcdcc87
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
> @@ -0,0 +1,550 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * ve_mount_accounting selftests
> + *
> + * Tests to check the correctness of mount accounting.
> + */
> +#define _GNU_SOURCE
> +#include <asm/unistd.h>
> +#include <linux/sched.h>
> +#include <linux/limits.h>
> +#include <sys/wait.h>
> +#include <sys/syscall.h>
> +#include <sys/stat.h>
> +#include <sys/mount.h>
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <sched.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <unistd.h>
> +#include <string.h>
> +#include <time.h>
> +
> +#include "../kselftest_harness.h"
> +#include "ve_selftest.h"
> +
> +#define TMP_DIR "/ve-mnt-tmp/"
> +#define VE_MOUNTS_MAX 128
> +
> +/*
> + * These values take a while to calculate, the first test will
> + * do this and store the values for those who come after.
> + */
> +int *need_avail;
> +
> +FIXTURE(ve_mnt_acc)
> +{
> + int cgv2_fd;
> + int ctid;
> + struct namespace_ctx ns_ctx;
> + int orig_have_mnt;
> +};
> +
> +/*
> + * free_vfsmnt is called via call_rcu, so we need to ensure
> + * that all pending call_rcu tasks are finished.
> + * However, one barrier is not enough:
> + * mntput_no_expire
> + * init_task_work(&mnt->mnt_rcu, __cleanup_mnt);
> + * task_work_add(task, &mnt->mnt_rcu, TWA_RESUME)
> + * OR
> + * schedule_delayed_work(&delayed_mntput_work, 1)
> + * # Both later call cleanup_mnt
> + * cleanup_mnt
> + * call_rcu(&mnt->mnt_rcu, delayed_free_vfsmnt);
> + *
> + * We can't enforce workqueue flush from userspace, so instead
> + * read several times until the value is the one we expect.
> + */
Sashiko says:
This segment of the `wait_on_mnt_avail` function can potentially lead
to an infinite wait if the value `mnt_avail_nr` does not match
the expected value before timeout.
While this isn't a bug introduced by this patch, could we include
some mechanism to handle timeouts more gracefully, such as an error code
return or retry limit?
Something is off here.
1. There was no bug before this patch.
2. Retry and error return mechanism is what this code does.
> +static int wait_on_mnt_avail(FIXTURE_DATA(ve_mnt_acc) * self, int expected)
> +{
> +#define WAIT_TIMEOUT_NS (4096UL * 1000 * 1000)
> + char path_avail[64];
> + int mnt_avail_nr = 0;
> + struct timespec delay = { .tv_nsec = 1000000 };
> +
> + snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
> +
> + while (delay.tv_nsec <= WAIT_TIMEOUT_NS) {
> + if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr)) {
> + fprintf(stderr, "Failed to read from %s\n", path_avail);
> + return 1;
> + }
> + if (mnt_avail_nr == expected)
> + return 0;
> +
> + do_rcu_barrier();
> + if (nanosleep(&delay, NULL)) {
> + fprintf(stderr, "nanosleep failed: %s\n", strerror(errno));
> + return 1;
> + }
> + delay.tv_nsec *= 2;
> + }
> + fprintf(stderr, "Didn't get expected value at %s\n", path_avail);
> + fprintf(stderr, "Expected %d, last read %d\n", expected, mnt_avail_nr);
> + return -1;
> +}
> +
> +static int set_pseudosuper(FIXTURE_DATA(ve_mnt_acc) * self, int value)
> +{
> + char path[64];
> +
> + snprintf(path, sizeof(path), "%d/ve.pseudosuper", self->ctid);
> + return write_u64_at(self->cgv2_fd, path, value);
> +}
> +
Sashiko says:
In `_create_mount`, can the clean-up logic be more robust to ensure
that filesystem resources are consistently freed in the event of an error
or unexpected behavior?
No more cleanup here is possible.
> +static int _create_mount(void *id_ptr)
> +{
> + char path[PATH_MAX];
> + int id = *(int *)id_ptr, ret;
> +
> + snprintf(path, sizeof(path), TMP_DIR "%d", id);
> +
> + if (mkdir(path, 0755) < 0) {
> + fprintf(stderr, "Failed to create directory %s: %s\n", path, strerror(errno));
> + return -1;
> + }
> + ret = mount("tmpfs", path, "tmpfs", 0, "size=1M");
> + if (!ret)
> + return 0;
> + fprintf(stderr, "Failed to mount tmpfs to %s: %s\n", path, strerror(errno));
> +
> + rmdir(path);
> + return -1;
> +}
> +
> +/* Create a mount and wait until mount counter is valid */
> +static int create_mount(FIXTURE_DATA(ve_mnt_acc) * self, int id)
> +{
> + int ret, avail_before;
> + char path[64];
> +
> + snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
> +
> + if (read_s32_at(self->cgv2_fd, path, &avail_before)) {
> + fprintf(stderr, "Failed to read mnt_avail_nr in create_mount\n");
> + return 1;
> + }
> +
Sashiko says:
Would it be beneficial to include comprehensive error handling in `create_mount`
to cover cases when `run_in_ve` fails?
No, it wouldn't. Nothing we can do in this case and nothing
needs to be handled - no resources created.
> + ret = run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
> + CLONE_NEWVE | CLONE_NEWNS, _create_mount, &id);
> +
> + if (ret)
> + wait_on_mnt_avail(self, avail_before);
> + else
> + ret = wait_on_mnt_avail(self, avail_before - 1);
> + return ret;
> +}
> +
Sashiko says:
Does the `_destroy_mount` function correctly handle subsequent errors that may
occur during the unmount and directory removal stages?
Yes, it does.
> +static int _destroy_mount(void *id_ptr)
> +{
> + char path[PATH_MAX];
> + struct stat st;
> + int id = *(int *)id_ptr;
> +
> + snprintf(path, sizeof(path), TMP_DIR "%d", id);
> +
> + if (stat(path, &st))
> + return 1;
> + if (umount(path)) {
> + fprintf(stderr, "failed to umount directory %s: %s\n", path, strerror(errno));
> + return -1;
> + }
> + if (rmdir(path)) {
> + fprintf(stderr, "failed to remove directory %s: %s\n", path, strerror(errno));
> + return -1;
> + }
> + return 0;
> +}
> +
> ...
> +
> +int main(int argc, char **argv)
> +{
> + need_avail = mmap(NULL, sizeof(int),
> + PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
> +
> + if (need_avail == MAP_FAILED) {
> + fprintf(stderr, "Failed to mmap shared region: %s\n", strerror(errno));
> + return 1;
> + }
> + *need_avail = -1;
> +
> + return test_harness_run(argc, argv);
Sashiko says:
Does the `main` function properly handle the memory allocated by `mmap`,
especially considering that there might not be a corresponding `munmap` to free
the memory?
Doesn't matter, we can free need_avail only when test_harness_run finishes.
It is easier to just exit.
> +}
> diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
> index bf9b89b59a6c..8cc9b3238aaf 100644
> --- a/tools/testing/selftests/ve/ve_selftest.h
> +++ b/tools/testing/selftests/ve/ve_selftest.h
> @@ -413,4 +413,25 @@ static inline int is_mounted(const char *path)
> return errno == EXDEV ? 1 : -1;
> }
>
Sashiko says:
Could write operations within `do_rcu_barrier` be improperly assumed to be atomic
leading to potential scheduling issues when invoked in contexts that might sleep?
No, they couldn't. This is user-space code.
> +static inline int do_rcu_barrier(void)
> +{
> + int fd;
> +
> + errno = 0;
> + fd = open("/sys/module/rcutree/parameters/do_rcu_barrier", O_WRONLY);
> +
> + if (fd < 0) {
> + fprintf(stderr, "Failed to open do_rcu_barrier: %s\n", strerror(errno));
> + return -1;
> + }
> + errno = 0;
> + if (write(fd, "1", 2) != 2) {
> + fprintf(stderr, "Failed to issue rcu_barrier: %s\n", strerror(errno));
> + close(fd);
> + return -1;
> + }
> + close(fd);
> + return 0;
> +}
> +
> #endif /* __SELFTESTS_VE_VE_SELFTEST_H */
--
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* [PATCH hci-8.0 1/1] net/tap-linux.c: avoid abort when setting invalid fd #VSTOR-146012
@ 2026-09-18 14:44 8% Denis V. Lunev
0 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-18 14:44 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Houqi (Nick) Zuo <hzuo@redhat.com>
This patch removes abort() call in the tap_fd_set_vnet_hdr_len()
function. If the fd is found to be in a bad state (e.g., EBADFD
or ENODEV), the function will print an error message.
When QEMU creates a tap device automatically and the tap device is
manually removed from the host while the guest is running, the tap
device file descriptor becomes invalid. Later, when the guest executes
shutdown, the tap_fd_set_vnet_hdr_len() function may be called and
abort QEMU with a core dump when attempting to use the invalid fd.
The expected behavior for this negative test case is that QEMU should
report an error but continue running rather than aborting.
Testing:
- Start QEMU with automatically created tap device
- Manually remove the tap device on the host
- Execute shutdown in the guest
- Verify QEMU reports an error but does not abort
Fixes: 0caed25cd171 ("virtio: Call set_features during reset")
Signed-off-by: Houqi (Nick) Zuo <hzuo@redhat.com>
Signed-off-by: Jason Wang <jasowang@redhat.com>
(cherry picked from commit a1708203b583ec04fb9b793840dc282d3c89a20d)
---
net/tap-linux.c | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/net/tap-linux.c b/net/tap-linux.c
index f2dc56c1bb8..493c3bc7b11 100644
--- a/net/tap-linux.c
+++ b/net/tap-linux.c
@@ -214,10 +214,11 @@ bool tap_probe_has_tunnel(int fd)
void tap_fd_set_vnet_hdr_len(int fd, int len)
{
- if (ioctl(fd, TUNSETVNETHDRSZ, &len) == -1) {
- fprintf(stderr, "TUNSETVNETHDRSZ ioctl() failed: %s. Exiting.\n",
- strerror(errno));
- abort();
+ int ret;
+
+ ret = ioctl(fd, TUNSETVNETHDRSZ, &len);
+ if (ret != 0) {
+ error_report("TUNSETVNETHDRSZ ioctl() failed: %s.", strerror(errno));
}
}
--
2.53.0
^ permalink raw reply [relevance 8%]
* Re: [Devel] [PATCH VZ10 v9 6/8] ve: Introduce per-VE failcount
2026-09-15 17:22 9% ` [Devel] [PATCH VZ10 v9 6/8] ve: Introduce per-VE failcount Vladimir Riabchun
@ 2026-09-18 14:42 0% ` Vladimir Riabchun
2026-09-25 16:02 0% ` Konstantin Khorenko
2026-09-25 16:08 4% ` Konstantin Khorenko
2 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-18 14:42 UTC (permalink / raw)
To: devel
Sashiko review:
On 9/15/26 19:22, Vladimir Riabchun wrote:
> It may be useful to have a history of resource limit hits for every VE,
> this may simplify debugging and provide some information about the
> resources usage.
>
> This information is provided by ve.failcount file, any write to it
> resets all failcounts.
>
> To add a new failcounter we need to create a new atomic_t field
> name_failcount in ve structure and add a new VE_FC_ENTRY in
> ve_failcounts array.
>
> One change, unrelated to failcounts: aio fields are now initialized
> in ve0.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-135520
>
> Feature: per-ve failcounters
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> ---
> fs/aio.c | 1 +
> fs/namespace.c | 2 ++
> include/linux/ve.h | 16 +++++++++
> kernel/bpf/syscall.c | 1 +
> kernel/ve/ve.c | 70 ++++++++++++++++++++++++++++++++++++++++
> net/core/dev.c | 10 ++----
> net/core/neighbour.c | 3 +-
> net/core/net_namespace.c | 4 ++-
> 8 files changed, 96 insertions(+), 11 deletions(-)
>
> diff --git a/fs/aio.c b/fs/aio.c
> index cb63416af135..4ae06bcd47f2 100644
> --- a/fs/aio.c
> +++ b/fs/aio.c
> @@ -814,6 +814,7 @@ static struct kioctx *ioctx_alloc(unsigned nr_events)
> spin_lock(&ve->aio_nr_lock);
> if (ve->aio_nr + ctx->max_reqs > ve->aio_max_nr ||
> ve->aio_nr + ctx->max_reqs < ve->aio_nr) {
> + ve_failcount_inc(ve, aio);
> spin_unlock(&ve->aio_nr_lock);
> err = -EAGAIN;
> goto err_ctx;
> diff --git a/fs/namespace.c b/fs/namespace.c
> index 1d6e66cd7174..ea01664ff5f2 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -3380,6 +3380,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
>
> if (ret)
> get_ve(ve);
> + else
> + ve_failcount_inc(ve, mnt);
> return ret;
> }
>
> diff --git a/include/linux/ve.h b/include/linux/ve.h
> index fabc608e31f5..065129b42768 100644
> --- a/include/linux/ve.h
> +++ b/include/linux/ve.h
> @@ -80,12 +80,15 @@ struct ve_struct {
> struct kmapset_key proc_perms_key;
>
> atomic_t netns_avail_nr;
> + atomic_t netns_failcount;
> int netns_max_nr;
>
> atomic_t netif_avail_nr;
> + atomic_t netif_failcount;
> int netif_max_nr;
>
> atomic_t bpf_prog_avail_nr;
> + atomic_t bpf_prog_failcount;
> int bpf_prog_max_nr;
>
> atomic64_t _uevent_seqnum;
> @@ -94,6 +97,7 @@ struct ve_struct {
>
> atomic_t arp_neigh_nr;
> atomic_t nd_neigh_nr;
> + atomic_t neigh_tbl_failcount;
> unsigned long meminfo_val;
>
> /*
> @@ -102,6 +106,7 @@ struct ve_struct {
> * other containers.
> */
> atomic_t mnt_avail_nr; /* number of available VE mounts */
> + atomic_t mnt_failcount;
> int mnt_max_nr;
>
> #ifdef CONFIG_COREDUMP
> @@ -129,6 +134,7 @@ struct ve_struct {
> spinlock_t aio_nr_lock;
> unsigned long aio_nr;
> unsigned long aio_max_nr;
> + atomic_t aio_failcount;
> #endif
> struct vfsmount *devtmpfs_mnt;
> };
> @@ -151,6 +157,16 @@ extern int nr_ve;
> #define BPF_PROG_MAX_NR_DEFAULT 256 /* number of loaded BPF progs per-VE */
> #define MNT_MAX_NR_DEFAULT 4096 /* number of mounts per-VE */
>
> +#define ve_failcount_inc(ve, name) \
> +do { \
> + struct ve_struct *__ve = (ve); \
> + \
> + atomic_inc(&__ve->name##_failcount); \
> + ve_pr_warn_ratelimited(VE_LOG_BOTH, "CT%s: hits the " #name \
> + " limit, see the '" #name "' counter in ve.failcount " \
> + "of the container's ve cgroup\n", ve_name(__ve)); \
> +} while (0)
Sashiko says:
How does this handle concurrent modifications? The macro ve_failcount_inc increments
the failcount atomically, but when combined with the reset operation in
ve_failcount_write, concurrent operations may lead to race conditions
affecting data integrity.
No.
> +
> #ifdef CONFIG_VE
> static inline void ve_set_state(struct ve_struct *ve, int new_state)
> {
> diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
> index c94d4240e3d3..8498bc9df7ea 100644
> --- a/kernel/bpf/syscall.c
> +++ b/kernel/bpf/syscall.c
> @@ -2891,6 +2891,7 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
> if (!bpf_cap && type == BPF_PROG_TYPE_CGROUP_DEVICE) {
> load_ve = get_exec_env();
> if (atomic_dec_if_positive(&load_ve->bpf_prog_avail_nr) < 0) {
> + ve_failcount_inc(load_ve, bpf_prog);
> load_ve = NULL;
> err = -ENOSPC;
> goto put_token;
> diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
> index 5ebdb4b7baa7..9261565208e6 100644
> --- a/kernel/ve/ve.c
> +++ b/kernel/ve/ve.c
> @@ -100,10 +100,13 @@ struct ve_struct ve0 = {
> .features = -1,
> .sched_lat_ve.cur = &ve0_lat_stats,
> .netns_avail_nr = ATOMIC_INIT(INT_MAX),
> + .netns_failcount = ATOMIC_INIT(0),
> .netns_max_nr = INT_MAX,
> .netif_avail_nr = ATOMIC_INIT(INT_MAX),
> + .netif_failcount = ATOMIC_INIT(0),
> .netif_max_nr = INT_MAX,
> .bpf_prog_avail_nr = ATOMIC_INIT(INT_MAX),
> + .bpf_prog_failcount = ATOMIC_INIT(0),
> .bpf_prog_max_nr = INT_MAX,
> .fsync_enable = FSYNC_FILTERED,
> ._randomize_va_space =
> @@ -115,8 +118,16 @@ struct ve_struct ve0 = {
>
> .arp_neigh_nr = ATOMIC_INIT(0),
> .nd_neigh_nr = ATOMIC_INIT(0),
> + .neigh_tbl_failcount = ATOMIC_INIT(0),
> .mnt_avail_nr = ATOMIC_INIT(INT_MAX),
> .mnt_max_nr = INT_MAX,
> + .mnt_failcount = ATOMIC_INIT(0),
> +#ifdef CONFIG_AIO
> + .aio_nr_lock = __SPIN_LOCK_UNLOCKED(aio_nr_lock),
> + .aio_nr = 0,
> + .aio_max_nr = AIO_MAX_NR_DEFAULT,
> + .aio_failcount = ATOMIC_INIT(0),
> +#endif
> .meminfo_val = VE_MEMINFO_SYSTEM,
> #ifdef CONFIG_X86
> .umh_running_helpers = ATOMIC_INIT(0),
> @@ -787,12 +798,15 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
> ve->fsync_enable = FSYNC_FILTERED;
>
> atomic_set(&ve->netns_avail_nr, NETNS_MAX_NR_DEFAULT);
> + atomic_set(&ve->netns_failcount, 0);
> ve->netns_max_nr = NETNS_MAX_NR_DEFAULT;
>
> atomic_set(&ve->netif_avail_nr, NETIF_MAX_NR_DEFAULT);
> + atomic_set(&ve->netif_failcount, 0);
> ve->netif_max_nr = NETIF_MAX_NR_DEFAULT;
>
> atomic_set(&ve->bpf_prog_avail_nr, BPF_PROG_MAX_NR_DEFAULT);
> + atomic_set(&ve->bpf_prog_failcount, 0);
> ve->bpf_prog_max_nr = BPF_PROG_MAX_NR_DEFAULT;
>
> err = ve_log_init(ve);
> @@ -821,7 +835,9 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
>
> atomic_set(&ve->arp_neigh_nr, 0);
> atomic_set(&ve->nd_neigh_nr, 0);
> + atomic_set(&ve->neigh_tbl_failcount, 0);
> ve->mnt_max_nr = MNT_MAX_NR_DEFAULT;
> + atomic_set(&ve->mnt_failcount, 0);
> atomic_set(&ve->mnt_avail_nr, MNT_MAX_NR_DEFAULT);
>
> #ifdef CONFIG_COREDUMP
> @@ -834,6 +850,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
> spin_lock_init(&ve->aio_nr_lock);
> ve->aio_nr = 0;
> ve->aio_max_nr = AIO_MAX_NR_DEFAULT;
> + atomic_set(&ve->aio_failcount, 0);
> #endif
>
> return &ve->css;
> @@ -1074,6 +1091,53 @@ VE_RESOURCE(mnt);
> VE_RESOURCE(netif);
> VE_RESOURCE(bpf_prog);
>
> +static const struct ve_failcount_entry {
> + const char *name;
> + size_t offset;
> +} ve_failcounts[] = {
> +#define VE_FC_ENTRY(name) { #name, offsetof(struct ve_struct, name##_failcount) }
> + VE_FC_ENTRY(netns),
> + VE_FC_ENTRY(mnt),
> + VE_FC_ENTRY(netif),
> + VE_FC_ENTRY(bpf_prog),
> + VE_FC_ENTRY(neigh_tbl),
> +#ifdef CONFIG_AIO
> + VE_FC_ENTRY(aio),
> +#endif
> + {}
> +};
> +
> +static int ve_failcount_read(struct seq_file *sf, void *v)
> +{
> + struct ve_struct *ve = css_to_ve(seq_css(sf));
> + const struct ve_failcount_entry *entry;
> + atomic_t *fc;
> +
> + for (entry = ve_failcounts; entry->name; entry++) {
> + fc = (void *)ve + entry->offset;
> + seq_printf(sf, "%s: %d\n", entry->name, atomic_read(fc));
> + }
> + return 0;
> +}
> +
Sashiko says:
Could this cause synchronization issues?
The function ve_failcount_write resets the failcount, potentially leading to
race conditions as it does not account for concurrent increments of the failcount.
+
Is there a potential for incorrect reads?
This loop in the function ve_failcount_read reads the failcount values,
but does so without synchronization measures to handle concurrent writes,
which might result in stale or inconsistent data being read.
It uses atomic operations. The only race that could be is parallel read and flush.
Not a big deal - part of failcounts will be 0 and part - original ones.
> +static ssize_t ve_failcount_write(struct kernfs_open_file *of, char *buf,
> + size_t nbytes, loff_t off)
> +{
> + struct ve_struct *ve = css_to_ve(of_css(of));
> + const struct ve_failcount_entry *entry;
> + atomic_t *fc;
> +
> + if (!ve_is_super(get_exec_env()) && !ve->is_pseudosuper)
> + return -EPERM;
> +
> + for (entry = ve_failcounts; entry->name; entry++) {
> + fc = (void *)ve + entry->offset;
> + atomic_set(fc, 0);
> + }
> +
> + return nbytes;
> +}
> +
> static int ve_os_release_read(struct seq_file *sf, void *v)
> {
> struct cgroup_subsys_state *css = seq_css(sf);
> @@ -1613,6 +1677,12 @@ static struct cftype ve_cftypes[] = {
> .flags = CFTYPE_NOT_ON_ROOT,
> .write_u64 = ve_rpc_kill_write,
> },
> + {
> + .name = "failcount",
> + .flags = CFTYPE_NOT_ON_ROOT,
> + .seq_show = ve_failcount_read,
> + .write = ve_failcount_write,
> + },
> { }
> };
>
> diff --git a/net/core/dev.c b/net/core/dev.c
> index c7dddb200489..7632ad1fd015 100644
> --- a/net/core/dev.c
> +++ b/net/core/dev.c
> @@ -10997,10 +10997,7 @@ int register_netdevice(struct net_device *dev)
>
> ret = -ENOMEM;
> if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
> - ve_pr_warn_ratelimited(VE_LOG_BOTH,
> - "CT%s: hits max number of network devices, "
> - "increase ve::netif_max_nr parameter\n",
> - ve_name(net->owner_ve));
> + ve_failcount_inc(net->owner_ve, netif);
> return ret;
> }
>
> @@ -12211,10 +12208,7 @@ int __dev_change_net_namespace(struct net_device *dev, struct net *net,
>
> err = -ENOMEM;
> if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
> - ve_pr_warn_ratelimited(VE_LOG_BOTH,
> - "CT%s: hits max number of network devices, "
> - "increase ve::netif_max_nr parameter\n",
> - ve_name(net->owner_ve));
> + ve_failcount_inc(net->owner_ve, netif);
> goto out;
> }
> atomic_inc(&dev_net(dev)->owner_ve->netif_avail_nr);
> diff --git a/net/core/neighbour.c b/net/core/neighbour.c
> index f90deb17fb25..c7d5e6df50c2 100644
> --- a/net/core/neighbour.c
> +++ b/net/core/neighbour.c
> @@ -520,8 +520,7 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
> (glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
> time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
> if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
> - net_info_ratelimited("%s: neighbor table overflow!\n",
> - tbl->id);
> + ve_failcount_inc(ve, neigh_tbl);
> NEIGH_CACHE_STAT_INC(tbl, table_fulls);
> goto out_entries;
> }
> diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c
> index b3d54cad984a..328e1c96b6f4 100644
> --- a/net/core/net_namespace.c
> +++ b/net/core/net_namespace.c
> @@ -486,8 +486,10 @@ void net_drop_ns(void *p)
> #ifdef CONFIG_VE
> static int dec_netns_avail(struct ve_struct *ve)
> {
> - if (atomic_dec_if_positive(&ve->netns_avail_nr) < 0)
> + if (atomic_dec_if_positive(&ve->netns_avail_nr) < 0) {
> + ve_failcount_inc(ve, netns);
> return -ENOSPC;
> + }
> return 0;
> }
>
--
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH VZ10 v9 2/8] ve/fs: Rework per-ve mount count
2026-09-15 17:22 3% ` [Devel] [PATCH VZ10 v9 2/8] ve/fs: Rework per-ve mount count Vladimir Riabchun
@ 2026-09-18 13:22 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-18 13:22 UTC (permalink / raw)
To: devel
Sashiko review:
On 9/15/26 19:22, Vladimir Riabchun wrote:
> Previous approach with current mounts counter had an issue:
> there was a gap between ve_mount_allowed check and ve_mount_nr_inc,
> which could allow CT to have more mounts than expected.
>
> Fix this by tracking the number of available mounts instead
> of current ones. This also makes resources accounting
> more consistent - we are using ***_avail_nr approach more.
>
> One more issue with inconsistent ve value is fixed:
> ve_mount_allowed always used ve from get_exec_env, but
> ve_mount_nr_inc operated with owner_ve.
> Now actual ve value is calculated in the beginning of alloc_vfsmnt.
>
> To avoid incorrect accounting when is_pseudosuper is changed,
> update avail_nr count without > 0 check if VE is ve0 or pseudosuper.
> This also simplifies ve_mount_put, since increment is
> now unconditional.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-135520
>
> Feature: per-ve failcounters
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> ---
> fs/namespace.c | 78 ++++++++++++++++++++++++++++------------------
> include/linux/ve.h | 2 +-
> kernel/ve/ve.c | 12 +++----
> 3 files changed, 54 insertions(+), 38 deletions(-)
>
> diff --git a/fs/namespace.c b/fs/namespace.c
> index 4adc1db84b4c..1d6e66cd7174 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -317,19 +317,22 @@ int mnt_get_count(struct mount *mnt)
> #endif
> }
>
> -static inline int ve_mount_allowed(void);
> -static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve);
> -static inline void ve_mount_nr_dec(struct mount *mnt);
> +static inline int ve_try_reserve_mount(struct ve_struct *ve);
> +static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve);
> static inline void ve_mount_reown(struct mount *mnt, struct ve_struct *ve);
>
> static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
> {
> struct mount *mnt;
> + struct ve_struct *ve = owner_ve;
>
> - if (!ve_mount_allowed()) {
> + if (!ve)
> + ve = get_exec_env();
> +
> + if (!ve_try_reserve_mount(ve)) {
> pr_warn_ratelimited(
> "CT#%s reached the limit on mounts.\n",
> - ve_name(get_exec_env()));
> + ve_name(ve));
> return NULL;
> }
>
> @@ -337,6 +340,14 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
> if (mnt) {
> int err;
>
> +#ifdef CONFIG_VE
> + /*
> + * Got ve reference in ve_try_reserve_mount, set mnt ve data
> + * here, so in case of error ve_mount_put sees correct info.
> + */
> + mnt->ve_owner = ve;
> +#endif
> +
> err = mnt_alloc_id(mnt);
> if (err)
> goto out_free_cache;
> @@ -371,7 +382,8 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
> INIT_LIST_HEAD(&mnt->mnt_umounting);
> INIT_HLIST_HEAD(&mnt->mnt_stuck_children);
> mnt->mnt.mnt_idmap = &nop_mnt_idmap;
> - ve_mount_nr_inc(mnt, owner_ve);
> + } else {
> + ve_mount_put(mnt, ve);
> }
> return mnt;
Sashiko says:
In the function alloc_vfsmnt, can the decrement in ve_try_reserve_mount be
invalid if the 'mnt_alloc_id' fails subsequently?
This logic might lead to an incorrect mount count when the ve or the mount fails.
No, it can't. Passing ve_try_reserve_mount means mnt_avail was updated, so on
error path we must unconditionally increment it back.
>
> @@ -382,6 +394,8 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
> out_free_id:
> mnt_free_id(mnt);
> out_free_cache:
> + /* Got ve reference in ve_try_reserve_mount */
Sashiko says:
The comment suggests that ve_mount_put sees a non-null mount in an error case,
however, in test scenarios, isn't it possible for the ve to be incorrect?
No. ve is always correct, mnt = NULL is handled correctly by ve_mount_put.
> + ve_mount_put(mnt, ve);
> kmem_cache_free(mnt_cache, mnt);
> return NULL;
> }
> @@ -751,7 +765,9 @@ int sb_prepare_remount_readonly(struct super_block *sb)
> static void free_vfsmnt(struct mount *mnt)
> {
> mnt_idmap_put(mnt_idmap(&mnt->mnt));
> - ve_mount_nr_dec(mnt);
> +#ifdef CONFIG_VE
> + ve_mount_put(mnt, mnt->ve_owner);
> +#endif
> kfree_const(mnt->mnt_devname);
> #ifdef CONFIG_SMP
> free_percpu(mnt->mnt_pcp);
> @@ -3214,7 +3230,7 @@ int ve_devmnt_process(struct ve_struct *ve, dev_t dev, void **data_pp, int remou
> if (devmnt->dev == dev) {
> err = ve_devmnt_check(data, devmnt->allowed_options);
> /*
> - * In case of @is_pseudouser set, ie restore procedure,
> + * In case of @is_pseudosuper set, ie restore procedure,
> * we don't check for allowed options filtering, since
> * restore mode is special.
> */
> @@ -3353,30 +3369,30 @@ int ve_devmnt_verify(struct ve_struct *ve, dev_t dev, char *opts, bool new_mount
> return err;
> }
>
> -static inline int ve_mount_allowed(void)
> -{
> - struct ve_struct *ve = get_exec_env();
> -
> - return ve_is_super(ve) || ve->is_pseudosuper ||
> - atomic_read(&ve->mnt_nr) < (int)sysctl_ve_mount_nr;
> -}
> -
> -static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve)
> +static inline int ve_try_reserve_mount(struct ve_struct *ve)
> {
> - if (!ve)
> - ve = get_exec_env();
> + int ret = ve_is_super(ve) || ve->is_pseudosuper;
> + /* Ignore limits in ve0 and pseudosuper cases, but still count. */
> + if (ret)
> + atomic_dec(&ve->mnt_avail_nr);
> + else
> + ret = atomic_dec_if_positive(&ve->mnt_avail_nr) >= 0;
Sashiko says:
Is there a chance that, in ve_try_reserve_mount, the return logic misleads the condition
when ve is either ve0 or pseudosuper, thus bypassing necessary checks?
This counting might allow unexpected mounts while avoiding constraints.
We want to ignore avail checks in ve0 and pseudosuper case.
>
> - mnt->ve_owner = get_ve(ve);
> - atomic_inc(&ve->mnt_nr);
> + if (ret)
> + get_ve(ve);
> + return ret;
> }
>
> -static inline void ve_mount_nr_dec(struct mount *mnt)
> +static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve)
> {
> - struct ve_struct *ve = mnt->ve_owner;
> -
> - atomic_dec(&ve->mnt_nr);
> + /*
> + * ve argument is needed to reuse this function in alloc_vfsmnt error path.
> + * Other users should pass mnt->ve_owner value.
> + */
> + atomic_inc(&ve->mnt_avail_nr);
Sashio says:
The unconditional increment in ve_mount_put may cause unintended changes in mnt_avail_nr
when ve conditions are different. Could this result in inaccurate resource tracking?
No, ve_mount_put is called only when ve_try_reserve_mount is 1. It means it decremented
avail counter.
> put_ve(ve);
> - mnt->ve_owner = NULL;
> + if (mnt)
> + mnt->ve_owner = NULL;
> }
>
> ...
--
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* Re: [Devel] [PATCH vz10 v2 2/2] selftests: ve: introduce vtty pair teardown stress test
2026-09-16 12:31 5% ` [Devel] [PATCH vz10 v2 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
@ 2026-09-18 7:11 0% ` Vasileios Almpanis
2026-09-22 17:47 3% ` Konstantin Khorenko
1 sibling, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-09-18 7:11 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
> A vtty pair is created by the first master open and torn down by the
> last close. The close racing the peer's close used to let both sides
> decide they were the final one and call release_tty() twice, and a
> master opening again while a teardown was in flight used to hand out a
> reference to a tty_struct about to be freed.
>
> Hammer both ends. The master comes from the vzctl ioctl, which does
> not look the VE up. The slave is only reachable from inside the VE, as
> TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
> vttys index minor - 1, so the test builds a VE with cgroup2, sets
> ve.veid to the cgroup it made, and runs the slave side there.
>
> One of the symtoms of a buggy kernel is dmesg warning, not just an
> eventual crash, so the test checks that. A crashed node should also
> report test failure in CI, but that's less verbose, hence the check.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-144771
>
> Feature: ve selftest
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
> index afa4c568c2c9..9d8d86093827 100644
> --- a/tools/testing/selftests/ve/.gitignore
> +++ b/tools/testing/selftests/ve/.gitignore
> @@ -1,2 +1,3 @@
> ve_ns_owner_test
> ve_perms_test
> +ve_vtty_stress
> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
> index ec40cbc7b3a1..6cd5823c4d93 100644
> --- a/tools/testing/selftests/ve/Makefile
> +++ b/tools/testing/selftests/ve/Makefile
> @@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
>
> TEST_GEN_PROGS += ve_ns_owner_test
> TEST_GEN_PROGS += ve_perms_test
> +TEST_GEN_PROGS += ve_vtty_stress
>
> include ../lib.mk
> diff --git a/tools/testing/selftests/ve/settings b/tools/testing/selftests/ve/settings
> new file mode 100644
> index 000000000000..e2206265f67c
> --- /dev/null
> +++ b/tools/testing/selftests/ve/settings
> @@ -0,0 +1 @@
> +timeout=900
> diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
> new file mode 100644
> index 000000000000..57edbdeef0c8
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_vtty_stress.c
> @@ -0,0 +1,250 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * ve_vtty_stress - close a vtty pair from both ends at once.
> + *
> + * A vtty pair is created by the first master open and torn down by the
> + * last close. A close racing the peer's close used to let both sides
> + * decide they were the final one and call release_tty() twice, and a
> + * master opened again while a teardown was still in flight used to hand
> + * out a reference to a tty_struct that was about to be freed. Either
> + * way the next close walked freed memory.
> + *
> + * The master comes from the vzctl ioctl, which does not look the VE up.
> + * The slave is only reachable from inside the VE, as TTY_MAJOR minor
> + * 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to vttys index
> + * minor - 1. So the test makes a VE with cgroup2, hammers the master
> + * from the host and the slave from a task inside it, and fails if the
> + * kernel logs a complaint about the teardown.
> + *
> + * Surviving is not a pass on its own: the first symptom is a warning,
> + * which stops nothing by itself.
> + *
> + * Usage: ve_vtty_stress [seconds] (default 600, or $VE_VTTY_SECS)
> + */
> +#define _GNU_SOURCE
> +#include <sched.h>
> +#include <stdbool.h>
> +#include <sys/ioctl.h>
> +#include <linux/major.h>
> +#include <sys/sysmacros.h>
> +#include <sys/wait.h>
> +#include <time.h>
> +
> +#include "ve_selftest.h"
> +#include "../kselftest.h"
> +
> +/* The ioctl argument; the trailing data[] the uapi struct has is unused. */
> +struct vzctl_ve_configure {
> + unsigned int veid, key, val, size;
> +};
> +
> +#define VZCTL_VE_CONFIGURE _IOW('.', 15, struct vzctl_ve_configure)
> +#define VE_CONFIGURE_OPEN_TTY 5
> +
> +#define VTTY_IDX 0 /* the pair both ends work on */
> +#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
> +#define DEFAULT_SECS 600
> +
> +static int open_master(int ctl_fd, unsigned int veid)
> +{
> + struct vzctl_ve_configure c = {
> + .veid = veid,
> + .key = VE_CONFIGURE_OPEN_TTY,
> + .val = VTTY_IDX,
> + };
> +
> + return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
> +}
> +
> +/*
> + * The slave end, inside the VE. Every error here is one the race is
> + * expected to produce: the pair is gone once the master has closed it.
> + */
> +static int hammer_slave(time_t deadline, unsigned long *opened)
> +{
> + char node[] = "/tmp/ve_vtty_slave";
> +
> + unlink(node);
> + if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0)
> + return -1;
> +
> + while (time(NULL) < deadline) {
> + int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
> +
> + if (fd >= 0) {
> + (*opened)++;
> + close(fd);
> + }
> + }
> + unlink(node);
> + return 0;
> +}
> +
> +/* The kernel log, since a warning fails nothing by itself. */
> +static int kmsg_open(void)
> +{
> + int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
> +
> + if (fd >= 0)
> + lseek(fd, 0, SEEK_END);
> + return fd;
> +}
> +
> +static int kmsg_check(int fd)
> +{
> + static const char *const ours[] = {
> + "tty_release", "release_tty", "check_tty_count",
> + "vtty_open_master", "vtty_shutdown",
> + };
> + char buf[8192];
> + int found = 0;
> + ssize_t r;
> + size_t i;
> +
> + while (fd >= 0 && (r = read(fd, buf, sizeof(buf) - 1)) > 0) {
> + buf[r] = '\0';
> + if (!strstr(buf, "WARNING") && !strstr(buf, "BUG") &&
> + !strstr(buf, "Oops") && !strstr(buf, "general protection"))
> + continue;
> + for (i = 0; i < ARRAY_SIZE(ours); i++)
> + if (strstr(buf, ours[i])) {
> + ksft_print_msg("kernel log: %s", buf);
> + found++;
> + break;
> + }
> + }
> + return found;
> +}
> +
> +int main(int argc, char **argv)
> +{
> + unsigned long master_opened = 0, slave_opened = 0;
> + int cg, ctid, ctl, kmsg, ready[2], count[2], status;
> + const char *env = getenv("VE_VTTY_SECS");
> + char path[PATH_MAX], val[16], c;
> + time_t deadline;
> + long secs;
> + pid_t pid;
> +
> + ksft_print_header();
> + secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
> + if (secs <= 0)
> + secs = DEFAULT_SECS;
> +
> + ctl = open("/dev/vzctl", O_RDWR);
> + if (ctl < 0)
> + ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
> + "(vzdev, vzmon) are needed\n",
> + strerror(errno));
> +
> + cg = mount_cg2_fd();
> + if (cg < 0)
> + ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
> + write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS);
> +
> + ctid = make_ve(cg, CTID_MIN);
> + if (ctid < 0)
> + ksft_exit_skip("cannot make a VE cgroup\n");
> +
> + /*
> + * The pair is filed under the veid of the VE, so the two ends only
> + * meet if the cgroup carries the one the master is opened for.
> + */
> + snprintf(path, sizeof(path), "%d/ve.veid", ctid);
> + snprintf(val, sizeof(val), "%d", ctid);
> + if (write_file_at(cg, path, val) < 0) {
> + destroy_ve(cg, ctid);
> + ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
> + }
> +
> + /* Do not report a failure where the vtty pair cannot be had at all. */
> + status = open_master(ctl, ctid);
> + if (status < 0 && errno != EBUSY) {
> + destroy_ve(cg, ctid);
> + ksft_exit_skip("no vtty master: %s\n", strerror(errno));
> + }
> + if (status >= 0)
> + close(status);
> +
> + ksft_set_plan(1);
> + kmsg = kmsg_open();
> + if (kmsg < 0)
> + ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
> + "be noticed\n",
> + strerror(errno));
> +
> + if (pipe(ready) || pipe(count))
> + ksft_exit_fail_msg("pipe: %s\n", strerror(errno));
> +
> + deadline = time(NULL) + secs;
> + ksft_print_msg("VE %d, pair %d, %ld seconds\n", ctid, VTTY_IDX, secs);
> +
> + pid = fork();
> + if (pid == 0) {
> + close(ready[0]);
> + close(count[0]);
> + if (enter_cgroup(cg, ctid) < 0)
> + _exit(1);
> + /*
> + * CLONE_NEWVE puts the task in the VE the cgroup names, so
> + * the slave lookup finds the pair. The pid namespace is
> + * what a VE wants of its first task.
> + */
> + if (unshare(CLONE_NEWVE | CLONE_NEWPID | CLONE_NEWNS) < 0)
> + _exit(1);
> + if (fork() == 0) {
> + unsigned long n = 0;
> +
> + c = 'r';
> + if (write(ready[1], &c, 1) != 1)
> + _exit(1);
> + if (hammer_slave(deadline, &n) < 0)
> + _exit(1);
> + if (write(count[1], &n, sizeof(n)) != sizeof(n))
> + _exit(1);
> + _exit(0);
> + }
> + wait(&status);
> + _exit(WIFEXITED(status) ? WEXITSTATUS(status) : 1);
> + } else if (pid < 0) {
> + ksft_exit_fail_msg("fork: %s\n", strerror(errno));
> + }
> + close(ready[1]);
> + close(count[1]);
> +
> + if (read(ready[0], &c, 1) != 1) {
> + waitpid(pid, &status, 0);
> + destroy_ve(cg, ctid);
> + ksft_exit_fail_msg("the VE side never came up\n");
> + }
> +
> + while (time(NULL) < deadline) {
> + int fd = open_master(ctl, ctid);
> +
> + if (fd >= 0) {
> + master_opened++;
> + close(fd);
> + }
> + }
> +
> + if (read(count[0], &slave_opened, sizeof(slave_opened)) !=
> + sizeof(slave_opened))
> + slave_opened = 0;
> + waitpid(pid, &status, 0);
> +
> + ksft_print_msg("%lu master opens, %lu slave opens\n", master_opened,
> + slave_opened);
> +
> + status = kmsg_check(kmsg);
> + destroy_ve(cg, ctid);
> +
> + if (!master_opened)
> + ksft_exit_fail_msg("no master was ever opened\n");
> + if (!slave_opened)
> + ksft_exit_fail_msg("no slave was ever opened\n");
> + if (status)
> + ksft_exit_fail_msg("kernel complained %d time(s)\n", status);
> +
> + ksft_test_result_pass("vtty pair closed from both ends\n");
> + ksft_finished();
> +}
Reviewed-by: Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
--
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: [PATCH hci-8.0 5/5] qcow2: repair a dirty image when it becomes writable #VSTOR-140073
2026-09-16 16:26 5% ` Andrey Drobyshev
@ 2026-09-16 16:28 0% ` Denis V. Lunev
0 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-16 16:28 UTC (permalink / raw)
To: Andrey Drobyshev, Denis V. Lunev, svt-core
pls answer to mainstream first properly, I'll send there and here
Den
On 9/16/26 18:26, Andrey Drobyshev wrote:
> Other commits appear to be fine, but this commit has several issues:
>
> - Major: a failed repair makes a readable image unreadable.
> state->bs->drv = NULL at block/qcow2.c:2251 blocks reads as well as
> writes. The base of a running chain, or the active layer in the
> stream_start() case, answers every guest read with ENOMEDIUM from then
> on, for an image whose L1/L2 metadata is intact and was serving reads a
> moment earlier. Test 039's own expected output shows it: read failed: No
> medium found. Only allocation has to be stopped. A per-node gate in
> BDRVQcow2State that fails pwritev/pwrite_zeroes/pdiscard/truncate with
> -EIO (and skips qcow2_mark_clean() in qcow2_inactivate() so the dirty
> bit survives close) keeps the guest running and the job simply fails.
> The current approach also makes query-named-block-nodes/query-block fail
> wholesale while the node exists, and in the commit case the node cannot
> be deleted until the VM is restarted.
>
> - Major: use-after-free through the cache-clean coroutine. With
> bs->drv == NULL, bdrv_close() (block.c:5259) never calls qcow2_close(),
> so cache_clean_timer_del_and_wait() never runs, and bdrv_close() then
> frees bs->opaque. The coroutine at block/qcow2.c:895 is still sleeping
> on s->cache_clean_timer_wake (600 s default on Linux) and wakes on freed
> memory. Pre-existing for qcow2_signal_corruption(fatal), but this patch
> adds a second path and is the right place to stop the coroutine before
> dropping the driver. The same skipped close also leaks the L1 table,
> refcount table, both caches, crypto and bitmap state.
>
> - Major: the check runs under bdrv_drain_all_begin().
> bdrv_reopen_queue() drains every node (block.c:4593), and bdrv_check()
> polls from the main loop. A full refcount scan plus rebuild of a large
> image stalls all guest I/O on every disk and the monitor for its whole
> duration, as a side effect of
> block-commit/block-stream/change-backing-file. The QAPI text discloses
> it, but the cover letter should quantify it and argue why this beats
> failing the command.
>
> - Medium (downstream only): the new condition ignores s->keep_dirty,
> unlike qcow2_do_open() at block/qcow2.c:2025. A node opened with
> __vz_keep-dirty=on, then reopened read-write, gets checked and its dirty
> bit cleared, breaking that option's contract. Add !s->keep_dirty in the
> hci branch.
>
> - Low: after the failure the node keeps BDRV_O_RDWR, BLK_PERM_WRITE on
> bs->file and the write lock, so the qemu-img check -r the hint
> recommends is impossible until the node is gone, which for a commit base
> means VM shutdown. Say so in the hint or the docs.
>
> - Low: test 040 asserts exact error strings owned by other code ("Node
> 'base' is busy: node is used as backing hd of 'mid'", "Block device base
> is ejected"), which makes it brittle.
>
> On 9/16/26 6:24 PM, Denis V. Lunev wrote:
>> From: Denis V. Lunev <den@openvz.org>
>>
>> A dirty image must be repaired before anything allocates a cluster in
>> it. qcow2_do_open() does that, but only for a node that is writable
>> from the start. A node opened read-only skips it, and nothing revisits
>> the question once that node becomes writable, which block-commit does
>> routinely: commit_active_start() and commit_start() reopen the base
>> read-write for the duration of the job.
>>
>> With lazy refcounts the on-disk refcount block then still accounts for
>> the metadata clusters only, so the allocator restarts at the front of
>> the image and hands out clusters that L2 entries point at. Two guest
>> offsets end up sharing one host cluster. Nothing fails, the corrupt bit
>> stays clear, and a clean close clears the dirty bit, so no later open
>> repairs the image either. The bit also stays set for the whole writable
>> session, so a node which is merely writable says nothing.
>>
>> Refusing the reopen instead is simpler and keeps it atomic, but it
>> leaves nowhere to go: the base belongs to a chain the VM has open, so
>> the qemu-img check -r such an error would ask for cannot take the write
>> lock it needs. The repair does the trick in most cases anyway.
>>
>> Do the repair in qcow2_reopen_commit_post(), the earliest point where
>> the node is writable. An inactive node is skipped: bdrv_activate() calls
>> qcow2_do_open() again through qcow2_co_invalidate_cache().
>>
>> commit_post cannot reject the reopen, so a failed repair takes the
>> driver away from the node instead, which is what stops writes from
>> aliasing live clusters. qcow2_signal_corruption() does that as well,
>> but it also sends BLOCK_IMAGE_CORRUPTED and sets the corrupt bit,
>> which qcow2_do_open() honours by refusing every later read-write open.
>> The image is dirty and unrepaired, not corrupt, and qemu-img check -r
>> still fixes it, so neither belongs here. Return the error and skip
>> the bitmaps.
>>
>> Signed-off-by: Denis V. Lunev <den@openvz.org>
>> Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
>> CC: Kevin Wolf <kwolf@redhat.com>
>> CC: Hanna Reitz <hreitz@redhat.com>
>> CC: Eric Blake <eblake@redhat.com>
>> CC: Markus Armbruster <armbru@redhat.com>
>> CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
>> Cc: qemu-stable@nongnu.org
>> ---
>> block/qcow2.c | 21 +++++++
>> qapi/block-core.json | 19 ++++++
>> tests/qemu-iotests/039 | 60 ++++++++++++++++++
>> tests/qemu-iotests/039.out | 36 +++++++++++
>> tests/qemu-iotests/040 | 123 +++++++++++++++++++++++++++++++++++++
>> tests/qemu-iotests/040.out | 4 +-
>> 6 files changed, 261 insertions(+), 2 deletions(-)
>>
>> diff --git a/block/qcow2.c b/block/qcow2.c
>> index ddad39a3084..be346f0339b 100644
>> --- a/block/qcow2.c
>> +++ b/block/qcow2.c
>> @@ -2235,8 +2235,29 @@ static void qcow2_reopen_commit(BDRVReopenState *state)
>>
>> static int qcow2_reopen_commit_post(BDRVReopenState *state, Error **errp)
>> {
>> + ERRP_GUARD();
>> + BDRVQcow2State *s = state->bs->opaque;
>> +
>> GRAPH_RDLOCK_GUARD_MAINLOOP();
>>
>> + if (!bdrv_reopen_was_writable(state) && bdrv_is_writable(state->bs) &&
>> + (s->incompatible_features & QCOW2_INCOMPAT_DIRTY)) {
>> + BdrvCheckResult result = {0};
>> + int ret;
>> +
>> + ret = bdrv_check(state->bs, &result, BDRV_FIX_ERRORS | BDRV_FIX_LEAKS);
>> + if (ret < 0 || result.check_errors || !state->bs->drv) {
>> + ret = ret < 0 ? ret : -EIO;
>> + /* No write may reach an image whose refcounts are unaccounted */
>> + state->bs->drv = NULL;
>> + error_setg_errno(errp, -ret, "Could not repair dirty image '%s'",
>> + bdrv_get_device_or_node_name(state->bs));
>> + error_append_hint(errp, "The image is left dirty and this node "
>> + "holds it open until the node is removed\n");
>> + return ret;
>> + }
>> + }
>> +
>> if (state->flags & BDRV_O_RDWR) {
>> Error *local_err = NULL;
>>
>> diff --git a/qapi/block-core.json b/qapi/block-core.json
>> index c036a3a8858..6fb8c31210a 100644
>> --- a/qapi/block-core.json
>> +++ b/qapi/block-core.json
>> @@ -1843,6 +1843,9 @@
>> # r/w -> r/o, if needed). The new backing file string is written into
>> # the image file metadata, and the QEMU internal strings are updated.
>> #
>> +# A dirty qcow2 image is repaired during that reopen, which blocks
>> +# other requests and can fail the command.
>> +#
>> # @image-node-name: The name of the block driver state node of the
>> # image to modify. The "device" argument is used to verify
>> # "image-node-name" is in the chain described by "device".
>> @@ -1882,6 +1885,9 @@
>> # size to match the size of the smaller top, you can safely truncate
>> # it yourself once the commit operation successfully completes.
>> #
>> +# The base is opened read-write. A dirty qcow2 base is repaired
>> +# first, which blocks other requests and can fail the command.
>> +#
>> # @job-id: identifier for the newly-created block job. If omitted,
>> # the device name will be used. (Since 2.7)
>> #
>> @@ -2919,6 +2925,9 @@
>> # On successful completion the image file is updated to drop the
>> # backing file and the `BLOCK_JOB_COMPLETED` event is emitted.
>> #
>> +# The top image is opened read-write. A dirty qcow2 image is repaired
>> +# first, which blocks other requests and can fail the command.
>> +#
>> # In case @device is a filter node, `block-stream` modifies the first
>> # non-filter overlay node below it to point to the new backing node
>> # instead of modifying @device itself.
>> @@ -5070,6 +5079,16 @@
>> # transaction, so if one of them fails then the whole transaction is
>> # cancelled.
>> #
>> +# An error is also returned when a device cannot be used once it has
>> +# been reopened. Such a reopen is not undone, so an error does not
>> +# always mean that nothing has changed. A node the driver gave up on
>> +# serves nothing at all: it keeps its image open, makes
>> +# `query-named-block-nodes` fail for as long as it is in the graph,
>> +# and `blockdev-del` removes it only once nothing refers to it.
>> +#
>> +# Reopening a dirty qcow2 image read-write repairs it first, which
>> +# blocks other requests and can fail the command.
>> +#
>> # The command receives a list of block devices to reopen. For each
>> # one of them, the top-level @node-name option (from `BlockdevOptions`)
>> # must be specified and is used to select the block device to be
>> diff --git a/tests/qemu-iotests/039 b/tests/qemu-iotests/039
>> index 3d0c073d65d..3f37c36ca86 100755
>> --- a/tests/qemu-iotests/039
>> +++ b/tests/qemu-iotests/039
>> @@ -33,6 +33,7 @@ status=1 # failure is the default!
>> _cleanup()
>> {
>> _cleanup_test_img
>> + rm -f "$TEST_DIR/blkdebug.conf"
>> }
>> trap "_cleanup; exit \$status" 0 1 2 3 15
>>
>> @@ -176,6 +177,65 @@ $QEMU_IO -c "write 0 512" "$TEST_IMG" | _filter_qemu_io
>> # The dirty bit must not be set
>> _qcow2_dump_header | grep incompatible_features
>>
>> +echo
>> +echo "== Reopening a dirty image read/write should repair it =="
>> +
>> +_make_test_img -o "compat=1.1,lazy_refcounts=on" $size
>> +
>> +_NO_VALGRIND \
>> +$QEMU_IO -c "write -P 0x5a 0 512" \
>> + -c "sigraise $(kill -l KILL)" "$TEST_IMG" 2>&1 \
>> + | _filter_qemu_io
>> +
>> +# The dirty bit must be set
>> +_qcow2_dump_header | grep incompatible_features
>> +
>> +# Without the repair this write would alias the cluster at offset 0
>> +$QEMU_IO -r -c "reopen -w" \
>> + -c "write -P 0xb1 1M 512" \
>> + -c "read -P 0x5a 0 512" "$TEST_IMG" | _filter_qemu_io
>> +
>> +_check_test_img
>> +
>> +echo
>> +echo "== A read/write reopen must not check the image =="
>> +
>> +_make_test_img -o "compat=1.1,lazy_refcounts=on" $size
>> +
>> +_NO_VALGRIND \
>> +$QEMU_IO -c "write -P 0x5a 0 512" \
>> + -c "reopen -o l2-cache-size=1M" \
>> + -c "sigraise $(kill -l KILL)" "$TEST_IMG" 2>&1 \
>> + | _filter_qemu_io
>> +
>> +# The dirty bit must still be set, it belongs to the running session
>> +_qcow2_dump_header | grep incompatible_features
>> +
>> +echo
>> +echo "== A failed repair must fail the reopen =="
>> +
>> +_make_test_img -o "compat=1.1,lazy_refcounts=on" $size
>> +
>> +_NO_VALGRIND \
>> +$QEMU_IO -c "write -P 0x5a 0 512" \
>> + -c "sigraise $(kill -l KILL)" "$TEST_IMG" 2>&1 \
>> + | _filter_qemu_io
>> +
>> +cat > "$TEST_DIR/blkdebug.conf" <<EOF
>> +[inject-error]
>> +event = "none"
>> +iotype = "write"
>> +errno = "5"
>> +EOF
>> +
>> +# The repair cannot write, so the reopen itself must report the failure
>> +$QEMU_IO -r -c "reopen -w" -c "read -P 0x5a 0 512" \
>> + "blkdebug:$TEST_DIR/blkdebug.conf:$TEST_IMG" 2>&1 \
>> + | _filter_testdir | _filter_qemu_io | _filter_generated_node_ids
>> +
>> +# The corrupt bit needs a write of its own, so the image is only left dirty
>> +_qcow2_dump_header | grep incompatible_features
>> +
>> echo
>> echo "== Creating an image file with lazy_refcounts=off =="
>>
>> diff --git a/tests/qemu-iotests/039.out b/tests/qemu-iotests/039.out
>> index ce8ee577216..cc6ca3ab95b 100644
>> --- a/tests/qemu-iotests/039.out
>> +++ b/tests/qemu-iotests/039.out
>> @@ -79,6 +79,42 @@ wrote 512/512 bytes at offset 0
>> 512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
>> incompatible_features []
>>
>> +== Reopening a dirty image read/write should repair it ==
>> +Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=134217728
>> +wrote 512/512 bytes at offset 0
>> +512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
>> +./common.rc: Killed ( VALGRIND_QEMU="${VALGRIND_QEMU_IO}" _qemu_proc_exec "${VALGRIND_LOGFILE}" "$QEMU_IO_PROG" $QEMU_IO_ARGS "$@" )
>> +incompatible_features [0]
>> +ERROR cluster 5 refcount=0 reference=1
>> +Rebuilding refcount structure
>> +Repairing cluster 1 refcount=1 reference=0
>> +Repairing cluster 2 refcount=1 reference=0
>> +wrote 512/512 bytes at offset 1048576
>> +512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
>> +read 512/512 bytes at offset 0
>> +512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
>> +No errors were found on the image.
>> +
>> +== A read/write reopen must not check the image ==
>> +Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=134217728
>> +wrote 512/512 bytes at offset 0
>> +512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
>> +./common.rc: Killed ( VALGRIND_QEMU="${VALGRIND_QEMU_IO}" _qemu_proc_exec "${VALGRIND_LOGFILE}" "$QEMU_IO_PROG" $QEMU_IO_ARGS "$@" )
>> +incompatible_features [0]
>> +
>> +== A failed repair must fail the reopen ==
>> +Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=134217728
>> +wrote 512/512 bytes at offset 0
>> +512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
>> +./common.rc: Killed ( VALGRIND_QEMU="${VALGRIND_QEMU_IO}" _qemu_proc_exec "${VALGRIND_LOGFILE}" "$QEMU_IO_PROG" $QEMU_IO_ARGS "$@" )
>> +ERROR cluster 5 refcount=0 reference=1
>> +Rebuilding refcount structure
>> +qemu-io: ERROR writing refblock: Input/output error
>> +qemu-io: Could not repair dirty image 'NODE_NAME': Input/output error
>> +The image is left dirty and this node holds it open until the node is removed
>> +read failed: No medium found
>> +incompatible_features [0]
>> +
>> == Creating an image file with lazy_refcounts=off ==
>> Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=134217728
>> wrote 512/512 bytes at offset 0
>> diff --git a/tests/qemu-iotests/040 b/tests/qemu-iotests/040
>> index 5c18e413ec9..b25c76183a1 100755
>> --- a/tests/qemu-iotests/040
>> +++ b/tests/qemu-iotests/040
>> @@ -951,6 +951,129 @@ class TestCommitWithOverriddenBacking(iotests.QMPTestCase):
>> self.vm.qmp('block-job-complete', device='commit')
>> self.vm.event_wait('BLOCK_JOB_COMPLETED')
>>
>> +QCOW2_INCOMPAT_FEATURES_OFFSET = 72
>> +QCOW2_INCOMPAT_DIRTY = 1 << 0
>> +
>> +image_size = 4 * 1024 * 1024
>> +dirty_base = os.path.join(iotests.test_dir, 'dirty-base.img')
>> +mid = os.path.join(iotests.test_dir, 'dirty-mid.img')
>> +top = os.path.join(iotests.test_dir, 'dirty-top.img')
>> +
>> +
>> +class TestCommitDirtyBase(iotests.QMPTestCase):
>> + def setUp(self) -> None:
>> + if iotests.imgfmt != 'qcow2':
>> + self.case_skip('the dirty bit is a qcow2 feature')
>> + iotests.qemu_img_create('-f', iotests.imgfmt, '-o',
>> + 'compat=1.1,lazy_refcounts=on', dirty_base,
>> + str(image_size))
>> + # Killing the process leaves the refcounts of the written cluster stale
>> + iotests.qemu_io_popen('-t', 'writethrough',
>> + '-c', 'write -P 0x5a 0 512',
>> + '-c', 'sigraise 9', dirty_base).communicate()
>> + iotests.qemu_img_create('-f', iotests.imgfmt, '-b', dirty_base,
>> + '-F', iotests.imgfmt, mid)
>> + iotests.qemu_img_create('-f', iotests.imgfmt, '-b', mid,
>> + '-F', iotests.imgfmt, top)
>> + # The commit has to allocate for this, which is where the stale
>> + # refcounts hand out the cluster holding the data written above
>> + qemu_io('-c', 'write -P 0xb1 1M 512', mid)
>> +
>> + self.vm = iotests.VM()
>> + self.vm.launch()
>> + self.vm.cmd('blockdev-add', driver='file', filename=dirty_base,
>> + node_name='base-file')
>> +
>> + self.assertTrue(self.is_dirty())
>> +
>> + def tearDown(self) -> None:
>> + if self.vm.is_running():
>> + self.vm.shutdown()
>> + for image in (dirty_base, mid, top):
>> + os.remove(image)
>> +
>> + def add_chain(self, base_file: str) -> None:
>> + self.vm.cmd('blockdev-add', driver=iotests.imgfmt, file=base_file,
>> + node_name='base', read_only=True)
>> + self.vm.cmd('blockdev-add', driver='file', filename=mid,
>> + node_name='mid-file')
>> + self.vm.cmd('blockdev-add', driver=iotests.imgfmt, file='mid-file',
>> + node_name='mid', backing='base')
>> + self.vm.cmd('blockdev-add', driver='file', filename=top,
>> + node_name='top-file')
>> + self.vm.cmd('blockdev-add', driver=iotests.imgfmt, file='top-file',
>> + node_name='top', backing='mid')
>> +
>> + def check_base(self) -> None:
>> + result = iotests.qemu_img_check(dirty_base)
>> + self.assertEqual(result['check-errors'], 0)
>> + self.assertEqual(result.get('corruptions', 0), 0)
>> + # Without the repair the commit would have aliased this cluster
>> + qemu_io('-c', 'read -P 0x5a 0 512', '-c', 'read -P 0xb1 1M 512',
>> + dirty_base)
>> +
>> + def is_dirty(self) -> bool:
>> + with open(dirty_base, 'rb') as img:
>> + img.seek(QCOW2_INCOMPAT_FEATURES_OFFSET)
>> + features = struct.unpack('>Q', img.read(8))[0]
>> + return bool(features & QCOW2_INCOMPAT_DIRTY)
>> +
>> + def test_commit_repairs_base(self) -> None:
>> + self.add_chain('base-file')
>> +
>> + self.vm.cmd('block-commit', job_id='job0', device='top',
>> + top_node='mid', base_node='base')
>> + self.wait_until_completed(drive='job0')
>> +
>> + self.vm.shutdown()
>> + self.assertFalse(self.is_dirty())
>> + self.check_base()
>> +
>> + def test_active_commit_repairs_base(self) -> None:
>> + self.add_chain('base-file')
>> +
>> + # Without top-node the whole chain commits, through
>> + # commit_active_start() rather than commit_start()
>> + self.vm.cmd('block-commit', job_id='job0', device='top',
>> + base_node='base')
>> + self.complete_and_wait(drive='job0')
>> +
>> + self.vm.shutdown()
>> + self.assertFalse(self.is_dirty())
>> + self.check_base()
>> +
>> + def test_failed_repair_fails_the_commit(self) -> None:
>> + self.vm.cmd('blockdev-add', driver='blkdebug', image='base-file',
>> + node_name='base-blkdebug',
>> + inject_error=[{'event': 'none', 'iotype': 'write',
>> + 'errno': 5}])
>> + self.add_chain('base-blkdebug')
>> +
>> + result = self.vm.qmp('block-commit', job_id='job0', device='top',
>> + top_node='mid', base_node='base')
>> + self.assert_qmp(result, 'error/class', 'GenericError')
>> + self.assertIn("Could not repair dirty image 'base'",
>> + result['error']['desc'])
>> +
>> + # The base is left in the graph, and nothing can be queried while
>> + # it is there
>> + result = self.vm.qmp('query-named-block-nodes', flat=True)
>> + self.assert_qmp(result, 'error/desc', 'Block device base is ejected')
>> +
>> + # It only goes away once nothing refers to it
>> + result = self.vm.qmp('blockdev-del', node_name='base')
>> + self.assert_qmp(result, 'error/desc',
>> + "Node 'base' is busy: node is used as backing hd of "
>> + "'mid'")
>> +
>> + # Marking the image corrupt needs a write of its own, which fails too
>> + self.assertTrue(self.is_dirty())
>> +
>> + # Nothing can use the base any more, and that is what is reported
>> + result = self.vm.qmp('block-commit', job_id='job1', device='top',
>> + top_node='mid', base_node='base')
>> + self.assert_qmp(result, 'error/desc', 'Device has no medium')
>> +
>> if __name__ == '__main__':
>> iotests.main(supported_fmts=['qcow2', 'qed'],
>> supported_protocols=['file'])
>> diff --git a/tests/qemu-iotests/040.out b/tests/qemu-iotests/040.out
>> index 1bb1dc5f0ea..f3cbf73a01d 100644
>> --- a/tests/qemu-iotests/040.out
>> +++ b/tests/qemu-iotests/040.out
>> @@ -1,5 +1,5 @@
>> -.................................................................
>> +....................................................................
>> ----------------------------------------------------------------------
>> -Ran 65 tests
>> +Ran 68 tests
>>
>> OK
^ permalink raw reply [relevance 0%]
* Re: [PATCH hci-8.0 5/5] qcow2: repair a dirty image when it becomes writable #VSTOR-140073
2026-09-16 15:24 3% ` [PATCH hci-8.0 5/5] qcow2: repair a dirty image when it becomes writable #VSTOR-140073 Denis V. Lunev
@ 2026-09-16 16:26 5% ` Andrey Drobyshev
2026-09-16 16:28 0% ` Denis V. Lunev
0 siblings, 1 reply; 200+ results
From: Andrey Drobyshev @ 2026-09-16 16:26 UTC (permalink / raw)
To: Denis V. Lunev, svt-core
Other commits appear to be fine, but this commit has several issues:
- Major: a failed repair makes a readable image unreadable.
state->bs->drv = NULL at block/qcow2.c:2251 blocks reads as well as
writes. The base of a running chain, or the active layer in the
stream_start() case, answers every guest read with ENOMEDIUM from then
on, for an image whose L1/L2 metadata is intact and was serving reads a
moment earlier. Test 039's own expected output shows it: read failed: No
medium found. Only allocation has to be stopped. A per-node gate in
BDRVQcow2State that fails pwritev/pwrite_zeroes/pdiscard/truncate with
-EIO (and skips qcow2_mark_clean() in qcow2_inactivate() so the dirty
bit survives close) keeps the guest running and the job simply fails.
The current approach also makes query-named-block-nodes/query-block fail
wholesale while the node exists, and in the commit case the node cannot
be deleted until the VM is restarted.
- Major: use-after-free through the cache-clean coroutine. With
bs->drv == NULL, bdrv_close() (block.c:5259) never calls qcow2_close(),
so cache_clean_timer_del_and_wait() never runs, and bdrv_close() then
frees bs->opaque. The coroutine at block/qcow2.c:895 is still sleeping
on s->cache_clean_timer_wake (600 s default on Linux) and wakes on freed
memory. Pre-existing for qcow2_signal_corruption(fatal), but this patch
adds a second path and is the right place to stop the coroutine before
dropping the driver. The same skipped close also leaks the L1 table,
refcount table, both caches, crypto and bitmap state.
- Major: the check runs under bdrv_drain_all_begin().
bdrv_reopen_queue() drains every node (block.c:4593), and bdrv_check()
polls from the main loop. A full refcount scan plus rebuild of a large
image stalls all guest I/O on every disk and the monitor for its whole
duration, as a side effect of
block-commit/block-stream/change-backing-file. The QAPI text discloses
it, but the cover letter should quantify it and argue why this beats
failing the command.
- Medium (downstream only): the new condition ignores s->keep_dirty,
unlike qcow2_do_open() at block/qcow2.c:2025. A node opened with
__vz_keep-dirty=on, then reopened read-write, gets checked and its dirty
bit cleared, breaking that option's contract. Add !s->keep_dirty in the
hci branch.
- Low: after the failure the node keeps BDRV_O_RDWR, BLK_PERM_WRITE on
bs->file and the write lock, so the qemu-img check -r the hint
recommends is impossible until the node is gone, which for a commit base
means VM shutdown. Say so in the hint or the docs.
- Low: test 040 asserts exact error strings owned by other code ("Node
'base' is busy: node is used as backing hd of 'mid'", "Block device base
is ejected"), which makes it brittle.
On 9/16/26 6:24 PM, Denis V. Lunev wrote:
> From: Denis V. Lunev <den@openvz.org>
>
> A dirty image must be repaired before anything allocates a cluster in
> it. qcow2_do_open() does that, but only for a node that is writable
> from the start. A node opened read-only skips it, and nothing revisits
> the question once that node becomes writable, which block-commit does
> routinely: commit_active_start() and commit_start() reopen the base
> read-write for the duration of the job.
>
> With lazy refcounts the on-disk refcount block then still accounts for
> the metadata clusters only, so the allocator restarts at the front of
> the image and hands out clusters that L2 entries point at. Two guest
> offsets end up sharing one host cluster. Nothing fails, the corrupt bit
> stays clear, and a clean close clears the dirty bit, so no later open
> repairs the image either. The bit also stays set for the whole writable
> session, so a node which is merely writable says nothing.
>
> Refusing the reopen instead is simpler and keeps it atomic, but it
> leaves nowhere to go: the base belongs to a chain the VM has open, so
> the qemu-img check -r such an error would ask for cannot take the write
> lock it needs. The repair does the trick in most cases anyway.
>
> Do the repair in qcow2_reopen_commit_post(), the earliest point where
> the node is writable. An inactive node is skipped: bdrv_activate() calls
> qcow2_do_open() again through qcow2_co_invalidate_cache().
>
> commit_post cannot reject the reopen, so a failed repair takes the
> driver away from the node instead, which is what stops writes from
> aliasing live clusters. qcow2_signal_corruption() does that as well,
> but it also sends BLOCK_IMAGE_CORRUPTED and sets the corrupt bit,
> which qcow2_do_open() honours by refusing every later read-write open.
> The image is dirty and unrepaired, not corrupt, and qemu-img check -r
> still fixes it, so neither belongs here. Return the error and skip
> the bitmaps.
>
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
> CC: Kevin Wolf <kwolf@redhat.com>
> CC: Hanna Reitz <hreitz@redhat.com>
> CC: Eric Blake <eblake@redhat.com>
> CC: Markus Armbruster <armbru@redhat.com>
> CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
> Cc: qemu-stable@nongnu.org
> ---
> block/qcow2.c | 21 +++++++
> qapi/block-core.json | 19 ++++++
> tests/qemu-iotests/039 | 60 ++++++++++++++++++
> tests/qemu-iotests/039.out | 36 +++++++++++
> tests/qemu-iotests/040 | 123 +++++++++++++++++++++++++++++++++++++
> tests/qemu-iotests/040.out | 4 +-
> 6 files changed, 261 insertions(+), 2 deletions(-)
>
> diff --git a/block/qcow2.c b/block/qcow2.c
> index ddad39a3084..be346f0339b 100644
> --- a/block/qcow2.c
> +++ b/block/qcow2.c
> @@ -2235,8 +2235,29 @@ static void qcow2_reopen_commit(BDRVReopenState *state)
>
> static int qcow2_reopen_commit_post(BDRVReopenState *state, Error **errp)
> {
> + ERRP_GUARD();
> + BDRVQcow2State *s = state->bs->opaque;
> +
> GRAPH_RDLOCK_GUARD_MAINLOOP();
>
> + if (!bdrv_reopen_was_writable(state) && bdrv_is_writable(state->bs) &&
> + (s->incompatible_features & QCOW2_INCOMPAT_DIRTY)) {
> + BdrvCheckResult result = {0};
> + int ret;
> +
> + ret = bdrv_check(state->bs, &result, BDRV_FIX_ERRORS | BDRV_FIX_LEAKS);
> + if (ret < 0 || result.check_errors || !state->bs->drv) {
> + ret = ret < 0 ? ret : -EIO;
> + /* No write may reach an image whose refcounts are unaccounted */
> + state->bs->drv = NULL;
> + error_setg_errno(errp, -ret, "Could not repair dirty image '%s'",
> + bdrv_get_device_or_node_name(state->bs));
> + error_append_hint(errp, "The image is left dirty and this node "
> + "holds it open until the node is removed\n");
> + return ret;
> + }
> + }
> +
> if (state->flags & BDRV_O_RDWR) {
> Error *local_err = NULL;
>
> diff --git a/qapi/block-core.json b/qapi/block-core.json
> index c036a3a8858..6fb8c31210a 100644
> --- a/qapi/block-core.json
> +++ b/qapi/block-core.json
> @@ -1843,6 +1843,9 @@
> # r/w -> r/o, if needed). The new backing file string is written into
> # the image file metadata, and the QEMU internal strings are updated.
> #
> +# A dirty qcow2 image is repaired during that reopen, which blocks
> +# other requests and can fail the command.
> +#
> # @image-node-name: The name of the block driver state node of the
> # image to modify. The "device" argument is used to verify
> # "image-node-name" is in the chain described by "device".
> @@ -1882,6 +1885,9 @@
> # size to match the size of the smaller top, you can safely truncate
> # it yourself once the commit operation successfully completes.
> #
> +# The base is opened read-write. A dirty qcow2 base is repaired
> +# first, which blocks other requests and can fail the command.
> +#
> # @job-id: identifier for the newly-created block job. If omitted,
> # the device name will be used. (Since 2.7)
> #
> @@ -2919,6 +2925,9 @@
> # On successful completion the image file is updated to drop the
> # backing file and the `BLOCK_JOB_COMPLETED` event is emitted.
> #
> +# The top image is opened read-write. A dirty qcow2 image is repaired
> +# first, which blocks other requests and can fail the command.
> +#
> # In case @device is a filter node, `block-stream` modifies the first
> # non-filter overlay node below it to point to the new backing node
> # instead of modifying @device itself.
> @@ -5070,6 +5079,16 @@
> # transaction, so if one of them fails then the whole transaction is
> # cancelled.
> #
> +# An error is also returned when a device cannot be used once it has
> +# been reopened. Such a reopen is not undone, so an error does not
> +# always mean that nothing has changed. A node the driver gave up on
> +# serves nothing at all: it keeps its image open, makes
> +# `query-named-block-nodes` fail for as long as it is in the graph,
> +# and `blockdev-del` removes it only once nothing refers to it.
> +#
> +# Reopening a dirty qcow2 image read-write repairs it first, which
> +# blocks other requests and can fail the command.
> +#
> # The command receives a list of block devices to reopen. For each
> # one of them, the top-level @node-name option (from `BlockdevOptions`)
> # must be specified and is used to select the block device to be
> diff --git a/tests/qemu-iotests/039 b/tests/qemu-iotests/039
> index 3d0c073d65d..3f37c36ca86 100755
> --- a/tests/qemu-iotests/039
> +++ b/tests/qemu-iotests/039
> @@ -33,6 +33,7 @@ status=1 # failure is the default!
> _cleanup()
> {
> _cleanup_test_img
> + rm -f "$TEST_DIR/blkdebug.conf"
> }
> trap "_cleanup; exit \$status" 0 1 2 3 15
>
> @@ -176,6 +177,65 @@ $QEMU_IO -c "write 0 512" "$TEST_IMG" | _filter_qemu_io
> # The dirty bit must not be set
> _qcow2_dump_header | grep incompatible_features
>
> +echo
> +echo "== Reopening a dirty image read/write should repair it =="
> +
> +_make_test_img -o "compat=1.1,lazy_refcounts=on" $size
> +
> +_NO_VALGRIND \
> +$QEMU_IO -c "write -P 0x5a 0 512" \
> + -c "sigraise $(kill -l KILL)" "$TEST_IMG" 2>&1 \
> + | _filter_qemu_io
> +
> +# The dirty bit must be set
> +_qcow2_dump_header | grep incompatible_features
> +
> +# Without the repair this write would alias the cluster at offset 0
> +$QEMU_IO -r -c "reopen -w" \
> + -c "write -P 0xb1 1M 512" \
> + -c "read -P 0x5a 0 512" "$TEST_IMG" | _filter_qemu_io
> +
> +_check_test_img
> +
> +echo
> +echo "== A read/write reopen must not check the image =="
> +
> +_make_test_img -o "compat=1.1,lazy_refcounts=on" $size
> +
> +_NO_VALGRIND \
> +$QEMU_IO -c "write -P 0x5a 0 512" \
> + -c "reopen -o l2-cache-size=1M" \
> + -c "sigraise $(kill -l KILL)" "$TEST_IMG" 2>&1 \
> + | _filter_qemu_io
> +
> +# The dirty bit must still be set, it belongs to the running session
> +_qcow2_dump_header | grep incompatible_features
> +
> +echo
> +echo "== A failed repair must fail the reopen =="
> +
> +_make_test_img -o "compat=1.1,lazy_refcounts=on" $size
> +
> +_NO_VALGRIND \
> +$QEMU_IO -c "write -P 0x5a 0 512" \
> + -c "sigraise $(kill -l KILL)" "$TEST_IMG" 2>&1 \
> + | _filter_qemu_io
> +
> +cat > "$TEST_DIR/blkdebug.conf" <<EOF
> +[inject-error]
> +event = "none"
> +iotype = "write"
> +errno = "5"
> +EOF
> +
> +# The repair cannot write, so the reopen itself must report the failure
> +$QEMU_IO -r -c "reopen -w" -c "read -P 0x5a 0 512" \
> + "blkdebug:$TEST_DIR/blkdebug.conf:$TEST_IMG" 2>&1 \
> + | _filter_testdir | _filter_qemu_io | _filter_generated_node_ids
> +
> +# The corrupt bit needs a write of its own, so the image is only left dirty
> +_qcow2_dump_header | grep incompatible_features
> +
> echo
> echo "== Creating an image file with lazy_refcounts=off =="
>
> diff --git a/tests/qemu-iotests/039.out b/tests/qemu-iotests/039.out
> index ce8ee577216..cc6ca3ab95b 100644
> --- a/tests/qemu-iotests/039.out
> +++ b/tests/qemu-iotests/039.out
> @@ -79,6 +79,42 @@ wrote 512/512 bytes at offset 0
> 512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
> incompatible_features []
>
> +== Reopening a dirty image read/write should repair it ==
> +Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=134217728
> +wrote 512/512 bytes at offset 0
> +512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
> +./common.rc: Killed ( VALGRIND_QEMU="${VALGRIND_QEMU_IO}" _qemu_proc_exec "${VALGRIND_LOGFILE}" "$QEMU_IO_PROG" $QEMU_IO_ARGS "$@" )
> +incompatible_features [0]
> +ERROR cluster 5 refcount=0 reference=1
> +Rebuilding refcount structure
> +Repairing cluster 1 refcount=1 reference=0
> +Repairing cluster 2 refcount=1 reference=0
> +wrote 512/512 bytes at offset 1048576
> +512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
> +read 512/512 bytes at offset 0
> +512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
> +No errors were found on the image.
> +
> +== A read/write reopen must not check the image ==
> +Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=134217728
> +wrote 512/512 bytes at offset 0
> +512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
> +./common.rc: Killed ( VALGRIND_QEMU="${VALGRIND_QEMU_IO}" _qemu_proc_exec "${VALGRIND_LOGFILE}" "$QEMU_IO_PROG" $QEMU_IO_ARGS "$@" )
> +incompatible_features [0]
> +
> +== A failed repair must fail the reopen ==
> +Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=134217728
> +wrote 512/512 bytes at offset 0
> +512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
> +./common.rc: Killed ( VALGRIND_QEMU="${VALGRIND_QEMU_IO}" _qemu_proc_exec "${VALGRIND_LOGFILE}" "$QEMU_IO_PROG" $QEMU_IO_ARGS "$@" )
> +ERROR cluster 5 refcount=0 reference=1
> +Rebuilding refcount structure
> +qemu-io: ERROR writing refblock: Input/output error
> +qemu-io: Could not repair dirty image 'NODE_NAME': Input/output error
> +The image is left dirty and this node holds it open until the node is removed
> +read failed: No medium found
> +incompatible_features [0]
> +
> == Creating an image file with lazy_refcounts=off ==
> Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=134217728
> wrote 512/512 bytes at offset 0
> diff --git a/tests/qemu-iotests/040 b/tests/qemu-iotests/040
> index 5c18e413ec9..b25c76183a1 100755
> --- a/tests/qemu-iotests/040
> +++ b/tests/qemu-iotests/040
> @@ -951,6 +951,129 @@ class TestCommitWithOverriddenBacking(iotests.QMPTestCase):
> self.vm.qmp('block-job-complete', device='commit')
> self.vm.event_wait('BLOCK_JOB_COMPLETED')
>
> +QCOW2_INCOMPAT_FEATURES_OFFSET = 72
> +QCOW2_INCOMPAT_DIRTY = 1 << 0
> +
> +image_size = 4 * 1024 * 1024
> +dirty_base = os.path.join(iotests.test_dir, 'dirty-base.img')
> +mid = os.path.join(iotests.test_dir, 'dirty-mid.img')
> +top = os.path.join(iotests.test_dir, 'dirty-top.img')
> +
> +
> +class TestCommitDirtyBase(iotests.QMPTestCase):
> + def setUp(self) -> None:
> + if iotests.imgfmt != 'qcow2':
> + self.case_skip('the dirty bit is a qcow2 feature')
> + iotests.qemu_img_create('-f', iotests.imgfmt, '-o',
> + 'compat=1.1,lazy_refcounts=on', dirty_base,
> + str(image_size))
> + # Killing the process leaves the refcounts of the written cluster stale
> + iotests.qemu_io_popen('-t', 'writethrough',
> + '-c', 'write -P 0x5a 0 512',
> + '-c', 'sigraise 9', dirty_base).communicate()
> + iotests.qemu_img_create('-f', iotests.imgfmt, '-b', dirty_base,
> + '-F', iotests.imgfmt, mid)
> + iotests.qemu_img_create('-f', iotests.imgfmt, '-b', mid,
> + '-F', iotests.imgfmt, top)
> + # The commit has to allocate for this, which is where the stale
> + # refcounts hand out the cluster holding the data written above
> + qemu_io('-c', 'write -P 0xb1 1M 512', mid)
> +
> + self.vm = iotests.VM()
> + self.vm.launch()
> + self.vm.cmd('blockdev-add', driver='file', filename=dirty_base,
> + node_name='base-file')
> +
> + self.assertTrue(self.is_dirty())
> +
> + def tearDown(self) -> None:
> + if self.vm.is_running():
> + self.vm.shutdown()
> + for image in (dirty_base, mid, top):
> + os.remove(image)
> +
> + def add_chain(self, base_file: str) -> None:
> + self.vm.cmd('blockdev-add', driver=iotests.imgfmt, file=base_file,
> + node_name='base', read_only=True)
> + self.vm.cmd('blockdev-add', driver='file', filename=mid,
> + node_name='mid-file')
> + self.vm.cmd('blockdev-add', driver=iotests.imgfmt, file='mid-file',
> + node_name='mid', backing='base')
> + self.vm.cmd('blockdev-add', driver='file', filename=top,
> + node_name='top-file')
> + self.vm.cmd('blockdev-add', driver=iotests.imgfmt, file='top-file',
> + node_name='top', backing='mid')
> +
> + def check_base(self) -> None:
> + result = iotests.qemu_img_check(dirty_base)
> + self.assertEqual(result['check-errors'], 0)
> + self.assertEqual(result.get('corruptions', 0), 0)
> + # Without the repair the commit would have aliased this cluster
> + qemu_io('-c', 'read -P 0x5a 0 512', '-c', 'read -P 0xb1 1M 512',
> + dirty_base)
> +
> + def is_dirty(self) -> bool:
> + with open(dirty_base, 'rb') as img:
> + img.seek(QCOW2_INCOMPAT_FEATURES_OFFSET)
> + features = struct.unpack('>Q', img.read(8))[0]
> + return bool(features & QCOW2_INCOMPAT_DIRTY)
> +
> + def test_commit_repairs_base(self) -> None:
> + self.add_chain('base-file')
> +
> + self.vm.cmd('block-commit', job_id='job0', device='top',
> + top_node='mid', base_node='base')
> + self.wait_until_completed(drive='job0')
> +
> + self.vm.shutdown()
> + self.assertFalse(self.is_dirty())
> + self.check_base()
> +
> + def test_active_commit_repairs_base(self) -> None:
> + self.add_chain('base-file')
> +
> + # Without top-node the whole chain commits, through
> + # commit_active_start() rather than commit_start()
> + self.vm.cmd('block-commit', job_id='job0', device='top',
> + base_node='base')
> + self.complete_and_wait(drive='job0')
> +
> + self.vm.shutdown()
> + self.assertFalse(self.is_dirty())
> + self.check_base()
> +
> + def test_failed_repair_fails_the_commit(self) -> None:
> + self.vm.cmd('blockdev-add', driver='blkdebug', image='base-file',
> + node_name='base-blkdebug',
> + inject_error=[{'event': 'none', 'iotype': 'write',
> + 'errno': 5}])
> + self.add_chain('base-blkdebug')
> +
> + result = self.vm.qmp('block-commit', job_id='job0', device='top',
> + top_node='mid', base_node='base')
> + self.assert_qmp(result, 'error/class', 'GenericError')
> + self.assertIn("Could not repair dirty image 'base'",
> + result['error']['desc'])
> +
> + # The base is left in the graph, and nothing can be queried while
> + # it is there
> + result = self.vm.qmp('query-named-block-nodes', flat=True)
> + self.assert_qmp(result, 'error/desc', 'Block device base is ejected')
> +
> + # It only goes away once nothing refers to it
> + result = self.vm.qmp('blockdev-del', node_name='base')
> + self.assert_qmp(result, 'error/desc',
> + "Node 'base' is busy: node is used as backing hd of "
> + "'mid'")
> +
> + # Marking the image corrupt needs a write of its own, which fails too
> + self.assertTrue(self.is_dirty())
> +
> + # Nothing can use the base any more, and that is what is reported
> + result = self.vm.qmp('block-commit', job_id='job1', device='top',
> + top_node='mid', base_node='base')
> + self.assert_qmp(result, 'error/desc', 'Device has no medium')
> +
> if __name__ == '__main__':
> iotests.main(supported_fmts=['qcow2', 'qed'],
> supported_protocols=['file'])
> diff --git a/tests/qemu-iotests/040.out b/tests/qemu-iotests/040.out
> index 1bb1dc5f0ea..f3cbf73a01d 100644
> --- a/tests/qemu-iotests/040.out
> +++ b/tests/qemu-iotests/040.out
> @@ -1,5 +1,5 @@
> -.................................................................
> +....................................................................
> ----------------------------------------------------------------------
> -Ran 65 tests
> +Ran 68 tests
>
> OK
^ permalink raw reply [relevance 5%]
* [Devel] [PATCH vz10 v2 1/3] core: add skip_pci_blacklist option to probe RHEL-disabled devices
@ 2026-09-16 15:40 7% ` Konstantin Khorenko
0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-16 15:40 UTC (permalink / raw)
To: Alexey Kuznetsov, Konstantin Khorenko, devel
From: Alexey Kuznetsov <kuznet@virtuozzo.com>
RHEL refuses to drive the PCI IDs listed in rh_disabled_pci_devices[]:
pci_rh_check_status() matches the device and local_pci_probe() turns
the match into -EACCES, so no driver ever binds to it. The list is a
hardware lifecycle decision rather than a statement about a defect,
and a host whose only NIC or HBA is on it cannot be deployed at all.
Keep probing such a device when skip_pci_blacklist is set. It is still
reported, as unmaintained rather than disabled, so a support dump shows
that the hardware gets no testing and no fixes.
The option is writable at runtime and the list is consulted on every
probe, so a blacklisted device can be brought up without a reboot: set
the option and rebind the device through
/sys/bus/pci/drivers/<driver>/bind.
https://virtuozzo.atlassian.net/browse/VSTOR-145697
Feature: pci: bypass the RHEL disabled device blacklist
Signed-off-by: Alexey Kuznetsov <kuznet@virtuozzo.com>
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
Documentation/admin-guide/kernel-parameters.txt | 12 ++++++++++++
kernel/rh_messages.c | 9 +++++++++
2 files changed, 21 insertions(+)
diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index 96ffb86cfa762..aea11ef7d28dc 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -6313,6 +6313,18 @@
apic=verbose is specified.
Example: apic=debug show_lapic=all
+ skip_pci_blacklist
+ [KNL] Bind drivers to the PCI devices that RHEL lists
+ as disabled, instead of failing their probe. Such a
+ device is reported as unmaintained hardware: it gets
+ no testing and no fixes.
+ Also writable at runtime through
+ /sys/module/kernel/parameters/skip_pci_blacklist, and
+ the list is consulted on every probe, so a device can
+ be brought up by rebinding it afterwards.
+ Format: { "0" | "1" }
+ Default: 0
+
slab_debug[=options[,slabs][;[options[,slabs]]...] [MM]
Enabling slab_debug allows one to determine the
culprit if slab objects become corrupted. Enabling
diff --git a/kernel/rh_messages.c b/kernel/rh_messages.c
index bb69e8965748b..ac3c8bebb8512 100644
--- a/kernel/rh_messages.c
+++ b/kernel/rh_messages.c
@@ -149,6 +149,9 @@ static void pci_hw_unmaintained(struct pci_dev *dev)
dev->device, dev->vendor, pci_name(dev));
}
+static bool skip_pci_blacklist;
+core_param(skip_pci_blacklist, skip_pci_blacklist, bool, 0644);
+
/**
* pci_hw_disabled() - Mark a PCI device disabled.
* @dev: the PCI device structure to match against
@@ -164,6 +167,12 @@ static bool __maybe_unused pci_hw_disabled(struct pci_dev *dev)
if (!ret)
return false;
+ if (skip_pci_blacklist) {
+ mark_hardware_unmaintained(dev_driver_string(&dev->dev), "%04X:%04X @ %s",
+ dev->device, dev->vendor, pci_name(dev));
+ return false;
+ }
+
mark_hardware_disabled(dev_driver_string(&dev->dev), "%04X:%04X @ %s",
dev->device, dev->vendor, pci_name(dev));
return true;
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 7%]
* [PATCH hci-8.0 5/5] qcow2: repair a dirty image when it becomes writable #VSTOR-140073
2026-09-16 15:24 4% [PATCH hci-8.0 0/5] qcow2: silent corruption when a dirty image becomes writable Denis V. Lunev
2026-09-16 15:24 3% ` [PATCH hci-8.0 2/5] block: reject a reopen of an unusable node instead of crashing #VSTOR-140073 Denis V. Lunev
@ 2026-09-16 15:24 3% ` Denis V. Lunev
2026-09-16 16:26 5% ` Andrey Drobyshev
1 sibling, 1 reply; 200+ results
From: Denis V. Lunev @ 2026-09-16 15:24 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
A dirty image must be repaired before anything allocates a cluster in
it. qcow2_do_open() does that, but only for a node that is writable
from the start. A node opened read-only skips it, and nothing revisits
the question once that node becomes writable, which block-commit does
routinely: commit_active_start() and commit_start() reopen the base
read-write for the duration of the job.
With lazy refcounts the on-disk refcount block then still accounts for
the metadata clusters only, so the allocator restarts at the front of
the image and hands out clusters that L2 entries point at. Two guest
offsets end up sharing one host cluster. Nothing fails, the corrupt bit
stays clear, and a clean close clears the dirty bit, so no later open
repairs the image either. The bit also stays set for the whole writable
session, so a node which is merely writable says nothing.
Refusing the reopen instead is simpler and keeps it atomic, but it
leaves nowhere to go: the base belongs to a chain the VM has open, so
the qemu-img check -r such an error would ask for cannot take the write
lock it needs. The repair does the trick in most cases anyway.
Do the repair in qcow2_reopen_commit_post(), the earliest point where
the node is writable. An inactive node is skipped: bdrv_activate() calls
qcow2_do_open() again through qcow2_co_invalidate_cache().
commit_post cannot reject the reopen, so a failed repair takes the
driver away from the node instead, which is what stops writes from
aliasing live clusters. qcow2_signal_corruption() does that as well,
but it also sends BLOCK_IMAGE_CORRUPTED and sets the corrupt bit,
which qcow2_do_open() honours by refusing every later read-write open.
The image is dirty and unrepaired, not corrupt, and qemu-img check -r
still fixes it, so neither belongs here. Return the error and skip
the bitmaps.
Signed-off-by: Denis V. Lunev <den@openvz.org>
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
CC: Kevin Wolf <kwolf@redhat.com>
CC: Hanna Reitz <hreitz@redhat.com>
CC: Eric Blake <eblake@redhat.com>
CC: Markus Armbruster <armbru@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Cc: qemu-stable@nongnu.org
---
block/qcow2.c | 21 +++++++
qapi/block-core.json | 19 ++++++
tests/qemu-iotests/039 | 60 ++++++++++++++++++
tests/qemu-iotests/039.out | 36 +++++++++++
tests/qemu-iotests/040 | 123 +++++++++++++++++++++++++++++++++++++
tests/qemu-iotests/040.out | 4 +-
6 files changed, 261 insertions(+), 2 deletions(-)
diff --git a/block/qcow2.c b/block/qcow2.c
index ddad39a3084..be346f0339b 100644
--- a/block/qcow2.c
+++ b/block/qcow2.c
@@ -2235,8 +2235,29 @@ static void qcow2_reopen_commit(BDRVReopenState *state)
static int qcow2_reopen_commit_post(BDRVReopenState *state, Error **errp)
{
+ ERRP_GUARD();
+ BDRVQcow2State *s = state->bs->opaque;
+
GRAPH_RDLOCK_GUARD_MAINLOOP();
+ if (!bdrv_reopen_was_writable(state) && bdrv_is_writable(state->bs) &&
+ (s->incompatible_features & QCOW2_INCOMPAT_DIRTY)) {
+ BdrvCheckResult result = {0};
+ int ret;
+
+ ret = bdrv_check(state->bs, &result, BDRV_FIX_ERRORS | BDRV_FIX_LEAKS);
+ if (ret < 0 || result.check_errors || !state->bs->drv) {
+ ret = ret < 0 ? ret : -EIO;
+ /* No write may reach an image whose refcounts are unaccounted */
+ state->bs->drv = NULL;
+ error_setg_errno(errp, -ret, "Could not repair dirty image '%s'",
+ bdrv_get_device_or_node_name(state->bs));
+ error_append_hint(errp, "The image is left dirty and this node "
+ "holds it open until the node is removed\n");
+ return ret;
+ }
+ }
+
if (state->flags & BDRV_O_RDWR) {
Error *local_err = NULL;
diff --git a/qapi/block-core.json b/qapi/block-core.json
index c036a3a8858..6fb8c31210a 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -1843,6 +1843,9 @@
# r/w -> r/o, if needed). The new backing file string is written into
# the image file metadata, and the QEMU internal strings are updated.
#
+# A dirty qcow2 image is repaired during that reopen, which blocks
+# other requests and can fail the command.
+#
# @image-node-name: The name of the block driver state node of the
# image to modify. The "device" argument is used to verify
# "image-node-name" is in the chain described by "device".
@@ -1882,6 +1885,9 @@
# size to match the size of the smaller top, you can safely truncate
# it yourself once the commit operation successfully completes.
#
+# The base is opened read-write. A dirty qcow2 base is repaired
+# first, which blocks other requests and can fail the command.
+#
# @job-id: identifier for the newly-created block job. If omitted,
# the device name will be used. (Since 2.7)
#
@@ -2919,6 +2925,9 @@
# On successful completion the image file is updated to drop the
# backing file and the `BLOCK_JOB_COMPLETED` event is emitted.
#
+# The top image is opened read-write. A dirty qcow2 image is repaired
+# first, which blocks other requests and can fail the command.
+#
# In case @device is a filter node, `block-stream` modifies the first
# non-filter overlay node below it to point to the new backing node
# instead of modifying @device itself.
@@ -5070,6 +5079,16 @@
# transaction, so if one of them fails then the whole transaction is
# cancelled.
#
+# An error is also returned when a device cannot be used once it has
+# been reopened. Such a reopen is not undone, so an error does not
+# always mean that nothing has changed. A node the driver gave up on
+# serves nothing at all: it keeps its image open, makes
+# `query-named-block-nodes` fail for as long as it is in the graph,
+# and `blockdev-del` removes it only once nothing refers to it.
+#
+# Reopening a dirty qcow2 image read-write repairs it first, which
+# blocks other requests and can fail the command.
+#
# The command receives a list of block devices to reopen. For each
# one of them, the top-level @node-name option (from `BlockdevOptions`)
# must be specified and is used to select the block device to be
diff --git a/tests/qemu-iotests/039 b/tests/qemu-iotests/039
index 3d0c073d65d..3f37c36ca86 100755
--- a/tests/qemu-iotests/039
+++ b/tests/qemu-iotests/039
@@ -33,6 +33,7 @@ status=1 # failure is the default!
_cleanup()
{
_cleanup_test_img
+ rm -f "$TEST_DIR/blkdebug.conf"
}
trap "_cleanup; exit \$status" 0 1 2 3 15
@@ -176,6 +177,65 @@ $QEMU_IO -c "write 0 512" "$TEST_IMG" | _filter_qemu_io
# The dirty bit must not be set
_qcow2_dump_header | grep incompatible_features
+echo
+echo "== Reopening a dirty image read/write should repair it =="
+
+_make_test_img -o "compat=1.1,lazy_refcounts=on" $size
+
+_NO_VALGRIND \
+$QEMU_IO -c "write -P 0x5a 0 512" \
+ -c "sigraise $(kill -l KILL)" "$TEST_IMG" 2>&1 \
+ | _filter_qemu_io
+
+# The dirty bit must be set
+_qcow2_dump_header | grep incompatible_features
+
+# Without the repair this write would alias the cluster at offset 0
+$QEMU_IO -r -c "reopen -w" \
+ -c "write -P 0xb1 1M 512" \
+ -c "read -P 0x5a 0 512" "$TEST_IMG" | _filter_qemu_io
+
+_check_test_img
+
+echo
+echo "== A read/write reopen must not check the image =="
+
+_make_test_img -o "compat=1.1,lazy_refcounts=on" $size
+
+_NO_VALGRIND \
+$QEMU_IO -c "write -P 0x5a 0 512" \
+ -c "reopen -o l2-cache-size=1M" \
+ -c "sigraise $(kill -l KILL)" "$TEST_IMG" 2>&1 \
+ | _filter_qemu_io
+
+# The dirty bit must still be set, it belongs to the running session
+_qcow2_dump_header | grep incompatible_features
+
+echo
+echo "== A failed repair must fail the reopen =="
+
+_make_test_img -o "compat=1.1,lazy_refcounts=on" $size
+
+_NO_VALGRIND \
+$QEMU_IO -c "write -P 0x5a 0 512" \
+ -c "sigraise $(kill -l KILL)" "$TEST_IMG" 2>&1 \
+ | _filter_qemu_io
+
+cat > "$TEST_DIR/blkdebug.conf" <<EOF
+[inject-error]
+event = "none"
+iotype = "write"
+errno = "5"
+EOF
+
+# The repair cannot write, so the reopen itself must report the failure
+$QEMU_IO -r -c "reopen -w" -c "read -P 0x5a 0 512" \
+ "blkdebug:$TEST_DIR/blkdebug.conf:$TEST_IMG" 2>&1 \
+ | _filter_testdir | _filter_qemu_io | _filter_generated_node_ids
+
+# The corrupt bit needs a write of its own, so the image is only left dirty
+_qcow2_dump_header | grep incompatible_features
+
echo
echo "== Creating an image file with lazy_refcounts=off =="
diff --git a/tests/qemu-iotests/039.out b/tests/qemu-iotests/039.out
index ce8ee577216..cc6ca3ab95b 100644
--- a/tests/qemu-iotests/039.out
+++ b/tests/qemu-iotests/039.out
@@ -79,6 +79,42 @@ wrote 512/512 bytes at offset 0
512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
incompatible_features []
+== Reopening a dirty image read/write should repair it ==
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=134217728
+wrote 512/512 bytes at offset 0
+512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+./common.rc: Killed ( VALGRIND_QEMU="${VALGRIND_QEMU_IO}" _qemu_proc_exec "${VALGRIND_LOGFILE}" "$QEMU_IO_PROG" $QEMU_IO_ARGS "$@" )
+incompatible_features [0]
+ERROR cluster 5 refcount=0 reference=1
+Rebuilding refcount structure
+Repairing cluster 1 refcount=1 reference=0
+Repairing cluster 2 refcount=1 reference=0
+wrote 512/512 bytes at offset 1048576
+512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+read 512/512 bytes at offset 0
+512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+No errors were found on the image.
+
+== A read/write reopen must not check the image ==
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=134217728
+wrote 512/512 bytes at offset 0
+512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+./common.rc: Killed ( VALGRIND_QEMU="${VALGRIND_QEMU_IO}" _qemu_proc_exec "${VALGRIND_LOGFILE}" "$QEMU_IO_PROG" $QEMU_IO_ARGS "$@" )
+incompatible_features [0]
+
+== A failed repair must fail the reopen ==
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=134217728
+wrote 512/512 bytes at offset 0
+512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)
+./common.rc: Killed ( VALGRIND_QEMU="${VALGRIND_QEMU_IO}" _qemu_proc_exec "${VALGRIND_LOGFILE}" "$QEMU_IO_PROG" $QEMU_IO_ARGS "$@" )
+ERROR cluster 5 refcount=0 reference=1
+Rebuilding refcount structure
+qemu-io: ERROR writing refblock: Input/output error
+qemu-io: Could not repair dirty image 'NODE_NAME': Input/output error
+The image is left dirty and this node holds it open until the node is removed
+read failed: No medium found
+incompatible_features [0]
+
== Creating an image file with lazy_refcounts=off ==
Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=134217728
wrote 512/512 bytes at offset 0
diff --git a/tests/qemu-iotests/040 b/tests/qemu-iotests/040
index 5c18e413ec9..b25c76183a1 100755
--- a/tests/qemu-iotests/040
+++ b/tests/qemu-iotests/040
@@ -951,6 +951,129 @@ class TestCommitWithOverriddenBacking(iotests.QMPTestCase):
self.vm.qmp('block-job-complete', device='commit')
self.vm.event_wait('BLOCK_JOB_COMPLETED')
+QCOW2_INCOMPAT_FEATURES_OFFSET = 72
+QCOW2_INCOMPAT_DIRTY = 1 << 0
+
+image_size = 4 * 1024 * 1024
+dirty_base = os.path.join(iotests.test_dir, 'dirty-base.img')
+mid = os.path.join(iotests.test_dir, 'dirty-mid.img')
+top = os.path.join(iotests.test_dir, 'dirty-top.img')
+
+
+class TestCommitDirtyBase(iotests.QMPTestCase):
+ def setUp(self) -> None:
+ if iotests.imgfmt != 'qcow2':
+ self.case_skip('the dirty bit is a qcow2 feature')
+ iotests.qemu_img_create('-f', iotests.imgfmt, '-o',
+ 'compat=1.1,lazy_refcounts=on', dirty_base,
+ str(image_size))
+ # Killing the process leaves the refcounts of the written cluster stale
+ iotests.qemu_io_popen('-t', 'writethrough',
+ '-c', 'write -P 0x5a 0 512',
+ '-c', 'sigraise 9', dirty_base).communicate()
+ iotests.qemu_img_create('-f', iotests.imgfmt, '-b', dirty_base,
+ '-F', iotests.imgfmt, mid)
+ iotests.qemu_img_create('-f', iotests.imgfmt, '-b', mid,
+ '-F', iotests.imgfmt, top)
+ # The commit has to allocate for this, which is where the stale
+ # refcounts hand out the cluster holding the data written above
+ qemu_io('-c', 'write -P 0xb1 1M 512', mid)
+
+ self.vm = iotests.VM()
+ self.vm.launch()
+ self.vm.cmd('blockdev-add', driver='file', filename=dirty_base,
+ node_name='base-file')
+
+ self.assertTrue(self.is_dirty())
+
+ def tearDown(self) -> None:
+ if self.vm.is_running():
+ self.vm.shutdown()
+ for image in (dirty_base, mid, top):
+ os.remove(image)
+
+ def add_chain(self, base_file: str) -> None:
+ self.vm.cmd('blockdev-add', driver=iotests.imgfmt, file=base_file,
+ node_name='base', read_only=True)
+ self.vm.cmd('blockdev-add', driver='file', filename=mid,
+ node_name='mid-file')
+ self.vm.cmd('blockdev-add', driver=iotests.imgfmt, file='mid-file',
+ node_name='mid', backing='base')
+ self.vm.cmd('blockdev-add', driver='file', filename=top,
+ node_name='top-file')
+ self.vm.cmd('blockdev-add', driver=iotests.imgfmt, file='top-file',
+ node_name='top', backing='mid')
+
+ def check_base(self) -> None:
+ result = iotests.qemu_img_check(dirty_base)
+ self.assertEqual(result['check-errors'], 0)
+ self.assertEqual(result.get('corruptions', 0), 0)
+ # Without the repair the commit would have aliased this cluster
+ qemu_io('-c', 'read -P 0x5a 0 512', '-c', 'read -P 0xb1 1M 512',
+ dirty_base)
+
+ def is_dirty(self) -> bool:
+ with open(dirty_base, 'rb') as img:
+ img.seek(QCOW2_INCOMPAT_FEATURES_OFFSET)
+ features = struct.unpack('>Q', img.read(8))[0]
+ return bool(features & QCOW2_INCOMPAT_DIRTY)
+
+ def test_commit_repairs_base(self) -> None:
+ self.add_chain('base-file')
+
+ self.vm.cmd('block-commit', job_id='job0', device='top',
+ top_node='mid', base_node='base')
+ self.wait_until_completed(drive='job0')
+
+ self.vm.shutdown()
+ self.assertFalse(self.is_dirty())
+ self.check_base()
+
+ def test_active_commit_repairs_base(self) -> None:
+ self.add_chain('base-file')
+
+ # Without top-node the whole chain commits, through
+ # commit_active_start() rather than commit_start()
+ self.vm.cmd('block-commit', job_id='job0', device='top',
+ base_node='base')
+ self.complete_and_wait(drive='job0')
+
+ self.vm.shutdown()
+ self.assertFalse(self.is_dirty())
+ self.check_base()
+
+ def test_failed_repair_fails_the_commit(self) -> None:
+ self.vm.cmd('blockdev-add', driver='blkdebug', image='base-file',
+ node_name='base-blkdebug',
+ inject_error=[{'event': 'none', 'iotype': 'write',
+ 'errno': 5}])
+ self.add_chain('base-blkdebug')
+
+ result = self.vm.qmp('block-commit', job_id='job0', device='top',
+ top_node='mid', base_node='base')
+ self.assert_qmp(result, 'error/class', 'GenericError')
+ self.assertIn("Could not repair dirty image 'base'",
+ result['error']['desc'])
+
+ # The base is left in the graph, and nothing can be queried while
+ # it is there
+ result = self.vm.qmp('query-named-block-nodes', flat=True)
+ self.assert_qmp(result, 'error/desc', 'Block device base is ejected')
+
+ # It only goes away once nothing refers to it
+ result = self.vm.qmp('blockdev-del', node_name='base')
+ self.assert_qmp(result, 'error/desc',
+ "Node 'base' is busy: node is used as backing hd of "
+ "'mid'")
+
+ # Marking the image corrupt needs a write of its own, which fails too
+ self.assertTrue(self.is_dirty())
+
+ # Nothing can use the base any more, and that is what is reported
+ result = self.vm.qmp('block-commit', job_id='job1', device='top',
+ top_node='mid', base_node='base')
+ self.assert_qmp(result, 'error/desc', 'Device has no medium')
+
if __name__ == '__main__':
iotests.main(supported_fmts=['qcow2', 'qed'],
supported_protocols=['file'])
diff --git a/tests/qemu-iotests/040.out b/tests/qemu-iotests/040.out
index 1bb1dc5f0ea..f3cbf73a01d 100644
--- a/tests/qemu-iotests/040.out
+++ b/tests/qemu-iotests/040.out
@@ -1,5 +1,5 @@
-.................................................................
+....................................................................
----------------------------------------------------------------------
-Ran 65 tests
+Ran 68 tests
OK
--
2.53.0
^ permalink raw reply [relevance 3%]
* [PATCH hci-8.0 0/5] qcow2: silent corruption when a dirty image becomes writable
@ 2026-09-16 15:24 4% Denis V. Lunev
2026-09-16 15:24 3% ` [PATCH hci-8.0 2/5] block: reject a reopen of an unusable node instead of crashing #VSTOR-140073 Denis V. Lunev
2026-09-16 15:24 3% ` [PATCH hci-8.0 5/5] qcow2: repair a dirty image when it becomes writable #VSTOR-140073 Denis V. Lunev
0 siblings, 2 replies; 200+ results
From: Denis V. Lunev @ 2026-09-16 15:24 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
A customer volume was cross-mapped onto itself by this bug, so hci-8.0
wants the fix even though the upstream series has not been merged yet.
The problem
-----------
qcow2_do_open() repairs an image carrying QCOW2_INCOMPAT_DIRTY, but only
for a node that is writable from the start:
if (!(flags & BDRV_O_CHECK) && bdrv_is_writable(bs) &&
(s->incompatible_features & QCOW2_INCOMPAT_DIRTY)) {
A node opened read-only skips it, correctly, since it resolves nothing
and writes nothing. Nothing revisits the question when that same node
later becomes writable, and bdrv_reopen() is not an exotic way to get
there: commit_active_start() and commit_start() both reopen the base
read-write for the duration of the job, so an ordinary block-commit onto
a read-only backing file is enough.
With lazy refcounts the refcount blocks are not written again once the
dirty bit is set, so an image left behind by a killed QEMU has an
on-disk refcount block that accounts for the metadata clusters and
nothing else. Every data cluster reads as free. s->free_cluster_index
starts at 0, so the first allocation after such a reopen starts at the
front of the image and hands out clusters that L2 entries still point
at.
The result is aliasing: two guest offsets mapped onto one host cluster,
so the guest reads back data belonging to some other offset. Nothing
about this fails. No I/O error is reported, the corrupt bit stays clear,
and a clean close clears the dirty bit as well, so no later open will
repair the image either. The only runtime witness, if something
eventually frees one of those clusters, is a bare
qcow2_free_clusters failed: Invalid argument
on stderr, with the guest none the wiser.
How it looked in production
---------------------------
A VM was killed while its storage was unavailable, leaving a 100 GiB
volume dirty. It came back with a snapshot-revert overlay on top, so the
volume itself was now the read-only backing file, and the overlay was
committed into it two and a half hours later. 8082 host clusters ended
up referenced by two L2 entries each, roughly 8 GiB of guest data
cross-mapped. The guest filesystem began failing metadata verification
on buffers holding fragments of unrelated files.
qemu-img check -r all makes the metadata self-consistent again, and then
honestly reports no errors, but it cannot un-alias anything. The guest
data stays wrong.
Reproducer
----------
Under a second, no guest and no block job required:
qemu-img create -f qcow2 -o compat=1.1,lazy_refcounts=on base.qcow2 1G
qemu-io -f qcow2 -c "write -P 0xaa 0 100M" -c flush \
-c "sigraise 9" base.qcow2
qemu-io -r -f qcow2 base.qcow2 \
<<< $'reopen -w\nwrite -P 0xbb 900M 100M\nquit'
qemu-io -r -f qcow2 -c "read -v 0 16" base.qcow2
# 00000000: bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb bb
The flush is load-bearing: it writes out the L2 cache but not the
refcount blocks, which is exactly the asymmetry the bug needs. Guest
fsyncs supply it in production, so a long-running VM is the ideal
victim. qemu-img commit of an overlay reaches the same state through
commit_active_start().
The fix
-------
Patch 5 repairs the image in qcow2_reopen_commit_post(), the earliest
point at which the node is writable. That callback runs after the reopen
transaction has been committed and cannot reject it, so patch 3 gives it
a return value to report a node it has had to give up on, and patch 4
records the flags the reopen started from so the driver can tell a node
that has just become writable from one that was writable all along.
Patch 2 stops a reopen of a driverless node from crashing, which is how
a failed repair comes back through commit_clean(). Patch 1 is the
read-only header write that fails or asserts on a dirty image.
The repair runs under bdrv_drain_all(), so a block-commit onto a large
dirty base stalls guest I/O for the length of a full metadata scan. That
is the cost of the alternative being silent data loss; rejecting the
reopen would be cheap, but block-commit depends on it succeeding.
Port notes
----------
This is the hci-8.0 port of the v4 series posted upstream on 2026-08-24:
https://lore.kernel.org/qemu-devel/20260824133729.1141990-1-den@openvz.org/
It is still under review there, so the code may yet change. All five
patches cherry-pick onto hci-8.0 with no prerequisites and no conflicts,
and the diff is identical to the upstream one.
One thing differs, in patch 5: hci-8.0 creates qcow2 images with zstd
compression by default, so a fresh v3 image already carries
QCOW2_INCOMPAT_COMPRESSION. The iotests 040 helper therefore masks the
dirty bit out of the incompatible features rather than comparing the
whole word, which upstream can afford to do.
Built and tested on dev-cl04: iotests 039, 040 and 060 pass, and the
qcow2 auto group is otherwise unchanged. vz-mt-vendor fails there only
because that build was configured --disable-spice and so has no
qxl-vga device for the test to instantiate.
Signed-off-by: Denis V. Lunev <den@openvz.org>
Denis V. Lunev (5):
qcow2: do not clear the dirty bit when reopening a read-only node
#VSTOR-140073
block: reject a reopen of an unusable node instead of crashing
#VSTOR-140073
block: let bdrv_reopen_commit_post() report a failure #VSTOR-140073
block: remember the flags a reopen starts from #VSTOR-140073
qcow2: repair a dirty image when it becomes writable #VSTOR-140073
block.c | 55 ++++++++++++--
block/qcow2.c | 33 ++++++++-
include/block/block-common.h | 1 +
include/block/block_int-common.h | 11 ++-
qapi/block-core.json | 19 +++++
tests/qemu-iotests/039 | 110 +++++++++++++++++++++++++++
tests/qemu-iotests/039.out | 56 ++++++++++++++
tests/qemu-iotests/040 | 123 +++++++++++++++++++++++++++++++
tests/qemu-iotests/040.out | 4 +-
tests/qemu-iotests/060 | 30 ++++++++
tests/qemu-iotests/060.out | 13 ++++
11 files changed, 440 insertions(+), 15 deletions(-)
--
2.53.0
^ permalink raw reply [relevance 4%]
* [PATCH hci-8.0 2/5] block: reject a reopen of an unusable node instead of crashing #VSTOR-140073
2026-09-16 15:24 4% [PATCH hci-8.0 0/5] qcow2: silent corruption when a dirty image becomes writable Denis V. Lunev
@ 2026-09-16 15:24 3% ` Denis V. Lunev
2026-09-16 15:24 3% ` [PATCH hci-8.0 5/5] qcow2: repair a dirty image when it becomes writable #VSTOR-140073 Denis V. Lunev
1 sibling, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-16 15:24 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
qcow2_signal_corruption() drops bs->drv, so a node can lose its driver
at any time and a reopen has to expect that. Both ends of the path
assume otherwise:
$ qemu-io -c "read 0 64k" -c "reopen -r" corrupt.qcow2
qcow2: Marking image as corrupt: Cluster allocation offset 0x1200
unaligned (L2 offset: 0x40000, L2 index: 0); ...
Segmentation fault
bdrv_reopen_queue_child() dereferences bs->drv while descending into
the children the node opened itself, and bdrv_reopen_prepare() asserts
on it. commit_clean() reopens the base of a commit job back to
read-only, so a base which goes corrupt under the job arrives here too.
There is nothing to reopen for such a node, so stop descending into its
children and let bdrv_reopen_prepare() report what every caller of
bdrv_reopen() already handles. bdrv_reopen_commit() and
bdrv_reopen_abort() keep their assertion, only a prepared entry reaches
them.
Signed-off-by: Denis V. Lunev <den@openvz.org>
Reviewed-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
CC: Kevin Wolf <kwolf@redhat.com>
CC: Hanna Reitz <hreitz@redhat.com>
CC: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Cc: qemu-stable@nongnu.org
---
block.c | 14 +++++++++++++-
tests/qemu-iotests/060 | 30 ++++++++++++++++++++++++++++++
tests/qemu-iotests/060.out | 13 +++++++++++++
3 files changed, 56 insertions(+), 1 deletion(-)
diff --git a/block.c b/block.c
index 7b27cb4edf3..44afa191930 100644
--- a/block.c
+++ b/block.c
@@ -4526,6 +4526,11 @@ bdrv_reopen_queue_child(BlockReopenQueue *bs_queue, BlockDriverState *bs,
!qdict_haskey(options, "backing.driver");
}
+ /* An unusable node is rejected by bdrv_reopen_prepare(), do not descend */
+ if (!bs->drv) {
+ return bs_queue;
+ }
+
QLIST_FOREACH(child, &bs->children, next) {
QDict *new_child_options = NULL;
bool child_keep_old = keep_old_opts;
@@ -4921,9 +4926,16 @@ bdrv_reopen_prepare(BDRVReopenState *reopen_state, BlockReopenQueue *queue,
bool drv_prepared = false;
assert(reopen_state != NULL);
- assert(reopen_state->bs->drv != NULL);
GLOBAL_STATE_CODE();
+
drv = reopen_state->bs->drv;
+ if (drv == NULL) {
+ GRAPH_RDLOCK_GUARD_MAINLOOP();
+
+ error_setg(errp, "Block node '%s' has no driver left to reopen",
+ bdrv_get_device_or_node_name(reopen_state->bs));
+ return -ENOMEDIUM;
+ }
/* This function and each driver's bdrv_reopen_prepare() remove
* entries from reopen_state->options as they are processed, so
diff --git a/tests/qemu-iotests/060 b/tests/qemu-iotests/060
index 5cd21a6f686..ce49fc34ecc 100755
--- a/tests/qemu-iotests/060
+++ b/tests/qemu-iotests/060
@@ -486,6 +486,36 @@ echo
# Image should not have been marked corrupt
_img_info --format-specific | grep 'corrupt:'
+echo
+echo "=== Testing the reopen of an image corrupted at runtime ==="
+echo
+
+_make_test_img 64M
+poke_file "$TEST_IMG" "$l1_offset" "\x00\x00\x00\x00\x2a\x2a\x2a\x2a"
+
+# The read leaves the node unusable, the reopen must report that
+echo "{'execute': 'qmp_capabilities'}
+ {'execute': 'human-monitor-command',
+ 'arguments': {'command-line': 'qemu-io drive \"read 0 512\"'}}
+ {'execute': 'blockdev-reopen',
+ 'arguments': {'options': [{'node-name': 'drive',
+ 'driver': 'qcow2',
+ 'read-only': true,
+ 'file': {
+ 'driver': 'file',
+ 'filename': '$TEST_IMG'
+ }}]}}
+ {'execute': 'quit'}" \
+ | $QEMU -qmp stdio -nographic -nodefaults \
+ -blockdev "{'node-name': 'drive',
+ 'driver': 'qcow2',
+ 'file': {
+ 'driver': 'file',
+ 'filename': '$TEST_IMG'
+ }}" \
+ 2>&1 \
+ | _filter_qmp | _filter_qemu_io
+
# success, all done
echo "*** done"
rm -f $seq.full
diff --git a/tests/qemu-iotests/060.out b/tests/qemu-iotests/060.out
index a37bf446e94..ad1912a43be 100644
--- a/tests/qemu-iotests/060.out
+++ b/tests/qemu-iotests/060.out
@@ -436,4 +436,17 @@ qcow2: Image is corrupt: L2 table offset 0x2a2a2a00 unaligned (L1 index: 0); fur
{"return": {}}
corrupt: false
+
+=== Testing the reopen of an image corrupted at runtime ===
+
+Formatting 'TEST_DIR/t.IMGFMT', fmt=IMGFMT size=67108864
+QMP_VERSION
+{"return": {}}
+qcow2: Marking image as corrupt: L2 table offset 0x2a2a2a00 unaligned (L1 index: 0); further corruption events will be suppressed
+{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "BLOCK_IMAGE_CORRUPTED", "data": {"device": "", "msg": "L2 table offset 0x2a2a2a00 unaligned (L1 index: 0)", "node-name": "drive", "fatal": true}}
+read failed: Input/output error
+{"return": ""}
+{"error": {"class": "GenericError", "desc": "Block node 'drive' has no driver left to reopen"}}
+{"timestamp": {"seconds": TIMESTAMP, "microseconds": TIMESTAMP}, "event": "SHUTDOWN", "data": {"guest": false, "reason": "host-qmp-quit"}}
+{"return": {}}
*** done
--
2.53.0
^ permalink raw reply [relevance 3%]
* [PATCH hci-8.0 09/12] system/cpus: report a BQL held past a deadline #VSTOR-144507
2026-09-16 13:33 4% [PATCH hci-8.0 00/12] system: add a watchdog for the Big QEMU Lock Denis V. Lunev
` (3 preceding siblings ...)
2026-09-16 13:33 2% ` [PATCH hci-8.0 08/12] oslib: report each thread's program counter #VSTOR-144507 Denis V. Lunev
@ 2026-09-16 13:33 2% ` Denis V. Lunev
4 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-16 13:33 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
A vCPU thread reacquires the BQL on every exit that needs userspace, so
a lock held for tens of seconds stops those vCPUs dead and the guest
reports its own soft lockup. Nothing in qemu records that this
happened, which leaves a guest panic as the only evidence and no way to
tell a stuck holder from a guest that wedged on its own.
Add a BQL watchdog. A thread of its own samples a counter advanced on
every acquire and every release, so it is odd while the lock is held.
One odd value seen throughout the window means a single owner held the
lock for the whole window, because any handover would have advanced the
counter twice.
Advance it in bql_update_status(), which the mutex hooks already call
for exactly those two transitions, so a cond-wait that drops and
retakes the lock is covered without the wrappers knowing, and so is any
caller that reaches the BQL through the plain mutex API. Record the
holder's tid beside it, under the same lock, because the thread listing
alone cannot tell a thread parked on its own condvar from one blocked
on the BQL. Cache the tid per thread, since gettid is a syscall and
this is the lock path.
On expiry, name the holder and list every thread's state. The listing
separates a thread blocked on the BQL, which waits in a futex, from one
idle inside KVM, which waits in kvm_vcpu_block, and names the syscall a
wedged holder is stuck in. Report once per episode, or a lock held for
a minute buries the log in copies of one answer. Put the time on the
line unless -msg timestamp=on already does: a hold is only worth
reporting if it can be lined up against what the guest saw, and that
option has to be asked for in advance.
The deadline has to clear the longest legitimate hold, and migration
setup can hold the BQL for a while, so the watchdog stays off unless
asked for. The bql-watchdog-ms machine property is the whole interface:
it sets the deadline, on the command line or over qom-set at any time,
and 0 turns the watchdog off. Reject a deadline above a minute: a hold
that long has already shown up as a soft lockup inside the guest.
Cc: Paolo Bonzini <pbonzini@redhat.com>
Cc: Richard Henderson <richard.henderson@linaro.org>
Cc: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>
Cc: Zhao Liu <zhao1.liu@intel.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
hw/core/machine.c | 27 ++++++++++
include/system/cpus.h | 4 ++
qemu-options.hx | 6 +++
system/cpus.c | 120 ++++++++++++++++++++++++++++++++++++++++++
4 files changed, 157 insertions(+)
diff --git a/hw/core/machine.c b/hw/core/machine.c
index bd61aa69ccc..d5bb047744d 100644
--- a/hw/core/machine.c
+++ b/hw/core/machine.c
@@ -923,6 +923,27 @@ static void machine_set_dump_guest_core(Object *obj, bool value, Error **errp)
ms->dump_guest_core = value;
}
+static void machine_get_bql_watchdog_ms(Object *obj, Visitor *v,
+ const char *name, void *opaque,
+ Error **errp)
+{
+ uint64_t value = bql_watchdog_get_timeout_ms();
+
+ visit_type_uint64(v, name, &value, errp);
+}
+
+static void machine_set_bql_watchdog_ms(Object *obj, Visitor *v,
+ const char *name, void *opaque,
+ Error **errp)
+{
+ uint64_t value;
+
+ if (!visit_type_uint64(v, name, &value, errp)) {
+ return;
+ }
+ bql_watchdog_set_timeout_ms(value, errp);
+}
+
static bool machine_get_mem_merge(Object *obj, Error **errp)
{
MachineState *ms = MACHINE(obj);
@@ -1671,6 +1692,12 @@ static void machine_class_init(ObjectClass *oc, const void *data)
object_class_property_set_description(oc, "dump-guest-core",
"Include guest memory in a core dump");
+ object_class_property_add(oc, "bql-watchdog-ms", "uint64",
+ machine_get_bql_watchdog_ms, machine_set_bql_watchdog_ms, NULL, NULL);
+ object_class_property_set_description(oc, "bql-watchdog-ms",
+ "Watchdog deadline for a Big QEMU Lock hold, in ms. "
+ "Default: 0 (disabled)");
+
object_class_property_add_bool(oc, "mem-merge",
machine_get_mem_merge, machine_set_mem_merge);
object_class_property_set_description(oc, "mem-merge",
diff --git a/include/system/cpus.h b/include/system/cpus.h
index 4aebec48705..45adcacdc38 100644
--- a/include/system/cpus.h
+++ b/include/system/cpus.h
@@ -41,4 +41,8 @@ void cpu_synchronize_all_post_reset(void);
void cpu_synchronize_all_post_init(void);
void cpu_synchronize_all_pre_loadvm(void);
+/* Watchdog deadline for a BQL hold. 0 disables the watchdog. */
+bool bql_watchdog_set_timeout_ms(uint64_t ms, Error **errp);
+uint64_t bql_watchdog_get_timeout_ms(void);
+
#endif
diff --git a/qemu-options.hx b/qemu-options.hx
index aae51823d70..2809fe42fb9 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -32,6 +32,7 @@ DEF("machine", HAS_ARG, QEMU_OPTION_machine, \
" vmport=on|off|auto controls emulation of vmport (default: auto)\n"
" dump-guest-core=on|off include guest memory in a core dump (default=on)\n"
" mem-merge=on|off controls memory merge support (default: on)\n"
+ " bql-watchdog-ms=ms report a Big QEMU Lock held longer than ms (default: 0, off)\n"
" aes-key-wrap=on|off controls support for AES key wrapping (default=on)\n"
" dea-key-wrap=on|off controls support for DEA key wrapping (default=on)\n"
" suppress-vmdesc=on|off disables self-describing migration (default=off)\n"
@@ -84,6 +85,11 @@ SRST
supported by the host, de-duplicates identical memory pages
among VMs instances (enabled by default).
+ ``bql-watchdog-ms=ms``
+ Report when the Big QEMU Lock has been held for more than ``ms``
+ milliseconds, up to a minute. The default is 0, which disables the
+ watchdog.
+
``aes-key-wrap=on|off``
Enables or disables AES key wrapping support on s390-ccw hosts.
This feature controls whether AES wrapping keys will be created
diff --git a/system/cpus.c b/system/cpus.c
index 5c2bdc3f3d0..fe7f47ab1c3 100644
--- a/system/cpus.c
+++ b/system/cpus.c
@@ -36,6 +36,7 @@
#include "exec/cpu-common.h"
#include "qemu/thread.h"
#include "qemu/main-loop.h"
+#include "qemu/error-report.h"
#include "qemu/plugin.h"
#include "system/cpus.h"
#include "qemu/guest-random.h"
@@ -419,12 +420,129 @@ static QemuCond qemu_cpu_cond;
/* system init */
static QemuCond qemu_pause_cond;
+/*
+ * Advanced by bql_update_status() below, so odd while the BQL is held and
+ * even while it is free. A sampler that sees one odd value throughout its
+ * window knows a single owner held the lock for the whole window: any
+ * handover would have advanced it twice.
+ */
+static unsigned int bql_seq;
+
+/* Who advanced bql_seq to its current odd value, 0 while the BQL is free */
+static int bql_owner_tid;
+
+static int bql_this_tid(void)
+{
+ static __thread int tid;
+
+ if (tid) {
+ return tid;
+ }
+ tid = qemu_get_thread_id();
+ return tid;
+}
+
+/* Poll this many times per timeout, to bound how late the watchdog reacts */
+#define BQL_WATCHDOG_POLLS 4
+
+#define BQL_WATCHDOG_MAX_MS (60 * 1000)
+
+static QemuThread bql_watchdog_thread;
+static QemuSemaphore bql_watchdog_sem;
+static uint64_t bql_watchdog_timeout_ms;
+
+static void bql_report_thread_states(void)
+{
+ g_autofree char *states = qemu_thread_states();
+
+ if (!states) {
+ return;
+ }
+ qemu_flockfile(stderr);
+ fputs(states, stderr);
+ qemu_funlockfile(stderr);
+}
+
+static void *bql_watchdog_fn(void *opaque)
+{
+ unsigned int last = qatomic_read(&bql_seq);
+ unsigned int reported = 0;
+ int stuck_polls = 0;
+
+ for (;;) {
+ uint64_t timeout_ms = qatomic_read(&bql_watchdog_timeout_ms);
+ g_autofree char *when = NULL;
+ unsigned int seq;
+
+ if (!timeout_ms) {
+ /* Park rather than exit, so re-arming needs no new thread */
+ qemu_sem_wait(&bql_watchdog_sem);
+ last = qatomic_read(&bql_seq);
+ stuck_polls = 0;
+ continue;
+ }
+ qemu_sem_timedwait(&bql_watchdog_sem,
+ MAX(timeout_ms / BQL_WATCHDOG_POLLS, 1));
+
+ seq = qatomic_read(&bql_seq);
+ if (seq != last || !(seq & 1)) {
+ last = seq;
+ stuck_polls = 0;
+ continue;
+ }
+ if (++stuck_polls < BQL_WATCHDOG_POLLS) {
+ continue;
+ }
+ if (seq == reported) {
+ continue;
+ }
+ reported = seq;
+ if (!message_with_timestamp) {
+ when = real_time_iso8601();
+ }
+ error_report("%s%sBQL held for more than %" PRIu64 " ms by thread %d",
+ when ?: "", when ? " " : "", timeout_ms,
+ qatomic_read(&bql_owner_tid));
+ bql_report_thread_states();
+ }
+}
+
+uint64_t bql_watchdog_get_timeout_ms(void)
+{
+ return qatomic_read(&bql_watchdog_timeout_ms);
+}
+
+bool bql_watchdog_set_timeout_ms(uint64_t ms, Error **errp)
+{
+ static bool started;
+
+ assert(bql_locked());
+ if (ms > BQL_WATCHDOG_MAX_MS) {
+ error_setg(errp, "BQL watchdog deadline must not exceed %d ms",
+ BQL_WATCHDOG_MAX_MS);
+ return false;
+ }
+ qatomic_set(&bql_watchdog_timeout_ms, ms);
+ if (!ms) {
+ return true;
+ }
+ if (!started) {
+ started = true;
+ qemu_thread_create(&bql_watchdog_thread, "bql-watchdog",
+ bql_watchdog_fn, NULL, QEMU_THREAD_DETACHED);
+ } else {
+ qemu_sem_post(&bql_watchdog_sem);
+ }
+ return true;
+}
+
void qemu_init_cpu_loop(void)
{
qemu_init_sigbus();
qemu_cond_init(&qemu_cpu_cond);
qemu_cond_init(&qemu_pause_cond);
qemu_mutex_init(&bql);
+ qemu_sem_init(&bql_watchdog_sem, 0);
qemu_thread_get_self(&io_thread);
}
@@ -528,6 +646,8 @@ void bql_update_status(bool locked)
/* This function should only be used when an update happened.. */
assert(bql_locked() != locked);
set_bql_locked(locked);
+ qatomic_set(&bql_owner_tid, locked ? bql_this_tid() : 0);
+ qatomic_set(&bql_seq, qatomic_read(&bql_seq) + 1);
}
static uint32_t bql_unlock_blocked;
--
2.53.0
^ permalink raw reply [relevance 2%]
* [PATCH hci-8.0 08/12] oslib: report each thread's program counter #VSTOR-144507
2026-09-16 13:33 4% [PATCH hci-8.0 00/12] system: add a watchdog for the Big QEMU Lock Denis V. Lunev
` (2 preceding siblings ...)
2026-09-16 13:33 8% ` [PATCH hci-8.0 06/12] oslib: report kernel stacks in " Denis V. Lunev
@ 2026-09-16 13:33 2% ` Denis V. Lunev
2026-09-16 13:33 2% ` [PATCH hci-8.0 09/12] system/cpus: report a BQL held past a deadline #VSTOR-144507 Denis V. Lunev
4 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-16 13:33 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
A kernel stack describes a thread asleep in a syscall and says nothing
about one spinning in userspace, which is the other way a thread stops
making progress.
Ask each thread where it is by signalling it and reading the program
counter out of the signal frame. That is a register read and a store,
which a handler may do, unlike unwinding: backtrace() is not async
signal safe and takes the loader lock the first time it runs.
Ask all of them at once, once every passive read is taken. The signal
interrupts whatever a thread was blocked in, so probing earlier would
make the kernel stack and the wait channel describe the interruption
instead of the hang, and probing one at a time would spend the timeout
again for every thread that never answers. Insert the answers from the
back, so the offsets recorded during the reads still point where they
did.
A thread that never answers within the deadline can still run the
handler later, once whatever held it lets go, and by then the slots it
would write to are gone. Count handlers between entry and exit,
withdraw the slots when the deadline expires and wait for the last
handler out before returning to the caller that frees them. A handler
that arrives after the withdrawal finds nothing to write to.
Where the counter sits in the signal frame is a host property, so it is
answered from host/include like the other per-host questions, with a
generic fallback that reports no counter and leaves the host otherwise
untouched. The Linux hosts covered are the ones whose signal frames
linux-user already describes in host-signal.h.
Name the address through dladdr() where there is a symbol for it. There
often is not, because dladdr() sees exported symbols only and a thread
parked inside a library sits in its internals, so fall back to the
object and the offset within it. An offset resolves against the library
afterwards; the raw address does not survive the process.
Cc: Paolo Bonzini <pbonzini@redhat.com>
Cc: Richard Henderson <richard.henderson@linaro.org>
Cc: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>
Cc: Zhao Liu <zhao1.liu@intel.com>
Cc: Daniel P. Berrangé <berrange@redhat.com>
Cc: Marc-André Lureau <marcandre.lureau@redhat.com>
Cc: Pierrick Bouvier <pierrick.bouvier@oss.qualcomm.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
host/include/aarch64/host/signal-pc.h | 20 +++
host/include/generic/host/signal-pc.h | 11 ++
host/include/loongarch64/host/signal-pc.h | 20 +++
host/include/ppc64/host/signal-pc.h | 22 +++
host/include/riscv/host/signal-pc.h | 20 +++
host/include/s390x/host/signal-pc.h | 20 +++
host/include/x86_64/host/signal-pc.h | 20 +++
include/qemu/osdep.h | 7 +-
meson.build | 2 +
util/oslib-posix.c | 181 +++++++++++++++++++++-
10 files changed, 318 insertions(+), 5 deletions(-)
create mode 100644 host/include/aarch64/host/signal-pc.h
create mode 100644 host/include/generic/host/signal-pc.h
create mode 100644 host/include/loongarch64/host/signal-pc.h
create mode 100644 host/include/ppc64/host/signal-pc.h
create mode 100644 host/include/riscv/host/signal-pc.h
create mode 100644 host/include/s390x/host/signal-pc.h
create mode 100644 host/include/x86_64/host/signal-pc.h
diff --git a/host/include/aarch64/host/signal-pc.h b/host/include/aarch64/host/signal-pc.h
new file mode 100644
index 00000000000..fe60ed1c96c
--- /dev/null
+++ b/host/include/aarch64/host/signal-pc.h
@@ -0,0 +1,20 @@
+/*
+ * Program counter of an interrupted aarch64 thread, from its signal frame.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef AARCH64_HOST_SIGNAL_PC_H
+#define AARCH64_HOST_SIGNAL_PC_H
+
+#ifdef CONFIG_LINUX
+#define HAVE_HOST_SIGNAL_PC 1
+
+static inline uintptr_t host_signal_pc(const ucontext_t *uc)
+{
+ return uc->uc_mcontext.pc;
+}
+#else
+#define HAVE_HOST_SIGNAL_PC 0
+#endif
+
+#endif
diff --git a/host/include/generic/host/signal-pc.h b/host/include/generic/host/signal-pc.h
new file mode 100644
index 00000000000..8ce3822069a
--- /dev/null
+++ b/host/include/generic/host/signal-pc.h
@@ -0,0 +1,11 @@
+/*
+ * No host specific way to read the program counter from a signal frame.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HOST_SIGNAL_PC_H
+#define HOST_SIGNAL_PC_H
+
+#define HAVE_HOST_SIGNAL_PC 0
+
+#endif
diff --git a/host/include/loongarch64/host/signal-pc.h b/host/include/loongarch64/host/signal-pc.h
new file mode 100644
index 00000000000..ccb2ac1c56f
--- /dev/null
+++ b/host/include/loongarch64/host/signal-pc.h
@@ -0,0 +1,20 @@
+/*
+ * Program counter of an interrupted loongarch64 thread, from its signal frame.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef LOONGARCH64_HOST_SIGNAL_PC_H
+#define LOONGARCH64_HOST_SIGNAL_PC_H
+
+#ifdef CONFIG_LINUX
+#define HAVE_HOST_SIGNAL_PC 1
+
+static inline uintptr_t host_signal_pc(const ucontext_t *uc)
+{
+ return uc->uc_mcontext.__pc;
+}
+#else
+#define HAVE_HOST_SIGNAL_PC 0
+#endif
+
+#endif
diff --git a/host/include/ppc64/host/signal-pc.h b/host/include/ppc64/host/signal-pc.h
new file mode 100644
index 00000000000..1473570a39a
--- /dev/null
+++ b/host/include/ppc64/host/signal-pc.h
@@ -0,0 +1,22 @@
+/*
+ * Program counter of an interrupted ppc64 thread, from its signal frame.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef PPC64_HOST_SIGNAL_PC_H
+#define PPC64_HOST_SIGNAL_PC_H
+
+#ifdef CONFIG_LINUX
+#include <asm/ptrace.h>
+
+#define HAVE_HOST_SIGNAL_PC 1
+
+static inline uintptr_t host_signal_pc(const ucontext_t *uc)
+{
+ return uc->uc_mcontext.gp_regs[PT_NIP];
+}
+#else
+#define HAVE_HOST_SIGNAL_PC 0
+#endif
+
+#endif
diff --git a/host/include/riscv/host/signal-pc.h b/host/include/riscv/host/signal-pc.h
new file mode 100644
index 00000000000..8db92513465
--- /dev/null
+++ b/host/include/riscv/host/signal-pc.h
@@ -0,0 +1,20 @@
+/*
+ * Program counter of an interrupted riscv thread, from its signal frame.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef RISCV_HOST_SIGNAL_PC_H
+#define RISCV_HOST_SIGNAL_PC_H
+
+#ifdef CONFIG_LINUX
+#define HAVE_HOST_SIGNAL_PC 1
+
+static inline uintptr_t host_signal_pc(const ucontext_t *uc)
+{
+ return uc->uc_mcontext.__gregs[REG_PC];
+}
+#else
+#define HAVE_HOST_SIGNAL_PC 0
+#endif
+
+#endif
diff --git a/host/include/s390x/host/signal-pc.h b/host/include/s390x/host/signal-pc.h
new file mode 100644
index 00000000000..6ad809c9116
--- /dev/null
+++ b/host/include/s390x/host/signal-pc.h
@@ -0,0 +1,20 @@
+/*
+ * Program counter of an interrupted s390x thread, from its signal frame.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef S390X_HOST_SIGNAL_PC_H
+#define S390X_HOST_SIGNAL_PC_H
+
+#ifdef CONFIG_LINUX
+#define HAVE_HOST_SIGNAL_PC 1
+
+static inline uintptr_t host_signal_pc(const ucontext_t *uc)
+{
+ return uc->uc_mcontext.psw.addr;
+}
+#else
+#define HAVE_HOST_SIGNAL_PC 0
+#endif
+
+#endif
diff --git a/host/include/x86_64/host/signal-pc.h b/host/include/x86_64/host/signal-pc.h
new file mode 100644
index 00000000000..c94ad2e976f
--- /dev/null
+++ b/host/include/x86_64/host/signal-pc.h
@@ -0,0 +1,20 @@
+/*
+ * Program counter of an interrupted x86_64 thread, from its signal frame.
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef X86_64_HOST_SIGNAL_PC_H
+#define X86_64_HOST_SIGNAL_PC_H
+
+#ifdef CONFIG_LINUX
+#define HAVE_HOST_SIGNAL_PC 1
+
+static inline uintptr_t host_signal_pc(const ucontext_t *uc)
+{
+ return uc->uc_mcontext.gregs[REG_RIP];
+}
+#else
+#define HAVE_HOST_SIGNAL_PC 0
+#endif
+
+#endif
diff --git a/include/qemu/osdep.h b/include/qemu/osdep.h
index 31a13926013..8aebb639e4c 100644
--- a/include/qemu/osdep.h
+++ b/include/qemu/osdep.h
@@ -657,9 +657,10 @@ bool qemu_debugger_attached(void);
* qemu_thread_states:
*
* Returns a newly allocated, newline terminated description of every thread
- * in this process apart from the caller: its id, name, scheduler state and
- * kernel stack, or NULL on a host that cannot report any of that. Where the
- * kernel stack is privileged the description falls back to a wait channel.
+ * in this process apart from the caller: its id, name, scheduler state, the
+ * program counter it stopped at and its kernel stack, or NULL on a host that
+ * cannot report any of that. A privileged kernel stack falls back to a wait
+ * channel, and a host with no way to read a program counter omits it.
*/
char *qemu_thread_states(void);
diff --git a/meson.build b/meson.build
index 21ca6d0e2a9..1e799f7346a 100644
--- a/meson.build
+++ b/meson.build
@@ -2749,6 +2749,8 @@ config_host_data.set('HAVE_GLIB_WITH_SLICE_ALLOCATOR', glib_has_gslice)
config_host_data.set('HAVE_GLIB_WITH_ALIGNED_ALLOC', glib_has_aligned_alloc)
config_host_data.set('HAVE_OPENPTY', cc.has_function('openpty', dependencies: util))
config_host_data.set('HAVE_STRCHRNUL', cc.has_function('strchrnul', prefix: osdep_prefix))
+config_host_data.set('CONFIG_DLADDR', cc.has_function('dladdr',
+ prefix: osdep_prefix + '#include <dlfcn.h>'))
config_host_data.set('HAVE_SYSTEM_FUNCTION', cc.has_function('system', prefix: '#include <stdlib.h>'))
if rbd.found()
config_host_data.set('HAVE_RBD_NAMESPACE_EXISTS',
diff --git a/util/oslib-posix.c b/util/oslib-posix.c
index d5aa7d398f1..c960df6a313 100644
--- a/util/oslib-posix.c
+++ b/util/oslib-posix.c
@@ -58,8 +58,13 @@
#include <lwp.h>
#endif
+#ifdef CONFIG_DLADDR
+#include <dlfcn.h>
+#endif
+
#include "qemu/memalign.h"
#include "qemu/mmap-alloc.h"
+#include "host/signal-pc.h"
#define MAX_MEM_PREALLOC_THREAD_COUNT 16
@@ -168,8 +173,164 @@ static bool thread_stacks_readable(void)
return readable;
}
-static void append_thread_state(GString *out, const char *tid)
+/* Where a thread was, and where its line belongs in the report */
+typedef struct {
+ int tid;
+ size_t at;
+ void *pc;
+ int done;
+} ThreadPc;
+
+#if HAVE_HOST_SIGNAL_PC
+
+/* How long to wait for the answers, however many threads were asked */
+#define THREAD_PC_WAIT_MS 100
+
+/*
+ * Published to the handler, which runs in whichever thread was interrupted
+ * and so may only touch this with atomics: every qemu synchronisation
+ * primitive is built on a mutex, and none of them may be taken here.
+ * pc_handlers counts handlers between entry and exit, so the slots can be
+ * withdrawn and the last handler waited out before the caller frees them.
+ */
+static ThreadPc *pc_slots;
+static int pc_nslots;
+static int pc_handlers;
+
+static void thread_pc_handler(int sig, siginfo_t *si, void *ctx)
+{
+ ThreadPc *slots;
+ int me, i, n;
+
+ qatomic_inc(&pc_handlers);
+ smp_mb__after_rmw();
+ slots = qatomic_load_acquire(&pc_slots);
+ if (slots) {
+ n = qatomic_read(&pc_nslots);
+ me = qemu_get_thread_id();
+ for (i = 0; i < n; i++) {
+ if (qatomic_read(&slots[i].tid) == me) {
+ slots[i].pc = (void *)host_signal_pc(ctx);
+ qatomic_store_release(&slots[i].done, 1);
+ break;
+ }
+ }
+ }
+ qatomic_dec(&pc_handlers);
+}
+
+static bool thread_pc_armed(void)
+{
+ static int armed = -1;
+ struct sigaction sa;
+
+ if (armed >= 0) {
+ return armed;
+ }
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_sigaction = thread_pc_handler;
+ sa.sa_flags = SA_SIGINFO | SA_RESTART;
+ sigemptyset(&sa.sa_mask);
+ armed = sigaction(QEMU_SIG_INTROSPECT, &sa, NULL) == 0;
+ return armed;
+}
+
+/*
+ * Ask every thread at once. Asking one at a time would spend the timeout
+ * again for each thread that never answers, and the threads that never
+ * answer are the ones a hung qemu has most of.
+ */
+static void thread_pc_collect(GArray *slots)
{
+ int64_t deadline;
+ guint i;
+ int left;
+
+ if (!slots->len || !thread_pc_armed()) {
+ return;
+ }
+ qatomic_set(&pc_nslots, slots->len);
+ qatomic_store_release(&pc_slots, &g_array_index(slots, ThreadPc, 0));
+
+ for (i = 0; i < slots->len; i++) {
+ ThreadPc *slot = &g_array_index(slots, ThreadPc, i);
+
+ if (qemu_kill_thread(slot->tid, QEMU_SIG_INTROSPECT) < 0) {
+ qatomic_store_release(&slot->done, -1);
+ }
+ }
+
+ deadline = g_get_monotonic_time() + THREAD_PC_WAIT_MS * 1000;
+ do {
+ left = 0;
+ for (i = 0; i < slots->len; i++) {
+ if (!qatomic_load_acquire(&g_array_index(slots, ThreadPc,
+ i).done)) {
+ left++;
+ }
+ }
+ if (!left) {
+ break;
+ }
+ g_usleep(1000);
+ } while (g_get_monotonic_time() < deadline);
+
+ qatomic_set(&pc_slots, NULL);
+ /* Pairs with the barrier in thread_pc_handler() */
+ smp_mb();
+ while (qatomic_read(&pc_handlers)) {
+ g_usleep(1000);
+ }
+}
+
+static void insert_thread_pc(GString *out, const ThreadPc *slot)
+{
+ g_autofree char *line = NULL;
+ void *pc = slot->pc;
+#ifdef CONFIG_DLADDR
+ Dl_info info = { };
+#endif
+
+ if (qatomic_read(&slot->done) != 1 || !pc) {
+ return;
+ }
+#ifdef CONFIG_DLADDR
+ if (dladdr(pc, &info) && info.dli_sname) {
+ line = g_strdup_printf(" pc %p %s+0x%tx\n", pc, info.dli_sname,
+ (char *)pc - (char *)info.dli_saddr);
+ } else if (info.dli_fname && info.dli_fbase) {
+ /*
+ * dladdr() only knows exported symbols, so an address inside a
+ * library's own internals has none. The object and the offset
+ * within it still resolve later, the raw address does not.
+ */
+ g_autofree char *obj = g_path_get_basename(info.dli_fname);
+
+ line = g_strdup_printf(" pc %p %s+0x%tx\n", pc, obj,
+ (char *)pc - (char *)info.dli_fbase);
+ }
+#endif
+ if (!line) {
+ line = g_strdup_printf(" pc %p\n", pc);
+ }
+ g_string_insert(out, slot->at, line);
+}
+
+#else
+
+static void thread_pc_collect(GArray *slots)
+{
+}
+
+static void insert_thread_pc(GString *out, const ThreadPc *slot)
+{
+}
+
+#endif /* HAVE_HOST_SIGNAL_PC */
+
+static void append_thread_state(GString *out, GArray *slots, const char *tid)
+{
+ ThreadPc slot = { };
g_autofree char *comm = read_task_file(tid, "comm");
g_autofree char *stat = NULL;
g_autofree char *stack = NULL;
@@ -194,6 +355,10 @@ static void append_thread_state(GString *out, const char *tid)
}
g_string_append_printf(out, " %-7s %-16s %c\n", tid, comm, state);
+ slot.tid = atoi(tid);
+ slot.at = out->len;
+ g_array_append_val(slots, slot);
+
/* A running thread has no settled kernel stack to unwind */
if (state != 'R' && thread_stacks_readable()) {
stack = read_task_file(tid, "stack");
@@ -220,8 +385,10 @@ static void append_thread_state(GString *out, const char *tid)
char *qemu_thread_states(void)
{
g_autoptr(GDir) dir = g_dir_open("/proc/self/task", 0, NULL);
+ g_autoptr(GArray) slots = g_array_new(FALSE, TRUE, sizeof(ThreadPc));
const char *tid;
GString *out;
+ guint i;
if (!dir) {
return NULL;
@@ -232,13 +399,23 @@ char *qemu_thread_states(void)
out = g_string_new(NULL);
while ((tid = g_dir_read_name(dir))) {
if (atoi(tid) != qemu_get_thread_id()) {
- append_thread_state(out, tid);
+ append_thread_state(out, slots, tid);
}
}
if (!out->len) {
g_string_free(out, TRUE);
return NULL;
}
+
+ /*
+ * Only now, with every passive read taken, disturb the threads. Insert
+ * from the back so the offsets recorded above stay where they were.
+ */
+ thread_pc_collect(slots);
+ for (i = slots->len; i > 0; i--) {
+ insert_thread_pc(out, &g_array_index(slots, ThreadPc, i - 1));
+ }
+
return g_string_free(out, FALSE);
}
--
2.53.0
^ permalink raw reply [relevance 2%]
* [PATCH hci-8.0 06/12] oslib: report kernel stacks in qemu_thread_states() #VSTOR-144507
2026-09-16 13:33 4% [PATCH hci-8.0 00/12] system: add a watchdog for the Big QEMU Lock Denis V. Lunev
2026-09-16 13:33 4% ` [PATCH hci-8.0 02/12] error-report: make real_time_iso8601() public #VSTOR-144507 Denis V. Lunev
2026-09-16 13:33 5% ` [PATCH hci-8.0 05/12] oslib: add qemu_thread_states() #VSTOR-144507 Denis V. Lunev
@ 2026-09-16 13:33 8% ` Denis V. Lunev
2026-09-16 13:33 2% ` [PATCH hci-8.0 08/12] oslib: report each thread's program counter #VSTOR-144507 Denis V. Lunev
2026-09-16 13:33 2% ` [PATCH hci-8.0 09/12] system/cpus: report a BQL held past a deadline #VSTOR-144507 Denis V. Lunev
4 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-16 13:33 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
A wait channel names the primitive a thread is parked on but not the
path that got it there, and for a thread wedged in a syscall that path
is the answer.
Read /proc/tid/stack when the report is built, and keep the wait
channel for when it cannot be. The file is restricted to CAP_SYS_ADMIN
in the init namespace, so ask once rather than failing per thread, and
say why the report came back thinner. Skip a running thread, which has
no settled stack to unwind, and cap the frames, because the deep end of
a stack is rarely the part that explains the hang.
Cc: Paolo Bonzini <pbonzini@redhat.com>
Cc: Richard Henderson <richard.henderson@linaro.org>
Cc: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>
Cc: Zhao Liu <zhao1.liu@intel.com>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
include/qemu/osdep.h | 4 ++--
util/oslib-posix.c | 52 +++++++++++++++++++++++++++++++++++++++++---
2 files changed, 51 insertions(+), 5 deletions(-)
diff --git a/include/qemu/osdep.h b/include/qemu/osdep.h
index d3e85c606f0..e118f12693b 100644
--- a/include/qemu/osdep.h
+++ b/include/qemu/osdep.h
@@ -649,8 +649,8 @@ bool qemu_debugger_attached(void);
*
* Returns a newly allocated, newline terminated description of every thread
* in this process apart from the caller: its id, name, scheduler state and
- * the wait channel it is parked on, or NULL on a host that cannot report any
- * of that.
+ * kernel stack, or NULL on a host that cannot report any of that. Where the
+ * kernel stack is privileged the description falls back to a wait channel.
*/
char *qemu_thread_states(void);
diff --git a/util/oslib-posix.c b/util/oslib-posix.c
index c4856f8cba5..d5aa7d398f1 100644
--- a/util/oslib-posix.c
+++ b/util/oslib-posix.c
@@ -145,13 +145,39 @@ static char *read_task_file(const char *tid, const char *name)
return buf;
}
+#define THREAD_STACK_FRAMES 12
+
+/*
+ * /proc/tid/stack is restricted to CAP_SYS_ADMIN in the init namespace, so
+ * ask once instead of failing per thread, and say why the report is thinner.
+ */
+static bool thread_stacks_readable(void)
+{
+ static int readable = -1;
+ g_autofree char *path = NULL;
+ g_autofree char *buf = NULL;
+
+ if (readable >= 0) {
+ return readable;
+ }
+ path = g_strdup_printf("/proc/self/task/%d/stack", qemu_get_thread_id());
+ readable = g_file_get_contents(path, &buf, NULL, NULL);
+ if (!readable) {
+ warn_report("kernel stacks need CAP_SYS_ADMIN, falling back to wchan");
+ }
+ return readable;
+}
+
static void append_thread_state(GString *out, const char *tid)
{
g_autofree char *comm = read_task_file(tid, "comm");
g_autofree char *stat = NULL;
+ g_autofree char *stack = NULL;
g_autofree char *wchan = NULL;
+ g_auto(GStrv) lines = NULL;
char state = '?';
char *rparen;
+ int i;
if (!comm) {
return;
@@ -168,9 +194,26 @@ static void append_thread_state(GString *out, const char *tid)
}
g_string_append_printf(out, " %-7s %-16s %c\n", tid, comm, state);
- wchan = read_task_file(tid, "wchan");
- if (wchan && wchan[0]) {
- g_string_append_printf(out, " wchan %s\n", wchan);
+ /* A running thread has no settled kernel stack to unwind */
+ if (state != 'R' && thread_stacks_readable()) {
+ stack = read_task_file(tid, "stack");
+ }
+ if (!stack) {
+ wchan = read_task_file(tid, "wchan");
+ if (wchan && wchan[0]) {
+ g_string_append_printf(out, " wchan %s\n", wchan);
+ }
+ return;
+ }
+
+ lines = g_strsplit(stack, "\n", 0);
+ for (i = 0; i < THREAD_STACK_FRAMES && lines[i]; i++) {
+ if (lines[i][0]) {
+ g_string_append_printf(out, " %s\n", lines[i]);
+ }
+ }
+ if (lines[i]) {
+ g_string_append(out, " ...\n");
}
}
@@ -183,6 +226,9 @@ char *qemu_thread_states(void)
if (!dir) {
return NULL;
}
+ /* Emit the permission notice ahead of the listing, not inside it */
+ thread_stacks_readable();
+
out = g_string_new(NULL);
while ((tid = g_dir_read_name(dir))) {
if (atoi(tid) != qemu_get_thread_id()) {
--
2.53.0
^ permalink raw reply [relevance 8%]
* [PATCH hci-8.0 05/12] oslib: add qemu_thread_states() #VSTOR-144507
2026-09-16 13:33 4% [PATCH hci-8.0 00/12] system: add a watchdog for the Big QEMU Lock Denis V. Lunev
2026-09-16 13:33 4% ` [PATCH hci-8.0 02/12] error-report: make real_time_iso8601() public #VSTOR-144507 Denis V. Lunev
@ 2026-09-16 13:33 5% ` Denis V. Lunev
2026-09-16 13:33 8% ` [PATCH hci-8.0 06/12] oslib: report kernel stacks in " Denis V. Lunev
` (2 subsequent siblings)
4 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-16 13:33 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
A watchdog that reports a hang wants to say what every thread was doing,
and that answer is host specific. Put it beside the other process
introspection helpers rather than in the code that happens to need it
first.
Describe each thread by id, name, scheduler state and the wait channel
it is parked on, all of which Linux exposes in /proc/self/task without
privilege. That is already enough to separate a thread blocked on a
userspace lock, which waits in a futex, from one blocked in the kernel,
which names the primitive it sits on. Skip the caller, which is
reporting rather than hanging.
Hosts with no way to answer return NULL, leaving the caller to print
its own message and nothing more.
Cc: Paolo Bonzini <pbonzini@redhat.com>
Cc: Richard Henderson <richard.henderson@linaro.org>
Cc: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com>
Cc: Zhao Liu <zhao1.liu@intel.com>
Cc: Stefan Weil <sw@weilnetz.de>
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
include/qemu/osdep.h | 10 +++++++
util/oslib-posix.c | 71 ++++++++++++++++++++++++++++++++++++++++++++
util/oslib-win32.c | 5 ++++
3 files changed, 86 insertions(+)
diff --git a/include/qemu/osdep.h b/include/qemu/osdep.h
index 801dec40467..d3e85c606f0 100644
--- a/include/qemu/osdep.h
+++ b/include/qemu/osdep.h
@@ -644,6 +644,16 @@ int qemu_get_thread_id(void);
*/
bool qemu_debugger_attached(void);
+/**
+ * qemu_thread_states:
+ *
+ * Returns a newly allocated, newline terminated description of every thread
+ * in this process apart from the caller: its id, name, scheduler state and
+ * the wait channel it is parked on, or NULL on a host that cannot report any
+ * of that.
+ */
+char *qemu_thread_states(void);
+
/**
* qemu_kill_thread:
* @tid: thread id.
diff --git a/util/oslib-posix.c b/util/oslib-posix.c
index e3593f57eee..c4856f8cba5 100644
--- a/util/oslib-posix.c
+++ b/util/oslib-posix.c
@@ -134,6 +134,77 @@ bool qemu_debugger_attached(void)
#endif
}
+#ifdef CONFIG_LINUX
+
+static char *read_task_file(const char *tid, const char *name)
+{
+ g_autofree char *path = g_strdup_printf("/proc/self/task/%s/%s", tid, name);
+ char *buf = NULL;
+
+ g_file_get_contents(path, &buf, NULL, NULL);
+ return buf;
+}
+
+static void append_thread_state(GString *out, const char *tid)
+{
+ g_autofree char *comm = read_task_file(tid, "comm");
+ g_autofree char *stat = NULL;
+ g_autofree char *wchan = NULL;
+ char state = '?';
+ char *rparen;
+
+ if (!comm) {
+ return;
+ }
+ g_strchomp(comm);
+
+ stat = read_task_file(tid, "stat");
+ if (stat) {
+ /* comm can hold anything, ')' included, so scan from the right */
+ rparen = strrchr(stat, ')');
+ if (rparen && rparen[1] == ' ') {
+ state = rparen[2];
+ }
+ }
+ g_string_append_printf(out, " %-7s %-16s %c\n", tid, comm, state);
+
+ wchan = read_task_file(tid, "wchan");
+ if (wchan && wchan[0]) {
+ g_string_append_printf(out, " wchan %s\n", wchan);
+ }
+}
+
+char *qemu_thread_states(void)
+{
+ g_autoptr(GDir) dir = g_dir_open("/proc/self/task", 0, NULL);
+ const char *tid;
+ GString *out;
+
+ if (!dir) {
+ return NULL;
+ }
+ out = g_string_new(NULL);
+ while ((tid = g_dir_read_name(dir))) {
+ if (atoi(tid) != qemu_get_thread_id()) {
+ append_thread_state(out, tid);
+ }
+ }
+ if (!out->len) {
+ g_string_free(out, TRUE);
+ return NULL;
+ }
+ return g_string_free(out, FALSE);
+}
+
+#else
+
+char *qemu_thread_states(void)
+{
+ return NULL;
+}
+
+#endif /* CONFIG_LINUX */
+
int qemu_kill_thread(int tid, int sig)
{
#if defined(__linux__)
diff --git a/util/oslib-win32.c b/util/oslib-win32.c
index 4a6e66d021f..af39bab5620 100644
--- a/util/oslib-win32.c
+++ b/util/oslib-win32.c
@@ -233,6 +233,11 @@ bool qemu_debugger_attached(void)
return IsDebuggerPresent();
}
+char *qemu_thread_states(void)
+{
+ return NULL;
+}
+
char *
qemu_get_local_state_dir(void)
{
--
2.53.0
^ permalink raw reply [relevance 5%]
* [PATCH hci-8.0 02/12] error-report: make real_time_iso8601() public #VSTOR-144507
2026-09-16 13:33 4% [PATCH hci-8.0 00/12] system: add a watchdog for the Big QEMU Lock Denis V. Lunev
@ 2026-09-16 13:33 4% ` Denis V. Lunev
2026-09-16 13:33 5% ` [PATCH hci-8.0 05/12] oslib: add qemu_thread_states() #VSTOR-144507 Denis V. Lunev
` (3 subsequent siblings)
4 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-16 13:33 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
To be reused in the following commit.
Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Message-ID: <20260201173633.413934-3-vsementsov@yandex-team.ru>
(cherry picked from commit 2eed5472ec424d217786af97e6212e68e49c5d0b)
Backported because the BQL watchdog stamps its report itself: the
report goes to stderr from a thread, not through error_report()'s
timestamp.
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
include/qemu/error-report.h | 6 ++++++
util/error-report.c | 3 +--
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/include/qemu/error-report.h b/include/qemu/error-report.h
index 3ae2357fda5..dc423d36071 100644
--- a/include/qemu/error-report.h
+++ b/include/qemu/error-report.h
@@ -74,4 +74,10 @@ extern bool message_with_timestamp;
extern bool error_with_guestname;
extern const char *error_guest_name;
+/*
+ * Return current datetime in ISO 8601 format.
+ * Caller is responsible to g_free() the returned string.
+ */
+char *real_time_iso8601(void);
+
#endif
diff --git a/util/error-report.c b/util/error-report.c
index 1b17c11de19..d6b7448183a 100644
--- a/util/error-report.c
+++ b/util/error-report.c
@@ -169,8 +169,7 @@ static void print_loc(void)
}
}
-static char *
-real_time_iso8601(void)
+char *real_time_iso8601(void)
{
g_autoptr(GDateTime) dt = g_date_time_new_now_utc();
return g_date_time_format_iso8601(dt);
--
2.53.0
^ permalink raw reply [relevance 4%]
* [PATCH hci-8.0 00/12] system: add a watchdog for the Big QEMU Lock
@ 2026-09-16 13:33 4% Denis V. Lunev
2026-09-16 13:33 4% ` [PATCH hci-8.0 02/12] error-report: make real_time_iso8601() public #VSTOR-144507 Denis V. Lunev
` (4 more replies)
0 siblings, 5 replies; 200+ results
From: Denis V. Lunev @ 2026-09-16 13:33 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
A vCPU thread reacquires the BQL on every exit that needs userspace. A
holder that keeps it for tens of seconds stops every vCPU, the guest
reports a soft lockup or panics, and qemu itself records nothing. The
guest log is the only evidence, and it cannot say whether the hypervisor
held the lock or the guest wedged on its own.
This series adds a BQL watchdog. A sampler thread watches a sequence
counter that bql_update_status() advances on every acquire and release.
One odd value across the whole window means one owner held the lock for
the whole window. On expiry the watchdog names the holder and prints the
state of every other thread:
qemu-system-x86_64: 2026-09-16T13:21:04Z BQL held for more than 100 ms by thread 927240
927240 qemu-system-x86 S
pc 0x7f35a4871e06 ppoll+0xa6
[<0>] poll_schedule_timeout.constprop.0+0x3a/0x80
[<0>] do_sys_poll+0x1eb/0x290
[<0>] __x64_sys_ppoll+0xe0/0x160
927242 qemu-system-x86 S
pc 0x7f35a487c57b syscall+0x1b
[<0>] futex_do_wait+0x38/0x70
[<0>] __futex_wait+0x92/0x100
Without CAP_SYS_ADMIN the kernel stack collapses to a single wchan line.
The pc line comes from signalling each thread and reading the program
counter out of its signal frame, which is what tells a thread spinning
in userspace from one asleep in a syscall.
The watchdog is off by default and stays quiet under a debugger. Two
machine properties drive it: bql-watchdog-ms sets the deadline, on the
command line or over qom-set at any time, and bql-watchdog-abort turns
the report into a core dump for hosts where the answer is worth more
than the guest.
Patches 3 to 8 are the pieces: a fix for bql_locked across the timed
condvar wait, a debugger check, the per-thread state reader and its
kernel stack and program counter extensions, and the signal reserved for
the latter. Patches 9 to 11 are the watchdog itself. Patch 12 is a
functional test that holds the BQL through an HMP qemu-io write to a
null-co node with a latency longer than the deadline.
This is the hci-8.0 port of the series posted upstream on 2026-09-15:
https://lore.kernel.org/qemu-devel/20260915203455.580999-1-den@openvz.org/
Patches 1 and 2 are the two upstream commits hci-8.0 does not carry and
the rest needs. Patch 1 gives bql_update_status(), which is where the
sequence counter hooks in, so patch 9 does not build without it; patch 2
makes real_time_iso8601() public, because the watchdog stamps its own
report rather than going through error_report(). Both are unmodified
cherry-picks.
Three things differ from the upstream posting, all because 10.1 lays the
tree out differently: the riscv signal-pc.h header goes in
host/include/riscv rather than host/include/riscv64, since meson maps
both riscv32 and riscv64 to host_arch 'riscv'; the functional test sits
in tests/functional rather than tests/functional/generic, and registers
in tests_generic_system there; and hw/core/machine.c has no
x-change-vmfd-on-reset property to anchor against. No code changed.
Built and tested on dev-cl04 with --enable-debug: func-quick passes,
including the two bql_watchdog subtests, and the report above is what
this build prints under -machine none,bql-watchdog-ms=100.
The program counter lives at a host specific offset in the signal frame,
so patch 8 reads it through a host/include header. The Linux headers for
aarch64, ppc64, riscv, s390x and loongarch64 repeat what linux-user's
host-signal.h already knows about those frames, but only the x86_64 one
has been compiled, here as upstream. The Windows half of patch 3 has not
been compiled either.
Signed-off-by: Denis V. Lunev <den@openvz.org>
Denis V. Lunev (10):
util/qemu-thread: track bql_locked across the timed condvar wait
#VSTOR-144507
oslib: add qemu_debugger_attached() #VSTOR-144507
oslib: add qemu_thread_states() #VSTOR-144507
oslib: report kernel stacks in qemu_thread_states() #VSTOR-144507
util/qemu-thread: reserve a signal for thread introspection
#VSTOR-144507
oslib: report each thread's program counter #VSTOR-144507
system/cpus: report a BQL held past a deadline #VSTOR-144507
system/cpus: hold off the BQL watchdog under a debugger #VSTOR-144507
system/cpus: add bql-watchdog-abort #VSTOR-144507
tests/functional: cover the BQL watchdog #VSTOR-144507
Peter Xu (1):
bql: Fix bql_locked status with condvar APIs #VSTOR-144507
Vladimir Sementsov-Ogievskiy (1):
error-report: make real_time_iso8601() public #VSTOR-144507
host/include/aarch64/host/signal-pc.h | 20 ++
host/include/generic/host/signal-pc.h | 11 +
host/include/loongarch64/host/signal-pc.h | 20 ++
host/include/ppc64/host/signal-pc.h | 22 ++
host/include/riscv/host/signal-pc.h | 20 ++
host/include/s390x/host/signal-pc.h | 20 ++
host/include/x86_64/host/signal-pc.h | 20 ++
hw/core/machine.c | 44 +++
include/qemu/error-report.h | 6 +
include/qemu/main-loop.h | 18 ++
include/qemu/osdep.h | 29 ++
include/system/cpus.h | 8 +
meson.build | 2 +
qemu-options.hx | 11 +
stubs/iothread-lock.c | 9 +
system/cpus.c | 157 ++++++++++-
tests/functional/meson.build | 1 +
tests/functional/test_bql_watchdog.py | 60 ++++
util/error-report.c | 3 +-
util/oslib-posix.c | 316 ++++++++++++++++++++++
util/oslib-win32.c | 10 +
util/qemu-thread-common.h | 7 +
util/qemu-thread-posix.c | 8 +-
util/qemu-thread-win32.c | 4 +-
24 files changed, 818 insertions(+), 8 deletions(-)
create mode 100644 host/include/aarch64/host/signal-pc.h
create mode 100644 host/include/generic/host/signal-pc.h
create mode 100644 host/include/loongarch64/host/signal-pc.h
create mode 100644 host/include/ppc64/host/signal-pc.h
create mode 100644 host/include/riscv/host/signal-pc.h
create mode 100644 host/include/s390x/host/signal-pc.h
create mode 100644 host/include/x86_64/host/signal-pc.h
create mode 100755 tests/functional/test_bql_watchdog.py
--
2.53.0
^ permalink raw reply [relevance 4%]
* [Devel] [PATCH vz10 v2 2/2] selftests: ve: introduce vtty pair teardown stress test
2026-09-16 12:31 3% [Devel] [PATCH vz10 v2 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
@ 2026-09-16 12:31 5% ` Eva Kurchatova
2026-09-18 7:11 0% ` Vasileios Almpanis
2026-09-22 17:47 3% ` Konstantin Khorenko
2026-09-22 17:47 6% ` [Devel] [PATCH vz10 v2 1/2] selftests: ve: grant the directories the files sit in Konstantin Khorenko
1 sibling, 2 replies; 200+ results
From: Eva Kurchatova @ 2026-09-16 12:31 UTC (permalink / raw)
To: khorenko; +Cc: devel
A vtty pair is created by the first master open and torn down by the
last close. The close racing the peer's close used to let both sides
decide they were the final one and call release_tty() twice, and a
master opening again while a teardown was in flight used to hand out a
reference to a tty_struct about to be freed.
Hammer both ends. The master comes from the vzctl ioctl, which does
not look the VE up. The slave is only reachable from inside the VE, as
TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
vttys index minor - 1, so the test builds a VE with cgroup2, sets
ve.veid to the cgroup it made, and runs the slave side there.
One of the symtoms of a buggy kernel is dmesg warning, not just an
eventual crash, so the test checks that. A crashed node should also
report test failure in CI, but that's less verbose, hence the check.
https://virtuozzo.atlassian.net/browse/VSTOR-144771
Feature: ve selftest
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/ve/.gitignore | 1 +
tools/testing/selftests/ve/Makefile | 1 +
tools/testing/selftests/ve/settings | 1 +
tools/testing/selftests/ve/ve_vtty_stress.c | 250 ++++++++++++++++++++
4 files changed, 253 insertions(+)
create mode 100644 tools/testing/selftests/ve/settings
create mode 100644 tools/testing/selftests/ve/ve_vtty_stress.c
diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index afa4c568c2c9..9d8d86093827 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,2 +1,3 @@
ve_ns_owner_test
ve_perms_test
+ve_vtty_stress
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index ec40cbc7b3a1..6cd5823c4d93 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
TEST_GEN_PROGS += ve_ns_owner_test
TEST_GEN_PROGS += ve_perms_test
+TEST_GEN_PROGS += ve_vtty_stress
include ../lib.mk
diff --git a/tools/testing/selftests/ve/settings b/tools/testing/selftests/ve/settings
new file mode 100644
index 000000000000..e2206265f67c
--- /dev/null
+++ b/tools/testing/selftests/ve/settings
@@ -0,0 +1 @@
+timeout=900
diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
new file mode 100644
index 000000000000..57edbdeef0c8
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_vtty_stress.c
@@ -0,0 +1,250 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_vtty_stress - close a vtty pair from both ends at once.
+ *
+ * A vtty pair is created by the first master open and torn down by the
+ * last close. A close racing the peer's close used to let both sides
+ * decide they were the final one and call release_tty() twice, and a
+ * master opened again while a teardown was still in flight used to hand
+ * out a reference to a tty_struct that was about to be freed. Either
+ * way the next close walked freed memory.
+ *
+ * The master comes from the vzctl ioctl, which does not look the VE up.
+ * The slave is only reachable from inside the VE, as TTY_MAJOR minor
+ * 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to vttys index
+ * minor - 1. So the test makes a VE with cgroup2, hammers the master
+ * from the host and the slave from a task inside it, and fails if the
+ * kernel logs a complaint about the teardown.
+ *
+ * Surviving is not a pass on its own: the first symptom is a warning,
+ * which stops nothing by itself.
+ *
+ * Usage: ve_vtty_stress [seconds] (default 600, or $VE_VTTY_SECS)
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <stdbool.h>
+#include <sys/ioctl.h>
+#include <linux/major.h>
+#include <sys/sysmacros.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "ve_selftest.h"
+#include "../kselftest.h"
+
+/* The ioctl argument; the trailing data[] the uapi struct has is unused. */
+struct vzctl_ve_configure {
+ unsigned int veid, key, val, size;
+};
+
+#define VZCTL_VE_CONFIGURE _IOW('.', 15, struct vzctl_ve_configure)
+#define VE_CONFIGURE_OPEN_TTY 5
+
+#define VTTY_IDX 0 /* the pair both ends work on */
+#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
+#define DEFAULT_SECS 600
+
+static int open_master(int ctl_fd, unsigned int veid)
+{
+ struct vzctl_ve_configure c = {
+ .veid = veid,
+ .key = VE_CONFIGURE_OPEN_TTY,
+ .val = VTTY_IDX,
+ };
+
+ return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
+}
+
+/*
+ * The slave end, inside the VE. Every error here is one the race is
+ * expected to produce: the pair is gone once the master has closed it.
+ */
+static int hammer_slave(time_t deadline, unsigned long *opened)
+{
+ char node[] = "/tmp/ve_vtty_slave";
+
+ unlink(node);
+ if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0)
+ return -1;
+
+ while (time(NULL) < deadline) {
+ int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
+
+ if (fd >= 0) {
+ (*opened)++;
+ close(fd);
+ }
+ }
+ unlink(node);
+ return 0;
+}
+
+/* The kernel log, since a warning fails nothing by itself. */
+static int kmsg_open(void)
+{
+ int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+
+ if (fd >= 0)
+ lseek(fd, 0, SEEK_END);
+ return fd;
+}
+
+static int kmsg_check(int fd)
+{
+ static const char *const ours[] = {
+ "tty_release", "release_tty", "check_tty_count",
+ "vtty_open_master", "vtty_shutdown",
+ };
+ char buf[8192];
+ int found = 0;
+ ssize_t r;
+ size_t i;
+
+ while (fd >= 0 && (r = read(fd, buf, sizeof(buf) - 1)) > 0) {
+ buf[r] = '\0';
+ if (!strstr(buf, "WARNING") && !strstr(buf, "BUG") &&
+ !strstr(buf, "Oops") && !strstr(buf, "general protection"))
+ continue;
+ for (i = 0; i < ARRAY_SIZE(ours); i++)
+ if (strstr(buf, ours[i])) {
+ ksft_print_msg("kernel log: %s", buf);
+ found++;
+ break;
+ }
+ }
+ return found;
+}
+
+int main(int argc, char **argv)
+{
+ unsigned long master_opened = 0, slave_opened = 0;
+ int cg, ctid, ctl, kmsg, ready[2], count[2], status;
+ const char *env = getenv("VE_VTTY_SECS");
+ char path[PATH_MAX], val[16], c;
+ time_t deadline;
+ long secs;
+ pid_t pid;
+
+ ksft_print_header();
+ secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
+ if (secs <= 0)
+ secs = DEFAULT_SECS;
+
+ ctl = open("/dev/vzctl", O_RDWR);
+ if (ctl < 0)
+ ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
+ "(vzdev, vzmon) are needed\n",
+ strerror(errno));
+
+ cg = mount_cg2_fd();
+ if (cg < 0)
+ ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
+ write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS);
+
+ ctid = make_ve(cg, CTID_MIN);
+ if (ctid < 0)
+ ksft_exit_skip("cannot make a VE cgroup\n");
+
+ /*
+ * The pair is filed under the veid of the VE, so the two ends only
+ * meet if the cgroup carries the one the master is opened for.
+ */
+ snprintf(path, sizeof(path), "%d/ve.veid", ctid);
+ snprintf(val, sizeof(val), "%d", ctid);
+ if (write_file_at(cg, path, val) < 0) {
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
+ }
+
+ /* Do not report a failure where the vtty pair cannot be had at all. */
+ status = open_master(ctl, ctid);
+ if (status < 0 && errno != EBUSY) {
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("no vtty master: %s\n", strerror(errno));
+ }
+ if (status >= 0)
+ close(status);
+
+ ksft_set_plan(1);
+ kmsg = kmsg_open();
+ if (kmsg < 0)
+ ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
+ "be noticed\n",
+ strerror(errno));
+
+ if (pipe(ready) || pipe(count))
+ ksft_exit_fail_msg("pipe: %s\n", strerror(errno));
+
+ deadline = time(NULL) + secs;
+ ksft_print_msg("VE %d, pair %d, %ld seconds\n", ctid, VTTY_IDX, secs);
+
+ pid = fork();
+ if (pid == 0) {
+ close(ready[0]);
+ close(count[0]);
+ if (enter_cgroup(cg, ctid) < 0)
+ _exit(1);
+ /*
+ * CLONE_NEWVE puts the task in the VE the cgroup names, so
+ * the slave lookup finds the pair. The pid namespace is
+ * what a VE wants of its first task.
+ */
+ if (unshare(CLONE_NEWVE | CLONE_NEWPID | CLONE_NEWNS) < 0)
+ _exit(1);
+ if (fork() == 0) {
+ unsigned long n = 0;
+
+ c = 'r';
+ if (write(ready[1], &c, 1) != 1)
+ _exit(1);
+ if (hammer_slave(deadline, &n) < 0)
+ _exit(1);
+ if (write(count[1], &n, sizeof(n)) != sizeof(n))
+ _exit(1);
+ _exit(0);
+ }
+ wait(&status);
+ _exit(WIFEXITED(status) ? WEXITSTATUS(status) : 1);
+ } else if (pid < 0) {
+ ksft_exit_fail_msg("fork: %s\n", strerror(errno));
+ }
+ close(ready[1]);
+ close(count[1]);
+
+ if (read(ready[0], &c, 1) != 1) {
+ waitpid(pid, &status, 0);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("the VE side never came up\n");
+ }
+
+ while (time(NULL) < deadline) {
+ int fd = open_master(ctl, ctid);
+
+ if (fd >= 0) {
+ master_opened++;
+ close(fd);
+ }
+ }
+
+ if (read(count[0], &slave_opened, sizeof(slave_opened)) !=
+ sizeof(slave_opened))
+ slave_opened = 0;
+ waitpid(pid, &status, 0);
+
+ ksft_print_msg("%lu master opens, %lu slave opens\n", master_opened,
+ slave_opened);
+
+ status = kmsg_check(kmsg);
+ destroy_ve(cg, ctid);
+
+ if (!master_opened)
+ ksft_exit_fail_msg("no master was ever opened\n");
+ if (!slave_opened)
+ ksft_exit_fail_msg("no slave was ever opened\n");
+ if (status)
+ ksft_exit_fail_msg("kernel complained %d time(s)\n", status);
+
+ ksft_test_result_pass("vtty pair closed from both ends\n");
+ ksft_finished();
+}
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 5%]
* [Devel] [PATCH vz10 v2 1/2] selftests: ve: grant the directories the files sit in
@ 2026-09-16 12:31 3% Eva Kurchatova
2026-09-16 12:31 5% ` [Devel] [PATCH vz10 v2 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-09-22 17:47 6% ` [Devel] [PATCH vz10 v2 1/2] selftests: ve: grant the directories the files sit in Konstantin Khorenko
0 siblings, 2 replies; 200+ results
From: Eva Kurchatova @ 2026-09-16 12:31 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. A file at the top of the filesystem has none
and needs nothing, so the proc variant is unaffected.
The suite then passes 16 of 16, where it passed 14 before.
Fixes: 303ce49b7446 ("selftests/ve: add ve_perms test for proc and sysfs")
https://virtuozzo.atlassian.net/browse/VSTOR-144955
Feature: fix vz selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/ve/ve_perms_test.c | 33 ++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 25ffbd42c380..2b542dc30139 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -348,6 +348,37 @@ TEST_F(ve_perms, write_parser)
* read, and granting x on the directory lets it be traversed (the lookup then
* fails ENOENT on the missing child, not EACCES).
*/
+/*
+ * A leaf is only reachable once every directory above it can be traversed.
+ * The variants name their files relative to the mount point, so grant x on
+ * each directory along the way; a file at the top of the filesystem has
+ * none and needs nothing.
+ */
+static int allow_path(int cgv2_fd, const char *path, const char *file)
+{
+ char dir[240]; /* set_entry() writes "<dir> <mask>" into 256 */
+ const char *sep;
+
+ for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
+ if (sep - file >= (long)sizeof(dir))
+ return -1;
+ snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file), file);
+ if (set_entry(cgv2_fd, path, dir, "x") < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
+{
+ int i;
+
+ for (i = 0; i < NR_FILES; i++)
+ if (allow_path(cgv2_fd, path, files[i]) < 0)
+ return -1;
+ return 0;
+}
+
TEST_F(ve_perms, enforce_allows)
{
char path[PATH_MAX], absent[128];
@@ -358,6 +389,7 @@ TEST_F(ve_perms, enforce_allows)
ve_file(path, sizeof(path), self->ctid_a, variant->fs);
+ ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
for (i = 0; i < NR_FILES; i++)
EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
@@ -400,6 +432,7 @@ TEST_F(ve_perms, enforce_denies)
O_RDONLY), 0);
/* Granted w only: visible but not readable. */
+ ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
for (i = 0; i < NR_FILES; i++)
EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* [Devel] [PATCH VZ10 v9 8/8] selftests/ve: Add mount accounting selftest
2026-09-15 17:22 3% ` [Devel] [PATCH VZ10 v9 2/8] ve/fs: Rework per-ve mount count Vladimir Riabchun
2026-09-15 17:22 9% ` [Devel] [PATCH VZ10 v9 6/8] ve: Introduce per-VE failcount Vladimir Riabchun
@ 2026-09-15 17:22 6% ` Vladimir Riabchun
2026-09-18 15:06 0% ` Vladimir Riabchun
2026-09-25 16:04 0% ` Konstantin Khorenko
2 siblings, 2 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-15 17:22 UTC (permalink / raw)
To: devel
There are 6 test cases, covered in the new test:
1. Simple mount accouting correctness, just mount/umount.
2. Verification of correct limit hits and changes, including
negative values.
3. Partial mounts test, when mount limit is hit in the middle
of creation.
4. Test that enabled pseudosuper allows overuse.
5. Test that pseudosuper doesn't affect mount accounting.
6. Failcount feature verification.
https://virtuozzo.atlassian.net/browse/VSTOR-135520
Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
tools/testing/selftests/ve/.gitignore | 1 +
tools/testing/selftests/ve/Makefile | 1 +
.../selftests/ve/ve_mount_accounting_test.c | 550 ++++++++++++++++++
tools/testing/selftests/ve/ve_selftest.h | 21 +
4 files changed, 573 insertions(+)
create mode 100644 tools/testing/selftests/ve/ve_mount_accounting_test.c
diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index afa4c568c2c9..3df4d05888dc 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,2 +1,3 @@
ve_ns_owner_test
ve_perms_test
+ve_mount_accounting_test
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index ec40cbc7b3a1..c6efe7c4b4fb 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
TEST_GEN_PROGS += ve_ns_owner_test
TEST_GEN_PROGS += ve_perms_test
+TEST_GEN_PROGS += ve_mount_accounting_test
include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_mount_accounting_test.c b/tools/testing/selftests/ve/ve_mount_accounting_test.c
new file mode 100644
index 000000000000..6ba13dcdcc87
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
@@ -0,0 +1,550 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_mount_accounting selftests
+ *
+ * Tests to check the correctness of mount accounting.
+ */
+#define _GNU_SOURCE
+#include <asm/unistd.h>
+#include <linux/sched.h>
+#include <linux/limits.h>
+#include <sys/wait.h>
+#include <sys/syscall.h>
+#include <sys/stat.h>
+#include <sys/mount.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <time.h>
+
+#include "../kselftest_harness.h"
+#include "ve_selftest.h"
+
+#define TMP_DIR "/ve-mnt-tmp/"
+#define VE_MOUNTS_MAX 128
+
+/*
+ * These values take a while to calculate, the first test will
+ * do this and store the values for those who come after.
+ */
+int *need_avail;
+
+FIXTURE(ve_mnt_acc)
+{
+ int cgv2_fd;
+ int ctid;
+ struct namespace_ctx ns_ctx;
+ int orig_have_mnt;
+};
+
+/*
+ * free_vfsmnt is called via call_rcu, so we need to ensure
+ * that all pending call_rcu tasks are finished.
+ * However, one barrier is not enough:
+ * mntput_no_expire
+ * init_task_work(&mnt->mnt_rcu, __cleanup_mnt);
+ * task_work_add(task, &mnt->mnt_rcu, TWA_RESUME)
+ * OR
+ * schedule_delayed_work(&delayed_mntput_work, 1)
+ * # Both later call cleanup_mnt
+ * cleanup_mnt
+ * call_rcu(&mnt->mnt_rcu, delayed_free_vfsmnt);
+ *
+ * We can't enforce workqueue flush from userspace, so instead
+ * read several times until the value is the one we expect.
+ */
+static int wait_on_mnt_avail(FIXTURE_DATA(ve_mnt_acc) * self, int expected)
+{
+#define WAIT_TIMEOUT_NS (4096UL * 1000 * 1000)
+ char path_avail[64];
+ int mnt_avail_nr = 0;
+ struct timespec delay = { .tv_nsec = 1000000 };
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+ while (delay.tv_nsec <= WAIT_TIMEOUT_NS) {
+ if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr)) {
+ fprintf(stderr, "Failed to read from %s\n", path_avail);
+ return 1;
+ }
+ if (mnt_avail_nr == expected)
+ return 0;
+
+ do_rcu_barrier();
+ if (nanosleep(&delay, NULL)) {
+ fprintf(stderr, "nanosleep failed: %s\n", strerror(errno));
+ return 1;
+ }
+ delay.tv_nsec *= 2;
+ }
+ fprintf(stderr, "Didn't get expected value at %s\n", path_avail);
+ fprintf(stderr, "Expected %d, last read %d\n", expected, mnt_avail_nr);
+ return -1;
+}
+
+static int set_pseudosuper(FIXTURE_DATA(ve_mnt_acc) * self, int value)
+{
+ char path[64];
+
+ snprintf(path, sizeof(path), "%d/ve.pseudosuper", self->ctid);
+ return write_u64_at(self->cgv2_fd, path, value);
+}
+
+static int _create_mount(void *id_ptr)
+{
+ char path[PATH_MAX];
+ int id = *(int *)id_ptr, ret;
+
+ snprintf(path, sizeof(path), TMP_DIR "%d", id);
+
+ if (mkdir(path, 0755) < 0) {
+ fprintf(stderr, "Failed to create directory %s: %s\n", path, strerror(errno));
+ return -1;
+ }
+ ret = mount("tmpfs", path, "tmpfs", 0, "size=1M");
+ if (!ret)
+ return 0;
+ fprintf(stderr, "Failed to mount tmpfs to %s: %s\n", path, strerror(errno));
+
+ rmdir(path);
+ return -1;
+}
+
+/* Create a mount and wait until mount counter is valid */
+static int create_mount(FIXTURE_DATA(ve_mnt_acc) * self, int id)
+{
+ int ret, avail_before;
+ char path[64];
+
+ snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+
+ if (read_s32_at(self->cgv2_fd, path, &avail_before)) {
+ fprintf(stderr, "Failed to read mnt_avail_nr in create_mount\n");
+ return 1;
+ }
+
+ ret = run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
+ CLONE_NEWVE | CLONE_NEWNS, _create_mount, &id);
+
+ if (ret)
+ wait_on_mnt_avail(self, avail_before);
+ else
+ ret = wait_on_mnt_avail(self, avail_before - 1);
+ return ret;
+}
+
+static int _destroy_mount(void *id_ptr)
+{
+ char path[PATH_MAX];
+ struct stat st;
+ int id = *(int *)id_ptr;
+
+ snprintf(path, sizeof(path), TMP_DIR "%d", id);
+
+ if (stat(path, &st))
+ return 1;
+ if (umount(path)) {
+ fprintf(stderr, "failed to umount directory %s: %s\n", path, strerror(errno));
+ return -1;
+ }
+ if (rmdir(path)) {
+ fprintf(stderr, "failed to remove directory %s: %s\n", path, strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+/* Do umount and ensure that mnt_avail_nr is valid on exit(+1) */
+static int destroy_mount(FIXTURE_DATA(ve_mnt_acc) * self, int id)
+{
+ int ret, avail_before;
+ char path[64];
+
+ snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+
+ if (read_s32_at(self->cgv2_fd, path, &avail_before)) {
+ fprintf(stderr, "Failed to read mnt_avail_nr in create_mount\n");
+ return 1;
+ }
+
+ ret = run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
+ CLONE_NEWVE | CLONE_NEWNS, _destroy_mount, &id);
+
+ /* Cost must be exactly 1 */
+ if (!ret)
+ ret = wait_on_mnt_avail(self, avail_before + 1);
+
+ return ret;
+}
+
+#define MAX_MNT_ID 32
+
+static int get_free_mnt_id(void)
+{
+ int i;
+ struct stat st;
+ char path[PATH_MAX];
+
+ for (i = 0; i < MAX_MNT_ID; i++) {
+ snprintf(path, sizeof(path), TMP_DIR "%d", i);
+ if (stat(path, &st))
+ return i;
+ }
+ return -1;
+}
+
+/* Returns 0 if mount failed OR mount cost is not 1 */
+static int try_mount(FIXTURE_DATA(ve_mnt_acc) * self)
+{
+ int mnt_id = get_free_mnt_id();
+
+ if (mnt_id < 0 || create_mount(self, mnt_id))
+ return 0;
+
+ return destroy_mount(self, mnt_id) == 0;
+}
+
+/*
+ * Current kernel code first creates many mounts with ve_owner != ve0,
+ * so this VE needs more mounts than would be really consumed after commit_tree.
+ */
+static int get_required_avail_mnts(FIXTURE_DATA(ve_mnt_acc) * self)
+{
+ int l = 0, r = VE_MOUNTS_MAX + 1, mid;
+ char path_max_nr[64];
+
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+ while (r - l > 1) {
+ mid = (l + r) / 2;
+ if (write_u64_at(self->cgv2_fd, path_max_nr, mid + self->orig_have_mnt))
+ return -1;
+
+ if (try_mount(self))
+ r = mid;
+ else
+ l = mid;
+ }
+ if (r == VE_MOUNTS_MAX + 1) {
+ fprintf(stderr, "%s: %d avail mounts is not enough\n", __func__, r);
+ return -1;
+ }
+ if (write_u64_at(self->cgv2_fd, path_max_nr, VE_MOUNTS_MAX))
+ return -1;
+ return r;
+}
+
+/* Expect mount success and return new avail value */
+static int mount_and_get_avail(struct __test_metadata *_metadata,
+ FIXTURE_DATA(ve_mnt_acc) * self, int mnt_id)
+{
+ char path_avail[64];
+ int mnt_avail_nr;
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+ ASSERT_EQ(create_mount(self, mnt_id), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ return mnt_avail_nr;
+}
+
+/* Expect mount failure and ensure intact avail number */
+static int check_mount_fails(struct __test_metadata *_metadata,
+ FIXTURE_DATA(ve_mnt_acc) * self,
+ int mnt_id, int avail_count)
+{
+ char path_avail[64];
+ int mnt_avail_nr;
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+ if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr))
+ return 1;
+ if (mnt_avail_nr != avail_count) {
+ fprintf(stderr, "%s: mnt_avail(%d) != avail_count(%d) before mount\n",
+ __func__, mnt_avail_nr, avail_count);
+ return -1;
+ }
+
+ if (create_mount(self, mnt_id) == 0) {
+ fprintf(stderr, "%s: successful mount\n", __func__);
+ return -1;
+ }
+
+ if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr))
+ return 1;
+ if (mnt_avail_nr != avail_count) {
+ fprintf(stderr, "%s: mnt_avail(%d) != avail_count(%d) after mount\n",
+ __func__, mnt_avail_nr, avail_count);
+ return -1;
+ }
+ return 0;
+}
+
+FIXTURE_SETUP(ve_mnt_acc)
+{
+ int initial_mnt_avail_nr;
+ char path[64];
+
+ self->cgv2_fd = mount_cg2_fd();
+ ASSERT_GE(self->cgv2_fd, 0);
+ mkdir(TMP_DIR, 0755);
+
+ ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
+ VE_CONTROLLERS), 0);
+
+ self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
+ ASSERT_GE(self->ctid, 0);
+
+ snprintf(path, sizeof(path), "%d/ve.mnt_max_nr", self->ctid);
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path, VE_MOUNTS_MAX), 0);
+
+ /*
+ * The new ve cgroup has not been entered by anything yet, so its
+ * mnt_avail_nr counter should be VE_MOUNTS_MAX.
+ */
+ snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
+ ASSERT_EQ(initial_mnt_avail_nr, VE_MOUNTS_MAX);
+
+ ASSERT_EQ(prepare_namespaces(&self->ns_ctx, CLONE_NEWNS | CLONE_NEWVE,
+ self->cgv2_fd, self->ctid), 0);
+
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
+ self->orig_have_mnt = VE_MOUNTS_MAX - initial_mnt_avail_nr;
+
+ /* Calculate once - it is time-consuming */
+ if (*need_avail == -1)
+ *need_avail = get_required_avail_mnts(self);
+
+ ASSERT_GE(*need_avail, 1);
+ /* try_mount ensures cost is 1 */
+ ASSERT_EQ(try_mount(self), 1);
+};
+
+FIXTURE_TEARDOWN(ve_mnt_acc)
+{
+ for (int i = 0; i < MAX_MNT_ID; i++)
+ _destroy_mount((void *)&i);
+
+ close_namespace_fds(&self->ns_ctx);
+ destroy_ve(self->cgv2_fd, self->ctid);
+ close(self->cgv2_fd);
+ rmdir(TMP_DIR);
+}
+
+/* Simple test to check mount/umount accounting correctness */
+TEST_F(ve_mnt_acc, mount_umount)
+{
+ int original_mnt_avail, mnt_avail_nr;
+ char path[64];
+
+ snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &original_mnt_avail), 0);
+
+ ASSERT_LT(mount_and_get_avail(_metadata, self, 0),
+ original_mnt_avail);
+
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, original_mnt_avail);
+}
+
+/* Test mount limit hits */
+TEST_F(ve_mnt_acc, hit_limits)
+{
+ int mnt_avail_nr;
+ char path_avail[64], path_max_nr[64];
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+ /* Step 1: reduce number of available mounts to *need_avail */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+ self->orig_have_mnt + 1 * (*need_avail)), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, 1 * *need_avail);
+
+ /* Step 2: do one mount, no mounts should be available */
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), *need_avail - 1);
+
+ /* Step 3: check that one more mount fails */
+ ASSERT_EQ(check_mount_fails(_metadata, self, 1, (*need_avail) - 1), 0);
+
+ /* Step 4: increase by 1 and win now */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+ self->orig_have_mnt + 1 + *need_avail), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, *need_avail);
+
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 1), *need_avail - 1);
+
+ /* Step 5: reduce mnt_max_nr so we have more mounts than allowed */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+ self->orig_have_mnt + 1), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, -1);
+
+ /* Step 6: try to do mount when avail < 0, ensure number is intact */
+ ASSERT_EQ(check_mount_fails(_metadata, self, 2, -1), 0);
+
+ /* Step 7: remove one mount, check avail value update, mount should fail */
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+ ASSERT_EQ(check_mount_fails(_metadata, self, 2, 0), 0);
+
+ /* Step 8: remove one more mount and check that new mount succeeds */
+ ASSERT_EQ(destroy_mount(self, 1), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, 1);
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + *need_avail), 0);
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 2), *need_avail - 1);
+
+ ASSERT_EQ(destroy_mount(self, 2), 0);
+}
+
+/*
+ * Mount propagation makes one mount cost more.
+ * This test checks that if we run out of mounts in the middle of creating
+ * a new one, everything is restored smoothly and nothing leaks.
+ */
+TEST_F(ve_mnt_acc, partial_mounts)
+{
+ char path_avail[64], path_max_nr[64];
+ int i;
+
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+ if (*need_avail == 1)
+ SKIP(return, "mount cost is 1, no partial mounts possible");
+
+ for (i = 0; i < *need_avail; i++) {
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + i), 0);
+ ASSERT_EQ(check_mount_fails(_metadata, self, 0, i), 0);
+ }
+}
+
+/* Test that pseudosuper allows negative avail with correct accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_allows_overuse)
+{
+ char path_max_nr[64];
+
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + *need_avail), 0);
+ ASSERT_EQ(set_pseudosuper(self, 1), 0);
+
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), *need_avail - 1);
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 1), *need_avail - 2);
+
+ /* Go below zero */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt), 0);
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 2), -3);
+
+ ASSERT_EQ(set_pseudosuper(self, 0), 0);
+
+ /* Below zero should fail */
+ ASSERT_EQ(check_mount_fails(_metadata, self, 3, -3), 0);
+ /* Now need 2 more to add a new mount */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+ self->orig_have_mnt + 3 + *need_avail - 2), 0);
+ ASSERT_EQ(check_mount_fails(_metadata, self, 3, *need_avail - 2), 0);
+
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+ ASSERT_EQ(check_mount_fails(_metadata, self, 3, *need_avail - 1), 0);
+ ASSERT_EQ(destroy_mount(self, 2), 0);
+
+ ASSERT_EQ(try_mount(self), 1);
+}
+
+/* Test that pseudosuper doesn't disable accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_continues_accounting)
+{
+ int orig_mnt_avail, mnt_avail_nr;
+ char path_avail[64];
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &orig_mnt_avail), 0);
+
+ ASSERT_EQ(set_pseudosuper(self, 0), 0);
+
+ /* mnt 0 - mounted without pseudosuper, umounted with it. */
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), orig_mnt_avail - 1);
+ ASSERT_EQ(set_pseudosuper(self, 1), 0);
+
+ /* Cost is the same when mount/umount happen under pseudosuper. */
+ ASSERT_EQ(try_mount(self), 1);
+
+ /* mnt 1 - mounted with pseudosuper, umounted without it. */
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 1),
+ orig_mnt_avail - 2);
+
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, orig_mnt_avail - 1);
+
+ ASSERT_EQ(set_pseudosuper(self, 0), 0);
+
+ ASSERT_EQ(destroy_mount(self, 1), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, orig_mnt_avail);
+}
+
+/* Test failcount feature */
+TEST_F(ve_mnt_acc, failcount)
+{
+ char path_fc[64], failcount_str[512], path_max_nr[64];
+
+ snprintf(path_fc, sizeof(path_fc), "%d/ve.failcount", self->ctid);
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
+
+ /* Check successful mount doesn't affect failcount */
+ mount_and_get_avail(_metadata, self, 0);
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+
+ /* Check failcount update when mount fails */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, 0), 0);
+ ASSERT_LT(create_mount(self, 1), 0);
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 1\n") != NULL);
+
+ /* Check failcount flush */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_fc, 0), 0);
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
+
+ /* Check failcount update when mount fails again */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, 0), 0);
+ ASSERT_LT(create_mount(self, 1), 0);
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 1\n") != NULL);
+}
+
+int main(int argc, char **argv)
+{
+ need_avail = mmap(NULL, sizeof(int),
+ PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+
+ if (need_avail == MAP_FAILED) {
+ fprintf(stderr, "Failed to mmap shared region: %s\n", strerror(errno));
+ return 1;
+ }
+ *need_avail = -1;
+
+ return test_harness_run(argc, argv);
+}
diff --git a/tools/testing/selftests/ve/ve_selftest.h b/tools/testing/selftests/ve/ve_selftest.h
index bf9b89b59a6c..8cc9b3238aaf 100644
--- a/tools/testing/selftests/ve/ve_selftest.h
+++ b/tools/testing/selftests/ve/ve_selftest.h
@@ -413,4 +413,25 @@ static inline int is_mounted(const char *path)
return errno == EXDEV ? 1 : -1;
}
+static inline int do_rcu_barrier(void)
+{
+ int fd;
+
+ errno = 0;
+ fd = open("/sys/module/rcutree/parameters/do_rcu_barrier", O_WRONLY);
+
+ if (fd < 0) {
+ fprintf(stderr, "Failed to open do_rcu_barrier: %s\n", strerror(errno));
+ return -1;
+ }
+ errno = 0;
+ if (write(fd, "1", 2) != 2) {
+ fprintf(stderr, "Failed to issue rcu_barrier: %s\n", strerror(errno));
+ close(fd);
+ return -1;
+ }
+ close(fd);
+ return 0;
+}
+
#endif /* __SELFTESTS_VE_VE_SELFTEST_H */
--
2.47.1
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 6%]
* [Devel] [PATCH VZ10 v9 6/8] ve: Introduce per-VE failcount
2026-09-15 17:22 3% ` [Devel] [PATCH VZ10 v9 2/8] ve/fs: Rework per-ve mount count Vladimir Riabchun
@ 2026-09-15 17:22 9% ` Vladimir Riabchun
2026-09-18 14:42 0% ` Vladimir Riabchun
` (2 more replies)
2026-09-15 17:22 6% ` [Devel] [PATCH VZ10 v9 8/8] selftests/ve: Add mount accounting selftest Vladimir Riabchun
2 siblings, 3 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-15 17:22 UTC (permalink / raw)
To: devel
It may be useful to have a history of resource limit hits for every VE,
this may simplify debugging and provide some information about the
resources usage.
This information is provided by ve.failcount file, any write to it
resets all failcounts.
To add a new failcounter we need to create a new atomic_t field
name_failcount in ve structure and add a new VE_FC_ENTRY in
ve_failcounts array.
One change, unrelated to failcounts: aio fields are now initialized
in ve0.
https://virtuozzo.atlassian.net/browse/VSTOR-135520
Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
fs/aio.c | 1 +
fs/namespace.c | 2 ++
include/linux/ve.h | 16 +++++++++
kernel/bpf/syscall.c | 1 +
kernel/ve/ve.c | 70 ++++++++++++++++++++++++++++++++++++++++
net/core/dev.c | 10 ++----
net/core/neighbour.c | 3 +-
net/core/net_namespace.c | 4 ++-
8 files changed, 96 insertions(+), 11 deletions(-)
diff --git a/fs/aio.c b/fs/aio.c
index cb63416af135..4ae06bcd47f2 100644
--- a/fs/aio.c
+++ b/fs/aio.c
@@ -814,6 +814,7 @@ static struct kioctx *ioctx_alloc(unsigned nr_events)
spin_lock(&ve->aio_nr_lock);
if (ve->aio_nr + ctx->max_reqs > ve->aio_max_nr ||
ve->aio_nr + ctx->max_reqs < ve->aio_nr) {
+ ve_failcount_inc(ve, aio);
spin_unlock(&ve->aio_nr_lock);
err = -EAGAIN;
goto err_ctx;
diff --git a/fs/namespace.c b/fs/namespace.c
index 1d6e66cd7174..ea01664ff5f2 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -3380,6 +3380,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
if (ret)
get_ve(ve);
+ else
+ ve_failcount_inc(ve, mnt);
return ret;
}
diff --git a/include/linux/ve.h b/include/linux/ve.h
index fabc608e31f5..065129b42768 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -80,12 +80,15 @@ struct ve_struct {
struct kmapset_key proc_perms_key;
atomic_t netns_avail_nr;
+ atomic_t netns_failcount;
int netns_max_nr;
atomic_t netif_avail_nr;
+ atomic_t netif_failcount;
int netif_max_nr;
atomic_t bpf_prog_avail_nr;
+ atomic_t bpf_prog_failcount;
int bpf_prog_max_nr;
atomic64_t _uevent_seqnum;
@@ -94,6 +97,7 @@ struct ve_struct {
atomic_t arp_neigh_nr;
atomic_t nd_neigh_nr;
+ atomic_t neigh_tbl_failcount;
unsigned long meminfo_val;
/*
@@ -102,6 +106,7 @@ struct ve_struct {
* other containers.
*/
atomic_t mnt_avail_nr; /* number of available VE mounts */
+ atomic_t mnt_failcount;
int mnt_max_nr;
#ifdef CONFIG_COREDUMP
@@ -129,6 +134,7 @@ struct ve_struct {
spinlock_t aio_nr_lock;
unsigned long aio_nr;
unsigned long aio_max_nr;
+ atomic_t aio_failcount;
#endif
struct vfsmount *devtmpfs_mnt;
};
@@ -151,6 +157,16 @@ extern int nr_ve;
#define BPF_PROG_MAX_NR_DEFAULT 256 /* number of loaded BPF progs per-VE */
#define MNT_MAX_NR_DEFAULT 4096 /* number of mounts per-VE */
+#define ve_failcount_inc(ve, name) \
+do { \
+ struct ve_struct *__ve = (ve); \
+ \
+ atomic_inc(&__ve->name##_failcount); \
+ ve_pr_warn_ratelimited(VE_LOG_BOTH, "CT%s: hits the " #name \
+ " limit, see the '" #name "' counter in ve.failcount " \
+ "of the container's ve cgroup\n", ve_name(__ve)); \
+} while (0)
+
#ifdef CONFIG_VE
static inline void ve_set_state(struct ve_struct *ve, int new_state)
{
diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index c94d4240e3d3..8498bc9df7ea 100644
--- a/kernel/bpf/syscall.c
+++ b/kernel/bpf/syscall.c
@@ -2891,6 +2891,7 @@ static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
if (!bpf_cap && type == BPF_PROG_TYPE_CGROUP_DEVICE) {
load_ve = get_exec_env();
if (atomic_dec_if_positive(&load_ve->bpf_prog_avail_nr) < 0) {
+ ve_failcount_inc(load_ve, bpf_prog);
load_ve = NULL;
err = -ENOSPC;
goto put_token;
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index 5ebdb4b7baa7..9261565208e6 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -100,10 +100,13 @@ struct ve_struct ve0 = {
.features = -1,
.sched_lat_ve.cur = &ve0_lat_stats,
.netns_avail_nr = ATOMIC_INIT(INT_MAX),
+ .netns_failcount = ATOMIC_INIT(0),
.netns_max_nr = INT_MAX,
.netif_avail_nr = ATOMIC_INIT(INT_MAX),
+ .netif_failcount = ATOMIC_INIT(0),
.netif_max_nr = INT_MAX,
.bpf_prog_avail_nr = ATOMIC_INIT(INT_MAX),
+ .bpf_prog_failcount = ATOMIC_INIT(0),
.bpf_prog_max_nr = INT_MAX,
.fsync_enable = FSYNC_FILTERED,
._randomize_va_space =
@@ -115,8 +118,16 @@ struct ve_struct ve0 = {
.arp_neigh_nr = ATOMIC_INIT(0),
.nd_neigh_nr = ATOMIC_INIT(0),
+ .neigh_tbl_failcount = ATOMIC_INIT(0),
.mnt_avail_nr = ATOMIC_INIT(INT_MAX),
.mnt_max_nr = INT_MAX,
+ .mnt_failcount = ATOMIC_INIT(0),
+#ifdef CONFIG_AIO
+ .aio_nr_lock = __SPIN_LOCK_UNLOCKED(aio_nr_lock),
+ .aio_nr = 0,
+ .aio_max_nr = AIO_MAX_NR_DEFAULT,
+ .aio_failcount = ATOMIC_INIT(0),
+#endif
.meminfo_val = VE_MEMINFO_SYSTEM,
#ifdef CONFIG_X86
.umh_running_helpers = ATOMIC_INIT(0),
@@ -787,12 +798,15 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
ve->fsync_enable = FSYNC_FILTERED;
atomic_set(&ve->netns_avail_nr, NETNS_MAX_NR_DEFAULT);
+ atomic_set(&ve->netns_failcount, 0);
ve->netns_max_nr = NETNS_MAX_NR_DEFAULT;
atomic_set(&ve->netif_avail_nr, NETIF_MAX_NR_DEFAULT);
+ atomic_set(&ve->netif_failcount, 0);
ve->netif_max_nr = NETIF_MAX_NR_DEFAULT;
atomic_set(&ve->bpf_prog_avail_nr, BPF_PROG_MAX_NR_DEFAULT);
+ atomic_set(&ve->bpf_prog_failcount, 0);
ve->bpf_prog_max_nr = BPF_PROG_MAX_NR_DEFAULT;
err = ve_log_init(ve);
@@ -821,7 +835,9 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
atomic_set(&ve->arp_neigh_nr, 0);
atomic_set(&ve->nd_neigh_nr, 0);
+ atomic_set(&ve->neigh_tbl_failcount, 0);
ve->mnt_max_nr = MNT_MAX_NR_DEFAULT;
+ atomic_set(&ve->mnt_failcount, 0);
atomic_set(&ve->mnt_avail_nr, MNT_MAX_NR_DEFAULT);
#ifdef CONFIG_COREDUMP
@@ -834,6 +850,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
spin_lock_init(&ve->aio_nr_lock);
ve->aio_nr = 0;
ve->aio_max_nr = AIO_MAX_NR_DEFAULT;
+ atomic_set(&ve->aio_failcount, 0);
#endif
return &ve->css;
@@ -1074,6 +1091,53 @@ VE_RESOURCE(mnt);
VE_RESOURCE(netif);
VE_RESOURCE(bpf_prog);
+static const struct ve_failcount_entry {
+ const char *name;
+ size_t offset;
+} ve_failcounts[] = {
+#define VE_FC_ENTRY(name) { #name, offsetof(struct ve_struct, name##_failcount) }
+ VE_FC_ENTRY(netns),
+ VE_FC_ENTRY(mnt),
+ VE_FC_ENTRY(netif),
+ VE_FC_ENTRY(bpf_prog),
+ VE_FC_ENTRY(neigh_tbl),
+#ifdef CONFIG_AIO
+ VE_FC_ENTRY(aio),
+#endif
+ {}
+};
+
+static int ve_failcount_read(struct seq_file *sf, void *v)
+{
+ struct ve_struct *ve = css_to_ve(seq_css(sf));
+ const struct ve_failcount_entry *entry;
+ atomic_t *fc;
+
+ for (entry = ve_failcounts; entry->name; entry++) {
+ fc = (void *)ve + entry->offset;
+ seq_printf(sf, "%s: %d\n", entry->name, atomic_read(fc));
+ }
+ return 0;
+}
+
+static ssize_t ve_failcount_write(struct kernfs_open_file *of, char *buf,
+ size_t nbytes, loff_t off)
+{
+ struct ve_struct *ve = css_to_ve(of_css(of));
+ const struct ve_failcount_entry *entry;
+ atomic_t *fc;
+
+ if (!ve_is_super(get_exec_env()) && !ve->is_pseudosuper)
+ return -EPERM;
+
+ for (entry = ve_failcounts; entry->name; entry++) {
+ fc = (void *)ve + entry->offset;
+ atomic_set(fc, 0);
+ }
+
+ return nbytes;
+}
+
static int ve_os_release_read(struct seq_file *sf, void *v)
{
struct cgroup_subsys_state *css = seq_css(sf);
@@ -1613,6 +1677,12 @@ static struct cftype ve_cftypes[] = {
.flags = CFTYPE_NOT_ON_ROOT,
.write_u64 = ve_rpc_kill_write,
},
+ {
+ .name = "failcount",
+ .flags = CFTYPE_NOT_ON_ROOT,
+ .seq_show = ve_failcount_read,
+ .write = ve_failcount_write,
+ },
{ }
};
diff --git a/net/core/dev.c b/net/core/dev.c
index c7dddb200489..7632ad1fd015 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -10997,10 +10997,7 @@ int register_netdevice(struct net_device *dev)
ret = -ENOMEM;
if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
- ve_pr_warn_ratelimited(VE_LOG_BOTH,
- "CT%s: hits max number of network devices, "
- "increase ve::netif_max_nr parameter\n",
- ve_name(net->owner_ve));
+ ve_failcount_inc(net->owner_ve, netif);
return ret;
}
@@ -12211,10 +12208,7 @@ int __dev_change_net_namespace(struct net_device *dev, struct net *net,
err = -ENOMEM;
if (atomic_dec_if_positive(&net->owner_ve->netif_avail_nr) < 0) {
- ve_pr_warn_ratelimited(VE_LOG_BOTH,
- "CT%s: hits max number of network devices, "
- "increase ve::netif_max_nr parameter\n",
- ve_name(net->owner_ve));
+ ve_failcount_inc(net->owner_ve, netif);
goto out;
}
atomic_inc(&dev_net(dev)->owner_ve->netif_avail_nr);
diff --git a/net/core/neighbour.c b/net/core/neighbour.c
index f90deb17fb25..c7d5e6df50c2 100644
--- a/net/core/neighbour.c
+++ b/net/core/neighbour.c
@@ -520,8 +520,7 @@ static struct neighbour *neigh_alloc(struct neigh_table *tbl,
(glob_entries >= READ_ONCE(tbl->gc_thresh2) &&
time_after(now, READ_ONCE(tbl->last_flush) + 5 * HZ))) {
if (!neigh_forced_gc(tbl, ve) && entries >= gc_thresh3) {
- net_info_ratelimited("%s: neighbor table overflow!\n",
- tbl->id);
+ ve_failcount_inc(ve, neigh_tbl);
NEIGH_CACHE_STAT_INC(tbl, table_fulls);
goto out_entries;
}
diff --git a/net/core/net_namespace.c b/net/core/net_namespace.c
index b3d54cad984a..328e1c96b6f4 100644
--- a/net/core/net_namespace.c
+++ b/net/core/net_namespace.c
@@ -486,8 +486,10 @@ void net_drop_ns(void *p)
#ifdef CONFIG_VE
static int dec_netns_avail(struct ve_struct *ve)
{
- if (atomic_dec_if_positive(&ve->netns_avail_nr) < 0)
+ if (atomic_dec_if_positive(&ve->netns_avail_nr) < 0) {
+ ve_failcount_inc(ve, netns);
return -ENOSPC;
+ }
return 0;
}
--
2.47.1
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 9%]
* [Devel] [PATCH VZ10 v9 2/8] ve/fs: Rework per-ve mount count
@ 2026-09-15 17:22 3% ` Vladimir Riabchun
2026-09-18 13:22 0% ` Vladimir Riabchun
2026-09-15 17:22 9% ` [Devel] [PATCH VZ10 v9 6/8] ve: Introduce per-VE failcount Vladimir Riabchun
2026-09-15 17:22 6% ` [Devel] [PATCH VZ10 v9 8/8] selftests/ve: Add mount accounting selftest Vladimir Riabchun
2 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-09-15 17:22 UTC (permalink / raw)
To: devel
Previous approach with current mounts counter had an issue:
there was a gap between ve_mount_allowed check and ve_mount_nr_inc,
which could allow CT to have more mounts than expected.
Fix this by tracking the number of available mounts instead
of current ones. This also makes resources accounting
more consistent - we are using ***_avail_nr approach more.
One more issue with inconsistent ve value is fixed:
ve_mount_allowed always used ve from get_exec_env, but
ve_mount_nr_inc operated with owner_ve.
Now actual ve value is calculated in the beginning of alloc_vfsmnt.
To avoid incorrect accounting when is_pseudosuper is changed,
update avail_nr count without > 0 check if VE is ve0 or pseudosuper.
This also simplifies ve_mount_put, since increment is
now unconditional.
https://virtuozzo.atlassian.net/browse/VSTOR-135520
Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
fs/namespace.c | 78 ++++++++++++++++++++++++++++------------------
include/linux/ve.h | 2 +-
kernel/ve/ve.c | 12 +++----
3 files changed, 54 insertions(+), 38 deletions(-)
diff --git a/fs/namespace.c b/fs/namespace.c
index 4adc1db84b4c..1d6e66cd7174 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -317,19 +317,22 @@ int mnt_get_count(struct mount *mnt)
#endif
}
-static inline int ve_mount_allowed(void);
-static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve);
-static inline void ve_mount_nr_dec(struct mount *mnt);
+static inline int ve_try_reserve_mount(struct ve_struct *ve);
+static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve);
static inline void ve_mount_reown(struct mount *mnt, struct ve_struct *ve);
static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
{
struct mount *mnt;
+ struct ve_struct *ve = owner_ve;
- if (!ve_mount_allowed()) {
+ if (!ve)
+ ve = get_exec_env();
+
+ if (!ve_try_reserve_mount(ve)) {
pr_warn_ratelimited(
"CT#%s reached the limit on mounts.\n",
- ve_name(get_exec_env()));
+ ve_name(ve));
return NULL;
}
@@ -337,6 +340,14 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
if (mnt) {
int err;
+#ifdef CONFIG_VE
+ /*
+ * Got ve reference in ve_try_reserve_mount, set mnt ve data
+ * here, so in case of error ve_mount_put sees correct info.
+ */
+ mnt->ve_owner = ve;
+#endif
+
err = mnt_alloc_id(mnt);
if (err)
goto out_free_cache;
@@ -371,7 +382,8 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
INIT_LIST_HEAD(&mnt->mnt_umounting);
INIT_HLIST_HEAD(&mnt->mnt_stuck_children);
mnt->mnt.mnt_idmap = &nop_mnt_idmap;
- ve_mount_nr_inc(mnt, owner_ve);
+ } else {
+ ve_mount_put(mnt, ve);
}
return mnt;
@@ -382,6 +394,8 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
out_free_id:
mnt_free_id(mnt);
out_free_cache:
+ /* Got ve reference in ve_try_reserve_mount */
+ ve_mount_put(mnt, ve);
kmem_cache_free(mnt_cache, mnt);
return NULL;
}
@@ -751,7 +765,9 @@ int sb_prepare_remount_readonly(struct super_block *sb)
static void free_vfsmnt(struct mount *mnt)
{
mnt_idmap_put(mnt_idmap(&mnt->mnt));
- ve_mount_nr_dec(mnt);
+#ifdef CONFIG_VE
+ ve_mount_put(mnt, mnt->ve_owner);
+#endif
kfree_const(mnt->mnt_devname);
#ifdef CONFIG_SMP
free_percpu(mnt->mnt_pcp);
@@ -3214,7 +3230,7 @@ int ve_devmnt_process(struct ve_struct *ve, dev_t dev, void **data_pp, int remou
if (devmnt->dev == dev) {
err = ve_devmnt_check(data, devmnt->allowed_options);
/*
- * In case of @is_pseudouser set, ie restore procedure,
+ * In case of @is_pseudosuper set, ie restore procedure,
* we don't check for allowed options filtering, since
* restore mode is special.
*/
@@ -3353,30 +3369,30 @@ int ve_devmnt_verify(struct ve_struct *ve, dev_t dev, char *opts, bool new_mount
return err;
}
-static inline int ve_mount_allowed(void)
-{
- struct ve_struct *ve = get_exec_env();
-
- return ve_is_super(ve) || ve->is_pseudosuper ||
- atomic_read(&ve->mnt_nr) < (int)sysctl_ve_mount_nr;
-}
-
-static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve)
+static inline int ve_try_reserve_mount(struct ve_struct *ve)
{
- if (!ve)
- ve = get_exec_env();
+ int ret = ve_is_super(ve) || ve->is_pseudosuper;
+ /* Ignore limits in ve0 and pseudosuper cases, but still count. */
+ if (ret)
+ atomic_dec(&ve->mnt_avail_nr);
+ else
+ ret = atomic_dec_if_positive(&ve->mnt_avail_nr) >= 0;
- mnt->ve_owner = get_ve(ve);
- atomic_inc(&ve->mnt_nr);
+ if (ret)
+ get_ve(ve);
+ return ret;
}
-static inline void ve_mount_nr_dec(struct mount *mnt)
+static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve)
{
- struct ve_struct *ve = mnt->ve_owner;
-
- atomic_dec(&ve->mnt_nr);
+ /*
+ * ve argument is needed to reuse this function in alloc_vfsmnt error path.
+ * Other users should pass mnt->ve_owner value.
+ */
+ atomic_inc(&ve->mnt_avail_nr);
put_ve(ve);
- mnt->ve_owner = NULL;
+ if (mnt)
+ mnt->ve_owner = NULL;
}
/*
@@ -3397,9 +3413,9 @@ static inline void ve_mount_reown(struct mount *mnt, struct ve_struct *ve)
if (old == ve)
return;
- atomic_inc(&ve->mnt_nr);
+ atomic_dec(&ve->mnt_avail_nr);
WRITE_ONCE(mnt->ve_owner, get_ve(ve));
- atomic_dec(&old->mnt_nr);
+ atomic_inc(&old->mnt_avail_nr);
put_ve(old);
}
@@ -3422,10 +3438,10 @@ bool is_sb_ve_accessible(struct ve_struct *ve, struct super_block *sb)
#else /* CONFIG_VE */
-static inline int ve_mount_allowed(void) { return 1; }
-static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve) { }
-static inline void ve_mount_nr_dec(struct mount *mnt) { }
+static inline int ve_try_reserve_mount(struct ve_struct *ve) { return 1; }
+static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve) { }
static inline void ve_mount_reown(struct mount *mnt, struct ve_struct *ve) { }
+
#endif /* CONFIG_VE */
/*
diff --git a/include/linux/ve.h b/include/linux/ve.h
index f946abe4d859..65c058c0f34d 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -96,7 +96,7 @@ struct ve_struct {
atomic_t nd_neigh_nr;
unsigned long meminfo_val;
- atomic_t mnt_nr; /* number of present VE mounts */
+ atomic_t mnt_avail_nr; /* number of available VE mounts */
#ifdef CONFIG_COREDUMP
char core_pattern[CORENAME_MAX_SIZE];
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index 2d2934e1ec10..58a0c4b8e36e 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -82,7 +82,7 @@ struct ve_struct ve0 = {
.arp_neigh_nr = ATOMIC_INIT(0),
.nd_neigh_nr = ATOMIC_INIT(0),
- .mnt_nr = ATOMIC_INIT(0),
+ .mnt_avail_nr = ATOMIC_INIT(INT_MAX),
.meminfo_val = VE_MEMINFO_SYSTEM,
#ifdef CONFIG_X86
.umh_running_helpers = ATOMIC_INIT(0),
@@ -787,7 +787,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
atomic_set(&ve->arp_neigh_nr, 0);
atomic_set(&ve->nd_neigh_nr, 0);
- atomic_set(&ve->mnt_nr, 0);
+ atomic_set(&ve->mnt_avail_nr, sysctl_ve_mount_nr);
#ifdef CONFIG_COREDUMP
strcpy(ve->core_pattern, "core");
@@ -1063,9 +1063,9 @@ static u64 ve_netns_avail_nr_read(struct cgroup_subsys_state *css, struct cftype
return atomic_read(&css_to_ve(css)->netns_avail_nr);
}
-static u64 ve_mnt_nr_read(struct cgroup_subsys_state *css, struct cftype *cft)
+static s64 ve_mnt_avail_nr_read(struct cgroup_subsys_state *css, struct cftype *cft)
{
- return atomic_read(&css_to_ve(css)->mnt_nr);
+ return atomic_read(&css_to_ve(css)->mnt_avail_nr);
}
static u64 ve_netif_max_nr_read(struct cgroup_subsys_state *css, struct cftype *cft)
@@ -1627,8 +1627,8 @@ static struct cftype ve_cftypes[] = {
.read_u64 = ve_netns_avail_nr_read,
},
{
- .name = "mnt_nr",
- .read_u64 = ve_mnt_nr_read,
+ .name = "mnt_avail_nr",
+ .read_s64 = ve_mnt_avail_nr_read,
},
{
.name = "netif_max_nr",
--
2.47.1
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* [Devel] [PATCH VZ10] core: allow to bypass rhel pci blacklist
@ 2026-09-15 16:02 7% Alexey Kuznetsov
0 siblings, 0 replies; 200+ results
From: Alexey Kuznetsov @ 2026-09-15 16:02 UTC (permalink / raw)
To: devel, khorenko, kui.liu, azaitsev
Enforcing blacklist was rude decision from rhel side.
It makes it impossible to boot release kernels on some of our
lab equipment.
The patch removes from blacklist one and only one specific
device, it is one on Mellanox ConnectX-3 cards. I insist
the removal of ConnectX-3 has no technical ground,
X3 and X4 is the same hardware, their driver is highly
stable and did not even change between rhel9 and 10.
In case they are going to drop entire mlx4 driver,
we have to deal with this, but not now.
Also, kernel option skip_pci_blacklist is added just in case
some real customer hits the same obstacle. I am not sure
it is usable, but at least if the host with blacklisted
hardware has IPMI console it is possible to deploy VZ10 there.
Signed-off-by: Alexey Kuznetsov <kuznet@virtuozzo.com>
---
kernel/rh_messages.c | 10 ++++++++++
kernel/rh_messages.h | 1 -
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/kernel/rh_messages.c b/kernel/rh_messages.c
index bb69e8965748..db784e0e5c72 100644
--- a/kernel/rh_messages.c
+++ b/kernel/rh_messages.c
@@ -20,6 +20,8 @@
#include <linux/pci.h>
#include "rh_messages.h"
+static bool skip_pci_blacklist;
+
/**
* mark_hardware_unmaintained() - Mark hardware as unmaintained.
* @driver_name: driver name
@@ -164,6 +166,12 @@ static bool __maybe_unused pci_hw_disabled(struct pci_dev *dev)
if (!ret)
return false;
+ if (skip_pci_blacklist) {
+ mark_hardware_deprecated(dev_driver_string(&dev->dev), "%04X:%04X @ %s",
+ dev->device, dev->vendor, pci_name(dev));
+ return false;
+ }
+
mark_hardware_disabled(dev_driver_string(&dev->dev), "%04X:%04X @ %s",
dev->device, dev->vendor, pci_name(dev));
return true;
@@ -383,6 +391,8 @@ bool __maybe_unused pci_rh_check_status(struct pci_dev *pci_dev)
}
#endif
+core_param(skip_pci_blacklist, skip_pci_blacklist, bool, 0644);
+
/** module_rh_check_status - checks the status of a module.
* @module_name: Name of module to be examined
*
diff --git a/kernel/rh_messages.h b/kernel/rh_messages.h
index 05ad12af6a6d..9a184a9fa659 100644
--- a/kernel/rh_messages.h
+++ b/kernel/rh_messages.h
@@ -265,7 +265,6 @@ static const struct pci_device_id rh_disabled_pci_devices[] = {
{ 0x15B3, 0x1004, PCI_ANY_ID, PCI_ANY_ID },
{ 0x15B3, 0x1005, PCI_ANY_ID, PCI_ANY_ID },
{ 0x15B3, 0x1006, PCI_ANY_ID, PCI_ANY_ID },
- { 0x15B3, 0x1007, PCI_ANY_ID, PCI_ANY_ID },
{ 0x15B3, 0x1008, PCI_ANY_ID, PCI_ANY_ID },
{ 0x15B3, 0x1009, PCI_ANY_ID, PCI_ANY_ID },
{ 0x15B3, 0x100a, PCI_ANY_ID, PCI_ANY_ID },
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 7%]
* [Devel] [PATCH VZ10] fs/fuse pcs: logging improvement
@ 2026-09-15 16:01 9% Alexey Kuznetsov
0 siblings, 0 replies; 200+ results
From: Alexey Kuznetsov @ 2026-09-15 16:01 UTC (permalink / raw)
To: devel, khorenko, kui.liu, azaitsev
Engage the second log in kernel, some important not-abusive messages
are duplicated in the secondary log which is rotated out much slower.
Note, at the moment we are actually not sure exactly what messages
are really not-abusive. Now we follow the same guidelines as
we have recently done in user space. If some of them will be too
frequent we will eliminate them later, hopefully, before release.
F.e. one dubious place: errors like CSD_STALE_MAP and CSD_REPLICATING,
they are interesting and can help to understand some anomalies,
yet they are not actual errors and can be too frequent.
Also, convert all %p to %px in internal traces. These traces are internal
and their purpose is to link traced history to live state and/or
crashdumps so that it does not make sense to mangle them
Affects: #VSTOR-144510
Signed-off-by: Alexey Kuznetsov <kuznet@virtuozzo.com>
---
fs/fuse/kio/pcs/fuse_io.c | 16 +++---
fs/fuse/kio/pcs/fuse_ktrace.h | 17 +++++--
fs/fuse/kio/pcs/fuse_ktrace_prot.h | 1 +
fs/fuse/kio/pcs/pcs_cluster.c | 2 +-
fs/fuse/kio/pcs/pcs_cluster_core.c | 2 +-
fs/fuse/kio/pcs/pcs_cs.c | 38 +++++++--------
fs/fuse/kio/pcs/pcs_cs_accel.c | 24 ++++-----
fs/fuse/kio/pcs/pcs_fuse_kdirect.c | 31 ++++++------
fs/fuse/kio/pcs/pcs_map.c | 42 ++++++++--------
fs/fuse/kio/pcs/pcs_map.h | 12 ++---
fs/fuse/kio/pcs/pcs_rdma_conn.c | 12 ++---
fs/fuse/kio/pcs/pcs_rdma_io.c | 78 +++++++++++++++---------------
fs/fuse/kio/pcs/pcs_req.c | 2 +-
fs/fuse/kio/pcs/pcs_rpc.c | 22 ++++-----
fs/fuse/kio/pcs/pcs_sock_conn.c | 2 +-
fs/fuse/kio/pcs/pcs_sock_io.c | 22 ++++-----
16 files changed, 165 insertions(+), 158 deletions(-)
diff --git a/fs/fuse/kio/pcs/fuse_io.c b/fs/fuse/kio/pcs/fuse_io.c
index 0562809e8bee..9d0467a902e1 100644
--- a/fs/fuse/kio/pcs/fuse_io.c
+++ b/fs/fuse/kio/pcs/fuse_io.c
@@ -71,7 +71,7 @@ static void on_read_done(struct pcs_fuse_req *r, size_t size)
struct fuse_io_args *ia = container_of(r->req.args, typeof(*ia), ap.args);
struct fuse_read_in * in = &ia->read.in;
- DTRACE("do fuse_request_end req:%p op:%d err:%d\n", &r->req, r->req.in.h.opcode, r->req.out.h.error);
+ DTRACE("do fuse_request_end req:%px op:%d err:%d\n", &r->req, r->req.in.h.opcode, r->req.out.h.error);
if (r->req.out.h.error && r->req.args->page_zeroing) {
pcs_zero_fill_read_resp(&r->req, 0, in->size);
@@ -90,7 +90,7 @@ static void on_sync_done(struct pcs_fuse_req *r)
struct pcs_fuse_cluster *pfc = cl_from_req(r);
struct fuse_inode *fi = get_fuse_inode(r->req.args->io_inode);
- DTRACE("do fuse_request_end req:%p op:%d err:%d\n", &r->req, r->req.in.h.opcode, r->req.out.h.error);
+ DTRACE("do fuse_request_end req:%px op:%d err:%d\n", &r->req, r->req.in.h.opcode, r->req.out.h.error);
fuse_stat_observe(pfc->fc, KFUSE_OP_FSYNC, ktime_sub(ktime_get(), r->exec.ireq.ts));
fuse_stat_account(pfc->fc, KFUSE_OP_FSYNC, 0);
fuse_read_dio_end(fi);
@@ -107,7 +107,7 @@ static void on_write_done(struct pcs_fuse_req *r, off_t pos, size_t size)
out->size = size;
- DTRACE("do fuse_request_end req:%p op:%d err:%d\n", &r->req, r->req.in.h.opcode, r->req.out.h.error);
+ DTRACE("do fuse_request_end req:%px op:%d err:%d\n", &r->req, r->req.in.h.opcode, r->req.out.h.error);
fuse_stat_observe(pfc->fc, KFUSE_OP_WRITE, ktime_sub(ktime_get(), r->exec.ireq.ts));
fuse_stat_account(pfc->fc, KFUSE_OP_WRITE, size);
fuse_write_dio_end(fi);
@@ -119,7 +119,7 @@ static void on_fallocate_done(struct pcs_fuse_req *r, off_t pos, size_t size)
struct pcs_fuse_cluster *pfc = cl_from_req(r);
struct fuse_inode *fi = get_fuse_inode(r->req.args->io_inode);
- DTRACE("do fuse_request_end req:%p op:%d err:%d\n", &r->req, r->req.in.h.opcode, r->req.out.h.error);
+ DTRACE("do fuse_request_end req:%px op:%d err:%d\n", &r->req, r->req.in.h.opcode, r->req.out.h.error);
fuse_stat_observe(pfc->fc, KFUSE_OP_FALLOCATE, ktime_sub(ktime_get(), r->exec.ireq.ts));
fuse_stat_account(pfc->fc, KFUSE_OP_FALLOCATE, 0);
fuse_write_dio_end(fi);
@@ -131,7 +131,7 @@ static void on_fiemap_done(struct pcs_fuse_req *r)
{
struct fuse_inode *fi = get_fuse_inode(r->req.args->io_inode);
- DTRACE("do fuse_request_end req:%p op:%d err:%d\n", &r->req, r->req.in.h.opcode, r->req.out.h.error);
+ DTRACE("do fuse_request_end req:%px op:%d err:%d\n", &r->req, r->req.in.h.opcode, r->req.out.h.error);
fuse_write_dio_end(fi);
fuse_request_end(&r->req);
@@ -191,8 +191,8 @@ static void prepare_io_(struct pcs_fuse_req *r, unsigned short type, off_t offse
/* Use inline request structure */
struct pcs_int_request *ireq = &r->exec.ireq;
- TRACE("INIT r(%p) ireq:%p {%ld, %ld}\n", r, ireq, offset, size);
- FUSE_KDTRACE(ireq->cc->fc, "req %d {%p} " DENTRY_FMT " %ld+%ld", type, ireq, DENTRY_ARGS(ireq->dentry), offset, size);
+ TRACE("INIT r(%px) ireq:%px {%ld, %ld}\n", r, ireq, offset, size);
+ FUSE_KDTRACE(ireq->cc->fc, "req %d {%px} " DENTRY_FMT " %ld+%ld", type, ireq, DENTRY_ARGS(ireq->dentry), offset, size);
/* Initialize IO request */
switch (type)
@@ -293,7 +293,7 @@ static void falloc_req_complete(struct pcs_int_request *ireq)
list_del_init(&r->req.list);
spin_unlock(&di->kq_lock);
- DTRACE("do fuse_request_end req:%p op:%d err:%d\n", &r->req, r->req.in.h.opcode, r->req.out.h.error);
+ DTRACE("do fuse_request_end req:%px op:%d err:%d\n", &r->req, r->req.in.h.opcode, r->req.out.h.error);
fuse_stat_observe(pfc->fc, KFUSE_OP_FALLOCATE, ktime_sub(ktime_get(), ireq->ts));
fuse_write_dio_end(fi);
diff --git a/fs/fuse/kio/pcs/fuse_ktrace.h b/fs/fuse/kio/pcs/fuse_ktrace.h
index 2f3622ac83a6..c12d49bee7c8 100644
--- a/fs/fuse/kio/pcs/fuse_ktrace.h
+++ b/fs/fuse/kio/pcs/fuse_ktrace.h
@@ -60,10 +60,19 @@ static inline void * fuse_trace_prepare(struct fuse_ktrace * tr, int type, int l
#define FUSE_TRACE_PREPARE(tr, type, len) fuse_trace_prepare((tr), (type), (len))
#define FUSE_TRACE_COMMIT(tr) preempt_enable()
-void __printf(3, 4) __kfuse_trace(struct fuse_conn * fc, unsigned long ip, const char * fmt, ...);
+void __printf(4, 5) __kfuse_trace(struct fuse_conn *fc, int level, unsigned long ip, const char *fmt, ...);
-#define FUSE_KTRACE(fc, fmt, args...) do { struct fuse_conn * __fc = (fc); if (__fc->ktrace_level >= LOG_TRACE) __kfuse_trace(__fc, _THIS_IP_, "%s: " fmt, __FUNCTION__, ## args); } while (0)
-#define FUSE_KDTRACE(fc, fmt, args...) do { struct fuse_conn * __fc = (fc); if (__fc->ktrace_level >= LOG_DTRACE) __kfuse_trace(__fc, _THIS_IP_, "%s: " fmt, __FUNCTION__, ## args); } while (0)
-#define FUSE_KLOG(fc, level, fmt, args...) do { struct fuse_conn * __fc = (fc); if (__fc->ktrace_level >= (level)) __kfuse_trace(__fc, 0, "%s: " fmt, __FUNCTION__, ## args); } while (0)
+#define FUSE_KTRACE(fc, fmt, args...) do { struct fuse_conn *__fc = (fc); \
+ if (__fc->ktrace_level >= LOG_TRACE) \
+ __kfuse_trace(__fc, LOG_TRACE, _THIS_IP_, "%s: " fmt, __func__, ## args); } while (0)
+#define FUSE_KDTRACE(fc, fmt, args...) do { struct fuse_conn *__fc = (fc); \
+ if (__fc->ktrace_level >= LOG_DTRACE) \
+ __kfuse_trace(__fc, LOG_DTRACE, _THIS_IP_, "%s: " fmt, __func__, ## args); } while (0)
+#define FUSE_KDEBUG(fc, fmt, args...) do { struct fuse_conn *__fc = (fc); \
+ if (__fc->ktrace_level >= LOG_DEBUG) \
+ __kfuse_trace(__fc, LOG_DEBUG, _THIS_IP_, "%s: " fmt, __func__, ## args); } while (0)
+#define FUSE_KLOG(fc, level, fmt, args...) do { struct fuse_conn *__fc = (fc); \
+ if (__fc->ktrace_level >= (level)) \
+ __kfuse_trace(__fc, (level), 0, "%s: " fmt, __func__, ## args); } while (0)
#endif /* _FUSE_KTRACE_H_ */
diff --git a/fs/fuse/kio/pcs/fuse_ktrace_prot.h b/fs/fuse/kio/pcs/fuse_ktrace_prot.h
index 53356c7cfddc..c3e6472435d2 100644
--- a/fs/fuse/kio/pcs/fuse_ktrace_prot.h
+++ b/fs/fuse/kio/pcs/fuse_ktrace_prot.h
@@ -23,6 +23,7 @@ struct fuse_trace_hdr
#define FUSE_KTRACE_STRING 1
#define FUSE_KTRACE_IOTIMES 2
+#define FUSE_KTRACE_STRING_WARN 3
struct fuse_tr_iotimes_hdr
{
diff --git a/fs/fuse/kio/pcs/pcs_cluster.c b/fs/fuse/kio/pcs/pcs_cluster.c
index 85fdeb932611..e5ecb93447bb 100644
--- a/fs/fuse/kio/pcs/pcs_cluster.c
+++ b/fs/fuse/kio/pcs/pcs_cluster.c
@@ -347,7 +347,7 @@ static noinline void __pcs_cc_process_ireq_rw(struct pcs_int_request *ireq)
return;
}
- TRACE(DENTRY_FMT " %p op=%d at %llu [%llu]\n", DENTRY_ARGS(di), ireq, ireq->apireq.req->type,
+ TRACE(DENTRY_FMT " %px op=%d at %llu [%llu]\n", DENTRY_ARGS(di), ireq, ireq->apireq.req->type,
(unsigned long long)ireq->apireq.req->pos, (unsigned long long)ireq->apireq.req->size);
diff --git a/fs/fuse/kio/pcs/pcs_cluster_core.c b/fs/fuse/kio/pcs/pcs_cluster_core.c
index 098d8e02757b..f79ce1ae09d0 100644
--- a/fs/fuse/kio/pcs/pcs_cluster_core.c
+++ b/fs/fuse/kio/pcs/pcs_cluster_core.c
@@ -109,7 +109,7 @@ static void cc_workqueue_handler(struct work_struct *w)
struct pcs_int_request *ireq = list_first_entry(&queue, struct pcs_int_request, list);
list_del_init(&ireq->list);
- TRACE("process ireq:%p" DENTRY_FMT " type=%u\n", ireq, DENTRY_ARGS(ireq->dentry), ireq->type);
+ TRACE("process ireq:%px" DENTRY_FMT " type=%u\n", ireq, DENTRY_ARGS(ireq->dentry), ireq->type);
cc->op.ireq_process(ireq);
}
}
diff --git a/fs/fuse/kio/pcs/pcs_cs.c b/fs/fuse/kio/pcs/pcs_cs.c
index f6f2d16b0e48..85e5af2d28c1 100644
--- a/fs/fuse/kio/pcs/pcs_cs.c
+++ b/fs/fuse/kio/pcs/pcs_cs.c
@@ -208,7 +208,7 @@ struct pcs_cs *pcs_cs_find_create(struct pcs_cs_set *csset, PCS_NODE_ID_T *id, P
cs->addr_serno =
atomic_inc_return(&csset->cs_addr_change_sn);
- FUSE_KTRACE(cc_from_csset(csset)->fc,
+ FUSE_KDEBUG(cc_from_csset(csset)->fc,
"Port change CS" NODE_FMT " seq=%d",
NODE_ARGS(*id), cs->addr_serno);
pcs_rpc_set_address(rpc, addr);
@@ -380,7 +380,7 @@ static void cs_response_done(struct pcs_msg *msg)
pcs_map_verify_sync_state(ireq->dentry, ireq, msg);
} else {
- FUSE_KTRACE(ireq->cc->fc, XID_FMT " IO error %d %lu, ireq:%p : %llu:%u+%u",
+ FUSE_KDEBUG(ireq->cc->fc, XID_FMT " IO error %d %lu, ireq:%px : %llu:%u+%u",
XID_ARGS(ireq->iochunk.ir.hbuf.hdr.xid), msg->error.value,
msg->error.remote ? (unsigned long)msg->error.offender.val : 0UL,
ireq, (unsigned long long)ireq->iochunk.chunk,
@@ -408,7 +408,7 @@ static void cs_get_read_response_iter(struct pcs_msg *msg, int offset, struct io
msg->_inline_kv.iov_len = sizeof(struct pcs_cs_iohdr);
iov_iter_kvec(it, direction, &msg->_inline_kv, 1, sizeof(struct pcs_cs_iohdr));
iov_iter_advance(it, offset);
- TRACE("return msg:%p->size:%d off:%d it_len:%ld\n", msg, msg->size, offset, iov_iter_count(it));
+ TRACE("return msg:%px->size:%d off:%d it_len:%ld\n", msg, msg->size, offset, iov_iter_count(it));
return;
} else {
struct pcs_msg *req = msg->private;
@@ -426,7 +426,7 @@ static void cs_get_read_response_iter(struct pcs_msg *msg, int offset, struct io
iov_iter_truncate(it, msg->size - sizeof(struct pcs_cs_iohdr));
iov_iter_advance(it, offset);
- TRACE("return msg:%p->size:%d off:%d it_len:%ld\n", msg, msg->size, offset, iov_iter_count(it));
+ TRACE("return msg:%px->size:%d off:%d it_len:%ld\n", msg, msg->size, offset, iov_iter_count(it));
return;
} else
BUG();
@@ -493,7 +493,7 @@ static void cs_get_data(struct pcs_msg *msg, int offset, struct iov_iter *it, un
iov_iter_kvec(it, direction, &ireq->iochunk.ir.hbuf_kv, 1,
sizeof(struct pcs_cs_iohdr));
iov_iter_advance(it, offset);
- TRACE("return msg:%p->size:%d off:%d it_len:%ld\n", msg, msg->size, offset, iov_iter_count(it));
+ TRACE("return msg:%px->size:%d off:%d it_len:%ld\n", msg, msg->size, offset, iov_iter_count(it));
return;
} else {
@@ -508,7 +508,7 @@ static void cs_get_data(struct pcs_msg *msg, int offset, struct iov_iter *it, un
iov_iter_truncate(it, ireq->iochunk.size);
iov_iter_advance(it, offset);
- TRACE("return msg:%p->size:%d off:%d it_len:%ld\n", msg, msg->size, offset, iov_iter_count(it));
+ TRACE("return msg:%px->size:%d off:%d it_len:%ld\n", msg, msg->size, offset, iov_iter_count(it));
return;
} else
BUG();
@@ -570,7 +570,7 @@ static void ireq_complete_fo(struct pcs_int_request * ireq)
return;
if (pcs_if_error(&ireq->error)) {
- FUSE_KTRACE(ireq->cc->fc, "IO error %d %lu, ireq:%p : %llu:%u+%u",
+ FUSE_KDEBUG(ireq->cc->fc, "IO error %d %lu, ireq:%px : %llu:%u+%u",
ireq->error.value,
ireq->error.remote ? (unsigned long)ireq->error.offender.val : 0UL,
ireq, (unsigned long long)ireq->iochunk.chunk,
@@ -736,7 +736,7 @@ static void do_cs_submit(struct pcs_cs *cs, struct pcs_int_request *ireq)
ireq->wait_origin.val = 0;
- DTRACE(XID_FMT " About to send msg:%p, ireq:%p, cmd:%u,"
+ DTRACE(XID_FMT " About to send msg:%px, ireq:%px, cmd:%u,"
" id:"CUID_FMT" v:"VER_FMT" - %llu:%u+%u\n",
XID_ARGS(ireq->iochunk.ir.hbuf.hdr.xid), msg, ireq,
ireq->iochunk.cmd, CUID_ARGS(ioh->uid),
@@ -835,7 +835,7 @@ void cs_handle_congestion(struct pcs_rpc *ep, struct pcs_rpc_hdr *h)
struct pcs_cluster_core *cc = cc_from_rpc(ep->eng);
struct pcs_cs *who;
- FUSE_KTRACE(cc->fc, "Received congestion notification from CS" NODE_FMT, NODE_ARGS(h->xid.origin));
+ FUSE_KDEBUG(cc->fc, "Received congestion notification from CS" NODE_FMT, NODE_ARGS(h->xid.origin));
who = lookup_and_lock_cs(&cc->css, h->xid.origin.val);
if (unlikely(!who))
@@ -934,7 +934,7 @@ void cs_keep_waiting(struct pcs_rpc *ep, struct pcs_msg *req, struct pcs_msg *ms
&& may_reroute(ireq->iochunk.csl, h->xid.origin)) {
ireq->iochunk.banned_cs = h->xid.origin;
spin_unlock(&who->lock);
- FUSE_KTRACE(cc->fc, "Canceling read on CS" NODE_FMT, NODE_ARGS(h->xid.origin));
+ FUSE_KDEBUG(cc->fc, "Canceling read on CS" NODE_FMT, NODE_ARGS(h->xid.origin));
pcs_rpc_cancel_request(req);
return;
}
@@ -1228,7 +1228,7 @@ static void cs_probe_done(struct pcs_msg *msg)
if (!pcs_if_error(&msg->error)) {
cs_whitelist(cs, "probe");
} else {
- FUSE_KTRACE(cc_from_csset(css)->fc, "probe error %d", msg->error.value);
+ FUSE_KDEBUG(cc_from_csset(css)->fc, "probe error %d", msg->error.value);
cs_blacklist(cs, msg->error.value, "probe");
}
cs->use_count--;
@@ -1430,7 +1430,7 @@ void pcs_dislog_host_add(struct pcs_cs_set *css, u64 host_id)
if (h && nh)
kfree(nh);
if (!h)
- FUSE_KTRACE(cc_from_csset(css)->fc,
+ FUSE_KDEBUG(cc_from_csset(css)->fc,
"Host#%lu is down%s",
(unsigned long)val, complain ? ", failed to remember" : "");
}
@@ -1450,7 +1450,7 @@ void pcs_dislog_host_del(struct pcs_cs_set *css, u64 host_id)
hlist_del_rcu(&h->link);
spin_unlock(&css->lock);
if (h) {
- FUSE_KTRACE(cc_from_csset(css)->fc, "Host#%lu is up", (unsigned long)val);
+ FUSE_KDEBUG(cc_from_csset(css)->fc, "Host#%lu is up", (unsigned long)val);
call_rcu(&h->rcu, host_destroy_rcu);
}
}
@@ -1561,7 +1561,7 @@ int __pcs_cs_cong_enqueue_cond(struct pcs_int_request *ireq, struct pcs_cs *cs)
ireq->qdepth = cs->cong_queue_len;
}
if (queued && ireq->type == PCS_IREQ_IOCHUNK)
- FUSE_KDTRACE(ireq->cc->fc, "queued%d {%p} cpu%u %d %u/%u " DENTRY_FMT " %llu+%llu",
+ FUSE_KDTRACE(ireq->cc->fc, "queued%d {%px} cpu%u %d %u/%u " DENTRY_FMT " %llu+%llu",
queued, ireq, smp_processor_id(), cs->cong_queue_len,
cs->in_flight, cs->eff_cwnd, DENTRY_ARGS(ireq->dentry),
ireq->iochunk.chunk + ireq->iochunk.offset, ireq->iochunk.size);
@@ -1611,12 +1611,12 @@ int pcs_cs_dislog_event(struct pcs_cluster_core *cc, struct pcs_mds_cached_event
switch (evt->flags & PCS_MDS_EVT_F_TYPE_MASK) {
case PCS_MDS_EVT_F_ADDR_CHANGED:
cs->addr_serno = atomic_inc_return(&cc->css.cs_addr_change_sn);
- FUSE_KTRACE(cc_from_csset(cs->css)->fc,
+ FUSE_KDEBUG(cc_from_csset(cs->css)->fc,
"Dislog addr change CS" NODE_FMT " seq=%d",
NODE_ARGS(cs->id), cs->addr_serno);
break;
case PCS_MDS_EVT_F_MAINTENANCE_IN:
- FUSE_KTRACE(cc_from_csset(cs->css)->fc,
+ FUSE_KDEBUG(cc_from_csset(cs->css)->fc,
"Dislog enter maintanance CS" NODE_FMT,
NODE_ARGS(cs->id));
cs->mds_flags |= CS_FL_MAINTENANCE;
@@ -1625,7 +1625,7 @@ int pcs_cs_dislog_event(struct pcs_cluster_core *cc, struct pcs_mds_cached_event
case PCS_MDS_EVT_F_MAINTENANCE_OUT:
cs->mds_flags &= ~CS_FL_MAINTENANCE;
clear_bit(CS_SF_MAINTENANCE, &cs->state);
- FUSE_KTRACE(cc_from_csset(cs->css)->fc,
+ FUSE_KDEBUG(cc_from_csset(cs->css)->fc,
"Dislog leave maintanance CS" NODE_FMT,
NODE_ARGS(cs->id));
break;
@@ -1633,14 +1633,14 @@ int pcs_cs_dislog_event(struct pcs_cluster_core *cc, struct pcs_mds_cached_event
cs->mds_flags |= CS_FL_ILL;
set_bit(CS_SF_ILL, &cs->state);
atomic_inc(&cc->css.ill_cs_sn);
- FUSE_KTRACE(cc_from_csset(cs->css)->fc,
+ FUSE_KDEBUG(cc_from_csset(cs->css)->fc,
"Dislog enter ILL CS" NODE_FMT,
NODE_ARGS(cs->id));
break;
case PCS_MDS_EVT_F_ILL_OUT:
cs->mds_flags &= ~CS_FL_ILL;
clear_bit(CS_SF_ILL, &cs->state);
- FUSE_KTRACE(cc_from_csset(cs->css)->fc,
+ FUSE_KDEBUG(cc_from_csset(cs->css)->fc,
"Dislog leave ILL CS" NODE_FMT,
NODE_ARGS(cs->id));
break;
diff --git a/fs/fuse/kio/pcs/pcs_cs_accel.c b/fs/fuse/kio/pcs/pcs_cs_accel.c
index f57e93b98731..bc11f47352db 100644
--- a/fs/fuse/kio/pcs/pcs_cs_accel.c
+++ b/fs/fuse/kio/pcs/pcs_cs_accel.c
@@ -279,7 +279,7 @@ static int verify_crc(struct pcs_int_request *ireq, unsigned int size, u32 *crc)
ccrc = ~ccrc;
if (ccrc != crc[i]) {
- FUSE_KTRACE(ireq->cc->fc, "CRC error pg=%d@%u %08x %08x\n", i,
+ FUSE_KDEBUG(ireq->cc->fc, "CRC error pg=%d@%u %08x %08x", i,
(unsigned)ireq->iochunk.offset, ccrc, crc[i]);
return 1;
}
@@ -501,7 +501,7 @@ static void __pcs_csa_final_completion(struct pcs_aio_req *areq)
FUSE_TRACE_COMMIT(fc->ktrace);
}
} else {
- FUSE_KTRACE(ireq->cc->fc, "AIO error %d %lu, ireq:%p : %llu:%u+%u",
+ FUSE_KDEBUG(ireq->cc->fc, "AIO error %d %lu, ireq:%px : %llu:%u+%u",
ireq->error.value,
ireq->error.remote ? (unsigned long)ireq->error.offender.val : 0UL,
ireq, (unsigned long long)ireq->iochunk.chunk,
@@ -603,7 +603,7 @@ static void csa_crc_work(struct work_struct *w)
*/
sz = kernel_read(areq->cfile, areq->crc, ncrc, &pos);
if (sz != ncrc) {
- FUSE_KTRACE(ireq->cc->fc, "Did not read crc res=%u expected=%u", (unsigned)sz, (unsigned)ncrc);
+ FUSE_KDEBUG(ireq->cc->fc, "Did not read crc res=%u expected=%u", (unsigned int)sz, (unsigned int)ncrc);
goto out;
}
FUSE_KDTRACE(ireq->cc->fc, "Read crc page %08lx at %llu", (unsigned long)areq->cfile, pos);
@@ -694,7 +694,7 @@ static inline int csa_submit(struct file * file, struct file *cfile, int do_csum
if (ret != size) {
/* Do not drop refs, we do not want to complete ireq. */
fput(areq->iocb.ki_filp);
- FUSE_KTRACE(ireq->cc->fc, "AIO submit rejected ret=%d %lu, ireq:%p : %llu:%u+%u",
+ FUSE_KDEBUG(ireq->cc->fc, "AIO submit rejected ret=%d %lu, ireq:%px : %llu:%u+%u",
ret, ireq->error.remote ? (unsigned long)ireq->error.offender.val : 0UL,
ireq, (unsigned long long)ireq->iochunk.chunk,
(unsigned)ireq->iochunk.offset,
@@ -703,7 +703,7 @@ static inline int csa_submit(struct file * file, struct file *cfile, int do_csum
}
/* IO already finished. Drop AIO refcnt and proceed to crc */
- FUSE_KTRACE(ireq->cc->fc, "No good, AIO executed synchronously, ireq:%p : %llu:%u+%u",
+ FUSE_KTRACE(ireq->cc->fc, "No good, AIO executed synchronously, ireq:%px : %llu:%u+%u",
ireq, (unsigned long long)ireq->iochunk.chunk,
(unsigned)ireq->iochunk.offset,
(unsigned)size);
@@ -793,8 +793,8 @@ static void csa_rpc_aio_work(struct work_struct *w)
if (unlikely(ret != -EIOCBQUEUED)) {
if (ret != ireq->iochunk.size) {
- FUSE_KTRACE(ireq->cc->fc,
- "AIO submit rejected ret=%d %lu, ireq:%p : %llu:%u+%u",
+ FUSE_KDEBUG(ireq->cc->fc,
+ "AIO submit rejected ret=%d %lu, ireq:%px : %llu:%u+%u",
ret,
ireq->error.remote ? (unsigned long)ireq->error.offender.val
: 0UL,
@@ -802,7 +802,7 @@ static void csa_rpc_aio_work(struct work_struct *w)
(unsigned int)ireq->iochunk.offset,
(unsigned int)ireq->iochunk.size);
if (ret < 0) {
- FUSE_KTRACE(ireq->cc->fc, "segs=%d %d %u %u+%u", (int)it->nr_segs,
+ FUSE_KDEBUG(ireq->cc->fc, "segs=%d %d %u %u+%u", (int)it->nr_segs,
(int)it->count,
(unsigned int)it->iov_offset, it->bvec[0].bv_offset,
it->bvec[0].bv_len
@@ -812,7 +812,7 @@ static void csa_rpc_aio_work(struct work_struct *w)
} else {
/* IO already finished. Drop AIO refcnt and proceed to crc */
FUSE_KTRACE(ireq->cc->fc,
- "No good, AIO executed synchronously, ireq:%p : %llu:%u+%u",
+ "No good, AIO executed synchronously, ireq:%px : %llu:%u+%u",
ireq, (unsigned long long)ireq->iochunk.chunk,
(unsigned int)ireq->iochunk.offset,
(unsigned int)ireq->iochunk.size);
@@ -1084,7 +1084,7 @@ static void __complete_acr_work(struct work_struct * w)
}
if (pcs_if_error(&ireq->error)) {
- FUSE_KTRACE(ireq->cc->fc, "IO error %d %lu, ireq:%p : %llu:%u+%u",
+ FUSE_KDEBUG(ireq->cc->fc, "IO error %d %lu, ireq:%px : %llu:%u+%u",
ireq->error.value,
ireq->error.remote ? (unsigned long)ireq->error.offender.val : 0UL,
ireq, (unsigned long long)ireq->iochunk.chunk,
@@ -1403,7 +1403,7 @@ static inline int csa_submit_write(struct file * file, struct pcs_int_request *
if (ret != size) {
/* Do not drop refs, we do not want to complete ireq. */
fput(areq->iocb.ki_filp);
- FUSE_KTRACE(ireq->cc->fc, "AIO submit rejected ret=%d %lu, ireq:%p : %llu:%u+%u",
+ FUSE_KDEBUG(ireq->cc->fc, "AIO submit rejected ret=%d %lu, ireq:%px : %llu:%u+%u",
ret, ireq->error.remote ? (unsigned long)ireq->error.offender.val : 0UL,
ireq, (unsigned long long)ireq->iochunk.chunk,
(unsigned)ireq->iochunk.offset,
@@ -1414,7 +1414,7 @@ static inline int csa_submit_write(struct file * file, struct pcs_int_request *
}
/* IO already finished. Drop AIO refcnt yet. */
- FUSE_KTRACE(ireq->cc->fc, "No good, AIO executed synchronously, ireq:%p : %llu:%u+%u",
+ FUSE_KTRACE(ireq->cc->fc, "No good, AIO executed synchronously, ireq:%px : %llu:%u+%u",
ireq, (unsigned long long)ireq->iochunk.chunk,
(unsigned)ireq->iochunk.offset,
(unsigned)size);
diff --git a/fs/fuse/kio/pcs/pcs_fuse_kdirect.c b/fs/fuse/kio/pcs/pcs_fuse_kdirect.c
index 9f345daec2a2..efbab75654c8 100644
--- a/fs/fuse/kio/pcs/pcs_fuse_kdirect.c
+++ b/fs/fuse/kio/pcs/pcs_fuse_kdirect.c
@@ -301,7 +301,7 @@ static void kpcs_conn_fini(struct fuse_mount *fm)
if (!pfc)
return;
- TRACE("%s fc:%p\n", __FUNCTION__, fc);
+ TRACE("%s fc:%px\n", __func__, fc);
WRITE_ONCE(fc->kio.ctx, NULL);
unregister_client(pfc);
@@ -527,7 +527,7 @@ static void kpcs_file_close(struct file *file, struct inode *inode)
lockdep_assert_held(&inode->i_rwsem);
- TRACE("file close - fi: %p, di: %p", fi, di);
+ TRACE("file close - fi: %px, di: %px", fi, di);
if (!di)
return;
@@ -781,7 +781,7 @@ static void fuse_size_grow_work(struct work_struct *w)
BUG_ON(!r->exec.size_required);
if (size >= r->exec.size_required) {
- TRACE("resubmit ino:%ld r(%p)->size:%lld required:%lld\n",
+ TRACE("resubmit ino:%ld r(%px)->size:%lld required:%lld\n",
inode->i_ino, r, r->exec.size_required, size);
list_move(&ireq->list, &pending_reqs);
}
@@ -801,7 +801,7 @@ static void wait_grow(struct pcs_fuse_req *r, struct pcs_dentry_info *di, unsign
BUG_ON(r->req.in.h.opcode != FUSE_WRITE && r->req.in.h.opcode != FUSE_FALLOCATE);
BUG_ON(di->size.op != PCS_SIZE_INACTION && di->size.op != PCS_SIZE_GROW);
- TRACE("insert ino:%ld->required:%lld r(%p)->required:%lld\n",
+ TRACE("insert ino:%ld->required:%lld r(%px)->required:%lld\n",
r->req.args->io_inode->i_ino, di->size.required, r,
required);
r->exec.size_required = required;
@@ -821,7 +821,7 @@ static void wait_shrink(struct pcs_fuse_req *r, struct pcs_dentry_info *di)
/* Writes already blocked via fuse_set_nowrite */
BUG_ON(r->req.in.h.opcode != FUSE_READ && r->req.in.h.opcode != FUSE_FSYNC && r->req.in.h.opcode != FUSE_FLUSH);
- TRACE("insert ino:%ld r:%p\n", r->req.args->io_inode->i_ino, r);
+ TRACE("insert ino:%ld r:%px\n", r->req.args->io_inode->i_ino, r);
list_add_tail(&r->exec.ireq.list, &di->size.queue);
}
@@ -1078,7 +1078,7 @@ static void pcs_fuse_submit(struct pcs_fuse_cluster *pfc, struct fuse_req *req)
}
req->out.h.error = 0;
error:
- DTRACE("do fuse_request_end req:%p op:%d err:%d\n", req, req->in.h.opcode, req->out.h.error);
+ DTRACE("do fuse_request_end req:%px op:%d err:%d\n", req, req->in.h.opcode, req->out.h.error);
__fuse_request_end(req, false);
return;
@@ -1146,7 +1146,7 @@ static void _pcs_shrink_end(struct fuse_mount *fm, struct fuse_args *args, int e
BUG_ON(r->exec.size_required);
BUG_ON(r->req.in.h.opcode != FUSE_READ && r->req.in.h.opcode != FUSE_FSYNC && r->req.in.h.opcode != FUSE_FLUSH);
- TRACE("resubmit %p\n", &r->req);
+ TRACE("resubmit %px\n", &r->req);
list_del_init(&ireq->list);
pcs_fuse_submit(pfc, &r->req);
}
@@ -1264,7 +1264,7 @@ static int kpcs_req_classify(struct fuse_req *req, bool bg, bool lk)
return 1;
BUG_ON(!pfc);
- DTRACE("Classify req:%p op:%d end:%p bg:%d lk:%d\n", req, req->in.h.opcode,
+ DTRACE("Classify req:%px op:%d end:%px bg:%d lk:%d\n", req, req->in.h.opcode,
req->args->end, bg, lk);
ret = pcs_kio_classify_req(req, lk);
if (likely(!ret))
@@ -1295,7 +1295,7 @@ static void kpcs_req_send(struct fuse_req *req, bool bg)
*/
BUG_ON(!list_empty(&req->list));
- TRACE("Send req:%p op:%d end:%p bg:%d\n",
+ TRACE("Send req:%px op:%d end:%px bg:%d\n",
req, req->in.h.opcode, req->args->end, bg);
/* __request_end below will do fuse_put_request() */
@@ -1683,7 +1683,7 @@ static int fuse_ktrace_setup(struct fuse_conn * fc)
return ret;
}
-static void kfuse_tracer(struct fuse_conn *fc, unsigned long ip, const char *fmt, va_list va)
+static void kfuse_tracer(struct fuse_conn *fc, int level, unsigned long ip, const char *fmt, va_list va)
{
struct fuse_ktrace *tr;
int cpu;
@@ -1696,7 +1696,7 @@ static void kfuse_tracer(struct fuse_conn *fc, unsigned long ip, const char *fmt
int len;
len = vsnprintf(buf, KTRACE_LOG_BUF_SIZE, fmt, va);
- t = fuse_trace_prepare(tr, FUSE_KTRACE_STRING, len + 1);
+ t = fuse_trace_prepare(tr, level >= LOG_TRACE ? FUSE_KTRACE_STRING : FUSE_KTRACE_STRING_WARN, len + 1);
if (t)
memcpy(t + 1, buf, len + 1);
FUSE_TRACE_COMMIT(tr);
@@ -1710,12 +1710,12 @@ static void kfuse_tracer(struct fuse_conn *fc, unsigned long ip, const char *fmt
put_cpu();
}
-void __kfuse_trace(struct fuse_conn *fc, unsigned long ip, const char *fmt, ...)
+void __kfuse_trace(struct fuse_conn *fc, int level, unsigned long ip, const char *fmt, ...)
{
va_list va;
va_start(va, fmt);
- kfuse_tracer(fc, ip, fmt, va);
+ kfuse_tracer(fc, level, ip, fmt, va);
va_end(va);
}
@@ -1725,7 +1725,7 @@ static void kfuse_trace(struct fuse_conn *fc, const char *fmt, ...)
if (fc->ktrace_level >= LOG_TRACE) {
va_start(va, fmt);
- kfuse_tracer(fc, 0, fmt, va);
+ kfuse_tracer(fc, LOG_TRACE, 0, fmt, va);
va_end(va);
}
}
@@ -2095,9 +2095,6 @@ static int __init kpcs_mod_init(void)
if (IS_ERR(crc_tfm))
crc_tfm = NULL;
- printk("%s fuse_c:%p ireq_c:%p pcs_wq:%p\n", __FUNCTION__,
- pcs_fuse_req_cachep, pcs_ireq_cachep, pcs_wq);
-
return 0;
free_krpc:
pcs_krpc_fini();
diff --git a/fs/fuse/kio/pcs/pcs_map.c b/fs/fuse/kio/pcs/pcs_map.c
index 94ae8c41251a..8bccb1800988 100644
--- a/fs/fuse/kio/pcs/pcs_map.c
+++ b/fs/fuse/kio/pcs/pcs_map.c
@@ -82,7 +82,7 @@ void cslist_destroy(struct pcs_cs_list * csl)
{
int i;
- TRACE("csl:%p csl->map:%p refcnt:%d\n", csl, csl->map, atomic_read(&csl->refcnt));
+ TRACE("csl:%px csl->map:%px refcnt:%d\n", csl, csl->map, atomic_read(&csl->refcnt));
BUG_ON(csl->map);
rcu_read_lock();
@@ -139,7 +139,7 @@ static void __pcs_map_free(struct pcs_map_entry *m)
void __pcs_map_put(struct pcs_map_entry *m)
__releases(m->lock)
{
- TRACE(" %p id:%lld state:%x ref:%d\n",m, m->id, m->state, atomic_read(&m->__refcnt));
+ TRACE(" %px id:%lld state:%x ref:%d\n", m, m->id, m->state, atomic_read(&m->__refcnt));
assert_spin_locked(&m->lock);
if (m->state & PCS_MAP_DEAD) {
@@ -694,7 +694,7 @@ void cs_blacklist(struct pcs_cs * cs, int error, char * reason)
spin_lock(&cs->css->lock);
set_bit(CS_SF_BLACKLISTED, &cs->state);
cs->blacklist_reason = error;
- FUSE_KTRACE(cc_from_csset(cs->css)->fc, "Blacklisting CS" NODE_FMT " by %s, err=%d", NODE_ARGS(cs->id), reason, error);
+ FUSE_KDEBUG(cc_from_csset(cs->css)->fc, "Blacklisting CS" NODE_FMT " by %s, err=%d", NODE_ARGS(cs->id), reason, error);
if (list_empty(&cs->css->bl_list)) {
struct pcs_cluster_core *cc = cc_from_csset(cs->css);
@@ -719,7 +719,7 @@ void cs_whitelist(struct pcs_cs * cs, char * reason)
if (cs_is_blacklisted(cs)) {
clear_bit(CS_SF_BLACKLISTED, &cs->state);
- FUSE_KTRACE(cc_from_csset(cs->css)->fc, "Whitelisting CS" NODE_FMT " by %s", NODE_ARGS(cs->id), reason);
+ FUSE_KDEBUG(cc_from_csset(cs->css)->fc, "Whitelisting CS" NODE_FMT " by %s", NODE_ARGS(cs->id), reason);
map_recalc_maps(cs);
@@ -748,7 +748,7 @@ static inline void map_remote_error_nolock(struct pcs_map_entry *m , int error,
static void pcs_map_mark_stale(struct pcs_map_entry *m, struct pcs_cs *cs)
{
if (!(m->state & (PCS_MAP_ERROR|PCS_MAP_DEAD|PCS_MAP_RESOLVING|PCS_MAP_NEW))) {
- FUSE_KTRACE(cc_from_csset(cs->css)->fc,
+ FUSE_KDEBUG(cc_from_csset(cs->css)->fc,
MAP_FMT " invalidating due to state of CS#"NODE_FMT,
MAP_ARGS(m), NODE_ARGS(cs->id));
map_remote_error_nolock(m, PCS_ERR_CSD_STALE_MAP, cs->id.val);
@@ -849,7 +849,7 @@ static void force_dirty(struct pcs_cs_record * rec, struct pcs_map_entry * m)
if (!rec->sync.dirty_integrity)
rec->sync.dirty_integrity = rec->info.integrity_seq;
if (!rec->sync.dirty_integrity || !rec->sync.dirty_epoch || !rec->sync.dirty_seq) {
- FUSE_KTRACE(cc_from_maps(m->maps)->fc, "cannot dirty "NODE_FMT" [%u/%u,%u/%u,%u/%u]", NODE_ARGS(rec->info.id),
+ FUSE_KDEBUG(cc_from_maps(m->maps)->fc, "cannot dirty "NODE_FMT" [%u/%u,%u/%u,%u/%u]", NODE_ARGS(rec->info.id),
rec->sync.dirty_integrity, rec->info.integrity_seq,
rec->sync.dirty_epoch, rec->sync.sync_epoch,
rec->sync.dirty_seq, rec->sync.sync_seq);
@@ -886,7 +886,7 @@ static void evaluate_dirty_status(struct pcs_map_entry * m)
atomic_inc(&m->maps->dirty_count);
}
} else {
- FUSE_KTRACE(cc_from_maps(m->maps)->fc, MAP_FMT " integrity seq advanced on CS#"NODE_FMT,
+ FUSE_KDEBUG(cc_from_maps(m->maps)->fc, MAP_FMT " integrity seq advanced on CS#"NODE_FMT,
MAP_ARGS(m), NODE_ARGS(rec->info.id));
rec->sync.dirty_integrity = 0;
@@ -900,9 +900,9 @@ static void evaluate_dirty_status(struct pcs_map_entry * m)
if (!(m->flags & PCS_MAP_DIRTY)) {
map_sync_work_del(m);
- FUSE_KLOG(cc_from_maps(m->maps)->fc, LOG_DEBUG5, "map %p is clean", m);
+ FUSE_KLOG(cc_from_maps(m->maps)->fc, LOG_DEBUG5, "map %px is clean", m);
} else {
- FUSE_KLOG(cc_from_maps(m->maps)->fc, LOG_DEBUG5, "map %p is dirty", m);
+ FUSE_KLOG(cc_from_maps(m->maps)->fc, LOG_DEBUG5, "map %px is dirty", m);
if (!timer_pending(&m->sync_work.timer) && !(m->flags & PCS_MAP_FLUSHING))
map_sync_work_add(m, pcs_sync_timeout(cc_from_map(m)));
}
@@ -964,7 +964,7 @@ int pcs_map_encode_req(struct pcs_map_entry*m, struct pcs_ioc_getmap *map, int d
}
#ifdef __PCS_DEBUG
- printk("%s submit m(%p)->uid:%lld\n", __FUNCTION__, m, m->id);
+ printk("%s submit m(%px)->uid:%lld\n", __func__, m, m->id);
printk("map {id:%lld [%lld, %lld] v:{" VER_FMT "} st:%x, cnt:%d max:%d SZ:%ld}\n",
m->id, map->chunk_start, map->chunk_end, VER_ARGS(m->version),
map->state, map->cs_cnt, map->cs_max, map_sz);
@@ -1119,7 +1119,7 @@ void pcs_map_complete(struct pcs_map_entry *m, struct pcs_ioc_getmap *omap)
spin_lock(&m->lock);
- TRACE(" recv m:%p, state:%x resp{ st:%d, err:%d, v:" VER_FMT "}\n",
+ TRACE(" recv m:%px, state:%x resp{ st:%d, err:%d, v:" VER_FMT "}\n",
m, m->state, omap->state, omap->error.value, VER_ARGS(omap->version));
if (pcs_if_error(&omap->error))
@@ -1157,7 +1157,7 @@ void pcs_map_complete(struct pcs_map_entry *m, struct pcs_ioc_getmap *omap)
m->state = PCS_MAP_ERROR;
If m->state becomes atomic bit fields this will be impossible.
*/
- TRACE("skip getmap resp: m:%p, state:%x resp{ st:%d, err:%d, v:" VER_FMT "}\n",
+ TRACE("skip getmap resp: m:%px, state:%x resp{ st:%d, err:%d, v:" VER_FMT "}\n",
m, m->state, omap->state, omap->error.value, VER_ARGS(omap->version));
spin_unlock(&m->lock);
goto out_ignore;
@@ -1242,7 +1242,7 @@ void pcs_map_complete(struct pcs_map_entry *m, struct pcs_ioc_getmap *omap)
return;
error:
- TRACE("map error: %d, m:%p, index:%lu, state:%x\n", error.value, m,
+ TRACE("map error: %d, m:%px, index:%lu, state:%x\n", error.value, m,
m->index, m->state);
BUG_ON(!pcs_if_error(&error));
@@ -1269,9 +1269,9 @@ __releases(m->lock)
assert_spin_locked(&m->lock);
- DTRACE("enter m:%p, state:%x, ireq:%p dir:%d \n", m, m->state, ireq, direction);
+ DTRACE("enter m:%px, state:%x, ireq:%px dir:%d\n", m, m->state, ireq, direction);
DTRACE("dentry: "DENTRY_FMT, DENTRY_ARGS(pcs_dentry_from_map(m)));
- DTRACE("%p {%p %p}\n",ireq, ireq->list.next, ireq->list.prev);
+ DTRACE("%px {%px %px}\n", ireq, ireq->list.next, ireq->list.prev);
BUG_ON(!list_empty(&ireq->list));
list_add_tail(&ireq->list, &m->queue);
@@ -1577,7 +1577,7 @@ static void pcs_cs_wakeup(struct pcs_cs * cs)
}
FUSE_KDTRACE(sreq->cc->fc,
- "wakeup {%p} cpu%u %d %u/%u " DENTRY_FMT " %llu+%llu",
+ "wakeup {%px} cpu%u %d %u/%u " DENTRY_FMT " %llu+%llu",
sreq, smp_processor_id(), cs->cong_queue_len,
cs->in_flight, cs->eff_cwnd, DENTRY_ARGS(sreq->dentry),
sreq->iochunk.chunk + sreq->iochunk.offset,
@@ -2418,7 +2418,7 @@ void map_submit(struct pcs_map_entry * m, struct pcs_int_request *ireq)
return;
}
- DTRACE("enter m: " MAP_FMT ", ireq:%p \n", MAP_ARGS(m), ireq);
+ DTRACE("enter m: " MAP_FMT ", ireq:%px\n", MAP_ARGS(m), ireq);
csl = m->cs_list;
if (csl)
@@ -2548,7 +2548,7 @@ static void warp_sync_seq(struct pcs_map_entry * m, struct pcs_cs_record * rec,
pcs_sync_seq_compare(sync->sync_epoch, srec->sync_epoch) < 0 ||
(sync->sync_epoch == srec->sync_epoch &&
pcs_sync_seq_compare(sync->sync_current, srec->sync_seq) < 0))
- FUSE_KTRACE(cc_from_maps(m->maps)->fc, "Warp [" NODE_FMT ",%u/%u,%u/%u,%u/%u] -> [%u/%u,%u/%u]",
+ FUSE_KDEBUG(cc_from_maps(m->maps)->fc, "Warp [" NODE_FMT ",%u/%u,%u/%u,%u/%u] -> [%u/%u,%u/%u]",
NODE_ARGS(rec->info.id), rec->info.integrity_seq, srec->dirty_integrity,
srec->dirty_epoch, srec->dirty_seq, srec->sync_epoch, srec->sync_seq,
sync->sync_epoch, sync->sync_dirty, sync->sync_epoch, sync->sync_current);
@@ -2581,7 +2581,7 @@ static int commit_cs_record(struct pcs_map_entry * m, struct pcs_cs_record * rec
*
* The request is restarted with new map.
*/
- FUSE_KTRACE(cc_from_maps(m->maps)->fc, MAP_FMT " integrity seq mismatch CS" NODE_FMT " %d != %d, %d",
+ FUSE_KDEBUG(cc_from_maps(m->maps)->fc, MAP_FMT " integrity seq mismatch CS" NODE_FMT " %d != %d, %d",
MAP_ARGS(m),
NODE_ARGS(rec->info.id),
rec->info.integrity_seq, sync->integrity_seq, srec->dirty_integrity);
@@ -2639,7 +2639,7 @@ static int commit_cs_record(struct pcs_map_entry * m, struct pcs_cs_record * rec
/* This must not happen, dirty replica cannot remain dirty
* for >> PCS_SYNC_TIMEOUT unless the cluster is very sick.
*/
- FUSE_KTRACE(cc_from_maps(m->maps)->fc, MAP_FMT " replica " NODE_FMT " is dirty for %ld hz; [%u/%u,%u/%u,%u/%u] -> [%u/%u]",
+ FUSE_KDEBUG(cc_from_maps(m->maps)->fc, MAP_FMT " replica " NODE_FMT " is dirty for %ld hz; [%u/%u,%u/%u,%u/%u] -> [%u/%u]",
MAP_ARGS(m), NODE_ARGS(rec->info.id), (long)(jiffies - rec->dirty_ts),
rec->info.integrity_seq, srec->dirty_integrity,
srec->dirty_epoch, srec->dirty_seq, srec->sync_epoch, srec->sync_seq,
@@ -2673,7 +2673,7 @@ static int commit_cs_record(struct pcs_map_entry * m, struct pcs_cs_record * rec
}
if (!was_dirty && !dirtying && cs_is_dirty(srec)) {
- FUSE_KTRACE(cc_from_maps(m->maps)->fc, MAP_FMT " replica " NODE_FMT " is not expected to be dirty [%u/%u,%u/%u,%u/%u]",
+ FUSE_KDEBUG(cc_from_maps(m->maps)->fc, MAP_FMT " replica " NODE_FMT " is not expected to be dirty [%u/%u,%u/%u,%u/%u]",
MAP_ARGS(m), NODE_ARGS(rec->info.id),
rec->info.integrity_seq, srec->dirty_integrity,
srec->dirty_epoch, srec->dirty_seq, srec->sync_epoch, srec->sync_seq);
diff --git a/fs/fuse/kio/pcs/pcs_map.h b/fs/fuse/kio/pcs/pcs_map.h
index 788a8cb16d8e..8e5344b8e22c 100644
--- a/fs/fuse/kio/pcs/pcs_map.h
+++ b/fs/fuse/kio/pcs/pcs_map.h
@@ -238,31 +238,31 @@ void cslist_destroy(struct pcs_cs_list * csl);
*/
static inline bool try_cslist_get(struct pcs_cs_list *csl)
{
- TRACE("csl:%p csl->map:%p refcnt:%d\n", csl, csl->map, atomic_read(&csl->refcnt));
+ TRACE("csl:%px csl->map:%px refcnt:%d\n", csl, csl->map, atomic_read(&csl->refcnt));
return atomic_inc_not_zero(&csl->refcnt);
}
static inline void cslist_get(struct pcs_cs_list * csl)
{
- TRACE("csl:%p csl->map:%p refcnt:%d\n", csl, csl->map, atomic_read(&csl->refcnt));
+ TRACE("csl:%px csl->map:%px refcnt:%d\n", csl, csl->map, atomic_read(&csl->refcnt));
BUG_ON(!atomic_inc_not_zero(&csl->refcnt));
}
static inline void cslist_put(struct pcs_cs_list * csl)
{
- TRACE("csl:%p csl->map:%p refcnt:%d\n", csl, csl->map, atomic_read(&csl->refcnt));
+ TRACE("csl:%px csl->map:%px refcnt:%d\n", csl, csl->map, atomic_read(&csl->refcnt));
if (atomic_dec_and_test(&csl->refcnt))
cslist_destroy(csl);
}
-#define MAP_FMT "(%p) 0x%lld s:%x" DENTRY_FMT
+#define MAP_FMT "(%px) %lld s:%x" DENTRY_FMT
#define MAP_ARGS(m) (m), (long long)(m)->index, (m)->state, DENTRY_ARGS(pcs_dentry_from_map((m)))
static inline void pcs_map_put(struct pcs_map_entry *m)
{
- TRACE("m(%p)->index:%ld ref:%d \n", m, m->index, atomic_read(&m->__refcnt));
+ TRACE("m(%px)->index:%ld ref:%d\n", m, m->index, atomic_read(&m->__refcnt));
BUG_ON(atomic_read(&m->__refcnt) <= 0);
if (atomic_dec_and_lock(&m->__refcnt, &m->lock))
@@ -289,7 +289,7 @@ static inline void map_del_lru(struct pcs_map_entry *m)
static inline void pcs_map_put_locked(struct pcs_map_entry *m)
{
- TRACE("m(%p)->index:%ld ref:%d \n", m, m->index, atomic_read(&m->__refcnt));
+ TRACE("m(%px)->index:%ld ref:%d\n", m, m->index, atomic_read(&m->__refcnt));
BUG_ON(atomic_read(&m->__refcnt) <= 0);
BUG_ON(m->state & PCS_MAP_DEAD);
diff --git a/fs/fuse/kio/pcs/pcs_rdma_conn.c b/fs/fuse/kio/pcs/pcs_rdma_conn.c
index 1cb9d2aa995c..3ca844996716 100644
--- a/fs/fuse/kio/pcs/pcs_rdma_conn.c
+++ b/fs/fuse/kio/pcs/pcs_rdma_conn.c
@@ -81,7 +81,7 @@ static int pcs_rdma_cm_event_handler(struct rdma_cm_id *cmid,
conn_param_init(&conn_param, &rc->rio->conn_req, cmid);
if (rdma_connect_locked(cmid, &conn_param)) {
- TRACE("rdma_connect_locked failed: rio: 0x%p\n", rc->rio);
+ TRACE("rdma_connect_locked failed: rio: 0x%px\n", rc->rio);
complete(&rc->cm_done);
}
break;
@@ -90,7 +90,7 @@ static int pcs_rdma_cm_event_handler(struct rdma_cm_id *cmid,
complete(&rc->cm_done);
break;
case RDMA_CM_EVENT_REJECTED:
- TRACE("pcs_rdma_cm_event_handler reject: %s, rio: 0x%p\n",
+ TRACE("pcs_rdma_cm_event_handler reject: %s, rio: 0x%px\n",
rdma_reject_msg(cmid, event->status), rc->rio);
complete(&rc->cm_done);
break;
@@ -144,7 +144,7 @@ void pcs_rdmaconnect_start(struct pcs_rpc *ep)
goto fail_cm;
}
- TRACE(PEER_FMT " state: %d, rio: 0x%p\n", PEER_ARGS(ep), ep->state, rc.rio);
+ TRACE(PEER_FMT " state: %d, rio: 0x%px\n", PEER_ARGS(ep), ep->state, rc.rio);
cancel_delayed_work(&ep->timer_work);
ep->retries++;
@@ -164,7 +164,7 @@ void pcs_rdmaconnect_start(struct pcs_rpc *ep)
/* setup rxs */
if (pcs_rdma_setup_rxs((rc.rio))) {
- TRACE("pcs_rdma_setup_rxs failed, rio: 0x%p\n", rc.rio);
+ TRACE("pcs_rdma_setup_rxs failed, rio: 0x%px\n", rc.rio);
pcs_rpc_report_error(ep, PCS_RPC_ERR_CONNECT_ERROR);
goto fail;
}
@@ -172,7 +172,7 @@ void pcs_rdmaconnect_start(struct pcs_rpc *ep)
ret = rpc_client_start_auth(ep, PCS_AUTH_DIGEST,
cc_from_rpc(ep->eng)->cluster_name);
if (ret < 0) {
- TRACE("rdma authorization failed: %d, rio: 0x%p",
+ TRACE("rdma authorization failed: %d, rio: 0x%px",
ret, rc.rio);
pcs_rpc_report_error(ep, PCS_RPC_ERR_AUTH_ERR);
goto fail; /* since ep->conn is initialized,
@@ -180,7 +180,7 @@ void pcs_rdmaconnect_start(struct pcs_rpc *ep)
*/
}
- TRACE("rdma connection established: rio: 0x%p\n", rc.rio);
+ TRACE("rdma connection established: rio: 0x%px\n", rc.rio);
ep->state = PCS_RPC_APPWAIT;
pcs_rpc_enable(ep, 0);
diff --git a/fs/fuse/kio/pcs/pcs_rdma_io.c b/fs/fuse/kio/pcs/pcs_rdma_io.c
index 63fb1e7130db..2001297010a6 100644
--- a/fs/fuse/kio/pcs/pcs_rdma_io.c
+++ b/fs/fuse/kio/pcs/pcs_rdma_io.c
@@ -304,7 +304,7 @@ static int rio_rx_post(struct pcs_rdmaio *rio, struct rio_rx *rx,
ret = RE_INV(ib_post_recv(rio->cmid->qp, &rx->wr, NULL));
if (ret) {
- TRACE("ib_post_recv failed: %d, rio: 0x%p\n", ret, rio);
+ TRACE("ib_post_recv failed: %d, rio: 0x%px\n", ret, rio);
} else {
rio->n_rx_posted++;
}
@@ -341,13 +341,13 @@ static int rio_tx_post(struct pcs_rdmaio *rio, struct rio_tx *tx,
rio_cqe_init(&tx->err_cqe, 0, rio_tx_err_occured);
if (rio->n_tx_posted + ib_wr_count > rio->max_send_wr) {
- TRACE("ib send queue overflow: rio: 0x%p\n", rio);
+ TRACE("ib send queue overflow: rio: 0x%px\n", rio);
return -ENOMEM;
}
ret = RE_INV(ib_post_send(rio->cmid->qp, send_wr, NULL));
if (ret) {
- TRACE("ib_post_send failed: %d, rio: 0x%p\n", ret, rio);
+ TRACE("ib_post_send failed: %d, rio: 0x%px\n", ret, rio);
} else {
rio->n_tx_posted += ib_wr_count;
}
@@ -450,26 +450,26 @@ static int rio_submit_rdma_read(struct pcs_rdmaio *rio, struct pcs_msg *msg,
DMA_FROM_DEVICE, rb->rbuf, rb->rkey,
rio->dev->pd->local_dma_lkey, msg, offset,
offset + rb->rlen, GFP_NOIO, rio->cmid->qp->max_read_sge)) {
- TRACE("pcs_rdma_rw_init_from_msg failed, try fallback: rio: 0x%p\n", rio);
+ TRACE("pcs_rdma_rw_init_from_msg failed, try fallback: rio: 0x%px\n", rio);
goto fallback;
}
tx->cleanup = rio_tx_cleanup_rdma_rw;
if (rio_tx_post_rdma_rw_read(rio, tx)) {
- TRACE("rio_tx_post_rdma_rw_read failed: rio: 0x%p\n", rio);
+ TRACE("rio_tx_post_rdma_rw_read failed: rio: 0x%px\n", rio);
goto fail;
}
} else {
fallback:
if (pcs_rdma_msg_init(&tx->rdma_mr.msg, msg, offset, offset + rb->rlen,
&dev->rd_mr_pool, rdmaio_use_map_for_mr)) {
- TRACE("rio_rdma_mr_init failed: rio: 0x%p\n", rio);
+ TRACE("rio_rdma_mr_init failed: rio: 0x%px\n", rio);
goto fail;
}
tx->cleanup = rio_tx_cleanup_rdma_mr;
if (rio_tx_post_rdma_mr_read(rio, tx, rb->rbuf, rb->rkey, rb->rlen)) {
- TRACE("rio_tx_post_rdma_mr_read failed: rio: 0x%p\n", rio);
+ TRACE("rio_tx_post_rdma_mr_read failed: rio: 0x%px\n", rio);
goto fail;
}
}
@@ -672,7 +672,7 @@ static int rio_submit(struct pcs_rdmaio *rio, struct pcs_msg *msg, int type, u64
} else {
if (pcs_rdma_msg_init(&tx->send.msg, msg, offset, msg_size,
&dev->sd_mr_pool, rdmaio_use_map_for_mr)) {
- TRACE("rio_rdma_mr_init failed: rio: 0x%p\n", rio);
+ TRACE("rio_rdma_mr_init failed: rio: 0x%px\n", rio);
goto fail;
}
tx->cleanup = rio_tx_cleanup_send;
@@ -694,7 +694,7 @@ static int rio_submit(struct pcs_rdmaio *rio, struct pcs_msg *msg, int type, u64
}
if (rio_tx_post_send(rio, tx, tx_length, first_tx_wr, last_tx_wr)) {
- TRACE("rio_tx_post_send failed: rio: 0x%p\n", rio);
+ TRACE("rio_tx_post_send failed: rio: 0x%px\n", rio);
goto fail;
}
@@ -849,7 +849,7 @@ static int rio_handle_rx_immediate(struct pcs_rdmaio *rio, char *buf, int len,
}
if (len < rio->hdr_size) {
- TRACE("rio read short msg: %d < %d, rio: 0x%p\n", len,
+ TRACE("rio read short msg: %d < %d, rio: 0x%px\n", len,
rio->hdr_size, rio);
return PCS_ERR_NET_ABORT;
}
@@ -863,7 +863,7 @@ static int rio_handle_rx_immediate(struct pcs_rdmaio *rio, char *buf, int len,
err = PCS_ERR_NOMEM;
return err;
} else if (msg == PCS_TRASH_MSG) {
- TRACE("rio drop trash msg: %u, rio: 0x%p\n", msg_size, rio);
+ TRACE("rio drop trash msg: %u, rio: 0x%px\n", msg_size, rio);
/*
* We must Ack every RDMA_READ_REQ received from our peer in
* order even it's going to be dropped.
@@ -884,7 +884,7 @@ static int rio_handle_rx_immediate(struct pcs_rdmaio *rio, char *buf, int len,
}
if (msg->size != len + (rb ? rb->rlen : 0)) {
- TRACE("rio read wrong len: %d != %d (%llx/%x/%d), rio: 0x%p",
+ TRACE("rio read wrong len: %d != %d (%llx/%x/%d), rio: 0x%px",
len, msg->size, rb ? rb->rbuf : 0ULL,
rb ? rb->rkey : 0, rb ? rb->rlen : -1,
rio);
@@ -991,7 +991,7 @@ static void rio_handle_rx(struct pcs_rdmaio *rio, struct rio_rx *rx,
if (!throttle) {
if (rio_rx_post(rio, rx, RIO_MSG_SIZE)) {
- TRACE("rio_rx_post failed: rio: 0x%p\n", rio);
+ TRACE("rio_rx_post failed: rio: 0x%px\n", rio);
rio_abort(rio, PCS_ERR_NET_ABORT);
return;
}
@@ -1005,7 +1005,7 @@ static void rio_handle_rx(struct pcs_rdmaio *rio, struct rio_rx *rx,
BUG_ON(rio->n_reserved_credits > rio->queue_depth);
if (rio->n_os_credits > rio->queue_depth) {
- TRACE("n_os_credits overflow: rio: 0x%p\n", rio);
+ TRACE("n_os_credits overflow: rio: 0x%px\n", rio);
rio_abort(rio, PCS_ERR_NET_ABORT);
return;
}
@@ -1133,7 +1133,7 @@ static inline int rio_comp_perform(struct pcs_rdmaio *rio)
static void pcs_rdma_cq_event_handler(struct ib_event *event, void *private)
{
struct pcs_rdmaio *rio = private;
- TRACE("rio: 0x%p\n", rio);
+ TRACE("rio: 0x%px\n", rio);
}
static int pcs_rdma_io_event_handler(struct rdma_cm_id *cmid,
@@ -1143,7 +1143,7 @@ static int pcs_rdma_io_event_handler(struct rdma_cm_id *cmid,
struct pcs_rdmaio *rio = container_of(id, struct pcs_rdmaio, id);
struct pcs_rpc *ep = rio->netio.parent;
- TRACE("rio: 0x%p, event: %d, status: %d\n", rio, event->event, event->status);
+ TRACE("rio: 0x%px, event: %d, status: %d\n", rio, event->event, event->status);
set_bit(PCS_RDMA_IO_ERROR, &rio->io_flags);
pcs_rpc_kick_queue(ep);
@@ -1154,7 +1154,7 @@ static int pcs_rdma_io_event_handler(struct rdma_cm_id *cmid,
static void pcs_rdma_qp_event_handler(struct ib_event *event, void *context)
{
struct pcs_rdmaio *rio = context;
- TRACE("rio: 0x%p, event: %d\n", rio, event->event);
+ TRACE("rio: 0x%px, event: %d\n", rio, event->event);
}
static struct pcs_rdma_device *pcs_rdma_device_create(struct rdma_cm_id *cmid,
@@ -1177,7 +1177,7 @@ static struct pcs_rdma_device *pcs_rdma_device_create(struct rdma_cm_id *cmid,
for (i = 0; i < send_queue_depth; i++) {
tx = rio_alloc_tx(dev, TX_MSG_DONE);
if (!tx) {
- TRACE("rio_alloc_tx failed: dev: 0x%p\n", dev);
+ TRACE("rio_alloc_tx failed: dev: 0x%px\n", dev);
goto free_bufs;
}
rio_put_tx(dev, tx);
@@ -1185,28 +1185,28 @@ static struct pcs_rdma_device *pcs_rdma_device_create(struct rdma_cm_id *cmid,
dev->pd = RE_PTR_INV(ib_alloc_pd(dev->ib_dev, 0));
if (IS_ERR(dev->pd)) {
- TRACE("ib_alloc_pd failed: dev: 0x%p\n", dev);
+ TRACE("ib_alloc_pd failed: dev: 0x%px\n", dev);
goto free_bufs;
}
if (pcs_ib_mr_pool_init(&dev->ib_mr_pool, dev->pd, IB_MR_TYPE_MEM_REG,
max_num_sg,
queue_depth * 2)) {
- TRACE("pcs_ib_mr_pool_init failed: dev: 0x%p\n", dev);
+ TRACE("pcs_ib_mr_pool_init failed: dev: 0x%px\n", dev);
goto free_pd;
}
if (pcs_rdma_mr_pool_init(&dev->sd_mr_pool, RDMA_MAX_MSG_PAYLOAD,
queue_depth, dev->ib_dev, dev->pd, DMA_TO_DEVICE,
GFP_NOIO, &dev->ib_mr_pool)) {
- TRACE("pcs_rdma_mr_pool_init failed: dev: 0x%p\n", dev);
+ TRACE("pcs_rdma_mr_pool_init failed: dev: 0x%px\n", dev);
goto free_ib_mr;
}
if (pcs_rdma_mr_pool_init(&dev->rd_mr_pool, RDMA_MAX_MSG_PAYLOAD,
queue_depth, dev->ib_dev, dev->pd, DMA_FROM_DEVICE,
GFP_NOIO, &dev->ib_mr_pool)) {
- TRACE("pcs_rdma_mr_pool_init failed: dev: 0x%p\n", dev);
+ TRACE("pcs_rdma_mr_pool_init failed: dev: 0x%px\n", dev);
goto free_sd_mr;
}
@@ -1316,7 +1316,7 @@ struct pcs_rdmaio* pcs_rdma_create(int hdr_size, struct rdma_cm_id *cmid,
rio->recv_queue_depth = 0;
for (i = 0; i < recv_queue_depth; i++) {
if (!rio_init_rx(rio->rx_descs + i, rio->cmid->device)) {
- TRACE("rio_init_rx failed: rio: 0x%p\n", rio);
+ TRACE("rio_init_rx failed: rio: 0x%px\n", rio);
goto free_bufs;
}
rio->recv_queue_depth++;
@@ -1324,7 +1324,7 @@ struct pcs_rdmaio* pcs_rdma_create(int hdr_size, struct rdma_cm_id *cmid,
rio->dev = pcs_rdma_device_create(rio->cmid, queue_depth, send_queue_depth);
if (!rio->dev) {
- TRACE("pcs_rdma_device_create failed: rio: 0x%p\n", rio);
+ TRACE("pcs_rdma_device_create failed: rio: 0x%px\n", rio);
goto free_bufs;
}
@@ -1339,12 +1339,12 @@ struct pcs_rdmaio* pcs_rdma_create(int hdr_size, struct rdma_cm_id *cmid,
rio->cq = RE_PTR_INV(ib_create_cq(rio->cmid->device, pcs_rdma_cq_comp_handler,
pcs_rdma_cq_event_handler, rio, &cq_attr));
if (IS_ERR(rio->cq)) {
- TRACE("ib_alloc_cq failed: rio: 0x%p\n", rio);
+ TRACE("ib_alloc_cq failed: rio: 0x%px\n", rio);
goto free_dev;
}
if (cq_count && cq_period) {
int ret = rdma_set_cq_moderation(rio->cq, cq_count, cq_period);
- TRACE("rio: 0x%p, set cq moderation: cq_count %u, cq_period: %u, ret: %d\n",
+ TRACE("rio: 0x%px, set cq moderation: cq_count %u, cq_period: %u, ret: %d\n",
rio, cq_count, cq_period, ret);
}
ib_req_notify_cq(rio->cq, IB_CQ_NEXT_COMP);
@@ -1360,15 +1360,15 @@ struct pcs_rdmaio* pcs_rdma_create(int hdr_size, struct rdma_cm_id *cmid,
qp_attr.sq_sig_type = IB_SIGNAL_REQ_WR;
qp_attr.qp_type = IB_QPT_RC;
- TRACE("rio: 0x%p, max_send wr/sge: %u/%u, max_recv wr/sge: %u/%u\n",
+ TRACE("rio: 0x%px, max_send wr/sge: %u/%u, max_recv wr/sge: %u/%u\n",
rio, qp_attr.cap.max_send_wr, qp_attr.cap.max_send_sge,
qp_attr.cap.max_recv_wr, qp_attr.cap.max_recv_sge);
if (RE_INV(rdma_create_qp(rio->cmid, rio->dev->pd, &qp_attr))) {
- TRACE("rdma_create_qp failed: rio: 0x%p\n", rio);
+ TRACE("rdma_create_qp failed: rio: 0x%px\n", rio);
goto free_cq;
}
- TRACE("rio: 0x%p, dev: 0x%p, queue_depth: %d\n", rio, rio->dev, queue_depth);
+ TRACE("rio: 0x%px, dev: 0x%px, queue_depth: %d\n", rio, rio->dev, queue_depth);
return rio;
@@ -1392,7 +1392,7 @@ int pcs_rdma_setup_rxs(struct pcs_rdmaio *rio)
for (rx = rio->rx_descs; rx - rio->rx_descs < rio->recv_queue_depth; rx++)
if (rio_rx_post(rio, rx, RIO_MSG_SIZE)) {
- TRACE("rio_rx_post failed: rio: 0x%p\n", rio);
+ TRACE("rio_rx_post failed: rio: 0x%px\n", rio);
break;
}
@@ -1444,10 +1444,10 @@ static void rio_abort(struct pcs_rdmaio *rio, int error)
rio->rio_error = error;
if (rdma_disconnect(rio->cmid))
- TRACE("rdma_disconnect failed: rio: 0x%p\n", rio);
+ TRACE("rdma_disconnect failed: rio: 0x%px\n", rio);
if (ib_modify_qp(rio->cmid->qp, &qp_attr, IB_QP_STATE))
- TRACE("ib_modify_qp failed: rio: 0x%p\n", rio);
+ TRACE("ib_modify_qp failed: rio: 0x%px\n", rio);
if (netio->eof) {
void (*eof)(struct pcs_netio *) = netio->eof;
@@ -1472,7 +1472,7 @@ static void rio_destroy(struct work_struct *work)
mutex_lock(&ep->mutex);
- TRACE("rio: 0x%p\n", rio);
+ TRACE("rio: 0x%px\n", rio);
while (rio->n_rx_posted || rio->n_tx_posted) {
rio_req_notify_cq(rio);
@@ -1518,7 +1518,7 @@ void pcs_rdma_destroy(struct pcs_rdmaio *rio)
struct pcs_rpc *ep = rio->netio.parent;
int i;
- TRACE("rio: 0x%p\n", rio);
+ TRACE("rio: 0x%px\n", rio);
BUG_ON(!mutex_is_locked(&ep->mutex));
@@ -1544,7 +1544,7 @@ void pcs_rdma_ioconn_destruct(struct pcs_ioconn *ioconn)
struct pcs_rdmaio *rio = rio_from_ioconn(ioconn);
struct pcs_rpc *ep = rio->netio.parent;
- TRACE("rio: 0x%p\n", rio);
+ TRACE("rio: 0x%px\n", rio);
BUG_ON(!mutex_is_locked(&ep->mutex));
@@ -1558,7 +1558,7 @@ static void pcs_rdma_throttle(struct pcs_netio *netio)
struct pcs_rdmaio *rio = rio_from_netio(netio);
struct pcs_rpc *ep = netio->parent;
- TRACE("rio: 0x%p\n", rio);
+ TRACE("rio: 0x%px\n", rio);
BUG_ON(!mutex_is_locked(&ep->mutex));
@@ -1573,7 +1573,7 @@ static void pcs_rdma_unthrottle(struct pcs_netio *netio)
struct pcs_rdmaio *rio = rio_from_netio(netio);
struct pcs_rpc *ep = netio->parent;
- TRACE("rio: 0x%p\n", rio);
+ TRACE("rio: 0x%px\n", rio);
BUG_ON(!mutex_is_locked(&ep->mutex));
@@ -1613,7 +1613,7 @@ static int pcs_rdma_cancel_msg(struct pcs_msg *msg)
{
struct pcs_rpc *ep = msg->netio->parent;
- TRACE("msg: 0x%p\n", msg);
+ TRACE("msg: 0x%px\n", msg);
BUG_ON(!mutex_is_locked(&ep->mutex));
@@ -1631,7 +1631,7 @@ static void pcs_rdma_abort_io(struct pcs_netio *netio, int error)
struct pcs_rdmaio *rio = rio_from_netio(netio);
struct pcs_rpc *ep = netio->parent;
- TRACE("rio: 0x%p\n", rio);
+ TRACE("rio: 0x%px\n", rio);
BUG_ON(!mutex_is_locked(&ep->mutex));
diff --git a/fs/fuse/kio/pcs/pcs_req.c b/fs/fuse/kio/pcs/pcs_req.c
index 67b2daf509d2..cd35fce27016 100644
--- a/fs/fuse/kio/pcs/pcs_req.c
+++ b/fs/fuse/kio/pcs/pcs_req.c
@@ -107,7 +107,7 @@ void ireq_handle_hole(struct pcs_int_request *ireq)
offset = 0;
iov_iter_kvec(&it, READ, NULL, 0, 0);
- DTRACE("enter m: " MAP_FMT ", ireq:%p \n", MAP_ARGS(ireq->iochunk.map), ireq);
+ DTRACE("enter m: " MAP_FMT ", ireq:%px\n", MAP_ARGS(ireq->iochunk.map), ireq);
while (len > 0) {
size_t copy;
diff --git a/fs/fuse/kio/pcs/pcs_rpc.c b/fs/fuse/kio/pcs/pcs_rpc.c
index 540225006726..fa9912c407b3 100644
--- a/fs/fuse/kio/pcs/pcs_rpc.c
+++ b/fs/fuse/kio/pcs/pcs_rpc.c
@@ -127,7 +127,7 @@ void rpc_abort(struct pcs_rpc * ep, int fatal, int error)
struct list_head failed_list;
BUG_ON(!mutex_is_locked(&ep->mutex));
- TRACE("ep:%p->state:%d fatal:%d error:%d\n", ep, state, fatal, error);
+ TRACE("ep:%px->state:%d fatal:%d error:%d\n", ep, state, fatal, error);
ep->flags &= ~PCS_RPC_F_PEER_ID;
@@ -143,7 +143,7 @@ void rpc_abort(struct pcs_rpc * ep, int fatal, int error)
while (!list_empty(&ep->pending_queue)) {
struct pcs_msg * msg = list_first_entry(&ep->pending_queue, struct pcs_msg, list);
list_move_tail(&msg->list, &failed_list);
- FUSE_KTRACE(cc_from_rpc(ep->eng)->fc, "aborted msg to " PEER_FMT ", tmo=%d, err=%d, %ld", PEER_ARGS(ep),
+ FUSE_KDEBUG(cc_from_rpc(ep->eng)->fc, "aborted msg to " PEER_FMT ", tmo=%d, err=%d, %ld", PEER_ARGS(ep),
msg->timeout, error, (long)(msg->start_time + msg->timeout - jiffies));
pcs_msg_del_calendar(msg);
msg->stage = PCS_MSG_STAGE_NONE;
@@ -157,7 +157,7 @@ void rpc_abort(struct pcs_rpc * ep, int fatal, int error)
while (!list_empty(&ep->state_queue)) {
struct pcs_msg * msg = list_first_entry(&ep->state_queue, struct pcs_msg, list);
list_move_tail(&msg->list, &failed_list);
- FUSE_KTRACE(cc_from_rpc(ep->eng)->fc, "aborted unsent msg to " PEER_FMT ", tmo=%d, err=%d", PEER_ARGS(ep),
+ FUSE_KDEBUG(cc_from_rpc(ep->eng)->fc, "aborted unsent msg to " PEER_FMT ", tmo=%d, err=%d", PEER_ARGS(ep),
msg->timeout, error);
pcs_msg_del_calendar(msg);
msg->stage = PCS_MSG_STAGE_NONE;
@@ -420,7 +420,7 @@ void pcs_rpc_enable(struct pcs_rpc * ep, int error)
list_lru_add_obj(&ep->gc->lru, &ep->lru_link);
}
}
- TRACE("ep(%p)->state: WORK\n", ep);
+ TRACE("ep(%px)->state: WORK\n", ep);
ep->state = PCS_RPC_WORK;
ep->retries = 0;
ep->kill_deadline = 0;
@@ -492,7 +492,7 @@ static void handle_keep_waiting(struct pcs_rpc * ep, struct pcs_msg * msg)
if (h->hdr.len < sizeof(struct pcs_rpc_keep_waiting))
return;
- FUSE_KTRACE(cc_from_rpc(ep->eng)->fc, "Received keep wait from " NODE_FMT " for request " XID_FMT,
+ FUSE_KDEBUG(cc_from_rpc(ep->eng)->fc, "Received keep wait from " NODE_FMT " for request " XID_FMT,
NODE_ARGS(h->hdr.xid.origin), XID_ARGS(h->xid));
req = pcs_rpc_lookup_xid(ep, &h->xid);
@@ -654,7 +654,7 @@ void pcs_rpc_connect(struct pcs_rpc * ep)
if (ep->state != PCS_RPC_UNCONN)
return;
- FUSE_KTRACE(cc_from_rpc(ep->eng)->fc, "Connecting to node " NODE_FMT, NODE_ARGS(ep->peer_id));
+ FUSE_KDEBUG(cc_from_rpc(ep->eng)->fc, "Connecting to node " NODE_FMT, NODE_ARGS(ep->peer_id));
BUG_ON(!ep->ops->connect);
ep->ops->connect(ep);
@@ -668,7 +668,7 @@ static void pcs_rpc_send(struct pcs_rpc * ep, struct pcs_msg * msg, bool requeue
BUG_ON(!mutex_is_locked(&ep->mutex));
BUG_ON(msg->rpc != (requeue ? ep: NULL));
- TRACE("ENTER ep:%p state:%d msg:%p\n", ep, ep->state, msg);
+ TRACE("ENTER ep:%px state:%d msg:%px\n", ep, ep->state, msg);
if (ep->state == PCS_RPC_ABORT || ep->state == PCS_RPC_DESTROY) {
pcs_set_rpc_error(&msg->error, PCS_ERR_NET_ABORT, ep);
@@ -800,7 +800,7 @@ static void calendar_work(struct work_struct *w)
struct pcs_rpc_hdr * h = (struct pcs_rpc_hdr *)msg_inline_head(msg);
(void)h;
- FUSE_KTRACE(cc->fc, "killing msg to " PEER_FMT " type=%u xid=" XID_FMT " stage=%d tmo=%d exp=%ld rem=%ld",
+ FUSE_KDEBUG(cc->fc, "killing msg to " PEER_FMT " type=%u xid=" XID_FMT " stage=%d tmo=%d exp=%ld rem=%ld",
PEER_ARGS(msg->rpc), h->type, XID_ARGS(h->xid),
msg->stage, msg->timeout,
(long)(msg->start_time + msg->timeout - jiffies),
@@ -845,7 +845,7 @@ static void calendar_work(struct work_struct *w)
count++;
}
if (count)
- FUSE_KTRACE(cc->fc, "%s %d messages to "PEER_FMT" destroyed\n", __FUNCTION__,
+ FUSE_KTRACE(cc->fc, "%s %d messages to "PEER_FMT" destroyed", __func__,
count, PEER_ARGS(ep));
for (i=0; i < RPC_MAX_CALENDAR-1; i++) {
@@ -1280,7 +1280,7 @@ static void timer_work(struct work_struct *w)
}
pcs_rpc_report_error(ep, PCS_RPC_ERR_RESPONSE_TOUT);
- FUSE_KTRACE(cc_from_rpc(ep->eng)->fc, "rpc timer expired, killing connection to " PEER_FMT ", %d",
+ FUSE_KDEBUG(cc_from_rpc(ep->eng)->fc, "rpc timer expired, killing connection to " PEER_FMT ", %d",
PEER_ARGS(ep), err);
rpc_abort(ep, 0, err);
break;
@@ -1330,7 +1330,7 @@ static void pcs_rpc_fini_verify(struct pcs_rpc_engine *eng, struct hlist_head *r
struct pcs_rpc * ep =
hlist_entry(rpc_list->first, struct pcs_rpc, link);
- WARN(1, "rpc connection isn't closed ep: %p (flags: %u, "
+ WARN(1, "rpc connection isn't closed ep: %px (flags: %u, "
"state: %u, refcnt: %u)\n", ep, ep->flags, ep->state,
atomic_read(&ep->refcnt));
}
diff --git a/fs/fuse/kio/pcs/pcs_sock_conn.c b/fs/fuse/kio/pcs/pcs_sock_conn.c
index a2985a7874c6..89689a8993f3 100644
--- a/fs/fuse/kio/pcs/pcs_sock_conn.c
+++ b/fs/fuse/kio/pcs/pcs_sock_conn.c
@@ -122,7 +122,7 @@ void pcs_sockconnect_start(struct pcs_rpc *ep)
}
set_sock_parameters(sock, container_of(ep->eng, struct pcs_cluster_core, eng));
- TRACE(PEER_FMT " ->state:%d sock:%p\n", PEER_ARGS(ep), ep->state, sock);
+ TRACE(PEER_FMT " ->state:%d sock:%px\n", PEER_ARGS(ep), ep->state, sock);
cancel_delayed_work(&ep->timer_work);
ep->retries++;
diff --git a/fs/fuse/kio/pcs/pcs_sock_io.c b/fs/fuse/kio/pcs/pcs_sock_io.c
index d1f1560db6d2..fa26105b2185 100644
--- a/fs/fuse/kio/pcs/pcs_sock_io.c
+++ b/fs/fuse/kio/pcs/pcs_sock_io.c
@@ -150,7 +150,7 @@ static int do_send_one_seg(struct socket *sock, struct iov_iter *it, size_t left
if (unlikely(pcs_use_eor) && !more)
flags |= MSG_EOR;
- DTRACE("sock(%p) len:%ld, more:%d\n", sock, iov_iter_count(it), more);
+ DTRACE("sock(%px) len:%ld, more:%d\n", sock, iov_iter_count(it), more);
if (unlikely(!size))
return 0;
@@ -183,7 +183,7 @@ static int do_send_one_seg(struct socket *sock, struct iov_iter *it, size_t left
ret = sock_sendmsg(sock, &msg);
out:
- DTRACE("sock(%p) len:%ld, more:%d ret:%d\n", sock, iov_iter_count(it), more, ret);
+ DTRACE("sock(%px) len:%ld, more:%d ret:%d\n", sock, iov_iter_count(it), more, ret);
return ret;
}
@@ -262,7 +262,7 @@ static void pcs_sockio_recv(struct pcs_sockio *sio)
u32 msg_size;
unsigned long loop_timeout = jiffies + PCS_SIO_SLICE;
- TRACE("ENTER:" PEER_FMT " sio:%p cur_msg:%p\n", PEER_ARGS(ep), sio, sio->current_msg);
+ TRACE("ENTER:" PEER_FMT " sio:%px cur_msg:%px\n", PEER_ARGS(ep), sio, sio->current_msg);
while(!test_bit(PCS_IOCONN_BF_DEAD, &sio->io_flags)) {
int n;
@@ -302,7 +302,7 @@ static void pcs_sockio_recv(struct pcs_sockio *sio)
sio->current_msg = msg;
sio->current_msg_size = msg_size;
rcv_get_iter(msg, sio->read_offset, it, READ);
- TRACE(PEER_FMT" msg:%p read_off:%d iov_size:%ld\n", PEER_ARGS(ep), msg, sio->read_offset,
+ TRACE(PEER_FMT" msg:%px read_off:%d iov_size:%ld\n", PEER_ARGS(ep), msg, sio->read_offset,
iov_iter_count(it));
} else {
if (n == -EAGAIN)
@@ -320,7 +320,7 @@ static void pcs_sockio_recv(struct pcs_sockio *sio)
/* Current iter is exhausted, init new one */
rcv_get_iter(msg, sio->read_offset, it, READ);
- TRACE(PEER_FMT" msg:%p->size:%d off:%d it_count:%ld\n",
+ TRACE(PEER_FMT" msg:%px->size:%d off:%d it_count:%ld\n",
PEER_ARGS(ep), msg, msg_size, sio->read_offset,
iov_iter_count(it));
@@ -364,7 +364,7 @@ static void pcs_sockio_send(struct pcs_sockio *sio)
while (!list_empty(&sio->write_queue)) {
msg = list_first_entry(&sio->write_queue, struct pcs_msg, list);
- TRACE(PEER_FMT" sio(%p) offset:%d msg:%p\n", PEER_ARGS(ep), sio, sio->write_offset, msg);
+ TRACE(PEER_FMT" sio(%px) offset:%d msg:%px\n", PEER_ARGS(ep), sio, sio->write_offset, msg);
/* This is original check, but it is not clear how connection can becomes
dead before sio_abort() was called. Let's simplify it with BUG_ON
@@ -397,7 +397,7 @@ static void pcs_sockio_send(struct pcs_sockio *sio)
int has_more = (msg->list.next != &sio->write_queue);
int n;
- TRACE(PEER_FMT "offset:%d msg:%p left:%ld, it->len:%ld\n", PEER_ARGS(ep), sio->write_offset, msg,
+ TRACE(PEER_FMT "offset:%d msg:%px left:%ld, it->len:%ld\n", PEER_ARGS(ep), sio->write_offset, msg,
left, iov_iter_count(it));
if (!iov_iter_count(it)) {
@@ -479,7 +479,7 @@ static void pcs_sock_sendmsg(struct pcs_netio *netio, struct pcs_msg *msg)
struct pcs_sockio *sio = sio_from_netio(netio);
int was_idle = list_empty(&sio->write_queue);
- DTRACE("sio(%p) msg:%p\n", sio, msg);
+ DTRACE("sio(%px) msg:%px\n", sio, msg);
if (pcs_if_error(&sio->error)) {
pcs_set_local_error(&msg->error, sio->error.value);
@@ -561,7 +561,7 @@ void pcs_sock_ioconn_destruct(struct pcs_ioconn *ioconn)
{
struct pcs_sockio * sio = sio_from_ioconn(ioconn);
- TRACE("Sock destruct_cb, sio: %p", sio);
+ TRACE("Sock destruct_cb, sio: %px", sio);
BUG_ON(sio->current_msg);
BUG_ON(!list_empty(&sio->write_queue));
@@ -791,7 +791,7 @@ static void pcs_sock_throttle(struct pcs_netio *netio)
test_bit(PCS_IOCONN_BF_DEAD, &sio->io_flags))
return;
- FUSE_KDTRACE(cc_from_rpc(sio->netio.parent->eng)->fc, "Throttle on socket %p rpc=%p", sio, sio->netio.parent);
+ FUSE_KDTRACE(cc_from_rpc(sio->netio.parent->eng)->fc, "Throttle on socket %px rpc=%px", sio, sio->netio.parent);
sio->flags |= PCS_SOCK_F_THROTTLE;
}
@@ -803,7 +803,7 @@ static void pcs_sock_unthrottle(struct pcs_netio *netio)
test_bit(PCS_IOCONN_BF_DEAD, &sio->io_flags))
return;
- FUSE_KDTRACE(cc_from_rpc(sio->netio.parent->eng)->fc, "Unthrottle on socket %p rpc=%p", sio, sio->netio.parent);
+ FUSE_KDTRACE(cc_from_rpc(sio->netio.parent->eng)->fc, "Unthrottle on socket %px rpc=%px", sio, sio->netio.parent);
sio->flags &= ~PCS_SOCK_F_THROTTLE;
if ((sio->flags & PCS_SOCK_F_EOF))
return;
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 9%]
* Re: [Devel] [PATCH vz10 2/2] selftests: ve: introduce vtty pair teardown stress test
2026-09-14 9:16 5% ` [Devel] [PATCH vz10 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
@ 2026-09-15 14:47 0% ` Vasileios Almpanis
0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-09-15 14:47 UTC (permalink / raw)
To: Eva Kurchatova; +Cc: devel
> A vtty pair is created by the first master open and torn down by the
> last close. The close racing the peer's close used to let both sides
> decide they were the final one and call release_tty() twice, and a
> master opening again while a teardown was in flight used to hand out a
> reference to a tty_struct about to be freed.
>
> Hammer both ends. The master comes from the vzctl ioctl, which does
> not look the VE up. The slave is only reachable from inside the VE, as
> TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
> vttys index minor - 1, so the test builds a VE with cgroup2, sets
> ve.veid to the cgroup it made, and runs the slave side there.
>
> One of the symtoms of a buggy kernel is dmesg warning, not just an
> eventual crash, so the test checks that. A crashed node should also
> report test failure in CI, but that's less verbose, hence the check.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-144771
>
> Feature: ve selftest
> Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
> index ec40cbc7b3a1..6cd5823c4d93 100644
> --- a/tools/testing/selftests/ve/Makefile
> +++ b/tools/testing/selftests/ve/Makefile
> @@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
>
> TEST_GEN_PROGS += ve_ns_owner_test
> TEST_GEN_PROGS += ve_perms_test
> +TEST_GEN_PROGS += ve_vtty_stress
Please add to .gitignore too
>
> include ../lib.mk
> diff --git a/tools/testing/selftests/ve/settings b/tools/testing/selftests/ve/settings
> new file mode 100644
> index 000000000000..e2206265f67c
> --- /dev/null
> +++ b/tools/testing/selftests/ve/settings
> @@ -0,0 +1 @@
> +timeout=900
> diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
> new file mode 100644
> index 000000000000..9037f731c58d
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_vtty_stress.c
> @@ -0,0 +1,244 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * ve_vtty_stress - close a vtty pair from both ends at once.
> + *
> + * A vtty pair is created by the first master open and torn down by the
> + * last close. A close racing the peer's close used to let both sides
> + * decide they were the final one and call release_tty() twice, and a
> + * master opened again while a teardown was still in flight used to hand
> + * out a reference to a tty_struct that was about to be freed. Either
> + * way the next close walked freed memory.
> + *
> + * The master comes from the vzctl ioctl, which does not look the VE up.
> + * The slave is only reachable from inside the VE, as TTY_MAJOR minor
> + * 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to vttys index
> + * minor - 1. So the test makes a VE with cgroup2, hammers the master
> + * from the host and the slave from a task inside it, and fails if the
> + * kernel logs a complaint about the teardown.
> + *
> + * Surviving is not a pass on its own: the first symptom is a warning,
> + * which stops nothing by itself.
> + *
> + * Usage: ve_vtty_stress [seconds] (default 600, or $VE_VTTY_SECS)
> + */
> +#define _GNU_SOURCE
> +#include <sched.h>
> +#include <stdbool.h>
> +#include <sys/ioctl.h>
> +#include <linux/major.h>
> +#include <sys/sysmacros.h>
> +#include <sys/wait.h>
> +#include <time.h>
> +
> +#include "ve_selftest.h"
> +#include "../kselftest.h"
> +
> +/* The ioctl argument; the trailing data[] the uapi struct has is unused. */
> +struct vzctl_ve_configure {
> + unsigned int veid, key, val, size;
> +};
> +
> +#define VZCTL_VE_CONFIGURE _IOW('.', 15, struct vzctl_ve_configure)
> +#define VE_CONFIGURE_OPEN_TTY 5
> +
> +#define VTTY_IDX 0 /* the pair both ends work on */
> +#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
> +#define DEFAULT_SECS 600
> +
> +static int open_master(int ctl_fd, unsigned int veid)
> +{
> + struct vzctl_ve_configure c = {
> + .veid = veid, .key = VE_CONFIGURE_OPEN_TTY, .val = VTTY_IDX,
> + };
> +
> + return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
> +}
> +
> +/*
> + * The slave end, inside the VE. Every error here is one the race is
> + * expected to produce: the pair is gone once the master has closed it.
> + */
> +static int hammer_slave(time_t deadline, unsigned long *opened)
> +{
> + char node[] = "/tmp/ve_vtty_slave";
> +
> + unlink(node);
> + if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0)
> + return -1;
> +
> + while (time(NULL) < deadline) {
> + int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
> +
> + if (fd >= 0) {
> + (*opened)++;
> + close(fd);
> + }
> + }
> + unlink(node);
> + return 0;
> +}
> +
> +/* The kernel log, since a warning fails nothing by itself. */
> +static int kmsg_open(void)
> +{
> + int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
> +
> + if (fd >= 0)
> + lseek(fd, 0, SEEK_END);
> + return fd;
> +}
> +
> +static int kmsg_check(int fd)
> +{
> + static const char * const ours[] = {
> + "tty_release", "release_tty", "check_tty_count",
> + "vtty_open_master", "vtty_shutdown",
> + };
> + char buf[8192];
> + int found = 0;
> + ssize_t r;
> + size_t i;
> +
> + while (fd >= 0 && (r = read(fd, buf, sizeof(buf) - 1)) > 0) {
> + buf[r] = '\0';
> + if (!strstr(buf, "WARNING") && !strstr(buf, "BUG") &&
> + !strstr(buf, "Oops") && !strstr(buf, "general protection"))
> + continue;
> + for (i = 0; i < ARRAY_SIZE(ours); i++)
> + if (strstr(buf, ours[i])) {
> + ksft_print_msg("kernel log: %s", buf);
> + found++;
> + break;
> + }
> + }
> + return found;
> +}
> +
> +int main(int argc, char **argv)
> +{
> + unsigned long master_opened = 0, slave_opened = 0;
> + int cg, ctid, ctl, kmsg, ready[2], count[2], status;
> + const char *env = getenv("VE_VTTY_SECS");
> + char path[PATH_MAX], val[16], c;
> + time_t deadline;
> + long secs;
> + pid_t pid;
> +
> + ksft_print_header();
> + secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
> + if (secs <= 0)
> + secs = DEFAULT_SECS;
> +
> + ctl = open("/dev/vzctl", O_RDWR);
> + if (ctl < 0)
> + ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
> + "(vzdev, vzmon) are needed\n", strerror(errno));
> +
> + cg = mount_cg2_fd();
> + if (cg < 0)
> + ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
> + write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS);
> +
> + ctid = make_ve(cg, CTID_MIN);
> + if (ctid < 0)
> + ksft_exit_skip("cannot make a VE cgroup\n");
> +
> + /*
> + * The pair is filed under the veid of the VE, so the two ends only
> + * meet if the cgroup carries the one the master is opened for.
> + */
> + snprintf(path, sizeof(path), "%d/ve.veid", ctid);
> + snprintf(val, sizeof(val), "%d", ctid);
> + if (write_file_at(cg, path, val) < 0) {
> + destroy_ve(cg, ctid);
> + ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
> + }
> +
> + /* Do not report a failure where the vtty pair cannot be had at all. */
> + status = open_master(ctl, ctid);
> + if (status < 0 && errno != EBUSY) {
> + destroy_ve(cg, ctid);
> + ksft_exit_skip("no vtty master: %s\n", strerror(errno));
> + }
> + if (status >= 0)
> + close(status);
> +
> + ksft_set_plan(1);
> + kmsg = kmsg_open();
> + if (kmsg < 0)
> + ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
> + "be noticed\n", strerror(errno));
> +
> + if (pipe(ready) || pipe(count))
> + ksft_exit_fail_msg("pipe: %s\n", strerror(errno));
> +
> + deadline = time(NULL) + secs;
> + ksft_print_msg("VE %d, pair %d, %ld seconds\n", ctid, VTTY_IDX, secs);
> +
> + pid = fork();
Fork could also return error. Please handle it.
--
Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* [PATCH QEMU HCI-8.0 v3 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800
` (8 preceding siblings ...)
2026-09-14 19:13 6% ` [PATCH QEMU HCI-8.0 v3 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-09-14 19:13 20% ` Andrey Drobyshev
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-14 19:13 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
A previous commit added an unconditional blocker for CPR migrations.
That is to refuse CPR gracefully instead of crashing and for the previous
commits to remain bisectable.
Now the restore path is complete: the FD is preserved, in-flight URBs
are drained and replayed, the device is handed off across cpr-transfer,
and the endpoints and isochronous streams are reconstructed on the
target. So let's lift the CPR blocker for the devices which satisfy the
preconditions.
Still, the remaining preconditions for CPR to work are:
* We must open host device FD ourselves, so that libusb owns the FD.
This way we're able to preserve the FD during CPR;
* Device ID must be provided as a unique stable key for CPR FD registry.
Use them to guard the blocker addition. Apart from that, prohibit CPR on
WIN32 platform and on hosts with older libusb versions.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 50 +++++++++++++++++++++++++++++++++++---------------
1 file changed, 35 insertions(+), 15 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index a3fdde16c1a..578262518de 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1446,24 +1446,44 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
-#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
- if (s->hostdevice && DEVICE(s)->id) {
- migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
- MIG_MODE_CPR_TRANSFER,
- MIG_MODE_CPR_EXEC, -1);
+#ifdef CONFIG_WIN32
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "WIN32 platform is not supported",
+ DEVICE(s)->id ?: "(anonymous)");
+#elif LIBUSB_API_VERSION < 0x01000107
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "libusb API version 0x%08x is too old",
+ DEVICE(s)->id ?: "(anonymous)", (unsigned) LIBUSB_API_VERSION);
+#else
+ if (!(s->hostdevice && DEVICE(s)->id)) {
+ /*
+ * Host FD must be owned by libusb to support CPR migration. That's
+ * equivalent to hostdevice= property being present. Also device ID
+ * is required as stable key for CPR FD registry.
+ */
+ error_setg(&s->cpr_blocker,
+ "usb-host device %s does not support CPR: "
+ "hostdevice= and a device id are required",
+ DEVICE(s)->id ?: "(anonymous)");
}
#endif
- error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
- DEVICE(s)->id ?: "(anonymous)");
- if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
- MIG_MODE_CPR_TRANSFER,
- MIG_MODE_CPR_EXEC, -1) < 0) {
- qemu_remove_exit_notifier(&s->exit);
- if (s->needs_autoscan) {
- QTAILQ_REMOVE(&hostdevs, s, next);
- }
- usb_host_close(s);
+ if (s->cpr_blocker) {
+ if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1) < 0) {
+ qemu_remove_exit_notifier(&s->exit);
+ if (s->needs_autoscan) {
+ QTAILQ_REMOVE(&hostdevs, s, next);
+ }
+ usb_host_close(s);
+ }
+ } else {
+ migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1);
}
}
--
2.47.1
^ permalink raw reply [relevance 20%]
* [PATCH QEMU HCI-8.0 v3 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800
` (7 preceding siblings ...)
2026-09-14 19:13 26% ` [PATCH QEMU HCI-8.0 v3 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
@ 2026-09-14 19:13 6% ` Andrey Drobyshev
2026-09-14 19:13 20% ` [PATCH QEMU HCI-8.0 v3 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-14 19:13 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
On CPR incoming we skip the usb_host_open() rescan, so libusb only sets
up the endpoints from the device's default configuration. If the guest
had enabled an endpoint by selecting a non-zero altsetting (e.g. an
isochronous streaming endpoint), that endpoint is now missing, and the
guest's transfers to it simply stall.
The altsetting itself is restored by vmload now, so let's call
usb_host_ep_update() on the CPR target to rebuild the endpoints out of
it.
The kernel claims are still there on the preserved FD, but libusb's own
handle knows nothing about them, so a later SET_INTERFACE would fail with
LIBUSB_ERROR_NOT_FOUND. Let's re-claim the interfaces on the target as
well to bring libusb's view back in sync - on the preserved FD this is a
no-op at the usbfs level anyway.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 81 insertions(+)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index e29e6210fdd..a3fdde16c1a 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1956,6 +1956,75 @@ static void usb_host_post_load_bh(void *opaque)
usb_host_auto_check(NULL);
}
+/*
+ * On the CPR target libusb knows nothing about the interface claims the
+ * preserved fd still holds, so a later SET_INTERFACE would fail with
+ * NOT_FOUND. Re-claim them to bring libusb's view back in sync; on the
+ * preserved fd it's a no-op at the usbfs level anyway.
+ */
+static int usb_host_cpr_reclaim_interfaces(USBHostDevice *s)
+{
+ USBDevice *udev = USB_DEVICE(s);
+ struct libusb_config_descriptor *conf;
+ int n, rc;
+
+ rc = libusb_get_active_config_descriptor(s->dev, &conf);
+ if (rc != 0) {
+ usb_host_libusb_error("libusb_get_active_config_descriptor", rc);
+ return -1;
+ }
+
+ n = MIN(conf->bNumInterfaces, USB_MAX_INTERFACES);
+ for (int i = 0; i < n; i++) {
+ rc = libusb_claim_interface(s->dh, i);
+ if (rc != 0) {
+ usb_host_libusb_error("libusb_claim_interface", rc);
+ libusb_free_config_descriptor(conf);
+ return -1;
+ }
+ s->ifs[i].claimed = true;
+ }
+
+ udev->ninterfaces = conf->bNumInterfaces;
+ udev->configuration = conf->bConfigurationValue;
+ libusb_free_config_descriptor(conf);
+
+ return 0;
+}
+
+/*
+ * During CPR the altsetting array is included in the migration stream.
+ * Reject a value this device can't honour, so that usb_host_ep_update()
+ * on the target never sees an out of range altsetting. In this case
+ * we fail the incoming migration instead of aborting on it.
+ */
+static int usb_host_cpr_check_altsettings(USBHostDevice *s)
+{
+ USBDevice *udev = USB_DEVICE(s);
+ struct libusb_config_descriptor *conf;
+
+ if (libusb_get_active_config_descriptor(s->dev, &conf) != 0) {
+ error_report("usb-host: cannot read config descriptor on "
+ "CPR incoming");
+ return -1;
+ }
+
+ for (int i = 0; i < conf->bNumInterfaces; i++) {
+ int ifnum = conf->interface[i].altsetting[0].bInterfaceNumber;
+ int alt = udev->altsetting[ifnum];
+
+ if (alt != 0 && alt >= conf->interface[i].num_altsetting) {
+ error_report("usb-host: migrated altsetting %d out of range for "
+ "interface %d", alt, ifnum);
+ libusb_free_config_descriptor(conf);
+ return -1;
+ }
+ }
+
+ libusb_free_config_descriptor(conf);
+ return 0;
+}
+
static int usb_host_post_load(void *opaque, int version_id)
{
USBHostDevice *dev = opaque;
@@ -1967,6 +2036,18 @@ static int usb_host_post_load(void *opaque, int version_id)
* Skip it for CPR.
*/
if (cpr_is_incoming()) {
+ /*
+ * We kept the device open across CPR, so libusb still needs its
+ * interface claims and endpoints rebuilt here. And we only start
+ * reaping now, once the source has handed the fd off to us.
+ */
+ if (usb_host_cpr_check_altsettings(dev) < 0) {
+ return -1;
+ }
+ if (usb_host_cpr_reclaim_interfaces(dev) < 0) {
+ return -1;
+ }
+ usb_host_ep_update(dev);
usb_host_cpr_restart_events();
return 0;
}
--
2.47.1
^ permalink raw reply [relevance 6%]
* [PATCH QEMU HCI-8.0 v3 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800
` (6 preceding siblings ...)
2026-09-14 19:13 22% ` [PATCH QEMU HCI-8.0 v3 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
@ 2026-09-14 19:13 26% ` Andrey Drobyshev
2026-09-14 19:13 6% ` [PATCH QEMU HCI-8.0 v3 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-14 19:13 20% ` [PATCH QEMU HCI-8.0 v3 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-14 19:13 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
Unlike cpr-exec, cpr-transfer keeps the source QEMU alive: it hands the
usbfs FD to the new QEMU over the migration channel and only exits once
the target is up. While the handoff is in flight both processes hold the
same FD, and only one may reap URB events on it - if both do, one reaps
the other's in-flight completions and wedges an isochronous transfer that
cannot be retried. So reaping is handed off along with the FD:
* The target doesn't reap until it takes over: it stops events right
after opening the FD in .realize() and restarts in .post_load().
* The source stops reaping in .pre_save() and, since it stays alive,
restarts from the MIG_EVENT_PRECOPY_FAILED notifier on failure.
* On success the source's .exit() leaves the device untouched - no
reset, interface release or host-driver rebind - and just closes its
own copy of the FD.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 94 ++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 83 insertions(+), 11 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 922c1a5585e..e29e6210fdd 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -110,6 +110,7 @@ struct USBHostDevice {
NotifierWithReturn cpr_notifier;
GSList *cpr_inflight;
bool cpr_drained;
+ bool cpr_handed_off;
libusb_device *dev;
libusb_device_handle *dh;
struct libusb_device_descriptor ddesc;
@@ -1227,16 +1228,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;
@@ -1271,10 +1275,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);
}
}
@@ -1298,6 +1309,8 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
}
static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+static void usb_host_cpr_stop_events(void);
+static void usb_host_cpr_restart_events(void);
/*
* A failed CPR migration resumes the source VM with the drained
@@ -1318,6 +1331,11 @@ static int usb_host_cpr_notifier(NotifierWithReturn *notifier,
return 0;
}
+ if (s->cpr_handed_off) {
+ usb_host_cpr_restart_events();
+ s->cpr_handed_off = false;
+ }
+
for (it = s->cpr_inflight; it; it = it->next) {
p = it->data;
/* Replay from the start of the TD */
@@ -1390,6 +1408,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
error_setg(errp, "failed to open host usb device %s", s->hostdevice);
return;
}
+ if (cpr_is_incoming()) {
+ /*
+ * The source still owns the shared fd until it hands off. Don't
+ * reap URBs on it yet, or we would steal the source's in-flight
+ * completions; .post_load() restarts events once we take over.
+ */
+ usb_host_cpr_stop_events();
+ }
} else
#endif
if (s->match.addr && s->match.bus_num &&
@@ -1941,6 +1967,7 @@ static int usb_host_post_load(void *opaque, int version_id)
* Skip it for CPR.
*/
if (cpr_is_incoming()) {
+ usb_host_cpr_restart_events();
return 0;
}
@@ -1953,6 +1980,47 @@ static int usb_host_post_load(void *opaque, int version_id)
return 0;
}
+#ifndef CONFIG_WIN32
+
+/*
+ * cpr-transfer: the usbfs fd is shared with the new QEMU via SCM_RIGHTS.
+ * Stop this (source) process from reaping URBs on it, so the new QEMU can
+ * drive the device without both processes racing on the same fd. Restart
+ * on migration failure, when the source resumes.
+ */
+static void usb_host_cpr_stop_events(void)
+{
+ const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+ libusb_set_pollfd_notifiers(ctx, NULL, NULL, NULL);
+ if (poll) {
+ for (int i = 0; poll[i] != NULL; i++) {
+ usb_host_del_fd(poll[i]->fd, ctx);
+ }
+ free(poll);
+ }
+}
+
+static void usb_host_cpr_restart_events(void)
+{
+ const struct libusb_pollfd **poll = libusb_get_pollfds(ctx);
+
+ libusb_set_pollfd_notifiers(ctx, usb_host_add_fd, usb_host_del_fd, ctx);
+ if (poll) {
+ for (int i = 0; poll[i] != NULL; i++) {
+ usb_host_add_fd(poll[i]->fd, poll[i]->events, ctx);
+ }
+ free(poll);
+ }
+}
+
+#else
+
+static void usb_host_cpr_stop_events(void) {}
+static void usb_host_cpr_restart_events(void) {}
+
+#endif
+
static int usb_host_pre_save(void *opaque)
{
USBHostDevice *s = opaque;
@@ -1964,6 +2032,10 @@ static int usb_host_pre_save(void *opaque)
if (usb_host_cpr_drain_xfers(s) < 0) {
return -1;
}
+ if (mode == MIG_MODE_CPR_TRANSFER) {
+ usb_host_cpr_stop_events();
+ s->cpr_handed_off = true;
+ }
}
return 0;
}
--
2.47.1
^ permalink raw reply [relevance 26%]
* [PATCH QEMU HCI-8.0 v3 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800
` (5 preceding siblings ...)
2026-09-14 19:13 21% ` [PATCH QEMU HCI-8.0 v3 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
@ 2026-09-14 19:13 22% ` Andrey Drobyshev
2026-09-14 19:13 26% ` [PATCH QEMU HCI-8.0 v3 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
` (2 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-14 19:13 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
usb_host_open() reads the product string descriptor from the device
with a synchronous control transfer. On the CPR target this runs while
the device is being handed over, and the transfer can block there,
hanging the incoming migration.
The string only fills the cosmetic product_desc, so skip the read when
we're the CPR target and fall back to the synthetic "host:bus.addr"
name.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 77ee3ca1dd7..922c1a5585e 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1064,7 +1064,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 v3 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800
2026-09-14 19:13 31% ` [PATCH QEMU HCI-8.0 v3 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-09-14 19:13 13% ` [PATCH QEMU HCI-8.0 v3 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
@ 2026-09-14 19:13 26% ` Andrey Drobyshev
2026-09-14 19:13 25% ` [PATCH QEMU HCI-8.0 v3 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
` (6 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-14 19:13 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
In .realize(), save freshly opened host device FD into the CPR registry,
and then reuse it on CPR target. Also, add deletion of that FD from
registry to usb_host_open() / usb_host_close() cleanup paths.
For this to work, we also need to skip the .post-load() code which
closes, detaches the device, and then rescans the host bus to reopen
matching hostdevs. For the CPR-case migration we don't want any of that
as the hostdev FD stays preserved, and the guest shouldn't notice the
switchover.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 33 ++++++++++++++++++++++++++++++++-
1 file changed, 32 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 5c3da00d8eb..b946b0ef350 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -47,6 +47,7 @@
#include "qapi/error.h"
#include "migration/blocker.h"
+#include "migration/cpr.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -947,6 +948,21 @@ static void usb_host_ep_update(USBHostDevice *s)
libusb_free_config_descriptor(conf);
}
+static char *usb_host_cpr_fd_name(USBHostDevice *s)
+{
+ /* Prefix the key so it can't clash with another CPR label */
+ return DEVICE(s)->id ? g_strdup_printf("usb-host/%s", DEVICE(s)->id)
+ : NULL;
+}
+
+static void usb_host_cpr_delete_fd(USBHostDevice *s)
+{
+ g_autofree char *name = usb_host_cpr_fd_name(s);
+ if (name) {
+ cpr_delete_fd(name, 0);
+ }
+}
+
static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
{
USBDevice *udev = USB_DEVICE(s);
@@ -1073,6 +1089,7 @@ fail:
s->dev = NULL;
}
if (s->hostfd != -1) {
+ usb_host_cpr_delete_fd(s);
close(s->hostfd);
s->hostfd = -1;
}
@@ -1134,6 +1151,7 @@ static int usb_host_close(USBHostDevice *s)
s->dev = NULL;
if (s->hostfd != -1) {
+ usb_host_cpr_delete_fd(s);
close(s->hostfd);
s->hostfd = -1;
}
@@ -1220,9 +1238,12 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
if (s->hostdevice) {
+ g_autofree char *name = usb_host_cpr_fd_name(s);
int fd;
+
s->needs_autoscan = false;
- fd = qemu_open(s->hostdevice, O_RDWR, errp);
+ fd = name ? cpr_open_fd(s->hostdevice, O_RDWR, name, 0, errp)
+ : qemu_open(s->hostdevice, O_RDWR, errp);
if (fd < 0) {
return;
}
@@ -1761,6 +1782,16 @@ static int usb_host_post_load(void *opaque, int version_id)
{
USBHostDevice *dev = opaque;
+ /*
+ * For CPR migration, device wasn't released/reset, and the guest
+ * is unaware of the switchover. The detach/rescan performed in
+ * usb_host_post_load_bh() only exists for cross-host migration.
+ * Skip it for CPR.
+ */
+ if (cpr_is_incoming()) {
+ return 0;
+ }
+
if (!dev->bh_postld) {
dev->bh_postld = qemu_bh_new_guarded(usb_host_post_load_bh, dev,
&DEVICE(dev)->mem_reentrancy_guard);
--
2.47.1
^ permalink raw reply [relevance 26%]
* [PATCH QEMU HCI-8.0 v3 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800
` (2 preceding siblings ...)
2026-09-14 19:13 26% ` [PATCH QEMU HCI-8.0 v3 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
@ 2026-09-14 19:13 25% ` Andrey Drobyshev
2026-09-14 19:13 24% ` [PATCH QEMU HCI-8.0 v3 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
` (5 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-14 19:13 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
usb_host_abort_xfers() cancels every pending request and then pumps
libusb events until all of them are reaped, with a bounded wait.
Split the reap-wait loop into usb_host_reap_xfers(), so that the
following commit can reuse it to reap canceled transfers without going
through the abort path. No functional change.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b946b0ef350..9b70d6deef9 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -1096,15 +1096,11 @@ fail:
return -1;
}
-static void usb_host_abort_xfers(USBHostDevice *s)
+static void usb_host_reap_xfers(USBHostDevice *s)
{
USBHostRequest *r, *rtmp;
int limit = 100;
- QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
- usb_host_req_abort(r);
- }
-
while (QTAILQ_FIRST(&s->requests) != NULL) {
struct timeval tv;
memset(&tv, 0, sizeof(tv));
@@ -1126,6 +1122,16 @@ static void usb_host_abort_xfers(USBHostDevice *s)
}
}
+static void usb_host_abort_xfers(USBHostDevice *s)
+{
+ USBHostRequest *r, *rtmp;
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ usb_host_req_abort(r);
+ }
+ usb_host_reap_xfers(s);
+}
+
static int usb_host_close(USBHostDevice *s)
{
USBDevice *udev = USB_DEVICE(s);
--
2.47.1
^ permalink raw reply [relevance 25%]
* [PATCH QEMU HCI-8.0 v3 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800
` (3 preceding siblings ...)
2026-09-14 19:13 25% ` [PATCH QEMU HCI-8.0 v3 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
@ 2026-09-14 19:13 24% ` Andrey Drobyshev
2026-09-14 19:13 21% ` [PATCH QEMU HCI-8.0 v3 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
` (4 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-14 19:13 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
URBs submitted by the old QEMU must not outlive it across the
switchover: the kernel would complete them into buffers of an address
space which is gone after exec, and the new libusb context would reap
URB pointers it never submitted.
So, in .pre_save() for the CPR modes, cancel and reap every pending
transfer, without completing its packet. The packets stay ASYNC and
their TDs remain on the transfer rings, so the target re-executes them
when usb_xhci_post_load() kicks the endpoints - the same in-flight
replay as on regular live migration. A partially executed transfer is
simply re-run from the start of the TD.
Isochronous URBs are not on the request list but on their own rings,
and freeing them while in-flight would leave them queued on the
preserved FD. Cancel them so they are unlinked and reaped: an iso URB
in flight when the guest stops may never complete on its own. The
target reconstructs the iso stream from the guest's transfer ring.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 90 insertions(+), 2 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 9b70d6deef9..1331eeefee4 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"
@@ -1096,7 +1097,7 @@ fail:
return -1;
}
-static void usb_host_reap_xfers(USBHostDevice *s)
+static bool usb_host_reap_xfers(USBHostDevice *s)
{
USBHostRequest *r, *rtmp;
int limit = 100;
@@ -1117,9 +1118,10 @@ static void usb_host_reap_xfers(USBHostDevice *s)
QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
QTAILQ_REMOVE(&s->requests, r, next);
}
- return;
+ return false;
}
}
+ return true;
}
static void usb_host_abort_xfers(USBHostDevice *s)
@@ -1132,6 +1134,76 @@ static void usb_host_abort_xfers(USBHostDevice *s)
usb_host_reap_xfers(s);
}
+static bool usb_host_iso_inflight(USBHostDevice *s)
+{
+ USBHostIsoRing *ring;
+
+ QTAILQ_FOREACH(ring, &s->isorings, next) {
+ if (!QTAILQ_EMPTY(&ring->inflight)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/*
+ * Quiesce the device for a CPR switchover. Cancel and reap all URBs,
+ * so that no URB submitted by this process can outlive it.
+ */
+static int usb_host_cpr_drain_xfers(USBHostDevice *s)
+{
+ USBHostRequest *r, *rtmp;
+ USBHostIsoRing *ring;
+ USBHostIsoXfer *xfer;
+ int limit;
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ if (r->p) {
+ /*
+ * Clear r->p so the reap callback early-outs on it: the
+ * callback must still run (it frees the request), but it
+ * must not complete the packet.
+ */
+ r->p = NULL;
+ }
+ libusb_cancel_transfer(r->xfer);
+ }
+
+ if (!usb_host_reap_xfers(s)) {
+ /*
+ * No URB can be left live on the FD across CPR migration. If
+ * we can't reap them -> abort the migration.
+ */
+ return -1;
+ }
+
+ /*
+ * Iso URBs are on the rings, not s->requests. Cancel them: one in
+ * flight when the guest stops may never complete on its own, and none
+ * may be left on the preserved fd. The target replays from the ring.
+ */
+ QTAILQ_FOREACH(ring, &s->isorings, next) {
+ QTAILQ_FOREACH(xfer, &ring->inflight, next) {
+ libusb_cancel_transfer(xfer->xfer);
+ }
+ }
+
+ /*
+ * Cap the reap at 2x a full ring:
+ * iso_urb_count URBs x iso_urb_frames packets each
+ */
+ limit = 2 * s->iso_urb_count * s->iso_urb_frames;
+ while (usb_host_iso_inflight(s)) {
+ struct timeval tv = { .tv_usec = 1000 };
+ libusb_handle_events_timeout(ctx, &tv);
+ if (limit-- == 0) {
+ return -1;
+ }
+ }
+ usb_host_iso_free_all(s);
+ return 0;
+}
+
static int usb_host_close(USBHostDevice *s)
{
USBDevice *udev = USB_DEVICE(s);
@@ -1807,10 +1879,26 @@ static int usb_host_post_load(void *opaque, int version_id)
return 0;
}
+static int usb_host_pre_save(void *opaque)
+{
+ USBHostDevice *s = opaque;
+ MigMode mode = migrate_mode();
+
+ /* URBs submitted by this process must not outlive CPR migration */
+ if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
+ s->dh) {
+ if (usb_host_cpr_drain_xfers(s) < 0) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
static const VMStateDescription vmstate_usb_host = {
.name = "usb-host",
.version_id = 1,
.minimum_version_id = 1,
+ .pre_save = usb_host_pre_save,
.post_load = usb_host_post_load,
.fields = (const VMStateField[]) {
VMSTATE_USB_DEVICE(parent_obj, USBHostDevice),
--
2.47.1
^ permalink raw reply [relevance 24%]
* [PATCH QEMU HCI-8.0 v3 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800
` (4 preceding siblings ...)
2026-09-14 19:13 24% ` [PATCH QEMU HCI-8.0 v3 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
@ 2026-09-14 19:13 21% ` Andrey Drobyshev
2026-09-14 19:13 22% ` [PATCH QEMU HCI-8.0 v3 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
` (3 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-14 19:13 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
The previous commit drains the in-flight URBs in .pre_save() and leaves
their packets ASYNC for the target to replay. If migration fails, the
source resumes instead, with those packets still owned by the
controller and nothing left to complete them.
Remember the drained packets in a list, and re-issue them on the
preserved FD from a MIG_EVENT_PRECOPY_FAILED notifier. Control
transfers can't be reconstructed from the packet alone, so complete
those as errors and let the guest driver retry.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 76 insertions(+), 2 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 1331eeefee4..77ee3ca1dd7 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -107,6 +107,9 @@ struct USBHostDevice {
int hostfd;
Error *cpr_blocker;
+ NotifierWithReturn cpr_notifier;
+ GSList *cpr_inflight;
+ bool cpr_drained;
libusb_device *dev;
libusb_device_handle *dh;
struct libusb_device_descriptor ddesc;
@@ -1147,8 +1150,13 @@ static bool usb_host_iso_inflight(USBHostDevice *s)
}
/*
- * Quiesce the device for a CPR switchover. Cancel and reap all URBs,
- * so that no URB submitted by this process can outlive it.
+ * Quiesce the device for a CPR switchover. Cancel and reap all URBs
+ * without completing their packets, so that no URB submitted by this
+ * process can outlive it. CPR target re-executes them in
+ * usb_xhci_post_load() kicking the running endpoints.
+ *
+ * Keep the list of packets so that they can be re-issued on this side
+ * instead if MIG_EVENT_PRECOPY_FAILED fires.
*/
static int usb_host_cpr_drain_xfers(USBHostDevice *s)
{
@@ -1159,6 +1167,10 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
if (r->p) {
+ if (r->p->state == USB_PACKET_ASYNC) {
+ /* In-flight req, in submission order, for the failure path */
+ s->cpr_inflight = g_slist_append(s->cpr_inflight, r->p);
+ }
/*
* Clear r->p so the reap callback early-outs on it: the
* callback must still run (it frees the request), but it
@@ -1168,6 +1180,7 @@ static int usb_host_cpr_drain_xfers(USBHostDevice *s)
}
libusb_cancel_transfer(r->xfer);
}
+ s->cpr_drained = true;
if (!usb_host_reap_xfers(s)) {
/*
@@ -1284,6 +1297,53 @@ static libusb_device *usb_host_find_ref(int bus, int addr)
return ret;
}
+static void usb_host_handle_data(USBDevice *udev, USBPacket *p);
+
+/*
+ * A failed CPR migration resumes the source VM with the drained
+ * packets still owned by the host controller as in-flight. Re-issue
+ * them on the preserved fd. Control transfers cannot be reconstructed
+ * from the packet alone; complete them as errors and let the guest
+ * driver retry.
+ */
+static int usb_host_cpr_notifier(NotifierWithReturn *notifier,
+ MigrationEvent *e, Error **errp)
+{
+ USBHostDevice *s = container_of(notifier, USBHostDevice, cpr_notifier);
+ USBDevice *udev = USB_DEVICE(s);
+ GSList *it;
+ USBPacket *p;
+
+ if (e->type != MIG_EVENT_PRECOPY_FAILED || !s->cpr_drained) {
+ return 0;
+ }
+
+ for (it = s->cpr_inflight; it; it = it->next) {
+ p = it->data;
+ /* Replay from the start of the TD */
+ p->actual_length = 0;
+ if (p->ep->nr == 0) {
+ /*
+ * p->ep[0] is control endpoint. Control transfers can't
+ * be reconstructed reliably, so complete them as errors
+ * and let the guest retry.
+ */
+ p->status = USB_RET_IOERROR;
+ usb_generic_async_ctrl_complete(udev, p);
+ } else {
+ usb_host_handle_data(udev, p);
+ if (p->status != USB_RET_ASYNC) {
+ /* Completed synchronously with error */
+ usb_packet_complete(udev, p);
+ }
+ }
+ }
+ g_slist_free(s->cpr_inflight);
+ s->cpr_inflight = NULL;
+ s->cpr_drained = false;
+ return 0;
+}
+
static void usb_host_realize(USBDevice *udev, Error **errp)
{
USBHostDevice *s = USB_HOST_DEVICE(udev);
@@ -1360,6 +1420,14 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
+#if LIBUSB_API_VERSION >= 0x01000107 && !defined(CONFIG_WIN32)
+ if (s->hostdevice && DEVICE(s)->id) {
+ migration_add_notifier_modes(&s->cpr_notifier, usb_host_cpr_notifier,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1);
+ }
+#endif
+
error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
DEVICE(s)->id ?: "(anonymous)");
if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
@@ -1393,6 +1461,9 @@ static void usb_host_unrealize(USBDevice *udev)
}
usb_host_close(s);
migrate_del_blocker(&s->cpr_blocker);
+ migration_remove_notifier(&s->cpr_notifier);
+ g_slist_free(s->cpr_inflight);
+ s->cpr_inflight = NULL;
}
static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1400,6 +1471,9 @@ static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
USBHostDevice *s = USB_HOST_DEVICE(udev);
USBHostRequest *r;
+ /* Packet is being cancelled -> it mustn't be re-issued on failed CPR */
+ s->cpr_inflight = g_slist_remove(s->cpr_inflight, p);
+
if (p->combined) {
usb_combined_packet_cancel(udev, p);
return;
--
2.47.1
^ permalink raw reply [relevance 21%]
* [PATCH QEMU HCI-8.0 v3 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800
@ 2026-09-14 19:13 31% ` Andrey Drobyshev
2026-09-14 19:13 13% ` [PATCH QEMU HCI-8.0 v3 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
` (8 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-14 19:13 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
The FD passed into usb_host_open() for a wrapped device is only stored in
s->hostfd after libusb_wrap_sys_device() succeeds, and the failure path
doesn't close it. if wrapping fails, or a later step of the open sequence
fails (e.g. usb_device_attach), the FD is leaked, as usb_host_close() only
runs for fully opened devices, and the realize error path doesn't clean it
up either.
Store the FD in s->hostfd before wrapping and close it on the failure
path.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index b74670ae256..83645fbe0ce 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -954,6 +954,11 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
int rc;
Error *local_err = NULL;
+ /* Make sure failure path frees caller-owned FD */
+ if (!dev) {
+ s->hostfd = hostfd;
+ }
+
if (s->bh_postld_pending) {
return -1;
}
@@ -978,7 +983,6 @@ static int usb_host_open(USBHostDevice *s, libusb_device *dev, int hostfd)
if (rc != 0) {
goto fail;
}
- s->hostfd = hostfd;
dev = libusb_get_device(s->dh);
bus_num = libusb_get_bus_number(dev);
addr = libusb_get_device_address(dev);
@@ -1066,6 +1070,10 @@ fail:
s->dh = NULL;
s->dev = NULL;
}
+ if (s->hostfd != -1) {
+ close(s->hostfd);
+ s->hostfd = -1;
+ }
return -1;
}
--
2.47.1
^ permalink raw reply [relevance 31%]
* [PATCH QEMU HCI-8.0 v3 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800
2026-09-14 19:13 31% ` [PATCH QEMU HCI-8.0 v3 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
@ 2026-09-14 19:13 13% ` Andrey Drobyshev
2026-09-14 19:13 26% ` [PATCH QEMU HCI-8.0 v3 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
` (7 subsequent siblings)
9 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-14 19:13 UTC (permalink / raw)
To: svt-core; +Cc: den, andrey.drobyshev
We're about to add support for CPR migration to usb-host in the upcoming
commits. Right now CPR is crashing with usb-host devices. Let's add
migration blocker upfront, for both cpr-exec and cpr-transfer. The
blocker is added unconditionally, so that we resuse CPR migration instead
of crashing on it. That is for the sake of bisectability of the upcoming
commits. It is going to be lifted (conditioned) in a following patch
once CPR support is added. Also, blocker is added at the end of
usb_host_realize(), so we have to cleanup all the side effects done
earlier in .realize() in case migrate_add_blocker_modes() fails.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 83645fbe0ce..5c3da00d8eb 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -46,6 +46,7 @@
#endif
#include "qapi/error.h"
+#include "migration/blocker.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -103,6 +104,7 @@ struct USBHostDevice {
char port[16];
int hostfd;
+ Error *cpr_blocker;
libusb_device *dev;
libusb_device_handle *dh;
struct libusb_device_descriptor ddesc;
@@ -1258,6 +1260,18 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
+
+ error_setg(&s->cpr_blocker, "usb-host device %s does not support CPR",
+ DEVICE(s)->id ?: "(anonymous)");
+ if (migrate_add_blocker_modes(&s->cpr_blocker, errp,
+ MIG_MODE_CPR_TRANSFER,
+ MIG_MODE_CPR_EXEC, -1) < 0) {
+ qemu_remove_exit_notifier(&s->exit);
+ if (s->needs_autoscan) {
+ QTAILQ_REMOVE(&hostdevs, s, next);
+ }
+ usb_host_close(s);
+ }
}
static void usb_host_instance_init(Object *obj)
@@ -1279,6 +1293,7 @@ static void usb_host_unrealize(USBDevice *udev)
QTAILQ_REMOVE(&hostdevs, s, next);
}
usb_host_close(s);
+ migrate_del_blocker(&s->cpr_blocker);
}
static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
--
2.47.1
^ permalink raw reply [relevance 13%]
* Re: [Devel] [PATCH VZ10 v8 7/7] selftests/ve: Add mount accounting selftest
2026-09-09 21:35 5% ` [Devel] [PATCH VZ10 v8 7/7] selftests/ve: Add mount accounting selftest Vladimir Riabchun
@ 2026-09-14 9:35 0% ` Vasileios Almpanis
0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-09-14 9:35 UTC (permalink / raw)
To: Vladimir Riabchun; +Cc: devel
> There are 6 test cases, covered in the new test:
> 1. Simple mount accouting correctness, just mount/umount.
> 2. Verification of correct limit hits and changes, including
> negative values.
> 3. Partial mounts test, when mount limit is hit in the middle
> of creation.
> 4. Test that enabled pseudosuper allows overuse.
> 5. Test that pseudosuper doesn't affect mount accounting.
> 6. Failcount feature verification.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-135520
>
> Feature: per-ve failcounters
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
>
> diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
> index afa4c568c2c9..3df4d05888dc 100644
> --- a/tools/testing/selftests/ve/.gitignore
> +++ b/tools/testing/selftests/ve/.gitignore
> @@ -1,2 +1,3 @@
> ve_ns_owner_test
> ve_perms_test
> +ve_mount_accounting_test
> diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
> index ec40cbc7b3a1..c6efe7c4b4fb 100644
> --- a/tools/testing/selftests/ve/Makefile
> +++ b/tools/testing/selftests/ve/Makefile
> @@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
>
> TEST_GEN_PROGS += ve_ns_owner_test
> TEST_GEN_PROGS += ve_perms_test
> +TEST_GEN_PROGS += ve_mount_accounting_test
>
> include ../lib.mk
> diff --git a/tools/testing/selftests/ve/ve_mount_accounting_test.c b/tools/testing/selftests/ve/ve_mount_accounting_test.c
> new file mode 100644
> index 000000000000..e82d9bf23839
> --- /dev/null
> +++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
> @@ -0,0 +1,489 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * ve_mount_accounting selftests
> + *
> + * Tests to check the correctness of mount accounting.
> + */
> +#define _GNU_SOURCE
> +#include <asm/unistd.h>
> +#include <linux/sched.h>
> +#include <linux/limits.h>
> +#include <sys/wait.h>
> +#include <sys/syscall.h>
> +#include <sys/stat.h>
> +#include <sys/mount.h>
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <sched.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <unistd.h>
> +#include <string.h>
> +
> +#include "../kselftest_harness.h"
> +#include "ve_selftest.h"
> +
> +#define TMP_DIR "/ve-mnt-tmp/"
> +#define VE_MOUNTS_MAX 128
> +
> +/*
> + * These values take a while to calculate, the first test will
> + * do this and store the values for those who come after.
> + */
> +int *need_avail;
> +int *mnt_cost;
> +
> +FIXTURE(ve_mnt_acc)
> +{
> + int cgv2_fd;
> + int ctid;
> + struct namespace_ctx ns_ctx;
> + int orig_have_mnt;
> +};
> +
> +static int set_pseudosuper(FIXTURE_DATA(ve_mnt_acc) * self, int value)
> +{
> + char path[64];
> +
> + snprintf(path, sizeof(path), "%d/ve.pseudosuper", self->ctid);
> + return write_u64_at(self->cgv2_fd, path, value);
> +}
> +
> +static int _create_mount(void *id_ptr)
> +{
> + char path[PATH_MAX];
> + int id = *(int *)id_ptr, ret;
> +
> + snprintf(path, sizeof(path), TMP_DIR "%d", id);
> +
> + if (mkdir(path, 0755) < 0) {
> + fprintf(stderr, "Failed to create directory %s: %s\n", path, strerror(errno));
> + return -1;
> + }
> + ret = mount("tmpfs", path, "tmpfs", 0, "size=1M");
> + if (!ret)
> + return 0;
> + fprintf(stderr, "Failed to mount tmpfs to %s: %s\n", path, strerror(errno));
> +
> + rmdir(path);
> + return -1;
> +}
> +
> +static int create_mount(FIXTURE_DATA(ve_mnt_acc) * self, int id)
> +{
> + int ret = run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
> + CLONE_NEWVE | CLONE_NEWNS, _create_mount, &id);
> + /*
> + * If mount fails, cleanup by free_vfsmnt will be called
> + * via call_rcu, need to wait for update.
> + */
> + sleep(1);
> + return ret;
> +}
> +
> +static int _destroy_mount(void *id_ptr)
> +{
> + char path[PATH_MAX];
> + struct stat st;
> + int id = *(int *)id_ptr;
> +
> + snprintf(path, sizeof(path), TMP_DIR "%d", id);
> +
> + if (stat(path, &st))
> + return 1;
> + if (umount(path)) {
> + fprintf(stderr, "failed to umount directory %s: %s\n", path, strerror(errno));
> + return -1;
> + }
> + if (rmdir(path)) {
> + fprintf(stderr, "failed to remove directory %s: %s\n", path, strerror(errno));
> + return -1;
> + }
> + return 0;
> +}
> +
> +static int destroy_mount(FIXTURE_DATA(ve_mnt_acc) * self, int id)
> +{
> + int ret = run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
> + CLONE_NEWVE | CLONE_NEWNS, _destroy_mount, &id);
> + /* free_vfsmnt is called via call_rcu, need to wait for update */
> + sleep(1);
> + return ret;
> +}
> +
> +#define MAX_MNT_ID 32
> +
> +static int get_free_mnt_id(void)
> +{
> + int i;
> + struct stat st;
> + char path[PATH_MAX];
> +
> + for (i = 0; i < MAX_MNT_ID; i++) {
> + snprintf(path, sizeof(path), TMP_DIR "%d", i);
> + if (stat(path, &st))
> + return i;
> + }
> + return -1;
> +}
> +
> +static int get_mount_cost(FIXTURE_DATA(ve_mnt_acc) * self)
> +{
> + int avail1, avail2, mnt_id;
> + char path[64];
> +
> + mnt_id = get_free_mnt_id();
> +
> + snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
> + if (mnt_id < 0 ||
> + read_s32_at(self->cgv2_fd, path, &avail1) ||
> + create_mount(self, mnt_id) ||
> + read_s32_at(self->cgv2_fd, path, &avail2) ||
> + destroy_mount(self, mnt_id))
> + return -1;
> +
> + return avail1 - avail2;
> +}
> +
> +/*
> + * Current kernel code first creates many mounts with ve_owner != ve0,
> + * so this VE needs more mounts than would be really consumed after commit_tree.
> + */
> +static int get_required_avail_mnts(FIXTURE_DATA(ve_mnt_acc) * self)
> +{
> + int l = 0, r = VE_MOUNTS_MAX + 1, mid;
> + char path_max_nr[64];
> +
> + snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
> +
> + while (r - l > 1) {
> + mid = (l + r) / 2;
> + if (write_u64_at(self->cgv2_fd, path_max_nr, mid + self->orig_have_mnt))
> + return -1;
> +
> + if (get_mount_cost(self) < 0)
> + l = mid;
> + else
> + r = mid;
> + }
> + if (r == VE_MOUNTS_MAX + 1) {
> + fprintf(stderr, "%s: %d avail mounts is not enough\n", __func__, r);
> + return -1;
> + }
> + if (write_u64_at(self->cgv2_fd, path_max_nr, VE_MOUNTS_MAX))
> + return -1;
> + return r;
> +}
> +
> +/* Expect mount success and return new avail value */
> +static int mount_and_get_avail(struct __test_metadata *_metadata,
> + FIXTURE_DATA(ve_mnt_acc) * self, int mnt_id)
> +{
> + char path_avail[64];
> + int mnt_avail_nr;
> +
> + snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
> +
> + ASSERT_EQ(create_mount(self, mnt_id), 0);
> + ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
> + return mnt_avail_nr;
> +}
> +
> +/* Expect mount failure and ensure intact avail number */
> +static int check_mount_fails(struct __test_metadata *_metadata,
> + FIXTURE_DATA(ve_mnt_acc) * self,
> + int mnt_id, int avail_count)
> +{
> + char path_avail[64];
> + int mnt_avail_nr;
> +
> + snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
> +
> + if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr))
> + return 1;
> + if (mnt_avail_nr != avail_count) {
> + fprintf(stderr, "%s: mnt_avail(%d) != avail_count(%d) before mount\n",
> + __func__, mnt_avail_nr, avail_count);
> + return -1;
> + }
> +
> + if (create_mount(self, mnt_id) == 0) {
> + fprintf(stderr, "%s: successful mount\n", __func__);
> + return -1;
> + }
> +
> + if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr))
> + return 1;
> + if (mnt_avail_nr != avail_count) {
> + fprintf(stderr, "%s: mnt_avail(%d) != avail_count(%d) after mount\n",
> + __func__, mnt_avail_nr, avail_count);
> + return -1;
> + }
> + return 0;
> +}
> +
> +FIXTURE_SETUP(ve_mnt_acc)
> +{
> + int initial_mnt_avail_nr;
> + char path[64];
> +
> + self->cgv2_fd = mount_cg2_fd();
> + ASSERT_GE(self->cgv2_fd, 0);
> + mkdir(TMP_DIR, 0755);
> +
> + ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
> + VE_CONTROLLERS), 0);
> +
> + self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
> + ASSERT_GE(self->ctid, 0);
> +
> + snprintf(path, sizeof(path), "%d/ve.mnt_max_nr", self->ctid);
> + ASSERT_EQ(write_u64_at(self->cgv2_fd, path, VE_MOUNTS_MAX), 0);
> +
> + /*
> + * The new ve cgroup has not been entered by anything yet, so its
> + * mnt_avail_nr counter should be VE_MOUNTS_MAX.
> + */
> + snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
> + ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
> + ASSERT_EQ(initial_mnt_avail_nr, VE_MOUNTS_MAX);
> +
> + ASSERT_EQ(prepare_namespaces(&self->ns_ctx, CLONE_NEWNS | CLONE_NEWVE,
> + self->cgv2_fd, self->ctid), 0);
> +
> + ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
> + self->orig_have_mnt = VE_MOUNTS_MAX - initial_mnt_avail_nr;
> +
> + /* Calculate once - it is time-consuming */
> + if (*need_avail == -1)
> + *need_avail = get_required_avail_mnts(self);
> + if (*mnt_cost == -1)
> + *mnt_cost = get_mount_cost(self);
> + ASSERT_GE(*need_avail, 1);
> + ASSERT_GE(*mnt_cost, 1);
> +};
> +
> +FIXTURE_TEARDOWN(ve_mnt_acc)
> +{
> + for (int i = 0; i < MAX_MNT_ID; i++)
> + _destroy_mount((void *)&i);
> +
> + close_namespace_fds(&self->ns_ctx);
> + destroy_ve(self->cgv2_fd, self->ctid);
> + close(self->cgv2_fd);
> + rmdir(TMP_DIR);
> +}
> +
> +/* Simple test to check mount/umount accounting correctness */
> +TEST_F(ve_mnt_acc, mount_umount)
> +{
> + int original_mnt_avail, mnt_avail_nr;
> + char path[64];
> +
> + snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
> +
> + ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &original_mnt_avail), 0);
> +
> + ASSERT_LT(mount_and_get_avail(_metadata, self, 0),
> + original_mnt_avail);
> +
> + ASSERT_EQ(destroy_mount(self, 0), 0);
> + ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &mnt_avail_nr), 0);
> + ASSERT_EQ(mnt_avail_nr, original_mnt_avail);
> +}
> +
> +/* Test mount limit hits */
> +TEST_F(ve_mnt_acc, hit_limits)
> +{
> + int mnt_avail_nr;
> + char path_avail[64], path_max_nr[64];
> +
> + snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
> + snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
> +
> + /* Step 1: reduce number of available mounts to *need_avail */
> + ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
> + self->orig_have_mnt + 1 * (*need_avail)), 0);
> + ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
> + ASSERT_EQ(mnt_avail_nr, 1 * *need_avail);
> +
> + /* Step 2: do one mount, no mounts should be available */
> + ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), *need_avail - *mnt_cost);
> +
> + /* Step 3: check that one more mount fails */
> + ASSERT_EQ(check_mount_fails(_metadata, self, 1, (*need_avail) - 1), 0);
> +
> + /* Step 4: increase by 1 and win now */
> + ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
> + self->orig_have_mnt + *mnt_cost + *need_avail), 0);
> + ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
> + ASSERT_EQ(mnt_avail_nr, *need_avail);
> +
> + ASSERT_EQ(mount_and_get_avail(_metadata, self, 1), *need_avail - *mnt_cost);
> +
> + /* Step 5: reduce mnt_max_nr so we have more mounts than allowed */
> + ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
> + self->orig_have_mnt + 1 * (*mnt_cost)), 0);
> + ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
> + ASSERT_EQ(mnt_avail_nr, -1 * (*mnt_cost));
> +
> + /* Step 6: try to do mount when avail < 0, ensure number is intact */
> + ASSERT_EQ(check_mount_fails(_metadata, self, 2, -1 * (*mnt_cost)), 0);
> +
> + /* Step 7: remove one mount, check avail value update, mount should fail */
> + ASSERT_EQ(destroy_mount(self, 0), 0);
> + ASSERT_EQ(check_mount_fails(_metadata, self, 2, 0), 0);
> +
> + /* Step 8: remove one more mount and check that new mount succeeds */
> + ASSERT_EQ(destroy_mount(self, 1), 0);
> + ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
> + ASSERT_EQ(mnt_avail_nr, 1 * *mnt_cost);
> + ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + *need_avail), 0);
> + ASSERT_EQ(mount_and_get_avail(_metadata, self, 2), *need_avail - *mnt_cost);
> +
> + ASSERT_EQ(destroy_mount(self, 2), 0);
> +}
> +
> +/*
> + * Mount propagation makes one mount cost more.
> + * This test checks that if we run out of mounts in the middle of creating
> + * a new one, everything is restored smoothly and nothing leaks.
> + */
> +TEST_F(ve_mnt_acc, partial_mounts)
> +{
> + char path_avail[64], path_max_nr[64];
> + int i;
> +
> + snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
> + snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
> +
> + if (*need_avail == 1)
> + SKIP(return, "mount cost is 1, no partial mounts possible");
> +
> + for (i = 0; i < *need_avail; i++) {
> + ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + i), 0);
> + ASSERT_EQ(check_mount_fails(_metadata, self, 0, i), 0);
> + }
> +}
> +
> +/* Test that pseudosuper allows negative avail with correct accounting. */
> +TEST_F(ve_mnt_acc, pseudosuper_allows_overuse)
> +{
> + char path_max_nr[64];
> +
> + snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
> +
> + ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + *need_avail), 0);
> + ASSERT_EQ(set_pseudosuper(self, 1), 0);
> +
> + ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), *need_avail - *mnt_cost);
> + ASSERT_EQ(mount_and_get_avail(_metadata, self, 1), *need_avail - 2 * (*mnt_cost));
> +
> + /* Go below zero */
> + ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt), 0);
> + ASSERT_EQ(mount_and_get_avail(_metadata, self, 2), -3 * (*mnt_cost));
> +
> + ASSERT_EQ(set_pseudosuper(self, 0), 0);
> +
> + /* Below zero should fail */
> + ASSERT_EQ(check_mount_fails(_metadata, self, 3, -3 * (*mnt_cost)), 0);
> + /* Now need 2 more to add a new mount */
> + ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
> + self->orig_have_mnt + 3 * (*mnt_cost) + *need_avail - 2 * (*mnt_cost)), 0);
> + ASSERT_EQ(check_mount_fails(_metadata, self, 3, *need_avail - 2 * (*mnt_cost)), 0);
> +
> + ASSERT_EQ(destroy_mount(self, 0), 0);
> + ASSERT_EQ(check_mount_fails(_metadata, self, 3, *need_avail - *mnt_cost), 0);
> + ASSERT_EQ(destroy_mount(self, 2), 0);
> + ASSERT_EQ(get_mount_cost(self), *mnt_cost);
> +}
> +
> +/* Test that pseudosuper doesn't disable accounting. */
> +TEST_F(ve_mnt_acc, pseudosuper_continues_accounting)
> +{
> + int orig_mnt_avail, mnt_avail_nr;
> + char path_avail[64];
> +
> + snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
> + ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &orig_mnt_avail), 0);
> +
> + ASSERT_EQ(set_pseudosuper(self, 0), 0);
> +
> + /* mnt 0 - mounted without pseudosuper, umounted with it. */
> + ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), orig_mnt_avail - *mnt_cost);
> + ASSERT_EQ(set_pseudosuper(self, 1), 0);
> +
> + /* Cost is the same when mount/umount happen under pseudosuper. */
> + ASSERT_EQ(get_mount_cost(self), *mnt_cost);
> +
> + /* mnt 1 - mounted with pseudosuper, umounted without it. */
> + ASSERT_EQ(mount_and_get_avail(_metadata, self, 1),
> + orig_mnt_avail - 2 * (*mnt_cost));
> +
> + ASSERT_EQ(destroy_mount(self, 0), 0);
> + ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
> + ASSERT_EQ(mnt_avail_nr, orig_mnt_avail - *mnt_cost);
> +
> + ASSERT_EQ(set_pseudosuper(self, 0), 0);
> +
> + ASSERT_EQ(destroy_mount(self, 1), 0);
> + ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
> + ASSERT_EQ(mnt_avail_nr, orig_mnt_avail);
> +}
> +
> +/* Test failcount feature */
> +TEST_F(ve_mnt_acc, failcount)
> +{
> + char path_fc[64], failcount_str[512], path_max_nr[64];
> +
> + snprintf(path_fc, sizeof(path_fc), "%d/ve.failcount", self->ctid);
> + snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
> +
> + ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
> + failcount_str, sizeof(failcount_str)), 0);
> + ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
> +
> + /* Check successful mount doesn't affect failcount */
> + mount_and_get_avail(_metadata, self, 0);
> + ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
> + failcount_str, sizeof(failcount_str)), 0);
> + ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
> + ASSERT_EQ(destroy_mount(self, 0), 0);
> +
> + /* Check failcount update when mount fails */
> + ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, 0), 0);
> + ASSERT_LT(create_mount(self, 1), 0);
> + ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
> + failcount_str, sizeof(failcount_str)), 0);
> + ASSERT_TRUE(strstr(failcount_str, "mnt: 1\n") != NULL);
> +
> + /* Check failcount flush */
> + ASSERT_EQ(write_u64_at(self->cgv2_fd, path_fc, 0), 0);
> + ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
> + failcount_str, sizeof(failcount_str)), 0);
> + ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
> +
> + /* Check failcount update when mount fails again */
> + ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, 0), 0);
> + ASSERT_LT(create_mount(self, 1), 0);
> + ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
> + failcount_str, sizeof(failcount_str)), 0);
> + ASSERT_TRUE(strstr(failcount_str, "mnt: 1\n") != NULL);
> +}
> +
> +int main(int argc, char **argv)
> +{
> + int *shared_data = mmap(NULL, sizeof(int) * 2,
> + PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
> +
Better check against MAP_FAILED not NULL
--
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 VZ10 v8 6/7] ve: Introduce per-VE failcount
@ 2026-09-14 9:35 0% ` Vasileios Almpanis
0 siblings, 0 replies; 200+ results
From: Vasileios Almpanis @ 2026-09-14 9:35 UTC (permalink / raw)
To: Vladimir Riabchun; +Cc: devel
> It may be useful to have a history of resource limit hits for every VE,
> this may simplify debugging and provide some information about the
> resources usage.
>
> This information is provided by ve.failcount file, any write to it
> resets all failcounts.
>
> To add a new failcounter we need to create a new atomic_t field
> name_failcount in ve structure and add a new VE_FC_ENTRY in
> ve_failcounts array.
>
> One change, unrelated to failcounts: aio fields are now initialized
> in ve0.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-135520
>
> Feature: per-ve failcounters
> Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
>
> diff --git a/fs/aio.c b/fs/aio.c
> index cb63416af135..4ae06bcd47f2 100644
> --- a/fs/aio.c
> +++ b/fs/aio.c
> @@ -814,6 +814,7 @@ static struct kioctx *ioctx_alloc(unsigned nr_events)
> spin_lock(&ve->aio_nr_lock);
> if (ve->aio_nr + ctx->max_reqs > ve->aio_max_nr ||
> ve->aio_nr + ctx->max_reqs < ve->aio_nr) {
> + ve_failcount_inc(ve, aio);
> spin_unlock(&ve->aio_nr_lock);
> err = -EAGAIN;
> goto err_ctx;
> diff --git a/fs/namespace.c b/fs/namespace.c
> index 1d6e66cd7174..ea01664ff5f2 100644
> --- a/fs/namespace.c
> +++ b/fs/namespace.c
> @@ -3380,6 +3380,8 @@ static inline int ve_try_reserve_mount(struct ve_struct *ve)
>
> if (ret)
> get_ve(ve);
> + else
> + ve_failcount_inc(ve, mnt);
> return ret;
> }
>
> diff --git a/include/linux/ve.h b/include/linux/ve.h
> index fabc608e31f5..065129b42768 100644
> --- a/include/linux/ve.h
> +++ b/include/linux/ve.h
> @@ -80,12 +80,15 @@ struct ve_struct {
> struct kmapset_key proc_perms_key;
>
> atomic_t netns_avail_nr;
> + atomic_t netns_failcount;
> int netns_max_nr;
>
> atomic_t netif_avail_nr;
> + atomic_t netif_failcount;
> int netif_max_nr;
>
> atomic_t bpf_prog_avail_nr;
> + atomic_t bpf_prog_failcount;
> int bpf_prog_max_nr;
>
> atomic64_t _uevent_seqnum;
> @@ -94,6 +97,7 @@ struct ve_struct {
>
> atomic_t arp_neigh_nr;
> atomic_t nd_neigh_nr;
> + atomic_t neigh_tbl_failcount;
> unsigned long meminfo_val;
>
> /*
> @@ -102,6 +106,7 @@ struct ve_struct {
> * other containers.
> */
> atomic_t mnt_avail_nr; /* number of available VE mounts */
> + atomic_t mnt_failcount;
> int mnt_max_nr;
>
> #ifdef CONFIG_COREDUMP
> @@ -129,6 +134,7 @@ struct ve_struct {
> spinlock_t aio_nr_lock;
> unsigned long aio_nr;
> unsigned long aio_max_nr;
> + atomic_t aio_failcount;
> #endif
> struct vfsmount *devtmpfs_mnt;
> };
> @@ -151,6 +157,16 @@ extern int nr_ve;
> #define BPF_PROG_MAX_NR_DEFAULT 256 /* number of loaded BPF progs per-VE */
> #define MNT_MAX_NR_DEFAULT 4096 /* number of mounts per-VE */
>
> +#define ve_failcount_inc(ve, name) \
> +do { \
> + struct ve_struct *__ve = (ve); \
> + \
> + atomic_inc(&__ve->name##_failcount); \
> + ve_pr_warn_ratelimited(VE_LOG_BOTH, "CT%s: hits the " #name \
> + " limit, see the '" #name "' counter in ve.failcount " \
> + "of the container's ve cgroup\n", ve_name(__ve)); \
If this is called in softirq context for example like neigh_alloc it
will print into ve of whatever task was just interrupted. So a random ve
will receive this warning into its dmesg. I don't think this is good.
--
Vasileios Almpanis <vasileios.almpanis@virtuozzo.com>
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* [Devel] [PATCH vz10 1/2] selftests: ve: grant the directories the files sit in
@ 2026-09-14 9:16 3% Eva Kurchatova
2026-09-14 9:16 5% ` [Devel] [PATCH vz10 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
0 siblings, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-09-14 9:16 UTC (permalink / raw)
To: khorenko; +Cc: devel
The sysfs variant names nested leaves, kernel/vmcoreinfo and
kernel/notes, while the proc one names files at the top of the
filesystem. Both enforcement tests grant the leaves and nothing else,
so in the sysfs case the lookup stops at /sys/kernel, which a fresh VE
cannot traverse, and every open answers ENOENT:
not ok 14 ve_perms.sysfs.enforce_allows
not ok 15 ve_perms.sysfs.enforce_denies
Grant x on each directory along the way, taken from the path the
variant already gives. A file at the top of the filesystem has none
and needs nothing, so the proc variant is unaffected.
The suite then passes 16 of 16, where it passed 14 before.
https://virtuozzo.atlassian.net/browse/VSTOR-144955
Feature: fix vz selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/ve/ve_perms_test.c | 33 ++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/tools/testing/selftests/ve/ve_perms_test.c b/tools/testing/selftests/ve/ve_perms_test.c
index 25ffbd42c380..2b542dc30139 100644
--- a/tools/testing/selftests/ve/ve_perms_test.c
+++ b/tools/testing/selftests/ve/ve_perms_test.c
@@ -348,6 +348,37 @@ TEST_F(ve_perms, write_parser)
* read, and granting x on the directory lets it be traversed (the lookup then
* fails ENOENT on the missing child, not EACCES).
*/
+/*
+ * A leaf is only reachable once every directory above it can be traversed.
+ * The variants name their files relative to the mount point, so grant x on
+ * each directory along the way; a file at the top of the filesystem has
+ * none and needs nothing.
+ */
+static int allow_path(int cgv2_fd, const char *path, const char *file)
+{
+ char dir[240]; /* set_entry() writes "<dir> <mask>" into 256 */
+ const char *sep;
+
+ for (sep = strchr(file, '/'); sep; sep = strchr(sep + 1, '/')) {
+ if (sep - file >= (long)sizeof(dir))
+ return -1;
+ snprintf(dir, sizeof(dir), "%.*s", (int)(sep - file), file);
+ if (set_entry(cgv2_fd, path, dir, "x") < 0)
+ return -1;
+ }
+ return 0;
+}
+
+static int allow_paths(int cgv2_fd, const char *path, const char *const files[])
+{
+ int i;
+
+ for (i = 0; i < NR_FILES; i++)
+ if (allow_path(cgv2_fd, path, files[i]) < 0)
+ return -1;
+ return 0;
+}
+
TEST_F(ve_perms, enforce_allows)
{
char path[PATH_MAX], absent[128];
@@ -358,6 +389,7 @@ TEST_F(ve_perms, enforce_allows)
ve_file(path, sizeof(path), self->ctid_a, variant->fs);
+ ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "r"), 0);
for (i = 0; i < NR_FILES; i++)
EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
@@ -400,6 +432,7 @@ TEST_F(ve_perms, enforce_denies)
O_RDONLY), 0);
/* Granted w only: visible but not readable. */
+ ASSERT_EQ(allow_paths(self->cgv2_fd, path, variant->files), 0);
ASSERT_EQ(set_entries(self->cgv2_fd, path, variant->files, "w"), 0);
for (i = 0; i < NR_FILES; i++)
EXPECT_EQ(ve_open_rel(self->cgv2_fd, self->ctid_a,
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* [Devel] [PATCH vz10 2/2] selftests: ve: introduce vtty pair teardown stress test
2026-09-14 9:16 3% [Devel] [PATCH vz10 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
@ 2026-09-14 9:16 5% ` Eva Kurchatova
2026-09-15 14:47 0% ` Vasileios Almpanis
0 siblings, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-09-14 9:16 UTC (permalink / raw)
To: khorenko; +Cc: devel
A vtty pair is created by the first master open and torn down by the
last close. The close racing the peer's close used to let both sides
decide they were the final one and call release_tty() twice, and a
master opening again while a teardown was in flight used to hand out a
reference to a tty_struct about to be freed.
Hammer both ends. The master comes from the vzctl ioctl, which does
not look the VE up. The slave is only reachable from inside the VE, as
TTY_MAJOR minor 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to
vttys index minor - 1, so the test builds a VE with cgroup2, sets
ve.veid to the cgroup it made, and runs the slave side there.
One of the symtoms of a buggy kernel is dmesg warning, not just an
eventual crash, so the test checks that. A crashed node should also
report test failure in CI, but that's less verbose, hence the check.
https://virtuozzo.atlassian.net/browse/VSTOR-144771
Feature: ve selftest
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/ve/Makefile | 1 +
tools/testing/selftests/ve/settings | 1 +
tools/testing/selftests/ve/ve_vtty_stress.c | 244 ++++++++++++++++++++
3 files changed, 246 insertions(+)
create mode 100644 tools/testing/selftests/ve/settings
create mode 100644 tools/testing/selftests/ve/ve_vtty_stress.c
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index ec40cbc7b3a1..6cd5823c4d93 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
TEST_GEN_PROGS += ve_ns_owner_test
TEST_GEN_PROGS += ve_perms_test
+TEST_GEN_PROGS += ve_vtty_stress
include ../lib.mk
diff --git a/tools/testing/selftests/ve/settings b/tools/testing/selftests/ve/settings
new file mode 100644
index 000000000000..e2206265f67c
--- /dev/null
+++ b/tools/testing/selftests/ve/settings
@@ -0,0 +1 @@
+timeout=900
diff --git a/tools/testing/selftests/ve/ve_vtty_stress.c b/tools/testing/selftests/ve/ve_vtty_stress.c
new file mode 100644
index 000000000000..9037f731c58d
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_vtty_stress.c
@@ -0,0 +1,244 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_vtty_stress - close a vtty pair from both ends at once.
+ *
+ * A vtty pair is created by the first master open and torn down by the
+ * last close. A close racing the peer's close used to let both sides
+ * decide they were the final one and call release_tty() twice, and a
+ * master opened again while a teardown was still in flight used to hand
+ * out a reference to a tty_struct that was about to be freed. Either
+ * way the next close walked freed memory.
+ *
+ * The master comes from the vzctl ioctl, which does not look the VE up.
+ * The slave is only reachable from inside the VE, as TTY_MAJOR minor
+ * 1..MAX_NR_VTTY_CONSOLES, which vtty_driver() maps to vttys index
+ * minor - 1. So the test makes a VE with cgroup2, hammers the master
+ * from the host and the slave from a task inside it, and fails if the
+ * kernel logs a complaint about the teardown.
+ *
+ * Surviving is not a pass on its own: the first symptom is a warning,
+ * which stops nothing by itself.
+ *
+ * Usage: ve_vtty_stress [seconds] (default 600, or $VE_VTTY_SECS)
+ */
+#define _GNU_SOURCE
+#include <sched.h>
+#include <stdbool.h>
+#include <sys/ioctl.h>
+#include <linux/major.h>
+#include <sys/sysmacros.h>
+#include <sys/wait.h>
+#include <time.h>
+
+#include "ve_selftest.h"
+#include "../kselftest.h"
+
+/* The ioctl argument; the trailing data[] the uapi struct has is unused. */
+struct vzctl_ve_configure {
+ unsigned int veid, key, val, size;
+};
+
+#define VZCTL_VE_CONFIGURE _IOW('.', 15, struct vzctl_ve_configure)
+#define VE_CONFIGURE_OPEN_TTY 5
+
+#define VTTY_IDX 0 /* the pair both ends work on */
+#define SLAVE_MINOR (VTTY_IDX + 1) /* vtty_driver(): index = minor - 1 */
+#define DEFAULT_SECS 600
+
+static int open_master(int ctl_fd, unsigned int veid)
+{
+ struct vzctl_ve_configure c = {
+ .veid = veid, .key = VE_CONFIGURE_OPEN_TTY, .val = VTTY_IDX,
+ };
+
+ return ioctl(ctl_fd, VZCTL_VE_CONFIGURE, &c);
+}
+
+/*
+ * The slave end, inside the VE. Every error here is one the race is
+ * expected to produce: the pair is gone once the master has closed it.
+ */
+static int hammer_slave(time_t deadline, unsigned long *opened)
+{
+ char node[] = "/tmp/ve_vtty_slave";
+
+ unlink(node);
+ if (mknod(node, S_IFCHR | 0600, makedev(TTY_MAJOR, SLAVE_MINOR)) < 0)
+ return -1;
+
+ while (time(NULL) < deadline) {
+ int fd = open(node, O_RDWR | O_NOCTTY | O_NONBLOCK);
+
+ if (fd >= 0) {
+ (*opened)++;
+ close(fd);
+ }
+ }
+ unlink(node);
+ return 0;
+}
+
+/* The kernel log, since a warning fails nothing by itself. */
+static int kmsg_open(void)
+{
+ int fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+
+ if (fd >= 0)
+ lseek(fd, 0, SEEK_END);
+ return fd;
+}
+
+static int kmsg_check(int fd)
+{
+ static const char * const ours[] = {
+ "tty_release", "release_tty", "check_tty_count",
+ "vtty_open_master", "vtty_shutdown",
+ };
+ char buf[8192];
+ int found = 0;
+ ssize_t r;
+ size_t i;
+
+ while (fd >= 0 && (r = read(fd, buf, sizeof(buf) - 1)) > 0) {
+ buf[r] = '\0';
+ if (!strstr(buf, "WARNING") && !strstr(buf, "BUG") &&
+ !strstr(buf, "Oops") && !strstr(buf, "general protection"))
+ continue;
+ for (i = 0; i < ARRAY_SIZE(ours); i++)
+ if (strstr(buf, ours[i])) {
+ ksft_print_msg("kernel log: %s", buf);
+ found++;
+ break;
+ }
+ }
+ return found;
+}
+
+int main(int argc, char **argv)
+{
+ unsigned long master_opened = 0, slave_opened = 0;
+ int cg, ctid, ctl, kmsg, ready[2], count[2], status;
+ const char *env = getenv("VE_VTTY_SECS");
+ char path[PATH_MAX], val[16], c;
+ time_t deadline;
+ long secs;
+ pid_t pid;
+
+ ksft_print_header();
+ secs = argc > 1 ? atol(argv[1]) : (env ? atol(env) : DEFAULT_SECS);
+ if (secs <= 0)
+ secs = DEFAULT_SECS;
+
+ ctl = open("/dev/vzctl", O_RDWR);
+ if (ctl < 0)
+ ksft_exit_skip("cannot open /dev/vzctl: %s, the vz modules "
+ "(vzdev, vzmon) are needed\n", strerror(errno));
+
+ cg = mount_cg2_fd();
+ if (cg < 0)
+ ksft_exit_skip("cannot mount cgroup2: %s\n", strerror(errno));
+ write_file_at(cg, "cgroup.subtree_control", VE_CONTROLLERS);
+
+ ctid = make_ve(cg, CTID_MIN);
+ if (ctid < 0)
+ ksft_exit_skip("cannot make a VE cgroup\n");
+
+ /*
+ * The pair is filed under the veid of the VE, so the two ends only
+ * meet if the cgroup carries the one the master is opened for.
+ */
+ snprintf(path, sizeof(path), "%d/ve.veid", ctid);
+ snprintf(val, sizeof(val), "%d", ctid);
+ if (write_file_at(cg, path, val) < 0) {
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("cannot set ve.veid: %s\n", strerror(errno));
+ }
+
+ /* Do not report a failure where the vtty pair cannot be had at all. */
+ status = open_master(ctl, ctid);
+ if (status < 0 && errno != EBUSY) {
+ destroy_ve(cg, ctid);
+ ksft_exit_skip("no vtty master: %s\n", strerror(errno));
+ }
+ if (status >= 0)
+ close(status);
+
+ ksft_set_plan(1);
+ kmsg = kmsg_open();
+ if (kmsg < 0)
+ ksft_print_msg("cannot read /dev/kmsg: %s, only a crash will "
+ "be noticed\n", strerror(errno));
+
+ if (pipe(ready) || pipe(count))
+ ksft_exit_fail_msg("pipe: %s\n", strerror(errno));
+
+ deadline = time(NULL) + secs;
+ ksft_print_msg("VE %d, pair %d, %ld seconds\n", ctid, VTTY_IDX, secs);
+
+ pid = fork();
+ if (pid == 0) {
+ close(ready[0]);
+ close(count[0]);
+ if (enter_cgroup(cg, ctid) < 0)
+ _exit(1);
+ /*
+ * CLONE_NEWVE puts the task in the VE the cgroup names, so
+ * the slave lookup finds the pair. The pid namespace is
+ * what a VE wants of its first task.
+ */
+ if (unshare(CLONE_NEWVE | CLONE_NEWPID | CLONE_NEWNS) < 0)
+ _exit(1);
+ if (fork() == 0) {
+ unsigned long n = 0;
+
+ c = 'r';
+ if (write(ready[1], &c, 1) != 1)
+ _exit(1);
+ if (hammer_slave(deadline, &n) < 0)
+ _exit(1);
+ if (write(count[1], &n, sizeof(n)) != sizeof(n))
+ _exit(1);
+ _exit(0);
+ }
+ wait(&status);
+ _exit(WIFEXITED(status) ? WEXITSTATUS(status) : 1);
+ }
+ close(ready[1]);
+ close(count[1]);
+
+ if (read(ready[0], &c, 1) != 1) {
+ waitpid(pid, &status, 0);
+ destroy_ve(cg, ctid);
+ ksft_exit_fail_msg("the VE side never came up\n");
+ }
+
+ while (time(NULL) < deadline) {
+ int fd = open_master(ctl, ctid);
+
+ if (fd >= 0) {
+ master_opened++;
+ close(fd);
+ }
+ }
+
+ if (read(count[0], &slave_opened, sizeof(slave_opened)) !=
+ sizeof(slave_opened))
+ slave_opened = 0;
+ waitpid(pid, &status, 0);
+
+ ksft_print_msg("%lu master opens, %lu slave opens\n",
+ master_opened, slave_opened);
+
+ status = kmsg_check(kmsg);
+ destroy_ve(cg, ctid);
+
+ if (!master_opened)
+ ksft_exit_fail_msg("no master was ever opened\n");
+ if (!slave_opened)
+ ksft_exit_fail_msg("no slave was ever opened\n");
+ if (status)
+ ksft_exit_fail_msg("kernel complained %d time(s)\n", status);
+
+ ksft_test_result_pass("vtty pair closed from both ends\n");
+ ksft_finished();
+}
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 5%]
* [Devel] [PATCH vz10] bridge: stp: Fix a potential use-after-free when deleting a bridge
@ 2026-09-11 17:37 4% Konstantin Khorenko
0 siblings, 0 replies; 200+ results
From: Konstantin Khorenko @ 2026-09-11 17:37 UTC (permalink / raw)
To: Vasileios Almpanis, OpenVZ devel list; +Cc: Denis V. Lunev
From: Ido Schimmel <idosch@nvidia.com>
The three STP timers are not supposed to be armed while the bridge is
administratively down. They are synchronously deactivated when the
bridge is put administratively down and the various call sites check for
'IFF_UP' before arming them.
This check is missing from br_topology_change_detection() and it is
possible to engineer a situation in which the topology change timer is
armed while the bridge is administratively down, resulting in a
use-after-free [1] when the bridge is deleted.
Fix by adding the missing check and for good measures synchronously
shutdown the three timers when the bridge is deleted.
[1]
ODEBUG: free active (active state 0) object: ffff88811662b9b0 object type: timer_list hint: br_topology_change_timer_expired (net/bridge/br_stp_timer.c:120)
WARNING: lib/debugobjects.c:629 at debug_print_object+0x1bc/0x450, CPU#9: ip/359
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Reported-by: Noam Rathaus <noamr@ssd-disclosure.com>
Reported-by: Neil Young <contact@ssd-disclosure.com>
Acked-by: Nikolay Aleksandrov <nikolay@nvidia.com>
Signed-off-by: Ido Schimmel <idosch@nvidia.com>
Reviewed-by: Breno Leitao <leitao@debian.org>
Link: https://patch.msgid.link/20260629072117.497959-1-idosch@nvidia.com
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
(cherry picked from commit 2a00517db8de4be7df3d483b215c5544fb30a191)
https://virtuozzo.atlassian.net/browse/VSTOR-145092
Feature: fix ms/net
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
net/bridge/br_if.c | 3 +++
net/bridge/br_stp.c | 3 ++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/net/bridge/br_if.c b/net/bridge/br_if.c
index b7bf3e0aced66..0fa4d5362d041 100644
--- a/net/bridge/br_if.c
+++ b/net/bridge/br_if.c
@@ -391,6 +391,9 @@ void br_dev_delete(struct net_device *dev, struct list_head *head)
br_fdb_delete_by_port(br, NULL, 0, 1);
+ timer_shutdown_sync(&br->hello_timer);
+ timer_shutdown_sync(&br->topology_change_timer);
+ timer_shutdown_sync(&br->tcn_timer);
cancel_delayed_work_sync(&br->gc_work);
br_sysfs_delbr(br->dev);
diff --git a/net/bridge/br_stp.c b/net/bridge/br_stp.c
index 024210f954683..76a2df165ffc8 100644
--- a/net/bridge/br_stp.c
+++ b/net/bridge/br_stp.c
@@ -371,7 +371,8 @@ void br_topology_change_detection(struct net_bridge *br)
{
int isroot = br_is_root_bridge(br);
- if (br->stp_enabled != BR_KERNEL_STP)
+ if (br->stp_enabled != BR_KERNEL_STP ||
+ !(br->dev->flags & IFF_UP))
return;
br_info(br, "topology change detected, %s\n",
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* Re: [Devel] [PATCH vz7] ms/bridge: stp: Fix a potential use-after-free when deleting a bridge
2026-09-11 16:10 7% [Devel] [PATCH vz7] ms/bridge: stp: Fix a potential use-after-free when deleting a bridge Konstantin Khorenko
@ 2026-09-11 17:17 0% ` Vladimir Riabchun
0 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-11 17:17 UTC (permalink / raw)
To: Konstantin Khorenko, Vasileios Almpanis, OpenVZ devel list; +Cc: Denis V. Lunev
On 9/11/26 18:10, Konstantin Khorenko wrote:
> From: Ido Schimmel <idosch@nvidia.com>
>
> The three STP timers are not supposed to be armed while the bridge is
> administratively down. They are synchronously deactivated when the
> bridge is put administratively down and the various call sites check for
> 'IFF_UP' before arming them.
>
> This check is missing from br_topology_change_detection() and it is
> possible to engineer a situation in which the topology change timer is
> armed while the bridge is administratively down, resulting in a
> use-after-free [1] when the bridge is deleted.
>
> Fix by adding the missing check and for good measures synchronously
> shutdown the three timers when the bridge is deleted.
>
> [1]
> ODEBUG: free active (active state 0) object: ffff88811662b9b0 object type: timer_list hint: br_topology_change_timer_expired (net/bridge/br_stp_timer.c:120)
> WARNING: lib/debugobjects.c:629 at debug_print_object+0x1bc/0x450, CPU#9: ip/359
>
> Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
> Reported-by: Noam Rathaus <noamr@ssd-disclosure.com>
> Reported-by: Neil Young <contact@ssd-disclosure.com>
> Acked-by: Nikolay Aleksandrov <nikolay@nvidia.com>
> Signed-off-by: Ido Schimmel <idosch@nvidia.com>
> Reviewed-by: Breno Leitao <leitao@debian.org>
> Link: https://patch.msgid.link/20260629072117.497959-1-idosch@nvidia.com
> Signed-off-by: Paolo Abeni <pabeni@redhat.com>
>
> (cherry picked from commit 2a00517db8de4be7df3d483b215c5544fb30a191)
> Changes:
> - s/timer_shutdown_sync/del_timer_sync/
>
> timer_shutdown_sync() came with ms commit f571faf6e443 ("timers: Provide
> timer_shutdown[_sync]()") in v6.2, so it does not exist here. There is no
> stable backport to copy from either: 5.15.y and 6.1.y took this patch
> unchanged only because that timer API had been backported into them first,
> and 5.10.y got neither.
>
> del_timer_sync() is enough at this call site. Both functions dequeue the
> timer and wait for a handler already running on another CPU, so on return
> the timer is neither queued nor running and the following free_netdev() no
> longer frees a live timer_list. The one thing timer_shutdown_sync() adds
> is clearing timer->function, which turns a later re-arm into a no-op, and
> nothing can re-arm these timers by the time br_dev_delete() reaches them:
>
> - del_nbp() has already unregistered the rx handler of every port, so
> no BPDU reaches the bridge;
> - the remaining arming sites, br_become_root_bridge(),
> br_received_config_bpdu(), br_topology_change_detection(),
> br_stp_enable_bridge() and br_stp_start(), run either from that rx
> path or from ndo_open/ioctl under RTNL, which br_dev_delete() holds;
> - the handlers themselves are stopped by del_timer_sync().
>
> br_stp_disable_bridge() already uses del_timer_sync() on the same three
> timers, on a path where the bridge survives and they can legitimately be
> armed again.
>
> https://virtuozzo.atlassian.net/browse/VSTOR-145092
>
> Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
Reviewed-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
> ---
> net/bridge/br_if.c | 3 +++
> net/bridge/br_stp.c | 3 ++-
> 2 files changed, 5 insertions(+), 1 deletion(-)
>
> diff --git a/net/bridge/br_if.c b/net/bridge/br_if.c
> index 494293c999c20..8c9d2f0dd055e 100644
> --- a/net/bridge/br_if.c
> +++ b/net/bridge/br_if.c
> @@ -311,6 +311,9 @@ void br_dev_delete(struct net_device *dev, struct list_head *head)
>
> br_fdb_delete_by_port(br, NULL, 0, 1);
>
> + del_timer_sync(&br->hello_timer);
> + del_timer_sync(&br->topology_change_timer);
> + del_timer_sync(&br->tcn_timer);
> cancel_delayed_work_sync(&br->gc_work);
>
> br_sysfs_delbr(br->dev);
> diff --git a/net/bridge/br_stp.c b/net/bridge/br_stp.c
> index 8f56c2d1f1a70..65b10d3c968b6 100644
> --- a/net/bridge/br_stp.c
> +++ b/net/bridge/br_stp.c
> @@ -337,7 +337,8 @@ void br_topology_change_detection(struct net_bridge *br)
> {
> int isroot = br_is_root_bridge(br);
>
> - if (br->stp_enabled != BR_KERNEL_STP)
> + if (br->stp_enabled != BR_KERNEL_STP ||
> + !(br->dev->flags & IFF_UP))
> return;
>
> br_info(br, "topology change detected, %s\n",
--
Best regards, Riabchun Vladimir
Linux Kernel Developer, Virtuozzo
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 0%]
* [Devel] [PATCH vz7] ms/bridge: stp: Fix a potential use-after-free when deleting a bridge
@ 2026-09-11 16:10 7% Konstantin Khorenko
2026-09-11 17:17 0% ` Vladimir Riabchun
0 siblings, 1 reply; 200+ results
From: Konstantin Khorenko @ 2026-09-11 16:10 UTC (permalink / raw)
To: Vasileios Almpanis, OpenVZ devel list; +Cc: Denis V. Lunev
From: Ido Schimmel <idosch@nvidia.com>
The three STP timers are not supposed to be armed while the bridge is
administratively down. They are synchronously deactivated when the
bridge is put administratively down and the various call sites check for
'IFF_UP' before arming them.
This check is missing from br_topology_change_detection() and it is
possible to engineer a situation in which the topology change timer is
armed while the bridge is administratively down, resulting in a
use-after-free [1] when the bridge is deleted.
Fix by adding the missing check and for good measures synchronously
shutdown the three timers when the bridge is deleted.
[1]
ODEBUG: free active (active state 0) object: ffff88811662b9b0 object type: timer_list hint: br_topology_change_timer_expired (net/bridge/br_stp_timer.c:120)
WARNING: lib/debugobjects.c:629 at debug_print_object+0x1bc/0x450, CPU#9: ip/359
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Reported-by: Noam Rathaus <noamr@ssd-disclosure.com>
Reported-by: Neil Young <contact@ssd-disclosure.com>
Acked-by: Nikolay Aleksandrov <nikolay@nvidia.com>
Signed-off-by: Ido Schimmel <idosch@nvidia.com>
Reviewed-by: Breno Leitao <leitao@debian.org>
Link: https://patch.msgid.link/20260629072117.497959-1-idosch@nvidia.com
Signed-off-by: Paolo Abeni <pabeni@redhat.com>
(cherry picked from commit 2a00517db8de4be7df3d483b215c5544fb30a191)
Changes:
- s/timer_shutdown_sync/del_timer_sync/
timer_shutdown_sync() came with ms commit f571faf6e443 ("timers: Provide
timer_shutdown[_sync]()") in v6.2, so it does not exist here. There is no
stable backport to copy from either: 5.15.y and 6.1.y took this patch
unchanged only because that timer API had been backported into them first,
and 5.10.y got neither.
del_timer_sync() is enough at this call site. Both functions dequeue the
timer and wait for a handler already running on another CPU, so on return
the timer is neither queued nor running and the following free_netdev() no
longer frees a live timer_list. The one thing timer_shutdown_sync() adds
is clearing timer->function, which turns a later re-arm into a no-op, and
nothing can re-arm these timers by the time br_dev_delete() reaches them:
- del_nbp() has already unregistered the rx handler of every port, so
no BPDU reaches the bridge;
- the remaining arming sites, br_become_root_bridge(),
br_received_config_bpdu(), br_topology_change_detection(),
br_stp_enable_bridge() and br_stp_start(), run either from that rx
path or from ndo_open/ioctl under RTNL, which br_dev_delete() holds;
- the handlers themselves are stopped by del_timer_sync().
br_stp_disable_bridge() already uses del_timer_sync() on the same three
timers, on a path where the bridge survives and they can legitimately be
armed again.
https://virtuozzo.atlassian.net/browse/VSTOR-145092
Signed-off-by: Konstantin Khorenko <khorenko@virtuozzo.com>
---
net/bridge/br_if.c | 3 +++
net/bridge/br_stp.c | 3 ++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/net/bridge/br_if.c b/net/bridge/br_if.c
index 494293c999c20..8c9d2f0dd055e 100644
--- a/net/bridge/br_if.c
+++ b/net/bridge/br_if.c
@@ -311,6 +311,9 @@ void br_dev_delete(struct net_device *dev, struct list_head *head)
br_fdb_delete_by_port(br, NULL, 0, 1);
+ del_timer_sync(&br->hello_timer);
+ del_timer_sync(&br->topology_change_timer);
+ del_timer_sync(&br->tcn_timer);
cancel_delayed_work_sync(&br->gc_work);
br_sysfs_delbr(br->dev);
diff --git a/net/bridge/br_stp.c b/net/bridge/br_stp.c
index 8f56c2d1f1a70..65b10d3c968b6 100644
--- a/net/bridge/br_stp.c
+++ b/net/bridge/br_stp.c
@@ -337,7 +337,8 @@ void br_topology_change_detection(struct net_bridge *br)
{
int isroot = br_is_root_bridge(br);
- if (br->stp_enabled != BR_KERNEL_STP)
+ if (br->stp_enabled != BR_KERNEL_STP ||
+ !(br->dev->flags & IFF_UP))
return;
br_info(br, "topology change detected, %s\n",
--
2.43.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 7%]
* Re: [PATCH hci-8.0 0/2] pci: batch memory transactions around mapping updates #VSTOR-129348
2026-09-10 20:32 3% [PATCH hci-8.0 0/2] pci: batch memory transactions around mapping updates #VSTOR-129348 Denis V. Lunev
2026-09-10 20:32 5% ` [PATCH hci-8.0 1/2] pci: batch BAR remapping into one memory transaction #VSTOR-129348 Denis V. Lunev
2026-09-10 20:32 12% ` [PATCH hci-8.0 2/2] pci: load a device's config inside " Denis V. Lunev
@ 2026-09-11 10:56 0% ` Andrey Drobyshev
2 siblings, 0 replies; 200+ results
From: Andrey Drobyshev @ 2026-09-11 10:56 UTC (permalink / raw)
To: Denis V. Lunev, svt-core
Ack
Applied to hci-8.0
On 9/10/26 11:32 PM, Denis V. Lunev wrote:
> Restoring a PCI device's config space remaps its BARs, updates the bridge
> windows when it is a bridge, and toggles the bus master region. Each of
> those ends a memory transaction of its own, and a transaction commit
> re-renders the flatviews it touches, at a cost which grows with the number
> of memory regions in the machine.
>
> So a machine with a rich PCIe topology pays that render many times over
> while an incoming migration stream is read, once or more per device, even
> though nothing between the steps looks at an address space.
>
> Both patches wrap an existing sequence in a single transaction. There is
> no new API and no behaviour a guest can observe: each transaction opens and
> closes inside the function that starts it, so nothing sees an intermediate
> state which was not already reachable. pci_bridge_update_mappings() has
> worked this way for the bridge windows for a long time; the first patch
> just makes pci_update_mappings() consistent with it.
>
> Measured on a Windows Server 2022 guest with 64 vCPUs and 65 pcie-root-ports,
> the non-iterable part of an incoming migration drops by about a third. The
> saving scales with the number of PCI devices, so it is small for a plain
> machine and large for the topologies which hurt today.
>
> Note that pci_update_mappings() is also reached from the runtime config
> write path, not only from migration. Several BAR moves in one config write
> now share a rebuild instead of taking one each, which is the same win in
> miniature and equally invisible to the guest.
>
> CC: Michael S. Tsirkin <mst@redhat.com>
> Signed-off-by: Denis V. Lunev <den@virtuozzo.com>
>
> Denis V. Lunev (2):
> pci: batch BAR remapping into one memory transaction #VSTOR-129348
> pci: load a device's config inside one memory transaction
> #VSTOR-129348
>
> hw/pci/pci.c | 8 ++++++++
> 1 file changed, 8 insertions(+)
>
^ permalink raw reply [relevance 0%]
* [PATCH hci-8.0 1/2] pci: batch BAR remapping into one memory transaction #VSTOR-129348
2026-09-10 20:32 3% [PATCH hci-8.0 0/2] pci: batch memory transactions around mapping updates #VSTOR-129348 Denis V. Lunev
@ 2026-09-10 20:32 5% ` Denis V. Lunev
2026-09-10 20:32 12% ` [PATCH hci-8.0 2/2] pci: load a device's config inside " Denis V. Lunev
2026-09-11 10:56 0% ` [PATCH hci-8.0 0/2] pci: batch memory transactions around mapping updates #VSTOR-129348 Andrey Drobyshev
2 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-10 20:32 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
pci_update_mappings() deletes and re-adds a subregion for each BAR
whose address changed, and both of those commit their own transaction,
so one BAR move costs two full flatview rebuilds.
Nothing between the delete and the add touches an address space, so
the walk can run inside a single transaction. pci_bridge_update_mappings()
already does this for the bridge windows.
CC: Michael S. Tsirkin <mst@redhat.com>
Signed-off-by: Denis V. Lunev <den@virtuozzo.com>
Reviewed-by: Michael S. Tsirkin <mst@redhat.com>
---
hw/pci/pci.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/hw/pci/pci.c b/hw/pci/pci.c
index c998ea7f8fc..21eadee2e37 100644
--- a/hw/pci/pci.c
+++ b/hw/pci/pci.c
@@ -1696,6 +1696,8 @@ static void pci_update_mappings(PCIDevice *d)
int i;
pcibus_t new_addr;
+ memory_region_transaction_begin();
+
for(i = 0; i < PCI_NUM_REGIONS; i++) {
r = &d->io_regions[i];
@@ -1732,6 +1734,8 @@ static void pci_update_mappings(PCIDevice *d)
}
pci_update_vga(d);
+
+ memory_region_transaction_commit();
}
int pci_irq_disabled(PCIDevice *d)
--
2.53.0
^ permalink raw reply [relevance 5%]
* [PATCH hci-8.0 2/2] pci: load a device's config inside one memory transaction #VSTOR-129348
2026-09-10 20:32 3% [PATCH hci-8.0 0/2] pci: batch memory transactions around mapping updates #VSTOR-129348 Denis V. Lunev
2026-09-10 20:32 5% ` [PATCH hci-8.0 1/2] pci: batch BAR remapping into one memory transaction #VSTOR-129348 Denis V. Lunev
@ 2026-09-10 20:32 12% ` Denis V. Lunev
2026-09-11 10:56 0% ` [PATCH hci-8.0 0/2] pci: batch memory transactions around mapping updates #VSTOR-129348 Andrey Drobyshev
2 siblings, 0 replies; 200+ results
From: Denis V. Lunev @ 2026-09-10 20:32 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
From: Denis V. Lunev <den@openvz.org>
Restoring config space remaps the BARs, updates the bridge windows when
the device is a bridge, and toggles the bus master region. Each of those
commits a transaction of its own, so one device costs several flatview
rebuilds where one would do, and the cost of a rebuild grows with the
number of regions in the machine.
Nothing between them touches an address space, so the three can share a
transaction. The saving scales with the number of PCI devices.
CC: Michael S. Tsirkin <mst@redhat.com>
Signed-off-by: Denis V. Lunev <den@virtuozzo.com>
Reviewed-by: Michael S. Tsirkin <mst@redhat.com>
---
hw/pci/pci.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/hw/pci/pci.c b/hw/pci/pci.c
index 21eadee2e37..5d7d17f6529 100644
--- a/hw/pci/pci.c
+++ b/hw/pci/pci.c
@@ -811,6 +811,8 @@ static int get_pci_config_device(QEMUFile *f, void *pv, size_t size,
}
memcpy(s->config, config, size);
+ memory_region_transaction_begin();
+
pci_update_mappings(s);
if (IS_PCI_BRIDGE(s)) {
pci_bridge_update_mappings(PCI_BRIDGE(s));
@@ -819,6 +821,8 @@ static int get_pci_config_device(QEMUFile *f, void *pv, size_t size,
pci_set_master(s, pci_get_word(s->config + PCI_COMMAND)
& PCI_COMMAND_MASTER);
+ memory_region_transaction_commit();
+
g_free(config);
return 0;
}
--
2.53.0
^ permalink raw reply [relevance 12%]
* [PATCH hci-8.0 0/2] pci: batch memory transactions around mapping updates #VSTOR-129348
@ 2026-09-10 20:32 3% Denis V. Lunev
2026-09-10 20:32 5% ` [PATCH hci-8.0 1/2] pci: batch BAR remapping into one memory transaction #VSTOR-129348 Denis V. Lunev
` (2 more replies)
0 siblings, 3 replies; 200+ results
From: Denis V. Lunev @ 2026-09-10 20:32 UTC (permalink / raw)
To: svt-core; +Cc: andrey.drobyshev, den
Restoring a PCI device's config space remaps its BARs, updates the bridge
windows when it is a bridge, and toggles the bus master region. Each of
those ends a memory transaction of its own, and a transaction commit
re-renders the flatviews it touches, at a cost which grows with the number
of memory regions in the machine.
So a machine with a rich PCIe topology pays that render many times over
while an incoming migration stream is read, once or more per device, even
though nothing between the steps looks at an address space.
Both patches wrap an existing sequence in a single transaction. There is
no new API and no behaviour a guest can observe: each transaction opens and
closes inside the function that starts it, so nothing sees an intermediate
state which was not already reachable. pci_bridge_update_mappings() has
worked this way for the bridge windows for a long time; the first patch
just makes pci_update_mappings() consistent with it.
Measured on a Windows Server 2022 guest with 64 vCPUs and 65 pcie-root-ports,
the non-iterable part of an incoming migration drops by about a third. The
saving scales with the number of PCI devices, so it is small for a plain
machine and large for the topologies which hurt today.
Note that pci_update_mappings() is also reached from the runtime config
write path, not only from migration. Several BAR moves in one config write
now share a rebuild instead of taking one each, which is the same win in
miniature and equally invisible to the guest.
CC: Michael S. Tsirkin <mst@redhat.com>
Signed-off-by: Denis V. Lunev <den@virtuozzo.com>
Denis V. Lunev (2):
pci: batch BAR remapping into one memory transaction #VSTOR-129348
pci: load a device's config inside one memory transaction
#VSTOR-129348
hw/pci/pci.c | 8 ++++++++
1 file changed, 8 insertions(+)
--
2.53.0
^ permalink raw reply [relevance 3%]
* [Devel] [PATCH vz10 v2] selftests: pci_endpoint: skip when the test device is absent
@ 2026-09-10 3:21 3% Eva Kurchatova
2026-09-23 18:03 0% ` Konstantin Khorenko
0 siblings, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-09-10 3:21 UTC (permalink / raw)
To: khorenko; +Cc: devel
Every fixture opens /dev/pci-endpoint-test.0 and asserts on the result,
so on a machine that has no PCI endpoint test device all 17 cases fail
in fixture setup rather than saying the hardware is not there.
Fix by checking the device once before handing over to the harness.
Doing it that way returns one SKIP line from the kselftest.
https://virtuozzo.atlassian.net/browse/VSTOR-142446
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
.../selftests/pci_endpoint/pci_endpoint_test.c | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c b/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
index da0db0e7c969..a6106b6c52e3 100644
--- a/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
+++ b/tools/testing/selftests/pci_endpoint/pci_endpoint_test.c
@@ -257,4 +257,20 @@ TEST_F(pcie_ep_doorbell, DOORBELL_TEST)
pci_ep_ioctl(PCITEST_DOORBELL, 0);
EXPECT_FALSE(ret) TH_LOG("Test failed for Doorbell\n");
}
-TEST_HARNESS_MAIN
+
+static bool test_device_available(void)
+{
+ return access(test_device, F_OK) == 0;
+}
+
+int main(int argc, char **argv)
+{
+ /*
+ * Only the plain invocation, which is the one kselftest uses, turns
+ * into a skip, so that -l and -h keep working without the hardware.
+ */
+ if (argc == 1 && !test_device_available())
+ ksft_exit_skip("no PCI endpoint test device %s\n", test_device);
+
+ return test_harness_run(argc, argv);
+}
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* [Devel] [PATCH vz10 v2 1/2] selftests: pstore: skip when no backend is registered
@ 2026-09-10 3:17 4% Eva Kurchatova
2026-09-23 18:03 0% ` Konstantin Khorenko
2026-09-23 18:37 0% ` Konstantin Khorenko
0 siblings, 2 replies; 200+ results
From: Eva Kurchatova @ 2026-09-10 3:17 UTC (permalink / raw)
To: khorenko; +Cc: devel
An unregistered pstore backend reads back "(null)", and every latter
check fails. This also contradicts the pstore_crash_test documentation.
Fix by skipping an unregistered backend in common_tests
https://virtuozzo.atlassian.net/browse/VSTOR-142447
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
tools/testing/selftests/pstore/common_tests | 15 +++++++++++++--
.../selftests/pstore/pstore_post_reboot_tests | 2 --
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/tools/testing/selftests/pstore/common_tests b/tools/testing/selftests/pstore/common_tests
index 4509f0cc9c91..71cd4473f45a 100755
--- a/tools/testing/selftests/pstore/common_tests
+++ b/tools/testing/selftests/pstore/common_tests
@@ -55,6 +55,8 @@ operate_files() { # tested value, files, operation
# Parameters
TEST_STRING_PATTERN="Testing pstore: uuid="
+# Kselftest framework requirement - SKIP code is 4.
+ksft_skip=4
UUID=`cat /proc/sys/kernel/random/uuid`
TOP_DIR=`absdir $0`
LOG_DIR=$TOP_DIR/logs/`date +%Y%m%d-%H%M%S`_${UUID}/
@@ -74,8 +76,17 @@ prlog "=== Pstore unit tests (`basename $0`) ==="
prlog "UUID="$UUID
prlog -n "Checking pstore backend is registered ... "
-backend=`cat /sys/module/pstore/parameters/backend`
-show_result $?
+# A tristate pstore leaves no parameter file at all when it is not built
+# or not loaded, and an unregistered backend reads back as "(null)".
+backend=`cat /sys/module/pstore/parameters/backend 2>/dev/null`
+if [ -z "$backend" ] || [ "$backend" = "(null)" ]; then
+ prlog "SKIP"
+ prlog -e "\tbackend=${backend:-<none>}"
+ prlog -e "\tcmdline=`cat /proc/cmdline`"
+ prlog "pstore backend is not registered, skipping"
+ exit $ksft_skip
+fi
+show_result 0
prlog -e "\tbackend=${backend}"
prlog -e "\tcmdline=`cat /proc/cmdline`"
if [ $rc -ne 0 ]; then
diff --git a/tools/testing/selftests/pstore/pstore_post_reboot_tests b/tools/testing/selftests/pstore/pstore_post_reboot_tests
index d6da5e86efbf..a5464407f5b2 100755
--- a/tools/testing/selftests/pstore/pstore_post_reboot_tests
+++ b/tools/testing/selftests/pstore/pstore_post_reboot_tests
@@ -7,8 +7,6 @@
# Written by Hiraku Toyooka <hiraku.toyooka.gu@hitachi.com>
#
-# Kselftest framework requirement - SKIP code is 4.
-ksft_skip=4
. ./common_tests
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* [Devel] [PATCH vz10 v2] selftests: binderfs: skip the stress test without binderfs
@ 2026-09-10 3:10 4% Eva Kurchatova
2026-09-23 18:03 0% ` Konstantin Khorenko
0 siblings, 1 reply; 200+ results
From: Eva Kurchatova @ 2026-09-10 3:10 UTC (permalink / raw)
To: khorenko; +Cc: devel
binderfs_stress mounts binderfs and asserts on the first mount, so on
a kernel without CONFIG_ANDROID_BINDERFS the case fails rather than
reporting that the filesystem is not there.
Ask /proc/filesystems whether the kernel has it, the way
supports_filesystem() in the landlock tests and check_resctrlfs_support()
in the resctrl ones already do, and skip when it does not.
Probing with a mount instead would not work here: the test is meant to
run unprivileged, and path_mount() rejects an unprivileged caller in
may_mount() before do_new_mount() ever looks the filesystem type up, so
the probe would see EPERM whether binderfs is there or not. The child
would then still reach the real mount inside its userns and fail.
https://virtuozzo.atlassian.net/browse/VSTOR-142449
Feature: fix selftests
Signed-off-by: Eva Kurchatova <eva.kurchatova@virtuozzo.com>
---
.../filesystems/binderfs/binderfs_test.c | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/tools/testing/selftests/filesystems/binderfs/binderfs_test.c b/tools/testing/selftests/filesystems/binderfs/binderfs_test.c
index 319567f0fae1..fd1f9b6e5d46 100644
--- a/tools/testing/selftests/filesystems/binderfs/binderfs_test.c
+++ b/tools/testing/selftests/filesystems/binderfs/binderfs_test.c
@@ -36,6 +36,28 @@
fd = -EBADF; \
}
+static bool binderfs_supported(void)
+{
+ char line[128];
+ bool ret = false;
+ FILE *f;
+
+ f = fopen("/proc/filesystems", "r");
+ if (!f)
+ return true; /* Cannot tell, let the test run and report. */
+
+ while (fgets(line, sizeof(line), f)) {
+ /* binderfs has no backing device, hence the "nodev" prefix. */
+ if (!strcmp(line, "nodev\tbinder\n")) {
+ ret = true;
+ break;
+ }
+ }
+
+ fclose(f);
+ return ret;
+}
+
static void change_mountns(struct __test_metadata *_metadata)
{
int ret;
@@ -388,6 +410,9 @@ TEST(binderfs_stress)
char binderfs_mntpt[] = P_tmpdir "/binderfs_XXXXXX",
device_path[sizeof(P_tmpdir "/binderfs_XXXXXX/") + BINDERFS_MAX_NAME];
+ if (!binderfs_supported())
+ SKIP(return, "The Android binderfs filesystem is not available");
+
ret = socketpair(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, syncfds);
ASSERT_EQ(ret, 0) {
TH_LOG("%s - Failed to create socket pair", strerror(errno));
--
2.55.0
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 4%]
* [Devel] [PATCH VZ10 v8 2/7] ve/fs: Rework per-ve mount count
@ 2026-09-09 21:35 3% ` Vladimir Riabchun
2026-09-09 21:35 5% ` [Devel] [PATCH VZ10 v8 7/7] selftests/ve: Add mount accounting selftest Vladimir Riabchun
2 siblings, 0 replies; 200+ results
From: Vladimir Riabchun @ 2026-09-09 21:35 UTC (permalink / raw)
To: devel
Previous approach with current mounts counter had an issue:
there was a gap between ve_mount_allowed check and ve_mount_nr_inc,
which could allow CT to have more mounts than expected.
Fix this by tracking the number of available mounts instead
of current ones. This also makes resources accounting
more consistent - we are using ***_avail_nr approach more.
One more issue with inconsistent ve value is fixed:
ve_mount_allowed always used ve from get_exec_env, but
ve_mount_nr_inc operated with owner_ve.
Now actual ve value is calculated in the beginning of alloc_vfsmnt.
To avoid incorrect accounting when is_pseudosuper is changed,
update avail_nr count without > 0 check if VE is ve0 or pseudosuper.
This also simplifies ve_mount_put, since increment is
now unconditional.
https://virtuozzo.atlassian.net/browse/VSTOR-135520
Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
fs/namespace.c | 78 ++++++++++++++++++++++++++++------------------
include/linux/ve.h | 2 +-
kernel/ve/ve.c | 12 +++----
3 files changed, 54 insertions(+), 38 deletions(-)
diff --git a/fs/namespace.c b/fs/namespace.c
index 4adc1db84b4c..1d6e66cd7174 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -317,19 +317,22 @@ int mnt_get_count(struct mount *mnt)
#endif
}
-static inline int ve_mount_allowed(void);
-static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve);
-static inline void ve_mount_nr_dec(struct mount *mnt);
+static inline int ve_try_reserve_mount(struct ve_struct *ve);
+static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve);
static inline void ve_mount_reown(struct mount *mnt, struct ve_struct *ve);
static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
{
struct mount *mnt;
+ struct ve_struct *ve = owner_ve;
- if (!ve_mount_allowed()) {
+ if (!ve)
+ ve = get_exec_env();
+
+ if (!ve_try_reserve_mount(ve)) {
pr_warn_ratelimited(
"CT#%s reached the limit on mounts.\n",
- ve_name(get_exec_env()));
+ ve_name(ve));
return NULL;
}
@@ -337,6 +340,14 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
if (mnt) {
int err;
+#ifdef CONFIG_VE
+ /*
+ * Got ve reference in ve_try_reserve_mount, set mnt ve data
+ * here, so in case of error ve_mount_put sees correct info.
+ */
+ mnt->ve_owner = ve;
+#endif
+
err = mnt_alloc_id(mnt);
if (err)
goto out_free_cache;
@@ -371,7 +382,8 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
INIT_LIST_HEAD(&mnt->mnt_umounting);
INIT_HLIST_HEAD(&mnt->mnt_stuck_children);
mnt->mnt.mnt_idmap = &nop_mnt_idmap;
- ve_mount_nr_inc(mnt, owner_ve);
+ } else {
+ ve_mount_put(mnt, ve);
}
return mnt;
@@ -382,6 +394,8 @@ static struct mount *alloc_vfsmnt(const char *name, struct ve_struct *owner_ve)
out_free_id:
mnt_free_id(mnt);
out_free_cache:
+ /* Got ve reference in ve_try_reserve_mount */
+ ve_mount_put(mnt, ve);
kmem_cache_free(mnt_cache, mnt);
return NULL;
}
@@ -751,7 +765,9 @@ int sb_prepare_remount_readonly(struct super_block *sb)
static void free_vfsmnt(struct mount *mnt)
{
mnt_idmap_put(mnt_idmap(&mnt->mnt));
- ve_mount_nr_dec(mnt);
+#ifdef CONFIG_VE
+ ve_mount_put(mnt, mnt->ve_owner);
+#endif
kfree_const(mnt->mnt_devname);
#ifdef CONFIG_SMP
free_percpu(mnt->mnt_pcp);
@@ -3214,7 +3230,7 @@ int ve_devmnt_process(struct ve_struct *ve, dev_t dev, void **data_pp, int remou
if (devmnt->dev == dev) {
err = ve_devmnt_check(data, devmnt->allowed_options);
/*
- * In case of @is_pseudouser set, ie restore procedure,
+ * In case of @is_pseudosuper set, ie restore procedure,
* we don't check for allowed options filtering, since
* restore mode is special.
*/
@@ -3353,30 +3369,30 @@ int ve_devmnt_verify(struct ve_struct *ve, dev_t dev, char *opts, bool new_mount
return err;
}
-static inline int ve_mount_allowed(void)
-{
- struct ve_struct *ve = get_exec_env();
-
- return ve_is_super(ve) || ve->is_pseudosuper ||
- atomic_read(&ve->mnt_nr) < (int)sysctl_ve_mount_nr;
-}
-
-static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve)
+static inline int ve_try_reserve_mount(struct ve_struct *ve)
{
- if (!ve)
- ve = get_exec_env();
+ int ret = ve_is_super(ve) || ve->is_pseudosuper;
+ /* Ignore limits in ve0 and pseudosuper cases, but still count. */
+ if (ret)
+ atomic_dec(&ve->mnt_avail_nr);
+ else
+ ret = atomic_dec_if_positive(&ve->mnt_avail_nr) >= 0;
- mnt->ve_owner = get_ve(ve);
- atomic_inc(&ve->mnt_nr);
+ if (ret)
+ get_ve(ve);
+ return ret;
}
-static inline void ve_mount_nr_dec(struct mount *mnt)
+static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve)
{
- struct ve_struct *ve = mnt->ve_owner;
-
- atomic_dec(&ve->mnt_nr);
+ /*
+ * ve argument is needed to reuse this function in alloc_vfsmnt error path.
+ * Other users should pass mnt->ve_owner value.
+ */
+ atomic_inc(&ve->mnt_avail_nr);
put_ve(ve);
- mnt->ve_owner = NULL;
+ if (mnt)
+ mnt->ve_owner = NULL;
}
/*
@@ -3397,9 +3413,9 @@ static inline void ve_mount_reown(struct mount *mnt, struct ve_struct *ve)
if (old == ve)
return;
- atomic_inc(&ve->mnt_nr);
+ atomic_dec(&ve->mnt_avail_nr);
WRITE_ONCE(mnt->ve_owner, get_ve(ve));
- atomic_dec(&old->mnt_nr);
+ atomic_inc(&old->mnt_avail_nr);
put_ve(old);
}
@@ -3422,10 +3438,10 @@ bool is_sb_ve_accessible(struct ve_struct *ve, struct super_block *sb)
#else /* CONFIG_VE */
-static inline int ve_mount_allowed(void) { return 1; }
-static inline void ve_mount_nr_inc(struct mount *mnt, struct ve_struct *ve) { }
-static inline void ve_mount_nr_dec(struct mount *mnt) { }
+static inline int ve_try_reserve_mount(struct ve_struct *ve) { return 1; }
+static inline void ve_mount_put(struct mount *mnt, struct ve_struct *ve) { }
static inline void ve_mount_reown(struct mount *mnt, struct ve_struct *ve) { }
+
#endif /* CONFIG_VE */
/*
diff --git a/include/linux/ve.h b/include/linux/ve.h
index f946abe4d859..65c058c0f34d 100644
--- a/include/linux/ve.h
+++ b/include/linux/ve.h
@@ -96,7 +96,7 @@ struct ve_struct {
atomic_t nd_neigh_nr;
unsigned long meminfo_val;
- atomic_t mnt_nr; /* number of present VE mounts */
+ atomic_t mnt_avail_nr; /* number of available VE mounts */
#ifdef CONFIG_COREDUMP
char core_pattern[CORENAME_MAX_SIZE];
diff --git a/kernel/ve/ve.c b/kernel/ve/ve.c
index 2d2934e1ec10..58a0c4b8e36e 100644
--- a/kernel/ve/ve.c
+++ b/kernel/ve/ve.c
@@ -82,7 +82,7 @@ struct ve_struct ve0 = {
.arp_neigh_nr = ATOMIC_INIT(0),
.nd_neigh_nr = ATOMIC_INIT(0),
- .mnt_nr = ATOMIC_INIT(0),
+ .mnt_avail_nr = ATOMIC_INIT(INT_MAX),
.meminfo_val = VE_MEMINFO_SYSTEM,
#ifdef CONFIG_X86
.umh_running_helpers = ATOMIC_INIT(0),
@@ -787,7 +787,7 @@ static struct cgroup_subsys_state *ve_create(struct cgroup_subsys_state *parent_
atomic_set(&ve->arp_neigh_nr, 0);
atomic_set(&ve->nd_neigh_nr, 0);
- atomic_set(&ve->mnt_nr, 0);
+ atomic_set(&ve->mnt_avail_nr, sysctl_ve_mount_nr);
#ifdef CONFIG_COREDUMP
strcpy(ve->core_pattern, "core");
@@ -1063,9 +1063,9 @@ static u64 ve_netns_avail_nr_read(struct cgroup_subsys_state *css, struct cftype
return atomic_read(&css_to_ve(css)->netns_avail_nr);
}
-static u64 ve_mnt_nr_read(struct cgroup_subsys_state *css, struct cftype *cft)
+static s64 ve_mnt_avail_nr_read(struct cgroup_subsys_state *css, struct cftype *cft)
{
- return atomic_read(&css_to_ve(css)->mnt_nr);
+ return atomic_read(&css_to_ve(css)->mnt_avail_nr);
}
static u64 ve_netif_max_nr_read(struct cgroup_subsys_state *css, struct cftype *cft)
@@ -1627,8 +1627,8 @@ static struct cftype ve_cftypes[] = {
.read_u64 = ve_netns_avail_nr_read,
},
{
- .name = "mnt_nr",
- .read_u64 = ve_mnt_nr_read,
+ .name = "mnt_avail_nr",
+ .read_s64 = ve_mnt_avail_nr_read,
},
{
.name = "netif_max_nr",
--
2.47.1
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 3%]
* [Devel] [PATCH VZ10 v8 7/7] selftests/ve: Add mount accounting selftest
2026-09-09 21:35 3% ` [Devel] [PATCH VZ10 v8 2/7] ve/fs: Rework per-ve mount count Vladimir Riabchun
@ 2026-09-09 21:35 5% ` Vladimir Riabchun
2026-09-14 9:35 0% ` Vasileios Almpanis
2 siblings, 1 reply; 200+ results
From: Vladimir Riabchun @ 2026-09-09 21:35 UTC (permalink / raw)
To: devel
There are 6 test cases, covered in the new test:
1. Simple mount accouting correctness, just mount/umount.
2. Verification of correct limit hits and changes, including
negative values.
3. Partial mounts test, when mount limit is hit in the middle
of creation.
4. Test that enabled pseudosuper allows overuse.
5. Test that pseudosuper doesn't affect mount accounting.
6. Failcount feature verification.
https://virtuozzo.atlassian.net/browse/VSTOR-135520
Feature: per-ve failcounters
Signed-off-by: Vladimir Riabchun <vladimir.riabchun@virtuozzo.com>
---
tools/testing/selftests/ve/.gitignore | 1 +
tools/testing/selftests/ve/Makefile | 1 +
.../selftests/ve/ve_mount_accounting_test.c | 489 ++++++++++++++++++
3 files changed, 491 insertions(+)
create mode 100644 tools/testing/selftests/ve/ve_mount_accounting_test.c
diff --git a/tools/testing/selftests/ve/.gitignore b/tools/testing/selftests/ve/.gitignore
index afa4c568c2c9..3df4d05888dc 100644
--- a/tools/testing/selftests/ve/.gitignore
+++ b/tools/testing/selftests/ve/.gitignore
@@ -1,2 +1,3 @@
ve_ns_owner_test
ve_perms_test
+ve_mount_accounting_test
diff --git a/tools/testing/selftests/ve/Makefile b/tools/testing/selftests/ve/Makefile
index ec40cbc7b3a1..c6efe7c4b4fb 100644
--- a/tools/testing/selftests/ve/Makefile
+++ b/tools/testing/selftests/ve/Makefile
@@ -4,5 +4,6 @@ CFLAGS += -g -Wall -O2
TEST_GEN_PROGS += ve_ns_owner_test
TEST_GEN_PROGS += ve_perms_test
+TEST_GEN_PROGS += ve_mount_accounting_test
include ../lib.mk
diff --git a/tools/testing/selftests/ve/ve_mount_accounting_test.c b/tools/testing/selftests/ve/ve_mount_accounting_test.c
new file mode 100644
index 000000000000..e82d9bf23839
--- /dev/null
+++ b/tools/testing/selftests/ve/ve_mount_accounting_test.c
@@ -0,0 +1,489 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * ve_mount_accounting selftests
+ *
+ * Tests to check the correctness of mount accounting.
+ */
+#define _GNU_SOURCE
+#include <asm/unistd.h>
+#include <linux/sched.h>
+#include <linux/limits.h>
+#include <sys/wait.h>
+#include <sys/syscall.h>
+#include <sys/stat.h>
+#include <sys/mount.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+#include "../kselftest_harness.h"
+#include "ve_selftest.h"
+
+#define TMP_DIR "/ve-mnt-tmp/"
+#define VE_MOUNTS_MAX 128
+
+/*
+ * These values take a while to calculate, the first test will
+ * do this and store the values for those who come after.
+ */
+int *need_avail;
+int *mnt_cost;
+
+FIXTURE(ve_mnt_acc)
+{
+ int cgv2_fd;
+ int ctid;
+ struct namespace_ctx ns_ctx;
+ int orig_have_mnt;
+};
+
+static int set_pseudosuper(FIXTURE_DATA(ve_mnt_acc) * self, int value)
+{
+ char path[64];
+
+ snprintf(path, sizeof(path), "%d/ve.pseudosuper", self->ctid);
+ return write_u64_at(self->cgv2_fd, path, value);
+}
+
+static int _create_mount(void *id_ptr)
+{
+ char path[PATH_MAX];
+ int id = *(int *)id_ptr, ret;
+
+ snprintf(path, sizeof(path), TMP_DIR "%d", id);
+
+ if (mkdir(path, 0755) < 0) {
+ fprintf(stderr, "Failed to create directory %s: %s\n", path, strerror(errno));
+ return -1;
+ }
+ ret = mount("tmpfs", path, "tmpfs", 0, "size=1M");
+ if (!ret)
+ return 0;
+ fprintf(stderr, "Failed to mount tmpfs to %s: %s\n", path, strerror(errno));
+
+ rmdir(path);
+ return -1;
+}
+
+static int create_mount(FIXTURE_DATA(ve_mnt_acc) * self, int id)
+{
+ int ret = run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
+ CLONE_NEWVE | CLONE_NEWNS, _create_mount, &id);
+ /*
+ * If mount fails, cleanup by free_vfsmnt will be called
+ * via call_rcu, need to wait for update.
+ */
+ sleep(1);
+ return ret;
+}
+
+static int _destroy_mount(void *id_ptr)
+{
+ char path[PATH_MAX];
+ struct stat st;
+ int id = *(int *)id_ptr;
+
+ snprintf(path, sizeof(path), TMP_DIR "%d", id);
+
+ if (stat(path, &st))
+ return 1;
+ if (umount(path)) {
+ fprintf(stderr, "failed to umount directory %s: %s\n", path, strerror(errno));
+ return -1;
+ }
+ if (rmdir(path)) {
+ fprintf(stderr, "failed to remove directory %s: %s\n", path, strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+static int destroy_mount(FIXTURE_DATA(ve_mnt_acc) * self, int id)
+{
+ int ret = run_in_ve(self->cgv2_fd, self->ctid, &self->ns_ctx,
+ CLONE_NEWVE | CLONE_NEWNS, _destroy_mount, &id);
+ /* free_vfsmnt is called via call_rcu, need to wait for update */
+ sleep(1);
+ return ret;
+}
+
+#define MAX_MNT_ID 32
+
+static int get_free_mnt_id(void)
+{
+ int i;
+ struct stat st;
+ char path[PATH_MAX];
+
+ for (i = 0; i < MAX_MNT_ID; i++) {
+ snprintf(path, sizeof(path), TMP_DIR "%d", i);
+ if (stat(path, &st))
+ return i;
+ }
+ return -1;
+}
+
+static int get_mount_cost(FIXTURE_DATA(ve_mnt_acc) * self)
+{
+ int avail1, avail2, mnt_id;
+ char path[64];
+
+ mnt_id = get_free_mnt_id();
+
+ snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+ if (mnt_id < 0 ||
+ read_s32_at(self->cgv2_fd, path, &avail1) ||
+ create_mount(self, mnt_id) ||
+ read_s32_at(self->cgv2_fd, path, &avail2) ||
+ destroy_mount(self, mnt_id))
+ return -1;
+
+ return avail1 - avail2;
+}
+
+/*
+ * Current kernel code first creates many mounts with ve_owner != ve0,
+ * so this VE needs more mounts than would be really consumed after commit_tree.
+ */
+static int get_required_avail_mnts(FIXTURE_DATA(ve_mnt_acc) * self)
+{
+ int l = 0, r = VE_MOUNTS_MAX + 1, mid;
+ char path_max_nr[64];
+
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+ while (r - l > 1) {
+ mid = (l + r) / 2;
+ if (write_u64_at(self->cgv2_fd, path_max_nr, mid + self->orig_have_mnt))
+ return -1;
+
+ if (get_mount_cost(self) < 0)
+ l = mid;
+ else
+ r = mid;
+ }
+ if (r == VE_MOUNTS_MAX + 1) {
+ fprintf(stderr, "%s: %d avail mounts is not enough\n", __func__, r);
+ return -1;
+ }
+ if (write_u64_at(self->cgv2_fd, path_max_nr, VE_MOUNTS_MAX))
+ return -1;
+ return r;
+}
+
+/* Expect mount success and return new avail value */
+static int mount_and_get_avail(struct __test_metadata *_metadata,
+ FIXTURE_DATA(ve_mnt_acc) * self, int mnt_id)
+{
+ char path_avail[64];
+ int mnt_avail_nr;
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+ ASSERT_EQ(create_mount(self, mnt_id), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ return mnt_avail_nr;
+}
+
+/* Expect mount failure and ensure intact avail number */
+static int check_mount_fails(struct __test_metadata *_metadata,
+ FIXTURE_DATA(ve_mnt_acc) * self,
+ int mnt_id, int avail_count)
+{
+ char path_avail[64];
+ int mnt_avail_nr;
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+ if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr))
+ return 1;
+ if (mnt_avail_nr != avail_count) {
+ fprintf(stderr, "%s: mnt_avail(%d) != avail_count(%d) before mount\n",
+ __func__, mnt_avail_nr, avail_count);
+ return -1;
+ }
+
+ if (create_mount(self, mnt_id) == 0) {
+ fprintf(stderr, "%s: successful mount\n", __func__);
+ return -1;
+ }
+
+ if (read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr))
+ return 1;
+ if (mnt_avail_nr != avail_count) {
+ fprintf(stderr, "%s: mnt_avail(%d) != avail_count(%d) after mount\n",
+ __func__, mnt_avail_nr, avail_count);
+ return -1;
+ }
+ return 0;
+}
+
+FIXTURE_SETUP(ve_mnt_acc)
+{
+ int initial_mnt_avail_nr;
+ char path[64];
+
+ self->cgv2_fd = mount_cg2_fd();
+ ASSERT_GE(self->cgv2_fd, 0);
+ mkdir(TMP_DIR, 0755);
+
+ ASSERT_EQ(write_file_at(self->cgv2_fd, "cgroup.subtree_control",
+ VE_CONTROLLERS), 0);
+
+ self->ctid = make_ve(self->cgv2_fd, CTID_MIN);
+ ASSERT_GE(self->ctid, 0);
+
+ snprintf(path, sizeof(path), "%d/ve.mnt_max_nr", self->ctid);
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path, VE_MOUNTS_MAX), 0);
+
+ /*
+ * The new ve cgroup has not been entered by anything yet, so its
+ * mnt_avail_nr counter should be VE_MOUNTS_MAX.
+ */
+ snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
+ ASSERT_EQ(initial_mnt_avail_nr, VE_MOUNTS_MAX);
+
+ ASSERT_EQ(prepare_namespaces(&self->ns_ctx, CLONE_NEWNS | CLONE_NEWVE,
+ self->cgv2_fd, self->ctid), 0);
+
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &initial_mnt_avail_nr), 0);
+ self->orig_have_mnt = VE_MOUNTS_MAX - initial_mnt_avail_nr;
+
+ /* Calculate once - it is time-consuming */
+ if (*need_avail == -1)
+ *need_avail = get_required_avail_mnts(self);
+ if (*mnt_cost == -1)
+ *mnt_cost = get_mount_cost(self);
+ ASSERT_GE(*need_avail, 1);
+ ASSERT_GE(*mnt_cost, 1);
+};
+
+FIXTURE_TEARDOWN(ve_mnt_acc)
+{
+ for (int i = 0; i < MAX_MNT_ID; i++)
+ _destroy_mount((void *)&i);
+
+ close_namespace_fds(&self->ns_ctx);
+ destroy_ve(self->cgv2_fd, self->ctid);
+ close(self->cgv2_fd);
+ rmdir(TMP_DIR);
+}
+
+/* Simple test to check mount/umount accounting correctness */
+TEST_F(ve_mnt_acc, mount_umount)
+{
+ int original_mnt_avail, mnt_avail_nr;
+ char path[64];
+
+ snprintf(path, sizeof(path), "%d/ve.mnt_avail_nr", self->ctid);
+
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &original_mnt_avail), 0);
+
+ ASSERT_LT(mount_and_get_avail(_metadata, self, 0),
+ original_mnt_avail);
+
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, original_mnt_avail);
+}
+
+/* Test mount limit hits */
+TEST_F(ve_mnt_acc, hit_limits)
+{
+ int mnt_avail_nr;
+ char path_avail[64], path_max_nr[64];
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+ /* Step 1: reduce number of available mounts to *need_avail */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+ self->orig_have_mnt + 1 * (*need_avail)), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, 1 * *need_avail);
+
+ /* Step 2: do one mount, no mounts should be available */
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), *need_avail - *mnt_cost);
+
+ /* Step 3: check that one more mount fails */
+ ASSERT_EQ(check_mount_fails(_metadata, self, 1, (*need_avail) - 1), 0);
+
+ /* Step 4: increase by 1 and win now */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+ self->orig_have_mnt + *mnt_cost + *need_avail), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, *need_avail);
+
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 1), *need_avail - *mnt_cost);
+
+ /* Step 5: reduce mnt_max_nr so we have more mounts than allowed */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+ self->orig_have_mnt + 1 * (*mnt_cost)), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, -1 * (*mnt_cost));
+
+ /* Step 6: try to do mount when avail < 0, ensure number is intact */
+ ASSERT_EQ(check_mount_fails(_metadata, self, 2, -1 * (*mnt_cost)), 0);
+
+ /* Step 7: remove one mount, check avail value update, mount should fail */
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+ ASSERT_EQ(check_mount_fails(_metadata, self, 2, 0), 0);
+
+ /* Step 8: remove one more mount and check that new mount succeeds */
+ ASSERT_EQ(destroy_mount(self, 1), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, 1 * *mnt_cost);
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + *need_avail), 0);
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 2), *need_avail - *mnt_cost);
+
+ ASSERT_EQ(destroy_mount(self, 2), 0);
+}
+
+/*
+ * Mount propagation makes one mount cost more.
+ * This test checks that if we run out of mounts in the middle of creating
+ * a new one, everything is restored smoothly and nothing leaks.
+ */
+TEST_F(ve_mnt_acc, partial_mounts)
+{
+ char path_avail[64], path_max_nr[64];
+ int i;
+
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+
+ if (*need_avail == 1)
+ SKIP(return, "mount cost is 1, no partial mounts possible");
+
+ for (i = 0; i < *need_avail; i++) {
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + i), 0);
+ ASSERT_EQ(check_mount_fails(_metadata, self, 0, i), 0);
+ }
+}
+
+/* Test that pseudosuper allows negative avail with correct accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_allows_overuse)
+{
+ char path_max_nr[64];
+
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt + *need_avail), 0);
+ ASSERT_EQ(set_pseudosuper(self, 1), 0);
+
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), *need_avail - *mnt_cost);
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 1), *need_avail - 2 * (*mnt_cost));
+
+ /* Go below zero */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, self->orig_have_mnt), 0);
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 2), -3 * (*mnt_cost));
+
+ ASSERT_EQ(set_pseudosuper(self, 0), 0);
+
+ /* Below zero should fail */
+ ASSERT_EQ(check_mount_fails(_metadata, self, 3, -3 * (*mnt_cost)), 0);
+ /* Now need 2 more to add a new mount */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr,
+ self->orig_have_mnt + 3 * (*mnt_cost) + *need_avail - 2 * (*mnt_cost)), 0);
+ ASSERT_EQ(check_mount_fails(_metadata, self, 3, *need_avail - 2 * (*mnt_cost)), 0);
+
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+ ASSERT_EQ(check_mount_fails(_metadata, self, 3, *need_avail - *mnt_cost), 0);
+ ASSERT_EQ(destroy_mount(self, 2), 0);
+ ASSERT_EQ(get_mount_cost(self), *mnt_cost);
+}
+
+/* Test that pseudosuper doesn't disable accounting. */
+TEST_F(ve_mnt_acc, pseudosuper_continues_accounting)
+{
+ int orig_mnt_avail, mnt_avail_nr;
+ char path_avail[64];
+
+ snprintf(path_avail, sizeof(path_avail), "%d/ve.mnt_avail_nr", self->ctid);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &orig_mnt_avail), 0);
+
+ ASSERT_EQ(set_pseudosuper(self, 0), 0);
+
+ /* mnt 0 - mounted without pseudosuper, umounted with it. */
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 0), orig_mnt_avail - *mnt_cost);
+ ASSERT_EQ(set_pseudosuper(self, 1), 0);
+
+ /* Cost is the same when mount/umount happen under pseudosuper. */
+ ASSERT_EQ(get_mount_cost(self), *mnt_cost);
+
+ /* mnt 1 - mounted with pseudosuper, umounted without it. */
+ ASSERT_EQ(mount_and_get_avail(_metadata, self, 1),
+ orig_mnt_avail - 2 * (*mnt_cost));
+
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, orig_mnt_avail - *mnt_cost);
+
+ ASSERT_EQ(set_pseudosuper(self, 0), 0);
+
+ ASSERT_EQ(destroy_mount(self, 1), 0);
+ ASSERT_EQ(read_s32_at(self->cgv2_fd, path_avail, &mnt_avail_nr), 0);
+ ASSERT_EQ(mnt_avail_nr, orig_mnt_avail);
+}
+
+/* Test failcount feature */
+TEST_F(ve_mnt_acc, failcount)
+{
+ char path_fc[64], failcount_str[512], path_max_nr[64];
+
+ snprintf(path_fc, sizeof(path_fc), "%d/ve.failcount", self->ctid);
+ snprintf(path_max_nr, sizeof(path_max_nr), "%d/ve.mnt_max_nr", self->ctid);
+
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
+
+ /* Check successful mount doesn't affect failcount */
+ mount_and_get_avail(_metadata, self, 0);
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
+ ASSERT_EQ(destroy_mount(self, 0), 0);
+
+ /* Check failcount update when mount fails */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, 0), 0);
+ ASSERT_LT(create_mount(self, 1), 0);
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 1\n") != NULL);
+
+ /* Check failcount flush */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_fc, 0), 0);
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 0\n") != NULL);
+
+ /* Check failcount update when mount fails again */
+ ASSERT_EQ(write_u64_at(self->cgv2_fd, path_max_nr, 0), 0);
+ ASSERT_LT(create_mount(self, 1), 0);
+ ASSERT_GE(read_file_at(self->cgv2_fd, path_fc,
+ failcount_str, sizeof(failcount_str)), 0);
+ ASSERT_TRUE(strstr(failcount_str, "mnt: 1\n") != NULL);
+}
+
+int main(int argc, char **argv)
+{
+ int *shared_data = mmap(NULL, sizeof(int) * 2,
+ PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+
+ if (!shared_data) {
+ fprintf(stderr, "Failed to mmap shared region: %s\n", strerror(errno));
+ return 1;
+ }
+ need_avail = &shared_data[0];
+ *need_avail = -1;
+ mnt_cost = &shared_data[1];
+ *mnt_cost = -1;
+
+ return test_harness_run(argc, argv);
+}
--
2.47.1
_______________________________________________
Devel mailing list
Devel@openvz.org
https://lists.openvz.org/mailman/listinfo/devel
^ permalink raw reply [relevance 5%]
Results 201-400 of ~800 next (older) | prev (newer) | reverse | sort options + mbox downloads above
-- links below jump to the message on this page --
2026-08-31 22:48 [Devel] [PATCH vz10 1/7] ms/vxlan: do not reuse cached ip_hdr() value after skb_tunnel_check_pmtu() Eva Kurchatova
2026-08-31 22:48 ` [Devel] [PATCH vz10 3/7] selftests: net: skip what this kernel and iproute2 do not have Eva Kurchatova
2026-09-21 14:51 0% ` Vladimir Riabchun
2026-08-31 22:50 [Devel] [PATCH vz10 1/4] ms/selftests: net: Add python context manager for netns entering Eva Kurchatova
2026-08-31 22:50 ` [Devel] [PATCH vz10 4/4] selftests: drv-net-hw: skip the tests that need a real interface Eva Kurchatova
2026-09-24 13:23 3% ` Konstantin Khorenko
2026-08-31 23:43 [Devel] [PATCH vz10 1/3] selftests: drv-net: let NetDrvEnv take nsim_test Eva Kurchatova
2026-08-31 23:43 ` [Devel] [PATCH vz10 3/3] selftests: drv-net: read the channel count over netlink Eva Kurchatova
2026-09-24 11:53 0% ` Konstantin Khorenko
2026-09-09 21:35 [Devel] [PATCH VZ10 v8 0/7] Add per-VE failcount support Vladimir Riabchun
2026-09-09 21:35 3% ` [Devel] [PATCH VZ10 v8 2/7] ve/fs: Rework per-ve mount count Vladimir Riabchun
2026-09-09 21:35 ` [Devel] [PATCH VZ10 v8 6/7] ve: Introduce per-VE failcount Vladimir Riabchun
2026-09-14 9:35 0% ` Vasileios Almpanis
2026-09-09 21:35 5% ` [Devel] [PATCH VZ10 v8 7/7] selftests/ve: Add mount accounting selftest Vladimir Riabchun
2026-09-14 9:35 0% ` Vasileios Almpanis
2026-09-10 3:10 4% [Devel] [PATCH vz10 v2] selftests: binderfs: skip the stress test without binderfs Eva Kurchatova
2026-09-23 18:03 0% ` Konstantin Khorenko
2026-09-10 3:17 4% [Devel] [PATCH vz10 v2 1/2] selftests: pstore: skip when no backend is registered Eva Kurchatova
2026-09-23 18:03 0% ` Konstantin Khorenko
2026-09-23 18:37 0% ` Konstantin Khorenko
2026-09-10 3:21 3% [Devel] [PATCH vz10 v2] selftests: pci_endpoint: skip when the test device is absent Eva Kurchatova
2026-09-23 18:03 0% ` Konstantin Khorenko
2026-09-10 20:32 3% [PATCH hci-8.0 0/2] pci: batch memory transactions around mapping updates #VSTOR-129348 Denis V. Lunev
2026-09-10 20:32 5% ` [PATCH hci-8.0 1/2] pci: batch BAR remapping into one memory transaction #VSTOR-129348 Denis V. Lunev
2026-09-10 20:32 12% ` [PATCH hci-8.0 2/2] pci: load a device's config inside " Denis V. Lunev
2026-09-11 10:56 0% ` [PATCH hci-8.0 0/2] pci: batch memory transactions around mapping updates #VSTOR-129348 Andrey Drobyshev
2026-09-11 16:10 7% [Devel] [PATCH vz7] ms/bridge: stp: Fix a potential use-after-free when deleting a bridge Konstantin Khorenko
2026-09-11 17:17 0% ` Vladimir Riabchun
2026-09-11 17:37 4% [Devel] [PATCH vz10] bridge: " Konstantin Khorenko
2026-09-14 9:16 3% [Devel] [PATCH vz10 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-09-14 9:16 5% ` [Devel] [PATCH vz10 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-09-15 14:47 0% ` Vasileios Almpanis
2026-09-14 19:13 [PATCH QEMU HCI-8.0 v3 00/12] usb-host: support CPR migration Andrey Drobyshev
2026-09-14 19:13 31% ` [PATCH QEMU HCI-8.0 v3 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-09-14 19:13 13% ` [PATCH QEMU HCI-8.0 v3 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
2026-09-14 19:13 26% ` [PATCH QEMU HCI-8.0 v3 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
2026-09-14 19:13 25% ` [PATCH QEMU HCI-8.0 v3 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
2026-09-14 19:13 24% ` [PATCH QEMU HCI-8.0 v3 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
2026-09-14 19:13 21% ` [PATCH QEMU HCI-8.0 v3 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
2026-09-14 19:13 22% ` [PATCH QEMU HCI-8.0 v3 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-14 19:13 26% ` [PATCH QEMU HCI-8.0 v3 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
2026-09-14 19:13 6% ` [PATCH QEMU HCI-8.0 v3 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-14 19:13 20% ` [PATCH QEMU HCI-8.0 v3 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
2026-09-15 16:01 9% [Devel] [PATCH VZ10] fs/fuse pcs: logging improvement Alexey Kuznetsov
2026-09-15 16:02 7% [Devel] [PATCH VZ10] core: allow to bypass rhel pci blacklist Alexey Kuznetsov
2026-09-15 17:22 [Devel] [PATCH VZ10 v9 0/8] Add per-VE failcount support Vladimir Riabchun
2026-09-15 17:22 3% ` [Devel] [PATCH VZ10 v9 2/8] ve/fs: Rework per-ve mount count Vladimir Riabchun
2026-09-18 13:22 0% ` Vladimir Riabchun
2026-09-15 17:22 9% ` [Devel] [PATCH VZ10 v9 6/8] ve: Introduce per-VE failcount Vladimir Riabchun
2026-09-18 14:42 0% ` Vladimir Riabchun
2026-09-25 16:02 0% ` Konstantin Khorenko
2026-09-25 16:08 4% ` Konstantin Khorenko
2026-09-15 17:22 6% ` [Devel] [PATCH VZ10 v9 8/8] selftests/ve: Add mount accounting selftest Vladimir Riabchun
2026-09-18 15:06 0% ` Vladimir Riabchun
2026-09-25 16:04 0% ` Konstantin Khorenko
2026-09-16 12:31 3% [Devel] [PATCH vz10 v2 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-09-16 12:31 5% ` [Devel] [PATCH vz10 v2 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-09-18 7:11 0% ` Vasileios Almpanis
2026-09-22 17:47 3% ` Konstantin Khorenko
2026-09-22 17:47 6% ` [Devel] [PATCH vz10 v2 1/2] selftests: ve: grant the directories the files sit in Konstantin Khorenko
2026-09-16 13:33 4% [PATCH hci-8.0 00/12] system: add a watchdog for the Big QEMU Lock Denis V. Lunev
2026-09-16 13:33 4% ` [PATCH hci-8.0 02/12] error-report: make real_time_iso8601() public #VSTOR-144507 Denis V. Lunev
2026-09-16 13:33 5% ` [PATCH hci-8.0 05/12] oslib: add qemu_thread_states() #VSTOR-144507 Denis V. Lunev
2026-09-16 13:33 8% ` [PATCH hci-8.0 06/12] oslib: report kernel stacks in " Denis V. Lunev
2026-09-16 13:33 2% ` [PATCH hci-8.0 08/12] oslib: report each thread's program counter #VSTOR-144507 Denis V. Lunev
2026-09-16 13:33 2% ` [PATCH hci-8.0 09/12] system/cpus: report a BQL held past a deadline #VSTOR-144507 Denis V. Lunev
2026-09-16 15:24 4% [PATCH hci-8.0 0/5] qcow2: silent corruption when a dirty image becomes writable Denis V. Lunev
2026-09-16 15:24 3% ` [PATCH hci-8.0 2/5] block: reject a reopen of an unusable node instead of crashing #VSTOR-140073 Denis V. Lunev
2026-09-16 15:24 3% ` [PATCH hci-8.0 5/5] qcow2: repair a dirty image when it becomes writable #VSTOR-140073 Denis V. Lunev
2026-09-16 16:26 5% ` Andrey Drobyshev
2026-09-16 16:28 0% ` Denis V. Lunev
2026-09-16 15:40 [Devel] [PATCH vz10 v2 0/3] Restore ConnectX-3 Pro support dropped by RHEL 10 Konstantin Khorenko
2026-09-16 15:40 7% ` [Devel] [PATCH vz10 v2 1/3] core: add skip_pci_blacklist option to probe RHEL-disabled devices Konstantin Khorenko
2026-09-18 14:44 8% [PATCH hci-8.0 1/1] net/tap-linux.c: avoid abort when setting invalid fd #VSTOR-146012 Denis V. Lunev
2026-09-21 15:44 [PATCH 0/2] UefiCpuPkg/MpInitLib: recover from a lost Start-up IPI #VSTOR-146639 Denis V. Lunev
2026-09-21 15:44 3% ` [PATCH 2/2] UefiCpuPkg/MpInitLib: resend the Start-up IPI when an AP misses it #VSTOR-146639 Denis V. Lunev
2026-09-23 13:30 0% ` Andrey Drobyshev
2026-09-21 20:46 [Devel] [PATCH vz10 v2 0/3] selftests: timers: raw_skew fixes Eva Kurchatova
2026-09-21 20:46 4% ` [Devel] [PATCH vz10 v2 1/3] ms/selftests/posix_timers: Use CLOCK_THREAD_CPUTIME_ID for ITIMER_PROF measurements Eva Kurchatova
2026-09-22 12:02 [Devel] [PATCH vz10 v2 1/6] selftests: net: run the fdb and bind tests in their own netns Eva Kurchatova
2026-09-22 12:02 2% ` [Devel] [PATCH vz10 v2 2/6] selftests: net: skip what this kernel and iproute2 do not have Eva Kurchatova
2026-09-22 12:02 16% ` [Devel] [PATCH vz10 v2 6/6] selftests: net: let the bridged PMTU tests take the ICMP they ask for Eva Kurchatova
2026-09-22 14:27 0% ` Konstantin Khorenko
2026-09-22 15:00 [Devel] [PATCH hci-8.1 v3 0/5] selftests: net: make the net tests pass on a vz10 host Konstantin Khorenko
2026-09-22 15:00 2% ` [Devel] [PATCH hci-8.1 v3 2/5] selftests: net: skip what this kernel and iproute2 do not have Konstantin Khorenko
2026-09-22 15:00 10% ` [Devel] [PATCH hci-8.1 v3 5/5] ms/tunnels: Drop stale dst when building an ICMP error for PMTUD Konstantin Khorenko
2026-09-24 12:13 [PATCH hci-8.0/hci-8.1 0/2] migration/block-dirty-bitmap: fix postcopy abort with bitmaps on several nodes #VSTOR-147066 Denis V. Lunev
2026-09-24 12:13 35% ` [PATCH hci-8.0/hci-8.1 1/2] " Denis V. Lunev
2026-09-24 15:01 3% [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-09-24 15:01 6% ` [Devel] [PATCH hci-8.1 v3 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-09-24 16:17 0% ` [Devel] [PATCH hci-8.1 v3 1/2] selftests: ve: grant the directories the files sit in Konstantin Khorenko
2026-09-24 16:41 [PATCH QEMU HCI-8.0 v4 00/12] usb-host: support CPR migration Andrey Drobyshev
2026-09-24 16:41 31% ` [PATCH QEMU HCI-8.0 v4 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 13% ` [PATCH QEMU HCI-8.0 v4 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 26% ` [PATCH QEMU HCI-8.0 v4 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 25% ` [PATCH QEMU HCI-8.0 v4 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 23% ` [PATCH QEMU HCI-8.0 v4 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 21% ` [PATCH QEMU HCI-8.0 v4 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 22% ` [PATCH QEMU HCI-8.0 v4 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 26% ` [PATCH QEMU HCI-8.0 v4 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 11% ` [PATCH QEMU HCI-8.0 v4 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-24 16:41 20% ` [PATCH QEMU HCI-8.0 v4 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
2026-09-25 12:47 3% [Devel] [PATCH VZ10 v4 0/3] Improvements for ve selftests Konstantin Khorenko
2026-09-25 12:47 4% ` [Devel] [PATCH VZ10 v4 1/3] ve_namespace: Allow entering non-running VE namespace Konstantin Khorenko
2026-09-25 12:47 6% ` [Devel] [PATCH VZ10 v4 2/3] selftests/ve: Add namespace management helpers Konstantin Khorenko
2026-09-25 12:47 5% ` [Devel] [PATCH VZ10 v4 3/3] selftests/ve: Add more helpers Konstantin Khorenko
2026-09-25 13:18 [Devel] [PATCH hci-8.1 v3 0/2] selftests: pstore: fix backend/frontend skip Eva Kurchatova
2026-09-25 13:18 4% ` [Devel] [PATCH hci-8.1 v3 1/2] selftests: pstore: skip when no backend is registered Eva Kurchatova
2026-09-25 14:11 [PATCH QEMU HCI-8.0 v5 00/13] usb-host: support CPR migration Andrey Drobyshev
2026-09-25 14:11 31% ` [PATCH QEMU HCI-8.0 v5 01/13] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 13% ` [PATCH QEMU HCI-8.0 v5 02/13] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 26% ` [PATCH QEMU HCI-8.0 v5 03/13] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 25% ` [PATCH QEMU HCI-8.0 v5 04/13] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 25% ` [PATCH QEMU HCI-8.0 v5 05/13] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 21% ` [PATCH QEMU HCI-8.0 v5 07/13] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 22% ` [PATCH QEMU HCI-8.0 v5 08/13] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 26% ` [PATCH QEMU HCI-8.0 v5 09/13] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 11% ` [PATCH QEMU HCI-8.0 v5 11/13] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-09-25 14:11 20% ` [PATCH QEMU HCI-8.0 v5 13/13] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
2026-09-25 16:53 [Devel] [PATCH hci-8.1 v10 00/13] Add per-VE failcount support Konstantin Khorenko
2026-09-25 16:53 3% ` [Devel] [PATCH hci-8.1 v10 02/13] ve/fs: count available mounts instead of used ones Konstantin Khorenko
2026-09-25 16:53 4% ` [Devel] [PATCH hci-8.1 v10 07/13] ve: introduce per-VE failcounter infrastructure Konstantin Khorenko
2026-09-25 16:53 4% ` [Devel] [PATCH hci-8.1 v10 09/13] ve: add mnt failcounter Konstantin Khorenko
2026-09-25 16:53 6% ` [Devel] [PATCH hci-8.1 v10 10/13] ve: add netif failcounter Konstantin Khorenko
2026-09-25 16:53 6% ` [Devel] [PATCH hci-8.1 v10 13/13] selftests/ve: add mount accounting selftest Konstantin Khorenko
2026-09-28 13:23 30% [QEMU HCI-7.4 PATCH] migration: set .vm_old_state in migration_stop_vm() #VSTOR-147066 Andrey Drobyshev
2026-09-28 16:51 0% ` Denis V. Lunev
2026-09-28 15:16 [Devel] [PATCH hci-8.1 v4 0/2] selftests: ve: fix ve_perms, introduce vtty_stress Eva Kurchatova
2026-09-28 15:16 11% ` [Devel] [PATCH hci-8.1 v4 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-09-28 15:16 6% ` [Devel] [PATCH hci-8.1 v4 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-09-28 15:24 [Devel] [PATCH hci-8.1 0/2] ve/fs: allow host to go over container mount limit Pavel Tikhomirov
2026-09-28 15:24 5% ` [Devel] [PATCH hci-8.1 2/2] selftests/ve: check that host can " Pavel Tikhomirov
2026-09-28 19:37 [Devel] [PATCH hci-8.1 v2 0/3] selftests: drv-net: bunch of fixes Eva Kurchatova
2026-09-28 19:37 4% ` [Devel] [PATCH hci-8.1 v2 3/3] selftests: drv-net: read the channel count over netlink Eva Kurchatova
2026-09-29 12:48 6% [Devel] [PATCH hci-8.1] ms/selftests: net: reduce txtimestamp deschedule flakes Eva Kurchatova
2026-09-29 12:52 0% ` no-reply
2026-09-30 5:44 [Devel] [PATCH hci-8.1 0/3] fs/fuse: rework close_wait handling Liu Kui
2026-09-30 5:44 2% ` [Devel] [PATCH hci-8.1 3/3] fs/fuse: use wait_var_event to wait for fuse file references Liu Kui
2026-09-30 5:51 0% ` no-reply
2026-09-30 7:34 [Devel] [PATCH hci-8.1 0/3] fs/fuse kio: cut per-4k-page sendmsg/recvmsg calls in the sockio path Liu Kui
2026-09-30 7:34 ` [Devel] [PATCH hci-8.1 2/3] fs/fuse kio: refactor transmit path in pcs_sockio_send() Liu Kui
2026-09-30 7:42 3% ` no-reply
2026-09-30 16:29 0% ` Konstantin Khorenko
2026-09-30 11:17 [PATCH hci-8.0 0/9] block: cheaper zero handling in backup and commit #VSTOR-134085 Denis V. Lunev
2026-09-30 11:17 21% ` [PATCH hci-8.0 1/9] block/commit: pass BDRV_WANT_PRECISE to block-status #VSTOR-134085 Denis V. Lunev
2026-09-30 11:17 3% ` [PATCH hci-8.0 2/9] iotests/040: cover large and fragmented commit runs #VSTOR-134085 Denis V. Lunev
2026-09-30 11:17 18% ` [PATCH hci-8.0 3/9] block/commit: batch block-status queries #VSTOR-134085 Denis V. Lunev
2026-09-30 11:21 3% ` no-reply
2026-09-30 11:17 ` [PATCH hci-8.0 4/9] iotests/124: cover backup of zero clusters and holes #VSTOR-134085 Denis V. Lunev
2026-09-30 11:31 5% ` no-reply
2026-09-30 11:17 25% ` [PATCH hci-8.0 5/9] block/block-copy: don't reserve memory for zero tasks #VSTOR-134085 Denis V. Lunev
2026-09-30 11:17 23% ` [PATCH hci-8.0 6/9] block/block-copy: extract block_copy_set_task_method() #VSTOR-134085 Denis V. Lunev
2026-09-30 11:17 33% ` [PATCH hci-8.0 7/9] block/block-copy: track known-zero source clusters #VSTOR-134085 Denis V. Lunev
2026-09-30 11:30 6% ` no-reply
2026-09-30 11:17 25% ` [PATCH hci-8.0 8/9] block/backup: pre-fill zero_bitmap for full/bitmap #VSTOR-134085 Denis V. Lunev
2026-09-30 11:33 5% ` no-reply
2026-09-30 11:17 26% ` [PATCH hci-8.0 9/9] block/block-copy: coalesce write-zeroes tasks #VSTOR-134085 Denis V. Lunev
2026-09-30 11:34 3% ` no-reply
2026-10-01 3:24 [Devel] [PATCH hci-8.1 v5 0/2] selftests: ve: fix ve_perms, introduce vtty_stress Eva Kurchatova
2026-10-01 3:24 11% ` [Devel] [PATCH hci-8.1 v5 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-10-01 3:24 6% ` [Devel] [PATCH hci-8.1 v5 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-10-01 3:34 5% ` no-reply
2026-10-02 4:43 [Devel] [PATCH hci-8.1 v6 0/2] selftests: ve: fix ve_perms, introduce vtty_stress Eva Kurchatova
2026-10-02 4:43 11% ` [Devel] [PATCH hci-8.1 v6 1/2] selftests: ve: grant the directories the files sit in Eva Kurchatova
2026-10-02 4:43 6% ` [Devel] [PATCH hci-8.1 v6 2/2] selftests: ve: introduce vtty pair teardown stress test Eva Kurchatova
2026-10-02 4:54 4% ` no-reply
2026-10-02 10:58 4% [PATCH hci-8.0 1/1] hw/scsi: avoid deadlock upon TMF request cancelling with VirtIO #VSTOR-148264 Denis V. Lunev
2026-10-02 11:14 0% ` Andrey Drobyshev
2026-10-02 16:48 [PATCH 0/3] chardev: fix qemu-update destroying VMs with a console log #VSTOR-148304 Denis V. Lunev
2026-10-02 16:48 7% ` [PATCH 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304 Denis V. Lunev
2026-10-02 16:48 3% ` [PATCH 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
2026-10-02 16:54 3% ` no-reply
2026-10-02 16:48 ` [PATCH 3/3] iotests/migrate-cpr-exec: cover chardevs with logfile= #VSTOR-148304 Denis V. Lunev
2026-10-02 16:55 3% ` no-reply
2026-10-02 21:26 [Devel] [PATCH hci-8.1 0/6] ve/printk: Fix logs leakage via ve_printk called from IRQ context Vladimir Riabchun
2026-10-02 21:26 ` [Devel] [PATCH hci-8.1 1/6] ve/printk: Add sanity checks in ve_printk functions Vladimir Riabchun
2026-10-02 21:31 4% ` no-reply
2026-10-03 12:26 0% ` Vladimir Riabchun
2026-10-02 21:26 ` [Devel] [PATCH hci-8.1 2/6] ve/printk: Report potentially leaking usage of " Vladimir Riabchun
2026-10-02 21:32 3% ` no-reply
2026-10-03 12:26 0% ` Vladimir Riabchun
2026-10-02 21:26 15% ` [Devel] [PATCH hci-8.1 3/6] ve/printk: Specify ve in printks Vladimir Riabchun
2026-10-02 21:36 4% ` no-reply
2026-10-03 13:49 0% ` Vladimir Riabchun
2026-10-02 21:26 3% ` [Devel] [PATCH hci-8.1 4/6] ve/printk: Fix time virtualization Vladimir Riabchun
2026-10-02 21:37 3% ` no-reply
2026-10-05 8:45 0% ` Vladimir Riabchun
2026-10-02 21:26 8% ` [Devel] [PATCH hci-8.1 5/6] ve/failcount: Use ve_printk with specified ve Vladimir Riabchun
2026-10-02 21:38 5% ` no-reply
2026-10-05 9:56 0% ` Vladimir Riabchun
2026-10-02 21:26 4% ` [Devel] [PATCH hci-8.1 6/6] ve: add neigh_tbl failcounter Vladimir Riabchun
2026-10-02 21:40 3% ` no-reply
2026-10-05 10:02 0% ` Vladimir Riabchun
2026-10-05 9:26 [PATCH hci-8.0 v2 0/3] chardev: fix qemu-update destroying VMs with a console log #VSTOR-148304 Denis V. Lunev
2026-10-05 9:26 7% ` [PATCH hci-8.0 v2 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304 Denis V. Lunev
2026-10-05 9:26 3% ` [PATCH hci-8.0 v2 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
2026-10-05 9:30 3% ` no-reply
2026-10-05 11:23 [Devel] [PATCH hci-8.1 v4 0/1] selftests: net: skip the veth GRO checks where they cannot run Eva Kurchatova
2026-10-05 11:23 ` [Devel] [PATCH hci-8.1 v4 1/1] " Eva Kurchatova
2026-10-05 11:29 3% ` no-reply
2026-10-06 10:34 [PATCH QEMU HCI-8.0 v6 00/12] usb-host: support CPR migration Andrey Drobyshev
2026-10-06 10:34 31% ` [PATCH QEMU HCI-8.0 v6 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:37 7% ` no-reply
2026-10-06 10:34 13% ` [PATCH QEMU HCI-8.0 v6 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:34 25% ` [PATCH QEMU HCI-8.0 v6 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:34 26% ` [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:48 5% ` no-reply
2026-10-06 10:34 23% ` [PATCH QEMU HCI-8.0 v6 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:46 6% ` no-reply
2026-10-06 10:34 22% ` [PATCH QEMU HCI-8.0 v6 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:47 5% ` no-reply
2026-10-06 10:34 29% ` [PATCH QEMU HCI-8.0 v6 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:54 6% ` no-reply
2026-10-06 10:34 ` [PATCH QEMU HCI-8.0 v6 09/12] usb: migrate the interface altsetting #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:52 3% ` no-reply
2026-10-06 10:34 10% ` [PATCH QEMU HCI-8.0 v6 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:57 6% ` no-reply
2026-10-06 10:34 ` [PATCH QEMU HCI-8.0 v6 11/12] hcd-xhci: resync isochronous endpoints after CPR #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:57 3% ` no-reply
2026-10-06 10:34 20% ` [PATCH QEMU HCI-8.0 v6 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
2026-10-06 14:56 [Devel] [PATCH hci-8.1 0/5] x86/ve: CPUID override follow-ups: arm at once, prefixes, selftests Konstantin Khorenko
2026-10-06 14:56 3% ` [Devel] [PATCH hci-8.1 1/5] selftests/ve: check CPUID override emulation for VE tasks Konstantin Khorenko
2026-10-06 15:07 3% ` no-reply
2026-10-06 14:56 ` [Devel] [PATCH hci-8.1 3/5] selftests/ve: check CPUID override right after entering a VE Konstantin Khorenko
2026-10-06 15:03 3% ` no-reply
2026-10-06 15:35 [PATCH QEMU HCI-8.0 v7 00/14] usb-host: support CPR migration Andrey Drobyshev
2026-10-06 15:35 30% ` [PATCH QEMU HCI-8.0 v7 01/14] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:38 0% ` no-reply
2026-10-06 15:35 6% ` [PATCH QEMU HCI-8.0 v7 02/14] usb-host: don't leak config descriptor on claim " Andrey Drobyshev
2026-10-06 15:35 13% ` [PATCH QEMU HCI-8.0 v7 03/14] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 04/14] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:35 25% ` [PATCH QEMU HCI-8.0 v7 05/14] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:35 26% ` [PATCH QEMU HCI-8.0 v7 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:45 4% ` no-reply
2026-10-06 15:35 24% ` [PATCH QEMU HCI-8.0 v7 07/14] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:46 4% ` no-reply
2026-10-06 15:35 22% ` [PATCH QEMU HCI-8.0 v7 08/14] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:47 5% ` no-reply
2026-10-06 15:36 29% ` [PATCH QEMU HCI-8.0 v7 10/14] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:52 6% ` no-reply
2026-10-06 15:36 ` [PATCH QEMU HCI-8.0 v7 11/14] usb: migrate the interface altsetting #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:51 3% ` no-reply
2026-10-06 15:36 10% ` [PATCH QEMU HCI-8.0 v7 12/14] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-10-06 15:36 20% ` [PATCH QEMU HCI-8.0 v7 14/14] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
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.