From: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
To: svt-core@virtuozzo.com
Cc: den@openvz.org, andrey.drobyshev@virtuozzo.com
Subject: [QEMU HCI-8.0 PATCH 5/5] vhost-blk: filter uevents in the kernel
Date: Thu, 3 Sep 2026 15:32:04 +0300 [thread overview]
Message-ID: <20260903123204.24035-6-andrey.zhadchenko@virtuozzo.com> (raw)
In-Reply-To: <20260903123204.24035-1-andrey.zhadchenko@virtuozzo.com>
The uevent socket receives every uevent broadcast on the host:
NETLINK_KOBJECT_UEVENT group 1 has no kernel-side subscription by
subsystem or device. On a dense node every add/remove/change event
of every device (mass container starts creating dm and loop
devices, SCSI rescans, udevadm trigger) wakes up the main loop of
every QEMU with a vhost-blk device just to parse and discard the
message, and a burst can overflow the socket receive buffer.
Attach a classic BPF socket filter which passes only messages
starting with "change@" and containing a "RES"-prefixed property
within the first 512 bytes. Messages longer than the scan window
are passed to userspace instead of being dropped, so the filter
can have false positives but never false negatives:
vhost_blk_uevent_read() remains the authoritative parser. Also
enlarge the receive buffer to 1M: with the filter attached even a
large backlog consists of relevant events only.
Matching the full "RESIZE=1" property or MAJOR=/MINOR= of the
watched devices kernel-side was considered and rejected: the
kernel converts classic BPF to eBPF on attach and the converted
program must fit in BPF_MAXINSNS, which allows only ~1900 classic
instructions of this shape (three per scanned offset). Classic BPF
also cannot loop, so device numbers (variable-length decimal
strings at variable offsets) would need unrolled matching code
regenerated and re-attached on every device plug/unplug. Resize
events are rare; the coarse kernel filter drops all of the heavy
traffic and userspace keeps doing the exact matching.
https://virtuozzo.atlassian.net/browse/VSTOR-143437
Signed-off-by: Andrey Zhadchenko <andrey.zhadchenko@virtuozzo.com>
---
hw/block/vhost-blk.c | 123 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 123 insertions(+)
diff --git a/hw/block/vhost-blk.c b/hw/block/vhost-blk.c
index f8eca4d58a..80b0e06b4c 100644
--- a/hw/block/vhost-blk.c
+++ b/hw/block/vhost-blk.c
@@ -28,6 +28,7 @@
#include <sys/ioctl.h>
#include <linux/fs.h>
#include <linux/netlink.h>
+#include <linux/filter.h>
#include "system/runstate.h"
static int vhost_blk_uevent_fd = -1;
@@ -347,6 +348,126 @@ static void vhost_blk_uevent_read(void *opaque)
}
}
+/*
+ * The kernel broadcasts uevents of every device on the host to
+ * NETLINK_KOBJECT_UEVENT group 1 and provides no subscription by subsystem
+ * or device. Without a filter each uevent (device hotplug, SCSI rescan,
+ * udevadm trigger, ...) wakes up the main loop of every QEMU with a
+ * vhost-blk device just to parse and discard the message.
+ *
+ * Attach a classic BPF socket filter passing only what we are interested
+ * in: messages which start with "change@" and contain a property beginning
+ * with "RES" ("\0RES" match at every offset) within the first
+ * VHOST_BLK_UEVENT_SCAN_LEN bytes. Messages longer than the scan window
+ * are passed to userspace instead of being dropped. The filter can have
+ * false positives but never false negatives: vhost_blk_uevent_read()
+ * remains the authoritative parser.
+ *
+ * Matching the full "\0RESIZE=1\0" property would be nicer, but the kernel
+ * converts classic BPF to eBPF on attach and every packet load expands to
+ * several eBPF instructions; the converted program must fit in
+ * BPF_MAXINSNS (4096) instructions, which allows roughly 1900 classic
+ * instructions of this shape. Three instructions per scanned offset
+ * (load, match, accept-jump) fit with a good margin, seven do not.
+ *
+ * Filtering by MAJOR=/MINOR= of the watched devices is done in userspace
+ * only. Classic BPF cannot loop, so matching these variable-length decimal
+ * strings at variable offsets would require regenerating and re-attaching
+ * unrolled matching code on every device plug/unplug, and the instruction
+ * budget above does not allow anything close to that. Resize events are
+ * rare, all of the heavy traffic is already dropped by the "change@" and
+ * "\0RES" matches.
+ */
+#define VHOST_BLK_UEVENT_SCAN_LEN 512
+#define VHOST_BLK_UEVENT_FILTER_HEAD 10
+#define VHOST_BLK_UEVENT_FILTER_BLOCK 3
+#define VHOST_BLK_UEVENT_FILTER_INSNS (VHOST_BLK_UEVENT_FILTER_HEAD + \
+ VHOST_BLK_UEVENT_FILTER_BLOCK * \
+ VHOST_BLK_UEVENT_SCAN_LEN + 2)
+
+static void vhost_blk_uevent_apply_filter(int fd)
+{
+ g_autofree struct sock_filter *insns =
+ g_new0(struct sock_filter, VHOST_BLK_UEVENT_FILTER_INSNS);
+ const uint32_t accept = VHOST_BLK_UEVENT_FILTER_INSNS - 1;
+ struct sock_fprog prog = {
+ .len = VHOST_BLK_UEVENT_FILTER_INSNS,
+ .filter = insns,
+ };
+ int rcvbuf = 1024 * 1024;
+ uint32_t pc = 0;
+ uint32_t i;
+
+ QEMU_BUILD_BUG_ON(VHOST_BLK_UEVENT_FILTER_INSNS > BPF_MAXINSNS);
+
+ /* Drop everything which does not start with "change@" */
+ insns[pc++] = (struct sock_filter)
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, 0);
+ insns[pc++] = (struct sock_filter)
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x6368616e /* "chan" */, 0, 4);
+ insns[pc++] = (struct sock_filter)
+ BPF_STMT(BPF_LD | BPF_H | BPF_ABS, 4);
+ insns[pc++] = (struct sock_filter)
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x6765 /* "ge" */, 0, 2);
+ insns[pc++] = (struct sock_filter)
+ BPF_STMT(BPF_LD | BPF_B | BPF_ABS, 6);
+ insns[pc++] = (struct sock_filter)
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, '@', 1, 0);
+ insns[pc++] = (struct sock_filter)
+ BPF_STMT(BPF_RET | BPF_K, 0);
+
+ /*
+ * A message longer than the scan window cannot be scanned completely:
+ * pass it to userspace instead of risking a lost resize event.
+ */
+ insns[pc++] = (struct sock_filter)
+ BPF_STMT(BPF_LD | BPF_W | BPF_LEN, 0);
+ insns[pc++] = (struct sock_filter)
+ BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, VHOST_BLK_UEVENT_SCAN_LEN, 0, 1);
+ insns[pc] = (struct sock_filter)
+ BPF_STMT(BPF_JMP | BPF_JA, accept - pc - 1);
+ pc++;
+
+ /*
+ * Scan for "\0RES" at every offset. A load beyond the end of the
+ * message terminates the program with a drop verdict, which is
+ * correct: had the message contained the pattern, it would have been
+ * matched at an earlier, in-bounds offset.
+ */
+ for (i = 0; i < VHOST_BLK_UEVENT_SCAN_LEN; i++) {
+ insns[pc++] = (struct sock_filter)
+ BPF_STMT(BPF_LD | BPF_W | BPF_ABS, i);
+ insns[pc++] = (struct sock_filter)
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, 0x00524553 /* "\0RES" */,
+ 0, 1);
+ insns[pc] = (struct sock_filter)
+ BPF_STMT(BPF_JMP | BPF_JA, accept - pc - 1);
+ pc++;
+ }
+
+ /* Drop */
+ insns[pc++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, 0);
+ /* Accept */
+ insns[pc++] = (struct sock_filter)BPF_STMT(BPF_RET | BPF_K, 0xffffffff);
+ assert(pc == VHOST_BLK_UEVENT_FILTER_INSNS);
+
+ if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog))) {
+ warn_report("vhost-blk: unable to attach uevent filter: %s",
+ strerror(errno));
+ }
+
+ /*
+ * Make the socket resilient to main loop stalls. With the filter
+ * attached even a large backlog consists of relevant events only.
+ */
+ if (setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE,
+ &rcvbuf, sizeof(rcvbuf)) &&
+ setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf))) {
+ warn_report("vhost-blk: unable to enlarge uevent socket buffer: %s",
+ strerror(errno));
+ }
+}
+
static bool vhost_blk_uevent_init(Error **errp)
{
struct sockaddr_nl address = {
@@ -367,6 +488,8 @@ static bool vhost_blk_uevent_init(Error **errp)
return false;
}
+ vhost_blk_uevent_apply_filter(vhost_blk_uevent_fd);
+
if (bind(vhost_blk_uevent_fd, (struct sockaddr *)&address,
sizeof(address)) < 0) {
error_setg_errno(errp, errno,
--
2.43.5
prev parent reply other threads:[~2026-09-03 12:35 UTC|newest]
Thread overview: 15+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-03 12:31 [QEMU HCI-8.0 PATCH 0/5] vhost-blk change backend setup Andrey Zhadchenko
2026-09-03 12:32 ` [QEMU HCI-8.0 PATCH 1/5] blk: factor out validation Andrey Zhadchenko
2026-09-03 14:56 ` Andrey Drobyshev
2026-09-03 12:32 ` [QEMU HCI-8.0 PATCH 2/5] vhost-blk: change backend setup Andrey Zhadchenko
2026-09-03 14:56 ` Andrey Drobyshev
2026-09-03 15:27 ` Andrey Zhadchenko
2026-09-03 15:34 ` Andrey Drobyshev
2026-09-03 12:32 ` [QEMU HCI-8.0 PATCH 3/5] vhost-blk: add read-only flag Andrey Zhadchenko
2026-09-03 14:56 ` Andrey Drobyshev
2026-09-03 12:32 ` [QEMU HCI-8.0 PATCH 4/5] vhost-blk: watch the device for resize events Andrey Zhadchenko
2026-09-03 14:56 ` Andrey Drobyshev
2026-09-03 15:30 ` Andrey Zhadchenko
2026-09-03 15:45 ` Andrey Drobyshev
2026-09-03 15:50 ` Andrey Zhadchenko
2026-09-03 12:32 ` Andrey Zhadchenko [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260903123204.24035-6-andrey.zhadchenko@virtuozzo.com \
--to=andrey.zhadchenko@virtuozzo.com \
--cc=andrey.drobyshev@virtuozzo.com \
--cc=den@openvz.org \
--cc=svt-core@virtuozzo.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox